【尝试】关于二次计算型花指令的分析与改进

2.8k 词

– 引子 –

在今年年初的VNCTF 2025中,遇到了一个这样的题目(不知道是不是第一次遇到了,感觉以前也见过几次,但是最有印象的是这一次了)

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
v1 = rand();
byte_18000E1E0 = v1 + v1 / 255;
v2 = rand();
byte_18000E1F0 = v2 + v2 / 255;
v3 = rand();
byte_18000E1F2 = v3 + v3 / 255;
v4 = rand();
byte_18000E200 = v4 + v4 / 255;
v5 = rand();
byte_18000E204 = v5 + v5 / 255;
v6 = rand();
byte_18000E210 = v6 + v6 / 255;
v7 = rand();
byte_18000E950 = v7 + v7 / 255;
v8 = rand();
byte_18000E220 = v8 + v8 / 255;
v9 = rand();
byte_18000E228 = v9 + v9 / 255;
v10 = rand();
byte_18000E230 = v10 + v10 / 255;
v11 = rand();
byte_18000E232 = v11 + v11 / 255;
v12 = rand();
byte_18000E240 = v12 + v12 / 255;
v13 = rand();
byte_18000E244 = v13 + v13 / 255;
v14 = rand();
byte_18000E960 = v14 + v14 / 255;
v15 = rand();
byte_18000E962 = v15 + v15 / 255;
v16 = rand();
byte_18000E970 = v16 + v16 / 255;
return 6;

在某个函数的结尾有一个诡异的return 6(还见过return 39之类的),而这时如果你查看汇编会发现不对劲的地方:

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
......
.text:0000000180003379 mov cs:byte_18000E962, cl
.text:000000018000337F call rsi ; rand
.text:0000000180003381 cdqe
.text:0000000180003383 imul rcx, rax, 0FFFFFFFF80808081h
.text:000000018000338A shr rcx, 20h
.text:000000018000338E add ecx, eax
.text:0000000180003390 mov edx, ecx
.text:0000000180003392 shr edx, 1Fh
.text:0000000180003395 shr ecx, 7
.text:0000000180003398 add ecx, edx
.text:000000018000339A add ecx, eax
.text:000000018000339C mov cs:byte_18000E970, cl
.text:00000001800033A2 push rax
.text:00000001800033A3 xor rax, rax
.text:00000001800033A6 xor rax, 1
.text:00000001800033AA call $+5
.text:00000001800033AF add rax, 3
.text:00000001800033B3 add rax, 1
.text:00000001800033B7 sar rax, 1
.text:00000001800033BA shl rax, 1
.text:00000001800033BD sar rax, 1
.text:00000001800033C0 shl rax, 1
.text:00000001800033C3 shl rax, 1
.text:00000001800033C6 add rax, 4
.text:00000001800033CA shl rax, 1
.text:00000001800033CD sar rax, 1
.text:00000001800033D0 sub rax, 1
.text:00000001800033D4 shl rax, 1
.text:00000001800033D7 sar rax, 1
.text:00000001800033DA xor rax, 3
.text:00000001800033DE shl rax, 1
.text:00000001800033E1 sar rax, 1
.text:00000001800033E4 sub rax, 4
.text:00000001800033E8 add rax, 2
.text:00000001800033EC cmp rax, 12h
.text:00000001800033F0 jz short loc_1800033F3
.text:00000001800033F2 retn
.text:00000001800033F3 ; ---------------------------------------------------------------------------
.text:00000001800033F3
.text:00000001800033F3 loc_1800033F3: ; CODE XREF: ThreadProc(void *)+290↑j
.text:00000001800033F3 pop rax
.text:00000001800033F4 mov ecx, 1BF52h ; Seed
.text:00000001800033F9 call cs:srand
......

可以看到这里有一个突然插入的汇编段,对rax进行了一大串计算后返回,后续又是正常的函数逻辑了,但从ida的反编译来看,后续的逻辑并没有显示出来,而是直接在这里诡异地return了一个值。

– 分析 –

