第三届数信杯数据安全大赛初赛reverse wp

起因

看到决赛通知我才想起来,初赛当时还有道题没做完,当时时间太紧了,压根没时间认真做啊🤣

题目如下:

工程师小王认识到前面开发的程序并不能保证对数据的安全存储,现在对处理程序进行了改进,这次能行吗?分析程序功能,解密文件获取原始数据,提交第8行第2列数据。

花指令去除

一开始跟进 main 函数就能发现 ida 没识别成功

在 0x4016F1 处能看到第一处

image-20260228230005626

这样即可

image-20260228230144549

第二处在 0x4012A5 处

image-20260228230319167

同上处理

image-20260228230255697

第三处在 0x4014E6 处

image-20260228230344695

许久不见 call-ret 型的花指令了,这里真正的代码是从 0x4014F6 开始的,还原后如下

image-20260228230623090

重新识别函数之后就能开心的读代码了

加密分析

三个加密逻辑都在函数 sub_40127A

循环左移 rol1

dest 数组就是从文件中读进来的明文数据

image-20260228230858628

shellcode 自解密

注意他从明文的 0x339 开始取了两字节的数据作为 key

image-20260228231338709

shellcode 数据

image-20260228231455723

xor加密

image-20260228231730611

shellcode 解密

很显然题目的重点就在第二重加密了,既然 shellcode 是要执行的,那就先把 shellcode 全部转为代码看看吧

image-20260228232152184

入口部分

可以看到前几字节是正常的:

1
2
3
41 54                push r12
41 53 push r11
e8 45 00 00 00 call next

作用

1
2
3
push r12
push r11
call +0x45

这个 call 会:

  • 把下一条指令地址压栈
  • 跳到后面 0x45 字节处
数据区

前三条指令都是比较正常的,但后面就变的杂乱无章了,说明他们压根就不是正确的代码,他们很可能是需要解密的数据

call 落点

call 跳到这里:

1
2
3
41 5b              pop r11
4d 31 e4 xor r12, r12
45 31 c0 xor r8d, r8d

pop r11 拿到 call 返回地址。

也就是数据区首地址,所以 r11 = &encrypted_data

主循环

取一个加密字节

1
movzx eax, byte ptr [r11 + r12]

取 key

1
mov bl, [rsi + r8]

因为 r8 只能是 0 或 1 ,所以显然这里是取两字节的 key

异或解密

1
xor al, bl

所以最后的逻辑还原大概是这样

1
2
3
for (i = 0; i < 0x42; i++) {
data[i] ^= key[i % 2];
}
爆破密钥

由于这段 shellcode 需要的 key 是从明文里来的,也就是说这个程序只能用来加密那个对应的文件(小王这么写是要准备提桶跑路吗🤣),所以只能进行爆破

要注意的一点是,数据区只有 0x42 的长度,在 0x4040EC 还有一段shellcode执行完之后的代码,注意到有两个 pop r11 的操作,但上面只有一个 push r11 的操作,所以解密之后的数据肯定有压栈的操作,这样可以缩小范围(虽然我是做出来之后看到了才想到的)

image-20260301000604816

爆破代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from capstone import *

# 原始 Shellcode 数据
cipher_text = bytes([
0x33, 0x3A, 0x33, 0x3D, 0x3A, 0x91, 0xBD, 0x26, 0x8D, 0xA1,
0x3A, 0x91, 0xBD, 0x27, 0xFB, 0x92, 0x3B, 0xEF, 0x96, 0x91,
0x72, 0x6E, 0x72, 0x27, 0xF1, 0x8A, 0x7D, 0x27, 0xFB, 0x95,
0x3B, 0xEF, 0x91, 0x91, 0x72, 0x6E, 0x72, 0x27, 0xB3, 0x95,
0x76, 0x27, 0xF1, 0x8D, 0x7D, 0x27, 0xB3, 0x8A, 0x76, 0x23,
0x7B, 0x8D, 0x3E, 0xE7, 0xAA, 0x5A, 0x61, 0x6A, 0x76, 0x90,
0xB2, 0x90, 0xB2, 0x2F, 0x29, 0x2F, 0x2E
])

