[HNCTF 2022 WEEK4]flower plus
zach0ry

链接

花指令分析

image-20250714095208126

发现花指令

db 0C7h 的“垃圾”作用: 0x4012A6 处的 db 0C7h 是一个数据字节。它被 jnz 指令跳转到,但它本身不是一个有意义的执行路径。它的存在是为了干扰反汇编器的线性分析,使其认为这里存在一个实际的代码分支。反汇编器可能会将 0C7h 解码为 MOV EDI, EBP (如果加上前缀的话),或者只是一个数据字节,无论哪种,都与正常逻辑不符。

为了不影响汇编器的分析

要把这里进行判断和跳转的都NOP掉

push ebxpop ebx 都 NOP 掉的主要原因是为了完全消除这个花指令的副作用和混淆链

  1. 栈平衡: push ebxpop ebx 是一对用来维护栈平衡的操作。如果它们是花指令的一部分,并且它们保存/恢复的寄存器值在中间被修改(例如被 xor ebx, ebx 清零),那么这对 push/pop 实际上是无效的或者说是误导性的。它们的存在只是为了让代码看起来更复杂,或者在某些情况下通过不匹配的栈操作来干扰分析。

    • 如果只 NOP 掉中间的跳转,保留 push ebxpop ebx,那么在 push ebx 之后,ebx 立即被清零,然后又被 pop ebx 恢复。这可能会导致 ebx 的值在函数中被不必要地改变和恢复,即使它不是真正需要的。

第二个花指令

image-20250714090620492

  1. 执行 0x401358: call loc_40135E:
    • call 指令本身占用 5 个字节 (E8 01 00 00 00)。
    • call 指令的下一条指令的地址是 0x401358 + 5 = 0x40135D
    • CPU 将 0x40135D 压入栈中。此时,0x40135D 就是栈顶的返回地址。
    • CPU 跳转到 0x40135E 开始执行。
  2. 执行 0x40135E: db 36h:
    • 这是一个数据字节,但是 CPU 会尝试将其作为指令执行。这通常是花指令的混淆手法,它可能不会导致崩溃,而是作为无效指令执行或者被后续指令覆盖。
    • 假设它被执行了,或者被跳过了,程序流会继续到 0x40135F
  3. 执行 0x40135F: add dword ptr [esp], 8:
    • 这条指令非常关键。
    • [esp] 指的是栈顶的内存地址。当前栈顶存放的是 0x40135D(原始返回地址)。
    • add dword ptr [esp], 8 的作用是:将栈顶存放的那个 dword 值(即 0x40135D)加上 8,然后把结果再写回栈顶
    • 所以,栈顶的返回地址从 0x40135D 变成了 0x40135D + 8 = 0x401365
  4. 执行 0x401362: retn:
    • retn 指令会从栈顶弹出地址。
    • 它弹出的不再是原始的 0x40135D,而是被 add 指令修改后的 0x401365
    • CPU 跳转到 0x401365 开始执行。

原始的返回地址是 0x40135D。如果没有任何 add 操作,retn 会回到 0x40135D

但是,add dword ptr [esp], 8 将返回地址修改成了 0x401365

这意味着当 retn 执行时,程序会直接从 0x401365 开始执行,

所以,add dword ptr [esp], 8 的作用就是人为地调整了 retn 的目标地址,使其跳过了 0x40135D0x401364(包含 0x401364)这段区域,直接跳转到 0x401365 去执行。 这就是这种花指令用于混淆控制流的一种常见手法。

所以NOP的范围包括0x401364

image-20250714090555001

为什么是花指令

正常情况下,call 指令会直接跳转到被调用函数的入口点。这个入口点应该是一条合法的可执行指令。

但是在这里,当 call 指令跳转到 0x40135E 时,它遇到的第一个字节是 db 36h

  • 反汇编器困惑: 静态反汇编器在 0x40135E 看到 db 36h 时,通常会认为这是数据,而不是代码。它可能因此停止对该区域的代码分析,或者产生错误的解码。
  • CPU 行为: CPU 会尝试将 36h 作为指令来解码执行。如前所述,36h 是一个段超越前缀(SS:)。CPU 会尝试将其与随后的字节组合成一条指令。
    • 如果 36h 后面跟着的字节(即 add dword ptr [esp], 8 的字节码 83)不能与 36h 形成一条合法的指令,或者形成了程序员不期望的指令,这就会导致程序行为异常或崩溃。
    • 然而,在某些复杂的混淆中,可能会利用这种前缀的特性,使得处理器在特定上下文中能正确执行,而反汇编器却难以理解。

call花指令识别

call 目标是数据字节或非指令区。

call 后面紧跟数据定义。

call 目标内部有 add dword ptr [esp], N 来修改返回地址。

最后以 retn 结束,并且 retn 之后的字节可能也是混淆的一部分。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
stae=0x00401006
end=0x00401402
for i in range(stae, end+1):
if get_wide_dword(i)== 0x01740275:
patch_dword(i,0x90909090)
patch_dword(i-4,0x90909090)
patch_word(i+4,0x9090)
patch_word(i-5,0x90)
if get_wide_dword(i)== 0x000001E8:
if get_wide_dword(i+4)== 0x8336E800:
patch_dword(i,0x90909090)
patch_dword(i+4,0x90909090)
patch_dword(i+8,0x90909090)
patch_byte(i+12,0x90)

然后选中函数头U解构

c

C重构一下就好

就可以看待main函数了

image-20250714104404831

在这个函数中看到RC4的标志

image-20250714104602767

之后经过一个异或

然后进入sub_40128F进行核验

image-20250714105749583

这是其加密后的值

右键image-20250714110528278

提取数据