实际上这是一种花指令,对rax进行初始化(这里初始化值是1)后用call的指令对其进行了一些计算,之后进行比较,如果结果不对就return,否则跳转到后面的正常逻辑。问题就出在这个return上,ida似乎无法识别出来这个return是return到上面的call,而不是整个函数的return,所以误以为函数在这里中断,直接返回了第一次的计算结果(可以看到流程图上也没有建立联系):

retn后本应该有一个箭头指向call $+5的下方
选择的循环计算段也有一定的特殊要求,至少要能产生两种不同的结果(即第一次的返回值再次带入循环后能得到一个新的计算结果),cmp的地方要cmp第二次的结果,而非第一次,这样在实际运行时就能在运行两轮后回到正常逻辑,而ida则无法反编译这里的逻辑。

– 改进 –

那么我们可以自然而然的想到并发问:如果一定要用上各种复杂的指令,像这样的循环多吗?能否自由快捷地构建任意我想要构建的循环?有的兄弟,有的,像这样的循环还有无数个。实际上,我们可以编写一个python脚本来快速构建我们想要的循环:

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
import random

# 汇编指令定义
def op_add(a, b): return (b + a) & 0xFFFFFFFFFFFFFFFF
def op_sub(a, b): return (b - a) & 0xFFFFFFFFFFFFFFFF
def op_xor(a, b): return (b ^ a) & 0xFFFFFFFFFFFFFFFF
def op_and(a, b): return (b & a) & 0xFFFFFFFFFFFFFFFF
def op_or(a, b): return (b | a) & 0xFFFFFFFFFFFFFFFF
def op_shl(a, b): return ((b << a) & 0xFFFFFFFFFFFFFFFF)
def op_sal(a, b): return ((b << a) & 0xFFFFFFFFFFFFFFFF)
def op_shr(a, b): return ((b >> a) & 0xFFFFFFFFFFFFFFFF)
def op_sar(a, b):
from ctypes import c_int64
return c_int64((b >> a)).value & 0xFFFFFFFFFFFFFFFF

OPS = {'add': op_add,
'sub': op_sub,
'xor': op_xor,
'and': op_and,
'or': op_or,
'sal': op_sal,
'shl': op_shl,
'shr': op_shr,
'sar': op_sar}

# 指定循环体的长度
length = 18
init_ops = [""] * length
# 指定操作数
params = [1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 1, 0x11, 0x45, 0x14, 0x19, 0x19, 0x81]
# 指定一定要出现/不要出现哪些类型的操作符
def has_required_ops(ops_list):
return 'shl' in ops_list and 'sar' in ops_list and not 'sal' in ops_list
# 指定第一次的结果(显示结果)
target = 0
# 指定rax初始化值
rax_init = 1

def run_sequence(ops_list, init_rax):
rax = init_rax
for op_name, param in zip(ops_list, params):
op_func = OPS[op_name]
rax = op_func(param, rax)
return rax

def find_valid_op_combination():
possible_ops = ['add', 'sub', 'xor', 'and', 'or', 'shl', 'sal', 'shr', 'sar']
count = 0
while True:
count += 1
ops_candidate = [random.choice(possible_ops) for _ in init_ops]
if not has_required_ops(ops_candidate):
continue
rax1 = run_sequence(ops_candidate, rax_init)
if rax1 != target:
continue
rax2 = run_sequence(ops_candidate, rax1)
if rax2 == target:
continue
print(f"已找到符合要求的opcode序列({count}次尝试):")
print(" ".join([f"{op}(${param})" for op, param in zip(ops_candidate, params)]))
print(f"第一次结果: {hex(rax1)}")
print(f"第二次结果: {hex(rax2)}")
return ops_candidate
find_valid_op_combination()

输出类似于:

1
2
3
4
已找到符合要求的opcode序列(71872次尝试):
xor($1) add($1) or($4) add($5) add($1) xor($4) sub($1) xor($9) xor($1) xor($9) sub($8) add($1) shl($17) add($69) add($20) sar($25) and($25) and($129)
第一次结果: 0x0
第二次结果: 0x1

