首页
社区
课程
招聘
[原创]2021 KCTF 春季赛 第七题 千里寻根 解题方法
2021-5-21 22:10 7622

[原创]2021 KCTF 春季赛 第七题 千里寻根 解题方法

2021-5-21 22:10
7622

0x00 序

KCTF2021春季赛第七题《千里寻根》题目是道windows64位逆向题, 这题是"KCTF2019Q3第十题《传家之宝》"和"KCTF2019Q4第六题《三道八佛》"的升级版本.

 

这题的防御重点只有一个——壳.

 

题目由三部分组成:
1.PE保护壳(反调试, CRC, 导入表混淆)
2.蜗牛壳(SMC)
3.验证算法

 

题目蜗牛壳框架甚至代码流程都和前两题基本完全一样(如果去回顾之前题目框架, 寻找可利用的漏洞, 可以省去很多对抗此题混淆的时间).
题目算法部分是《三道八佛》里的算法(只修改了数字)加上稍作修改的base64(修改了编码表和编码分组逻辑). 算法部分没有难度, 哪怕不能逆算法, 参考大佬们编写的wp可以直接抄下逆算法.

KCTF 2019 Q4 第六题 一个支点 by HHHso
2019看雪CTF总决赛第六题:三道八佛WP by poyoten

 

也就是说, 只要能从题目中提取出算法实现代码, 就不难写出KeyGen.

 

此贴将详细介绍"PE保护壳"分析脱壳, 以及从"蜗牛壳"中提取算法的一种方法实现.

0x01 "PE保护壳"分析脱壳

壳行为分析(反调试和CRC)

直接x64dbg载入程序运行, 程序会进程结束(ExitProcess)或者出异常无法继续运行(DEADC0DE), 发现有反调试.

使用x64dbg的trace into功能, 跟踪到异常位置.
分析trace过的逻辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//初始化API地址
0x0000000146680E40 call 0x00000001466800A8
0x00000001466800A8 je 0x0000000146680B4E
0x00000001466800AE jne 0x0000000146680B4E
//获取kernel32地址
0x0000000146680B4E sub rsp, 0x28
0x0000000146680B52 call 0x000000014668413F
0x000000014668413F jmp 0x000000014668414E
0x000000014668414E mov rax, qword ptr gs:[0x0000000000000060]
0x0000000146684157 mov rax, qword ptr ds:[rax+0x18]
0x000000014668415B mov rax, qword ptr ds:[rax+0x30]
0x000000014668415F mov rax, qword ptr ds:[rax]
0x0000000146684162 mov rax, qword ptr ds:[rax]
0x0000000146684165 call 0x0000000146684144
0x0000000146684144 add rsp, 0x8
0x0000000146684148 mov rax, qword ptr ds:[rax+0x10]
0x000000014668414C ret
0x0000000146680B57 mov qword ptr ss:[rsp-0x8], r14
0x0000000146680B5C sub rsp, 0x8
//... ...
1
2
3
4
//GetProAddressByHash
//此处为Kernel32.VirtualAlloc(hash:0x09CE0D4A)
0x00000001466801EE mov rcx, qword ptr ds:[0x000000014691D22E]
0x00000001466801F5 call 0x000000014668219A
1
2
3
//初始化CRC32 table, 0xEDB88320
0x0000000146685F45 mov rsi, 0xEDB88320
//... ...
1
2
3
//GetProAddressByHash
//此处为ntdll.NtQueryInformationProcess(hash:0x5E7088ED)
0x0000000146680EC7 call 0x000000014668219A
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
//call qword ptr ds:[0x000000014691D266]像是调用导入表API
//0x6E0000一看就是堆地址
//这是壳的导入表保护
0x0000000146681672 call qword ptr ds:[0x000000014691D266]
0x00000000006E0000 jmp 0x00000000006E000A
0x00000000006E000A call 0x00000000006E000F
0x00000000006E000F sub qword ptr ss:[rsp], 0xD
0x00000000006E0014 push rax
0x00000000006E0015 push rbx
0x00000000006E0016 push rcx
0x00000000006E0017 push rdx
0x00000000006E0018 push rbp
0x00000000006E0019 push rsi
0x00000000006E001A push rdi
0x00000000006E001B push r8
0x00000000006E001D push r9
0x00000000006E001F push r10
0x00000000006E0021 push r11
0x00000000006E0023 push r12
0x00000000006E0025 push r13
0x00000000006E0027 push r14
0x00000000006E0029 push r15
0x00000000006E002B pushfq
0x00000000006E002C lea rcx, ss:[rsp+0x80]
0x00000000006E0034 call 0x00000000006E07A1
0x00000000006E07A1 mov qword ptr ss:[rsp-0x8], rdi
//...
//可以看到执行完ret跳转到了API地址
//此处是kernel32.GetSystemDirectoryA
0x00000000006E0B1A add rsp, 0x8
0x00000000006E0B1E ret
0x00007FFF4AF3D3E0 jmp 0x00007FFF4AF3D3EC
0x00007FFF4AF3D3EC mov qword ptr ss:[rsp+0x8], rbx
0x00007FFF4AF3D3F1 mov qword ptr ss:[rsp+0x10], rsi
 
