学期刚开始的羊城杯,拖到现在才复现
题目描述
//gogo,look the flag!
附件在下面链接有,NSSCTF也有,补一下之前的复现。
Release 附件下载 · CTF-Archives/2024-YCB-Undergraduate
看题目描述,应该是用Golang写的
题目附件给了一个可执行程序和一个flag.png
尴尬了,这里复现我找了半天找不到主函数在哪,但是记得当时比赛一下就找到了,好像是搜索flag.png就行了,看了别人的Writeup,才发现IDA 7.7恢复不了Golang的符号表,导致即使我找到了主函数引用的字符串,通过交叉引用也找不到主函数,换成IDA 8.3一下就找到了,好吧,又学到了。之前得知IDA 7.7的反编译插件更全,以为还是IDA 7.7更权威,看来高版本也有高版本的好。
这里给的flag.png格式不对,PNG图片的文件头为89 50 4E 47,文件尾为AE 42 60 82
【CTF杂项】常见文件文件头文件尾格式总结及各类文件头_ctf常见文件头-CSDN博客
题目这里应该是改了PNG文件数据,我们恢复回来应该能得到flag的图片。
主逻辑反编译后的代码如下,变量和函数我自己已经进行了重命名
// main.main
void __fastcall main_main()
{
__int64 v0; // rax
int v1; // ecx
__int64 v2; // rbx
int v3; // edi
int v4; // esi
int v5; // r8d
int v6; // r9d
int v7; // r10d
int v8; // r11d
__int64 v9; // r14
int v10; // ecx
int v11; // r8d
int v12; // r9d
int v13; // r10d
int v14; // r11d
string *p_string; // rax
int v16; // ebx
int v17; // r8d
int v18; // r9d
int v19; // r10d
int v20; // r11d
int v21; // ecx
int v22; // r8d
int v23; // r9d
int v24; // r10d
int v25; // r11d
char *keylen; // rbx
int v27; // r8d
int v28; // r9d
int v29; // r10d
int v30; // r11d
int rc4_key; // eax
int v32; // ecx
int v33; // r8d
int v34; // r9d
int v35; // r10d
int v36; // r11d
int v37; // r8d
int v38; // r9d
int v39; // r10d
int v40; // r11d
__int64 v41; // [rsp-36h] [rbp-C8h]
__int64 v42; // [rsp-36h] [rbp-C8h]
__int64 v43; // [rsp-36h] [rbp-C8h]
__int64 v44; // [rsp-36h] [rbp-C8h]
__int64 v45; // [rsp-36h] [rbp-C8h]
__int64 v46; // [rsp-36h] [rbp-C8h]
__int64 v47; // [rsp-2Eh] [rbp-C0h]
__int64 v48; // [rsp-26h] [rbp-B8h]
char v49; // [rsp+Ah] [rbp-88h] BYREF
__int64 v50; // [rsp+32h] [rbp-60h]
string *struct_input; // [rsp+3Ah] [rbp-58h]
__int128 v52; // [rsp+42h] [rbp-50h] BYREF
_QWORD v53[2]; // [rsp+52h] [rbp-40h] BYREF
_QWORD v54[2]; // [rsp+62h] [rbp-30h] BYREF
_QWORD v55[3]; // [rsp+72h] [rbp-20h] BYREF
while ( &v52 + 8 <= *(v9 + 16) )
v0 = runtime_morestack_noctxt();
v55[2] = 0LL;
if ( main_isDebuggerPresent(v0, v2, v1, v3, v4, v5, v6, v7, v8) )//
// 反调试函数
os_Exit(0, v2, v10, v3, v4, v11);
v55[0] = &RTYPE_string;
v55[1] = &off_FCD470;
fmt_Fprintln(off_FCD9F8, qword_1051D60, v55, 1, 1, &off_FCD470, v12, v13, v14, v41);//
// 打印"Welcome to the DASCTF,please input your key:"
p_string = runtime_newobject(&RTYPE_string);
struct_input = p_string;
p_string->ptr = 0LL;
v54[0] = &RTYPE__ptr_string;
v54[1] = p_string;
v16 = qword_1051D58;
fmt_Fscan(off_FCD9D8, qword_1051D58, v54, 1, 1, v17, v18, v19, v20, v42);
//要求输入
if ( struct_input->len != 5 )
//检查输入的长度是否为5
os_Exit(0, v16, v21, 1, 1, v22);
v53[0] = &RTYPE_string;
v53[1] = &off_FCD480;
fmt_Fprintln(off_FCD9F8, qword_1051D60, v53, 1, 1, v22, v23, v24, v25, v43);
keylen = struct_input->ptr;
rc4_key = runtime_stringtoslicebyte(&v49, struct_input->ptr, struct_input->len, 1, 1, v27, v28, v29, v30, v44);
//应该是转换成字节串的函数
v50 = main_NewCipher(rc4_key, keylen, v32, 1, 1, v33, v34, v35, v36, v45);
os_OpenFile("./flag.png", 10, 0, 0, 1, v37, v38, v39, v40, v46, v47, v48);
//打开flag.png文件
}
因为题目保留了符号表,所以分析起来并不是特别的难,这里动态调试一直跟踪就好了,很好发现struct_input 是一个结构体,由输入和输入长度组成,这一点Golang比C的反编译代码明显好多。
然后main_NewCipher就是个RC4加密,代码如下
// main.NewCipher
_DWORD *__golang main_NewCipher(
__int64 key,
__int64 keylen,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8,
__int64 a9)
{
__int64 v9; // r14
_DWORD *sbox; // rax
__int64 i; // rcx
__int64 i_0; // rbx
int j; // esi
int tmp; // r9d
void *retaddr; // [rsp+8h] [rbp+0h] BYREF
__int64 v16; // [rsp+10h] [rbp+8h]
__int64 keycopy; // [rsp+10h] [rbp+8h]
__int64 v19; // [rsp+20h] [rbp+18h]
while ( &retaddr <= *(v9 + 16) )
{
keycopy = key;
v19 = a3;
runtime_morestack_noctxt(key, keylen, a3, a4, a5, a6, a7, a8, a9);
key = keycopy;
a3 = v19;
}
if ( keylen && keylen <= 256 )
{
v16 = key;
sbox = runtime_newobject(&RTYPE_main_Cipher);
for ( i = 0LL; i < 256; ++i )
sbox[i] = i; // S盒初始化
i_0 = 0LL;
j = 0;
while ( i_0 < 256 )
{
tmp = sbox[i_0];
if ( i_0 % keylen >= keylen )
runtime_panicIndex(i_0 % keylen);
j += tmp + *(v16 + i_0 % keylen);
sbox[i_0] = sbox[j];
sbox[j] = tmp; // 打乱S盒
++i_0;
}
}
else
{
runtime_convT64(keylen, keylen, a3, a4, a5, a6, a7, a8, a9);
return 0LL;
}
return sbox;
}
这里的变量我也进行了重命名,看起来就很明显是RC4加密了,但是这里并不完整,还有生成密钥流并与明文异或的代码,这里并没有
但是后续打开了flag.png文件之后就没有伪代码了,这里不知道为什么,这是一个值得探讨的问题,是出题人故意的还是怎样,在这打一个问号 ?
所以这里就要求我们不得不去看汇编了,这里既然打开了flag.png文件,那么后续应该就会读取flag.png的数据,我们就动态调试追踪其数据放在了哪里
flag.png的数据如下
输入12345,后续阅读汇编,发现数据存储在rsi寄存器当中,如图
后续的汇编指令对flag.png处理的逻辑如下,rcx寄存器存储flag.png数据的大小,这里的指令将flag.png的所有数据与我们输入的第二位,即RC4密钥的第二位进行异或运算。
最后再进行数据处理的汇编指令如图
剩下的RC4加密的代码也包括在这里了,flag.png的数据与RC4加密算法产生的密钥流进行异或,这里最终还多异或了一个0x11,在撕这里的汇编的时候,学到了取模256的时候,通过r10寄存器r10d到r10b来实现。
那这里解题的思路就只有爆破RC4加密算法的密钥,也就是我们的输入,爆破直到flag.png的文件头匹配PNG的文件头。
爆破脚本如下
import itertools
from tqdm import tqdm
# 初始化 S 数组,使用 RC4 密钥调度算法(KSA)
def initialize(key):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + ord(key[i % len(key)])) % 256
S[i], S[j] = S[j], S[i]
return S
# 生成密钥流,使用伪随机生成算法(PRGA)
def generate_key_stream(S, length):
i = j = 0
key_stream = []
for _ in range(length):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
key_stream.append(S[(S[i] + S[j]) % 256])
return key_stream
# 解密函数,通过 RC4 密钥流与输入数据进行异或运算
def decrypt(data, key):
S = initialize(key)
key_stream = generate_key_stream(S, len(data))
flag = [(data[i] ^ key_stream[i] ^ 0x11 ^ ord(key[1])) for i in range(len(data))]
if flag == [0x89,0x50,0x4E,0x47]: # 检查是否匹配 PNG 文件头标识符
return key
# 生成所有可能的 5 字符 RC4 密钥
def generate_rc4_key():
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return (''.join(key_tuple) for key_tuple in itertools.product(chars, repeat=5))
# 主程序,尝试每一个密钥进行解密
data = [0x85, 0x43, 0x72, 0x78]
for key in tqdm(generate_rc4_key()):
decrypted_key = decrypt(data, key)
if decrypted_key:
print(f"Found key: {decrypted_key}")
break
#Found key: 0173d
来自博客🤗回来啦,就知道你舍不得~
打开可执行程序输入密钥即可得到图片
所以flag就是DASCTF{good_y0u_get_the_ffffflag!}
当然这里的博客爆破的时候是把数字放在前面,所以得到答案就会很快,但是如果爆破的时候把字母放在爆破表前面,就要爆破很久,建议还是用C语言,我还看到有用多线程爆破的,有空学习一下,不知道是不是效率会更高。
顺便把有爆破脚本的文章都放下面,下次学习的时候就不用找了
2024 羊城杯个人部分Writeup – lrhtony 的小站