这样我们就能随意地构建我们想要的花指令了,我们还可以写一个脚本打印和验证我们的花指令是否正确:

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
def add(a, b):
print(f"\"add ${a}, %%rax\\n\\t\"")
return b+a
def sub(a, b):
print(f"\"sub ${a}, %%rax\\n\\t\"")
return b-a
def shl(a, b):
print(f"\"shl ${a}, %%rax\\n\\t\"")
return b<<a
def sal(a, b):
print(f"\"sal ${a}, %%rax\\n\\t\"")
return b<<a
def shr(a, b):
print(f"\"shr ${a}, %%rax\\n\\t\"")
return b>>a
def sar(a, b): # 这里简化处理了,我猜没有人会想把rax叠到0xFFFFFFFFFFFFFFFF的
print(f"\"sar ${a}, %%rax\\n\\t\"")
return b>>a
def xor(a, b):
print(f"\"xor ${a}, %%rax\\n\\t\"")
return b^a
def AND(a, b):
print(f"\"and ${a}, %%rax\\n\\t\"")
return b&a
def OR(a, b):
print(f"\"or ${a}, %%rax\\n\\t\"")
return b|a

rax = 1
for i in range(2):
rax = add(1, rax)
rax = sub(1, rax)
rax = shl(4, rax)
rax = add(5, rax)
rax = add(1, rax)
rax = sub(4, rax)
rax = xor(1, rax)
rax = sub(9, rax)
rax = shl(1, rax)
rax = add(9, rax)
rax = sub(8, rax)
rax = sar(1, rax)
rax = xor(0x11, rax)
rax = OR(0x45, rax)
rax = OR(0x14, rax)
rax = xor(0x19, rax)
rax = sar(0x19, rax)
rax = AND(0x81, rax)
print(rax)

运行效果:

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
"add $1, %%rax\n\t"
"sub $1, %%rax\n\t"
"shl $4, %%rax\n\t"
"add $5, %%rax\n\t"
"add $1, %%rax\n\t"
"sub $4, %%rax\n\t"
"xor $1, %%rax\n\t"
"sub $9, %%rax\n\t"
"shl $1, %%rax\n\t"
"add $9, %%rax\n\t"
"sub $8, %%rax\n\t"
"sar $1, %%rax\n\t"
"xor $17, %%rax\n\t"
"or $69, %%rax\n\t"
"or $20, %%rax\n\t"
"xor $25, %%rax\n\t"
"sar $25, %%rax\n\t"
"and $129, %%rax\n\t"
0
"add $1, %%rax\n\t"
"sub $1, %%rax\n\t"
"shl $4, %%rax\n\t"
"add $5, %%rax\n\t"
"add $1, %%rax\n\t"
"sub $4, %%rax\n\t"
"xor $1, %%rax\n\t"
"sub $9, %%rax\n\t"
"shl $1, %%rax\n\t"
"add $9, %%rax\n\t"
"sub $8, %%rax\n\t"
"sar $1, %%rax\n\t"
"xor $17, %%rax\n\t"
"or $69, %%rax\n\t"
"or $20, %%rax\n\t"
"xor $25, %%rax\n\t"
"sar $25, %%rax\n\t"
"and $129, %%rax\n\t"
129

其中带引号的行可以直接粘贴进c语言或者c++的脚本中,用asm volatile();包裹:

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
asm volatile (
"push %%rax\n\t"
"xor %%rax, %%rax\n\t"
"xor $0x23, %%rax\n\t"
"add $4, %%rax\n\t"
"shr $3, %%rax\n\t"
"sub $3, %%rax\n\t" // rax在这一步结束后的值是初始化值
"call label1\n\t"
"label1:\n\t"
// 替换这部分,从这里开始
"add $1, %%rax\n\t"
"sub $1, %%rax\n\t"
"shl $4, %%rax\n\t"
"add $5, %%rax\n\t"
"add $1, %%rax\n\t"
"sub $4, %%rax\n\t"
"xor $1, %%rax\n\t"
"sub $9, %%rax\n\t"
"shl $1, %%rax\n\t"
"add $9, %%rax\n\t"
"sub $8, %%rax\n\t"
"sar $1, %%rax\n\t"
"xor $17, %%rax\n\t"
"or $69, %%rax\n\t"
"or $20, %%rax\n\t"
"xor $25, %%rax\n\t"
"sar $25, %%rax\n\t"
"and $129, %%rax\n\t"
// 到这里结束
"cmp $0x81, %%rax\n\t" // 这里改成第二次运算结果
"jz label_continue\n\t"
"ret\n\t"
"label_continue:\n\t"
"pop %%rax\n\t"
:
:
: "rax", "memory"
);

