关于二次计算型花指令的分析与改进·II (N1CTF 2025 True Operator Master 出题记录)

1.2k 词

– 思路转移 –

在上一篇文档中,提到了这种基于二次运算的花指令:

1
2
3
4
5
6
7
; ...
call $+5
function(t) ; func(t0) = t1 ; func(t1) = t2 != t1
cmp res with t2
if neq retn
continue
; ...

本次我们将不再考虑这个花指令结构的运算部分,而是尝试对其其余功能性进行拓展:

在正常的函数调用中,带入的参数一般从rcx开始,然后是rdx,r8等,结果一般会return到rax

那么如果我们在当前花指令的结构体基础上做出修改,使其形成这种结构:

1
2
3
4
5
6
7
8
9
push all regs
function(t)
---
fake function
---
if neq retn
continue
pop all regs
true function

如果我们在进入花指令前保存所有需要保存的寄存器,然后在花指令运算结束后,插入一段假逻辑作为“fake function”,然后再return,之后在continue时恢复寄存器,再进行真正的运算,那么,只要fake function满足以下条件:

  • 不会干扰用来cmp的那个寄存器(我设置为了rbx,刚好不会干扰到)
  • 使用外界传入的寄存器(如rcx,rdx等)构造有关rax的表达式
  • 不破坏栈结构等外层结构

就可以实现“伪造”逻辑,这些逻辑在程序运行过程中确实会执行到,只不过之后又恢复了寄存器,所以没有用处,属于垃圾代码,但对于ida来说,其并不能识别call $+5下方的return,于是会尝试在return的位置获取rax的来源,这时rax就被显示为了我们用寄存器构造出来的表达式,也就实现了伪造(F5反编译看不出任何问题)

– 实际应用 –

在刚刚的N1CTF 2025的True Operator Master中我便大量使用了这一技巧,构造了如下所示的模板函数:

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
__attribute__((naked)) unsigned int Obf_Func_<Operator_Name>_<id>(unsigned int arg0, unsigned int arg1) {
asm volatile (
".byte 0x55\n\t"
".byte 0x48\n\t"
".byte 0x89\n\t"
".byte 0xe5\n\t"
".byte 0x48\n\t"
".byte 0x83\n\t"
".byte 0xec\n\t"
".byte 0x10\n\t"
"push %%rax\n\t"
"push %%rbx\n\t"
"push %%rcx\n\t"
"push %%rdx\n\t"
"push %%r8\n\t"
"mov %%rcx, %%rax\n\t"
"xor %%rbx, %%rbx\n\t"
"xor $0x1, %%rbx\n\t"
"call label_loop_1510\n\t"
"label_loop_1510:\n\t"
"xor $0x1, %%rbx\n\t"
"cmp $0x1, %%rbx\n\t"
"jz label_end_of_loop_1510\n\t"
"or %%rdx, %%rax\n\t"
"ret\n\t"
"label_end_of_loop_1510:\n\t"
".byte 0xb8\n\t"
".byte 0x30\n\t"
".byte 0x0\n\t"
".byte 0x0\n\t"
".byte 0x0\n\t"
".byte 0x65\n\t"
".byte 0x48\n\t"
".byte 0x8b\n\t"
".byte 0x0\n\t"
".byte 0x48\n\t"
".byte 0x8b\n\t"
".byte 0x40\n\t"
".byte 0x60\n\t"
".byte 0xf\n\t"
".byte 0xb6\n\t"
".byte 0x40\n\t"
".byte 0x2\n\t"
"cmp $0x0, %%eax\n\t"
"pop %%r8\n\t"
"pop %%rdx\n\t"
"pop %%rcx\n\t"
"pop %%rbx\n\t"
"pop %%rax\n\t"
"mov %%rcx, %%rax\n\t"
"jnz label_debug_1510\n\t"
"sub %%rdx, %%rax\n\t"
"jmp label_end_1510\n\t"
"label_debug_1510:\n\t"
"or %%rdx, %%rax\n\t"
"label_end_1510:\n\t"
".byte 0x48\n\t"
".byte 0x83\n\t"
".byte 0xc4\n\t"
".byte 0x10\n\t"
".byte 0x5d\n\t"
"ret"
::: "cc", "memory"
);
}

以上这个函数就将减法伪造成了按位或,且在return后插入了反调试逻辑,如果处于调试状态则运行效果和显示出来的伪造效果一样,仅有非调试状态下才会执行真正的运算符,我在本题目中将一个魔改TEA的循环和运算进行了拆解和微打乱,将其转为了2622个经混淆的单一运算符函数,然后又将这些函数用花指令分隔,再装入一个101*26种分支的嵌套平坦化逻辑中,形成了加密函数.

解题时可以先去除花指令,然后hook这些函数的调用顺序和参数,之后提取每个函数内的真正运算符,发现循环结构后就能对循环进行重建,恢复出加密的逻辑.

– 缺陷 –

不过很可惜的是,由于伪造代码是要被真实执行的,所以但凡大一点的函数都会不可避免地破坏栈结构和打破栈平衡,还可能造成一些预期之外的异常效果,因此该花指令的伪造能力有限,本题目也仅驻足于简单的二元运算而没有深入研究(但已经证实可以嵌入循环等大型结构,只是需要自己手写汇编,比较麻烦);另一方面,该花指令是特地针对ida反编译的bug而设计的,如果直接观察汇编或者使用其他反编译软件就无法进行伪装,因此泛用性不足,对策性较强,优势区间较小,总的来说给到大杯上,不算一个特别优良的花指令设计.