//此处是kernel32.CreateFileA
0x0000000146681217 call qword ptr ds:[0x000000014691D26E]
0x00000000006F0000 jmp 0x00000000006F000A
//...
0x00000000006F0B1A add rsp, 0x8
0x00000000006F0B1E ret
0x00007FFF4AF44B50 jmp qword ptr ds:[0x00007FFF4AFA1158]
0x00007FFF4A4E97F0 mov qword ptr ss:[rsp+0x8], rbx
 
//此处是kernel32.SetFilePointer
//下断点观察参数可以看到定位到了ntdll.dll文件的NtQueryInformationProcess地址处
0x0000000146681543 call qword ptr ds:[0x000000014691D276]
0x0000000000700000 jmp 0x000000000070000A
//...
0x0000000000700B1A add rsp, 0x8
0x0000000000700B1E ret
0x00007FFF4AF44F70 jmp qword ptr ds:[0x00007FFF4AFA11E0]
0x00007FFF4A510340 push rbx
 
//此处是kernel32.ReadFile
0x00000001466815F9 call qword ptr ds:[0x000000014691D27E]
0x0000000000710000 jmp 0x000000000071000A
//...
0x0000000000710B1A add rsp, 0x8
0x0000000000710B1E ret
0x00007FFF4AF44EE0 jmp qword ptr ds:[0x00007FFF4AFA1208]
0x00007FFF4A4EAA60 mov qword ptr ss:[rsp+0x10], rbx
 
//此处是kernel32.CloseHandle
0x00000001466815FF mov rcx, qword ptr ss:[rsp+0x38]
0x0000000146681604 call qword ptr ds:[0x000000014691D286]
0x0000000000720000 jmp 0x000000000072000A
//...
0x0000000000720B1A add rsp, 0x8
0x0000000000720B1E ret
0x00007FFF4AF448E0 jmp qword ptr ds:[0x00007FFF4AFA1398]
0x00007FFF4A4E9F70 push rbx