这样我们就能获得一个任意的二次计算型花指令了!你可以随意修改其中的任何值,比如…:

——在操作数上搞怪🤪🤪🤪(比如像我一样把操作数改成homo特有的恶臭数字),藏彩蛋,甚至藏密钥和flag

——尝试有没有无解的情况🤔🤔🤔(除非你闲得慌,但我觉得只要循环段够短就大概率无解)

——建议:把显示返回值改成0什么的😰😰😰,大家可能要找半天才能发现你藏东西了

——非常建议:在隐藏段加上反调试并在无调试器时加密😨😨😨,想出来这个的家里要请哈基高了

– 改进??? –

那么我们可以自然而然的想到并发问:现在这个花指令还是太简单、太乏味、太无趣了,能否再改改,改成反编译器不敢反编译的样子?有的兄弟,有的,像这样的改法还有无数个。实际上,我们可以编写一个python脚本来…好吧,我做不到,还是太菜了😭,不过,我这里提供了一种改法,需要手搓汇编,有兴趣的可以也像我一样试着瞎改改(前提是改完能如期运行):

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
asm volatile (
// 初始化部分
"push %%rax\n\t"
"xor %%rax, %%rax\n\t"
"xor $0x23, %%rax\n\t"
"add $4, %%rax\n\t"
"shr $3, %%rax\n\t"
"sub $3, %%rax\n\t" // 这里rax的初始值是1

"label_cmp:\n\t"
"cmp $1, %%rax\n\t" // 第一次这里返回正确,第二次rax为0返回错误
"jz label_call\n\t" // 第一次跳转到call的label,第二次不跳转
"add $3, %%rax\n\t" // rax加3 = 3
"shl $1, %%rax\n\t" // rax乘2 = 6
"add $2, %%rax\n\t" // rax加2 = 8
"jmp label_calc\n\t" // 跳转到另一个计算段

"label_call:\n\t"
"call label_loop\n\t" // call进循环体
"cmp $0, %%rax\n\t" // 循环体运行完一圈后回到这一行,rax为0通过cmp校验
"jz label_cmp\n\t" // 跳转到cmp的label

"label_loop:\n\t"
"add $1, %%rax\n\t"
"sub $1, %%rax\n\t"
"shl $4, %%rax\n\t"
"add $5, %%rax\n\t"
"add $1, %%rax\n\t"
"sub $4, %%rax\n\t"
"xor $1, %%rax\n\t"
"sub $9, %%rax\n\t"
"shl $1, %%rax\n\t"
"add $9, %%rax\n\t"
"sub $8, %%rax\n\t"
"sar $1, %%rax\n\t"
"xor $17, %%rax\n\t"
"or $69, %%rax\n\t"
"or $20, %%rax\n\t"
"xor $25, %%rax\n\t"
"sar $25, %%rax\n\t"
"and $129, %%rax\n\t"
"cmp $0x81, %%rax\n\t" // 第一次运行结果是0,不通过,第二次通过,跳转到正常运行流
"jz label_continue\n\t"
"ret\n\t" // return到call的下面

// 第一圈循环结束后跳转回来的段,此时rax为8
"label_calc:\n\t"
"sar $3, %%rax\n\t" // rax除8 = 1
"sub $1, %%rax\n\t" // rax减1 = 0 (恢复了rax的值)
"jmp label_loop\n\t" // 跳转回到循环体

// 恢复源程序逻辑部分
"label_continue:\n\t"
"pop %%rax\n\t"
:
:
: "rax", "memory"
);

效果展示:

哦牛批
ida直接反编译不出来了,不过bn好像还行,只不过也比较难懂就是了

– ¿ –

这是我第一次在网站上发这类灵感(大粪),以后如果有新的灵感应该也会发的,希望我的灵感能一次比一次新颖(更加大粪)😋😋😋