异或脚本

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
a=[0x0000004D, 0xFFFFFFE6, 0x00000049, 0xFFFFFF95, 0x00000003, 0x0000002D, 0x0000002B, 0xFFFFFFBA, 
0xFFFFFFEA, 0x0000006D, 0xFFFFFFFF, 0x00000059, 0x00000070, 0x00000000, 0x0000001B, 0xFFFFFFA9,
0x0000002C, 0xFFFFFFB0, 0x00000032, 0xFFFFFF98, 0x0000006F, 0xFFFFFF8C, 0x00000056, 0xFFFFFFA2,
0x0000004C, 0x00000079, 0x0000007F]
for i in range(len(a)-1,-1,-1):
a[i]^=a[(i+1)%len(a)]
print(a[i], end=' ')
decimal_numbers =a

hex_output = []
for num in decimal_numbers:

unsigned_32bit_num = num & 0xFFFFFFFF

hex_str = hex(unsigned_32bit_num)[2:].upper().zfill(8)
hex_output.append("0x" + hex_str)

for i in range(0, len(hex_output), 4):
print(", ".join(hex_output[i:i+4]))
#十进制50 75 7 4294967205 4294967283 127 16 4294967176 4294967226 10 38 4294967183 4294967188 4294967188 4294967268 4294967229 66 47 4294967237 127 84 121 122 4294967279 4294967206 64 13
#十六进制0x0000000D, 0x00000040, 0xFFFFFFA6, 0xFFFFFFEF
0x0000007A, 0x00000079, 0x00000054, 0x0000007F
0xFFFFFFC5, 0x0000002F, 0x00000042, 0xFFFFFFBD
0xFFFFFFE4, 0xFFFFFF94, 0xFFFFFF94, 0xFFFFFF8F
0x00000026, 0x0000000A, 0xFFFFFFBA, 0xFFFFFF88
0x00000010, 0x0000007F, 0xFFFFFFF3, 0xFFFFFFA5
0x00000007, 0x0000004B, 0x00000032

处理RC4

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
90
def to_unsigned_32bit_and_byte(n):
if n < 0:
n += 0x100000000
return n & 0xFF

def to_signed_32bit(n):
if n > 0x7FFFFFFF:
n -= 0x100000000
return n

def to_unsigned_32bit(n):
if n < 0:
n += 0x100000000
return n & 0xFFFFFFFF

# --- RC4 阶段 1: KSA (密钥调度算法) ---
# 定义: 初始化 S 盒并根据密钥对其进行置换。
# 输入: 密钥字节 (key_bytes), 密钥长度模数 (key_mod_len)
# 输出: 经过初始置换的 S 盒 (s_box)
def s_init(key_bytes, key_mod_len):
s_box = list(range(256))
v4_ksa_t_box = [key_bytes[i % key_mod_len] for i in range(256)]

j = 0
for i in range(256):
j = (v4_ksa_t_box[i] + j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
return s_box

# --- RC4 阶段 2 & 3: PRGA (伪随机生成算法) & 最终数据处理 ---
# 定义:
# PRGA: 根据 S 盒生成伪随机密钥流。
# 最终数据处理: 使用生成的密钥流对输入数据进行异或操作。
# 输入: 待处理数据数组 (data_array), 密钥字节 (key_bytes),
# 密钥长度模数 (key_mod_len), 数据长度 (data_len)
# 输出: 经过处理后的数据数组 (result_am_array)
def rc4_variant_process(data_array, key_bytes, key_mod_len, data_len):
# KSA 阶段的调用
s_box = s_init(key_bytes, key_mod_len)

l = 0
v9 = 0

# 用于存储生成的密钥流
generated_keystream = [0] * data_len

# --- RC4 阶段 2: PRGA (生成密钥流) ---
for k in range(data_len):
l = (l + 3) % 256
v9 = (v9 + s_box[l] + 1) % 256

s_box[l], s_box[v9] = s_box[v9], s_box[l]

generated_keystream[k] = s_box[(s_box[v9] + s_box[l]) % 256]

# --- RC4 阶段 3: 最终数据处理 (异或操作) ---
result_am_array = list(data_array)
for i in range(data_len):
result_am_array[i] = to_signed_32bit(to_unsigned_32bit(result_am_array[i]) ^
to_unsigned_32bit(generated_keystream[i]))

return result_am_array

if __name__ == "__main__":
rc4_key_string = 'Hello_Ctfers!!!'
key_as_byte_list = list(map(ord, rc4_key_string))
if len(key_as_byte_list) < 16:
key_as_byte_list.append(0)
KEY_MOD_LEN = 16

am_initial_hex = [
0x0000000D, 0x00000040, 0xFFFFFFA6, 0xFFFFFFEF,
0x0000007A, 0x00000079, 0x00000054, 0x0000007F,
0xFFFFFFC5, 0x0000002F, 0x00000042, 0xFFFFFFBD,
0xFFFFFFE4, 0xFFFFFF94, 0xFFFFFF94, 0xFFFFFF8F,
0x00000026, 0x0000000A, 0xFFFFFFBA, 0xFFFFFF88,
0x00000010, 0x0000007F, 0xFFFFFFF3, 0xFFFFFFA5,
0x00000007, 0x0000004B, 0x00000032
]
am_initial_data = [to_signed_32bit(h) for h in am_initial_hex]
n = len(am_initial_data)

final_am_values = rc4_variant_process(am_initial_data, key_as_byte_list, KEY_MOD_LEN, n)

output_chars = bytearray()
for val in final_am_values:
output_chars.append(to_unsigned_32bit_and_byte(val))

print(output_chars.decode('latin-1'))
#NSSCTF{Hn_CtF_w111_end_Lol}