面对trace出来的混淆代码, 观察call指令后面跟着的指令, 如果是pop xxx或者add rsp,8, 说明这个call并不是真正的函数调用.(图片箭头所指的都是假call指令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//壳代码段CRC校验
0x0000000146680F4E call 0x0000000146681815
 
0x00000001466818F3 sub rsp, 0x28
0x00000001466818F7 call 0x00000001466818FC
0x00000001466818FC pop rcx
0x00000001466818FD add rcx, 0xFFFFFFFFFFFFE704
0x0000000146681904 jmp 0x00000001466819B0
0x00000001466819B0 mov rdx, 0x82E6
0x00000001466819B7 jmp 0x00000001466819BA
//rcx=校验起始地址, rdx=0x82E6校验长度
0x00000001466819BA call 0x0000000146685A4F
//加密 CRC value, 与正确的校验值相减为0就是验证通过
0x00000001466819D5 call 0x000000014668632B
0x00000001466819DA sub rax, qword ptr ds:[0x0000000146688307]
//...
//验证不通过会修改某数据
0x0000000146681938 test rax, rax
0x000000014668193B cmovne r10, rbx
0x000000014668198A mov qword ptr ds:[0x00000001466882FF], r10

把0x000000014668193B NOP掉, 即可过掉代码CRC校验.
继续trace.

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
//跟踪到反调试触发了之后的代码处
//这里会清空栈内存, 并且把所有寄存器值置为0xDEADC0DE
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CE4 mov byte ptr ds:[rcx], 0xCC
0x0000000146682CE7 lea rcx, ds:[rcx+0x1]
0x0000000146682CEB lea rbx, ds:[rbx-0x1]
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CE4 mov byte ptr ds:[rcx], 0xCC
0x0000000146682CE7 lea rcx, ds:[rcx+0x1]
0x0000000146682CEB lea rbx, ds:[rbx-0x1]
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CE4 mov byte ptr ds:[rcx], 0xCC
0x0000000146682CE7 lea rcx, ds:[rcx+0x1]
0x0000000146682CEB lea rbx, ds:[rbx-0x1]
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CE4 mov byte ptr ds:[rcx], 0xCC
0x0000000146682CE7 lea rcx, ds:[rcx+0x1]
0x0000000146682CEB lea rbx, ds:[rbx-0x1]
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CE4 mov byte ptr ds:[rcx], 0xCC
0x0000000146682CE7 lea rcx, ds:[rcx+0x1]
0x0000000146682CEB lea rbx, ds:[rbx-0x1]
0x0000000146682CEF test rbx, rbx
0x0000000146682CF2 jne 0x0000000146682CE4
0x0000000146682CF4 lea rbx, ds:[rax]
0x0000000146682CF7 lea rcx, ds:[rax]
0x0000000146682CFA lea rsp, ds:[rax]
0x0000000146682CFD jmp rax
0x00000000DEADC0DE ???

在0x146682CE4处下断点, 可以在栈中看到返回地址为0x146680064.
0x000000014668005F call 0x000000014668284F应该就是造成异常的地方.
trace日志中可以看到有地方跳过了

1
2
3
4
0x0000000146680051 sub rsp, 0x28
0x0000000146680055 call 0x0000000146681A08
0x000000014668005A test rax, rax
0x000000014668005D je 0x0000000146680064

不难猜测到call 0x0000000146681A08是检测调试器, 返回值为0为通过.
继续观察0x0000000146681A08里的内容, 可以找到syscall调用, 而且是调用完就把代码抹掉了.

1
2
3
4
5
6
7
8
9
10
0x0000000146681B02 call 0x0000000146681B07
0x0000000146681B07 pop r15
0x0000000146681B09 add r15, 0xF
0x0000000146681B0D xor byte ptr ds:[r15], 0xF
0x0000000146681B11 xor byte ptr ds:[r15+0x1], 0x5
0x0000000146681B16 syscall
0x0000000146681B18 mov word ptr ds:[r15], 0x0
0x0000000146681B1E add rsp, 0x8
0x0000000146681B22 not r13
0x0000000146681B25 mov rbx, 0xC0000353

下段看syscall调用时候的ID和参数, 可以发现是模拟N
tQueryInformationProcess函数检测调试器.
syscall功能查询

 

修改syscall的返回值rax=0xC0000353, 参数[rsp+0x30]指针内存为0, 参数[rsp+0x28]指针内存为8, 即可过掉此反调试.

 

内存中搜索"4180??0105", 还能发现另一处syscall调用, 一样的处理方法.

寻找OEP

处理完壳的反调试和CRC后, 从壳代码再次检测CRC的地方开始再trace一次.
可以看到跨段大条的代码.

1
2
3
4
5
6
7
8
9
0x000000014668754B mov rax, qword ptr ss:[rsp]
0x000000014668754F lea rsp, ss:[rsp+0x8]
0x0000000146687554 mov rsp, qword ptr ss:[rsp]
0x0000000146687558 jmp rax
0x0000000140001088 sub rsp, 0x28
0x000000014000108C mov rcx, 0x140001000
0x0000000140001096 mov rdx, 0x27BA61
0x000000014000109D mov qword ptr ss:[rsp-0x8], r15
0x00000001400010A2 sub rsp, 0x8

OEP:0x0000000140001088

 

重新运行程序, 在OEP处下硬件执行断点, 断下后dump下来.

修复导入表

IAT在0x14027D008.
导入表不大, 只有六个函数.
写shellcode分别调用一遍, 在堆地址偏移0xB1E的地址下断点, 就可以得到API.
使用ImportREC(或者其他工具), 修复重建上面dump下来程序的导入表, 即可完成脱壳.

0x02 从"蜗牛壳"中提取算法

准备工作

面对如此庞大的代码混淆变异, 如果想像做第五题和第六题那样trace硬怼, 肯定是行不通的.

面对强大的敌人, 正确的做法应该是找到其弱点, 有针对攻击.

 

代码混淆变异, 也是有弱点的:
1.有些不好混淆的代码不会处理(比如此题的修改栈)
2.不会破坏混淆前代码的运行环境(运行到同一个地方的时候, 栈与寄存器应该和混淆前的完全一样)

 

蜗牛壳中每次修改栈, 会修改fs段寄存器的值.

FS寄存器指向当前活动线程的TEB结构(线程结构)
偏移 说明
000 指向SEH链指针
004 线程堆栈顶部
008 线程堆栈底部
00C SubSystemTib
010 FiberData
014 ArbitraryUserPointer
018 FS段寄存器在内存中的镜像地址
020 进程PID
024 线程ID
02C 指向线程局部存储指针
030 PEB结构地址(进程结构)
034 上个错误号

 

32位中的fs:[8]对应64位中的gs:[10h].

 

我们的目标是提取蜗牛壳中的"肉代码", 还需要知道"蜗牛壳"的一个重大弱点:
每次执行"肉代码"时, 必须保证环境和前一次一样.

1
2
3
4
5
6
7
8
9
10
//蜗牛壳执行肉代码
 
//恢复肉代码执行环境
pop regs
 
//肉代码
//...
 
//保存肉代码执行环境
//push regs

比较容易发现利用的特征是每层修改栈时候对gs:[10]写入值, 这个在之前比赛的wp中也有选手提到过.
然后利用esp定律, 对栈下访问断点(因为每次执行"肉代码"都要先恢复寄存器环境,栈地址将对偏移都是固定的), 直接运行到"肉代码"处, 然后一直单步跟完这层的肉代码.

 

x64dbg脚本代码:

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
dbh
run 140001000
mov $dt_count,0
bphc
bph gs:[30]+10,w,1
 
LB_STARTFIND:
run
find cip,488BC4482BC3,6
cmp $RESULT,0
je LB_CONTINUE
 
mov $corecode,rsp+rdi-rbx+70
bph $corecode,r,1
run
 
find cip-9,6548893C2510000000,9
cmp $RESULT,0
je LB_RUNSECOND
run
LB_RUNSECOND:
bphc $corecode
 
find cip,F348A5,3
cmp $RESULT,0
jne LB_CONTINUE
 
inc $dt_count
log "第{$dt_count}次出现肉代码"
 
LB_STISTISTI:
sti
log "0x{p:cip} {i:cip}"
cmp rsp,$corecode-70
jne LB_STISTISTI
 
LB_CONTINUE:
jmp LB_STARTFIND
 
log "end."

脚本log出来的数据需要自己提取出有用的"肉代码". 可以人肉搞定, 或者找到规律后写脚本一通字符串操作也能搞定.
这里给出前几层肉代码的分析, 后面层的同理.






 


肉代码中的r13寄存器指向的是算法使用到的数据区, 如果肉代码中使用到了r13寄存器, 肉代码前会执行call $+5/pop r13/add r13,xxx来定位数据区指针给r13寄存器.

 

整理出来的肉代码大概是这样.

1
2
3
4
5
6
7
8
9
10
11
sub rsp, QWORD PTR [r13+0x6179]
mov QWORD PTR [rsp+16], rbp
mov QWORD PTR [rsp+24], rsi
mov QWORD PTR [rsp+32], rdi
push  r12
push  r14
push  r15
movzx r12d, BYTE PTR [r13+0x25]
mov rdx, 0x100
//...
//735

到这里, 肉代码已全部提取出, 剩下的就是写KeyGen的活了.

0x03 总结

对抗如此庞大体积的代码混淆, 想要在短时间内获取胜利, 最好不要选择"正面刚", 而是要寻找其中软肋进行发力.


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-5-22 13:38 被KuCha128编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回