def xor_decrypt(data, k1, k2):
"""双字节循环异或解密"""
dec = bytearray()
for i in range(len(data)):
if i % 2 == 0:
dec.append(data[i] ^ k1)
else:
dec.append(data[i] ^ k2)
return bytes(dec)

def brute_force():
# 初始化 Capstone x86-64 模式
md = Cs(CS_ARCH_X86, CS_MODE_64)
# 开启详细模式以过滤无效指令
md.detail = True

results = []

print("[*] Starting brute force... this may take a minute.")

for k1 in range(256):
for k2 in range(256):
decrypted = xor_decrypt(cipher_text, k1, k2)

# 尝试反汇编
insns = list(md.disasm(decrypted, 0x1000))

if not insns:
continue

# 评估解密结果的质量
score = 0
bad_insn = False
asm_code = []

total_bytes_disassembled = 0

for i in insns:
mnemonic = i.mnemonic.lower()
total_bytes_disassembled += len(i.bytes)

# 排除不符合要求的指令(jmp, call, ret等控制流指令)
if mnemonic.startswith('j') or mnemonic in ['call', 'ret', 'int', 'syscall']:
bad_insn = True
break

# 数据处理指令加分
if mnemonic in ['mov', 'xor', 'add', 'sub', 'lea', 'inc', 'dec', 'shl', 'shr', 'rol', 'ror', 'and', 'or']:
score += 1

asm_code.append(f"{i.address:#x}:\t{i.mnemonic}\t{i.op_str}")

# 如果反汇编覆盖率太低(说明中间有很多无法解析的机器码),则忽略
if total_bytes_disassembled < len(cipher_text) * 0.8:
continue

if not bad_insn and score > 2: # 设定一个基本的指令密度阈值
results.append({
'key': (k1, k2),
'score': score,
'asm': asm_code
})

# 按分数排序,输出最可能的解
results.sort(key=lambda x: x['score'], reverse=True)

print(f"[*] Found {len(results)} potential candidates.\n")

for top in results[:5]: # 输出前5个最像的
k1, k2 = top['key']
print(f"================ Key: 0x{k1:02X} 0x{k2:02X} (Score: {top['score']}) ================")
print("\n".join(top['asm']))
print("-" * 50)

if __name__ == "__main__":
brute_force()

结果如下:

image-20260301001013275

最终的加密逻辑为

1
f(b) = ( nibble_swap(b-3) ^ 0x13 ) + 6

解密

最后写出解密代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import hashlib

xor_key = b"e6911c24"

with open('info_81bedab.ori.en', 'rb') as infile:
cipher_data = infile.read()

buffer = bytearray(cipher_data)

for idx in range(len(buffer)):
# 第一阶段:XOR
buffer[idx] ^= xor_key[idx % 8]

# 第二阶段:减6 -> XOR 0x13 -> 交换高低4位 -> 加3
buffer[idx] = (buffer[idx] - 6) & 0xFF
buffer[idx] ^= 0x13
buffer[idx] = ((buffer[idx] & 0x0F) << 4) | ((buffer[idx] & 0xF0) >> 4)
buffer[idx] = (buffer[idx] + 3) & 0xFF

# 第三阶段:循环右移1位
current = buffer[idx]
buffer[idx] = ((current >> 1) | (current << 7)) & 0xFF

plain_data = bytes(buffer)

with open('info_81bedab.ori', 'wb') as outfile:
outfile.write(plain_data)
print("done")

解密结果如图:

image-20260301001731396

参考链接:

https://www.cnblogs.com/lantern-lab/p/19430548/2025-the-3rd-shuxin-cup-qxcuc#%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A82%E6%B2%A1%E5%81%9A%E5%87%BA%E6%9D%A5