-
-
[原创]2021 KCTF 春季赛 第七题 千里寻根 解题方法
-
发表于: 2021-5-21 22:10 8429
-
KCTF2021春季赛第七题《千里寻根》题目是道windows64位逆向题, 这题是"KCTF2019Q3第十题《传家之宝》"和"KCTF2019Q4第六题《三道八佛》"的升级版本.
这题的防御重点只有一个——壳.
题目由三部分组成:
1.PE保护壳(反调试, CRC, 导入表混淆)
2.蜗牛壳(SMC)
3.验证算法
题目蜗牛壳框架甚至代码流程都和前两题基本完全一样(如果去回顾之前题目框架, 寻找可利用的漏洞, 可以省去很多对抗此题混淆的时间).
题目算法部分是《三道八佛》里的算法(只修改了数字)加上稍作修改的base64(修改了编码表和编码分组逻辑). 算法部分没有难度, 哪怕不能逆算法, 参考大佬们编写的wp可以直接抄下逆算法.
KCTF 2019 Q4 第六题 一个支点 by HHHso
2019看雪CTF总决赛第六题:三道八佛WP by poyoten
也就是说, 只要能从题目中提取出算法实现代码, 就不难写出KeyGen.
此贴将详细介绍"PE保护壳"分析脱壳, 以及从"蜗牛壳"中提取算法的一种方法实现.
直接x64dbg载入程序运行, 程序会进程结束(ExitProcess)或者出异常无法继续运行(DEADC0DE), 发现有反调试.
使用x64dbg的trace into功能, 跟踪到异常位置.
分析trace过的逻辑.
面对trace出来的混淆代码, 观察call指令后面跟着的指令, 如果是pop xxx或者add rsp,8, 说明这个call并不是真正的函数调用.(图片箭头所指的都是假call指令)
把0x000000014668193B NOP掉, 即可过掉代码CRC校验.
继续trace.
在0x146682CE4处下断点, 可以在栈中看到返回地址为0x146680064.
0x000000014668005F call 0x000000014668284F应该就是造成异常的地方.
trace日志中可以看到有地方跳过了
不难猜测到call 0x0000000146681A08是检测调试器, 返回值为0为通过.
继续观察0x0000000146681A08里的内容, 可以找到syscall调用, 而且是调用完就把代码抹掉了.
下段看syscall调用时候的ID和参数, 可以发现是模拟N
tQueryInformationProcess函数检测调试器.
syscall功能查询
修改syscall的返回值rax=0xC0000353, 参数[rsp+0x30]指针内存为0, 参数[rsp+0x28]指针内存为8, 即可过掉此反调试.
内存中搜索"4180??0105", 还能发现另一处syscall调用, 一样的处理方法.
处理完壳的反调试和CRC后, 从壳代码再次检测CRC的地方开始再trace一次.
可以看到跨段大条的代码.
OEP:0x0000000140001088
重新运行程序, 在OEP处下硬件执行断点, 断下后dump下来.
IAT在0x14027D008.
导入表不大, 只有六个函数.
写shellcode分别调用一遍, 在堆地址偏移0xB1E的地址下断点, 就可以得到API.
使用ImportREC(或者其他工具), 修复重建上面dump下来程序的导入表, 即可完成脱壳.
面对如此庞大的代码混淆变异, 如果想像做第五题和第六题那样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].
我们的目标是提取蜗牛壳中的"肉代码", 还需要知道"蜗牛壳"的一个重大弱点:
每次执行"肉代码"时, 必须保证环境和前一次一样.
比较容易发现利用的特征是每层修改栈时候对gs:[10]写入值, 这个在之前比赛的wp中也有选手提到过.
然后利用esp定律, 对栈下访问断点(因为每次执行"肉代码"都要先恢复寄存器环境,栈地址将对偏移都是固定的), 直接运行到"肉代码"处, 然后一直单步跟完这层的肉代码.
x64dbg脚本代码:
脚本log出来的数据需要自己提取出有用的"肉代码". 可以人肉搞定, 或者找到规律后写脚本一通字符串操作也能搞定.
这里给出前几层肉代码的分析, 后面层的同理.
肉代码中的r13寄存器指向的是算法使用到的数据区, 如果肉代码中使用到了r13寄存器, 肉代码前会执行call $+5/pop r13/add r13,xxx来定位数据区指针给r13寄存器.
整理出来的肉代码大概是这样.
到这里, 肉代码已全部提取出, 剩下的就是写KeyGen的活了.
对抗如此庞大体积的代码混淆, 想要在短时间内获取胜利, 最好不要选择"正面刚", 而是要寻找其中软肋进行发力.
/
/
初始化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
/
/
... ...
/
/
初始化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
/
/
... ...
/
/
GetProAddressByHash
/
/
此处为Kernel32.VirtualAlloc(
hash
:
0x09CE0D4A
)
0x00000001466801EE
mov rcx, qword ptr ds:[
0x000000014691D22E
]
0x00000001466801F5
call
0x000000014668219A
/
/
GetProAddressByHash
/
/
此处为Kernel32.VirtualAlloc(
hash
:
0x09CE0D4A
)
0x00000001466801EE
mov rcx, qword ptr ds:[
0x000000014691D22E
]
0x00000001466801F5
call
0x000000014668219A
/
/
初始化CRC32 table,
0xEDB88320
0x0000000146685F45
mov rsi,
0xEDB88320
/
/
... ...
/
/
初始化CRC32 table,
0xEDB88320
0x0000000146685F45
mov rsi,
0xEDB88320
/
/
... ...
/
/
GetProAddressByHash
/
/
此处为ntdll.NtQueryInformationProcess(
hash
:
0x5E7088ED
)
0x0000000146680EC7
call
0x000000014668219A
/
/
GetProAddressByHash
/
/
此处为ntdll.NtQueryInformationProcess(
hash
:
0x5E7088ED
)
0x0000000146680EC7
call
0x000000014668219A
/
/
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
/
/
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
/
/
壳代码段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
/
/
壳代码段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
就是验证通过
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!