首页
社区
课程
招聘
[原创]关于Q4第6题多重壳的几点探讨
2019-12-20 13:59 5582

[原创]关于Q4第6题多重壳的几点探讨

2019-12-20 13:59
5582

前言一

最开始拿到这一题,就在OD里写脚本自动跑,最初思路是,这种复制到随机地址的类型,不管怎么随机,SN总要复制过去吧?每次复制到新地址,在新地址里搜索特征SN,在SN处下硬件读中断,根据EDI值就能知道目标地址了,是不是很完美?然后脚本开始跑了,很快就杯具了:先是发现硬件中断并不是很及时;然后又发现中断时SN有可能只复制了一部分,目标地址再下断的位置并不好确定;经过修修补补,脚本终于跑起来了,然后最大的杯具来了:脚本跑了几十秒之后,OD卡死了,动一下鼠标都要好几秒才有回应,猜测是OD强大的自动解析功能由于工作量太大,罢工了。这时候我的想法只有一个:还有这种操作?加大OD的工作量让OD卡死,这种ANTI方式还是第一次听说。。。

前言二

朋友告诉我,OD已经过时了,现在是x64dbg的天下。我最近这几年懒得动,居然都落伍到如此地步了,连工具都OUT了。。。悲衰啊。。。尽管此时这一题已经结束了,还是用这一题来学习一下x64dbg的用法吧。。。
网上竟然找不到关于x64dbg的教学,帮助文件简略得令人发指,只能靠自己了。经过一番研究,大体使用上基本差不多,先总结一下x64dbg与OD的区别:
1.x64dbg是基于命令行的,很多功能是自带的,比如脚本,命令行之类的,这种模式比OD用插件来实现脚本比起来强大多了,所有功能都可以在脚本中直接使用。
2.x64dbg的解析功能比较弱,使用插件可以大大增强API解析,远远超出了OD仅解析部分系统DLL的范围。
再说说x64dbg的缺点:
1.学习资源太少,帮助文件太简单,提高了入门门槛。
2.查看变量值太麻烦了,试了半天才发现输入varlist然后在引用里能看到,而且还不是实时更新的,下次再看还要重新输一次varlist开个新窗口看,这设计太逆天了,哪怕给个刷新按钮也好啊?(或许有方便的功能我没有发现?请各位指教)
3.内存和反汇编不支持按字节移位,或者我没试出来?
4.字符串引用的解析太差了,更别提中文支持了。
5.单步或运行命令偶尔会被忽略,需要再按一下才会执行。
6.异常处理偶尔会出问题,卡在原地忽略不过去,重新载入就恢复正常了。
不过瑕不掩玉,x64dbg作为开源软件仍在持续提高中,更何况对64位的支持了。

正文

终于开始进入正题了,谈谈用脚本对抗这一题的方法。
程序流程很简单,检测输入之后,生成新地址-复制过去-销毁旧代码-解密执行,然后重复这一过程。观察复制代码的过程,仅仅是把当前代码之后的数据复制过去,也就是说这些代码与位置无关。那么我们可以设想一下,这些代码能不能在原位置解密?再进一步,我们能不能跳过“生成新地址-复制过去-销毁旧代码”这些步骤,直接解密执行?
观察一下第一段解密代码,没有任何混淆:
00401628 | 68 83C404B8              | push B804C483                           |
0040162D | 68 44100050              | push 50001044                           |
00401632 | 68 C3C3C3C3              | push C3C3C3C3                           |
00401637 | 57                       | push edi                                |
00401638 | E8 C8FFFFFF              | call <crackme_dec.sub_401605>           |销毁旧代码
0040163D | E8 00000000              | call crackme_dec.401642                 | call $0 获取当前地址
00401642 | 5E                       | pop esi                                 |
00401643 | 81EE 42164000            | sub esi,crackme_dec.401642              |
00401649 | 81C6 AC164000            | add esi,crackme_dec.4016AC              |计算出解密地址入口
0040164F | B9 EFDD0F00              | mov ecx,FDDEF                           |长度
00401654 | FC                       | cld                                     |
00401655 | AC                       | lodsb                                   |
00401656 | 0FB6C0                   | movzx eax,al                            |
00401659 | 32C1                     | xor al,cl                               |
0040165B | 2C 01                    | sub al,1                                |
0040165D | 32C4                     | xor al,ah                               |
0040165F | 32C1                     | xor al,cl                               |
00401661 | 02C4                     | add al,ah                               |
00401663 | C0C8 53                  | ror al,53                               |
00401666 | 8846 FF                  | mov byte ptr ds:[esi-1],al              |解密放回原位置
00401669 | 49                       | dec ecx                                 |
0040166A | 85C9                     | test ecx,ecx                            |
0040166C | 75 E7                    | jne crackme_dec.401655                  |

而此后的解密代码几乎与这一段完全相同,仅仅是插入了一些跳转指令。这段代码的入口和出口都很清晰,完全可以用脚本跳过来执行,直接在原地解密。于是,脱壳脚本的第一版出来了:
mov addr,401600
findcontinue:
find addr,"E8000000005E81EE",200      //call $0; pop esi; sub esi, XXX; add esi, XXX  入口标志
cmp $RESULT,0
jne findsuccess
log "err addr:{x:addr} not found"     //没找到:停下来
pause
jmp findcontinue
findsuccess:
mov codeaddr, $RESULT
mov nextaddr, [codeaddr+0E], 4
cmp nextaddr, 420000               //入口代码与复制到新地址的代码相同,区别在于add esi,XXX会比较大,在这里判断一下
jb finddecode
add addr,12
jmp findcontinue
finddecode:
add nextaddr,addr
sub nextaddr,[codeaddr+08]     //再判断一下解密代码的位置是否位于当前代码之后
cmp nextaddr,addr
ja finddecode1
pause
jmp findcontinue
finddecode1:
find codeaddr,"8846FF",80             //mov [esi-1],al  解密标志
mov loopaddr,$RESULT
cmp loopaddr-codeaddr,80
jb finddecode2
add addr,12
jmp findcontinue
finddecode2:
find loopaddr,"75",80                 //jne XXX  出口标志
mov loopaddr,$RESULT
cmp loopaddr-codeaddr,80
jb finddecode3
add addr,12
jmp findcontinue
finddecode3:
mov cip,codeaddr               //跳到入口
add loopaddr,2
mov addr,loopaddr
bp addr                           //出口位置下断
log "addr:{x:codeaddr} decode:{x:nextaddr}"
run                           //执行解密代码
bc addr
jmp findcontinue

执行结果很完美,除了那个提示脚本运行时间过长的提示窗口。大约解密到460000时停下来了,过去看看成果,发现并没有解密完,只是解密代码发生了小变化,出口循环变成了长跳转,修改一下脚本,做个兼容:
mov addr,401600
findcontinue:
find addr,"E8000000005E81EE????????81C6",300      //call $0; pop esi; sub esi, XXX; add esi, XXX
cmp $RESULT,0
jne findsuccess
log "err addr:{x:addr} not found"
pause
jmp findcontinue
findsuccess:
mov codeaddr, $RESULT
mov nextaddr, [codeaddr+0E], 4
cmp nextaddr, 405000
jb finddecode
mov addr,codeaddr
add addr,12
jmp findcontinue
finddecode:
add nextaddr,addr
sub nextaddr,[codeaddr+08]
cmp nextaddr,addr
ja finddecode1
pause
jmp findcontinue
finddecode1:
find codeaddr,"8846FF",100                  //mov [esi-1],al
mov loopaddr,$RESULT
cmp loopaddr,0
ja finddecode2
add addr,12
jmp findcontinue
finddecode2:
find loopaddr,"0F85??FFFFFF",20          //jne XXX
mov bpaddr,$RESULT
add bpaddr,6
cmp $RESULT,0
ja finddecode3
find loopaddr,"75",20                           //jne XXX
mov bpaddr,$RESULT
add bpaddr,2
cmp $RESULT,0
ja finddecode3
add addr,12
jmp findcontinue
finddecode3:
mov cip,codeaddr
mov addr,bpaddr
bp addr
log "addr:{x:codeaddr}:{x:addr} decode:{x:nextaddr}"
run
bc addr
jmp findcontinue

这次解密时间稍长一点,最后停下来的位置是4FB939,往下看看,已经没有加密代码了,而且直接找到了最终判断结果的位置:

004FBA40 | 75 56                    | jne crackme_dec.4FBA98                  |
004FBA42 | E8 00000000              | call crackme_dec.4FBA47                 | call $0
004FBA47 | 58                       | pop eax                                 |
004FBA48 | 2D 22114000              | sub eax,crackme_dec.401122              |
004FBA4D | 05 474B4000              | add eax,<crackme_dec.sub_404B47>        |
004FBA52 | 50                       | push eax                                | const char* format
004FBA53 | FF15 00114000            | call dword ptr ds:[<&printf>]           | printf
004FBA59 | 83C4 04                  | add esp,4                               |
004FBA5C | FF15 04114000            | call dword ptr ds:[<&getchar>]          |
004FBA62 | FF15 04114000            | call dword ptr ds:[<&getchar>]          |
004FBA68 | E8 00000000              | call crackme_dec.4FBA6D                 | call $0
004FBA6D | 58                       | pop eax                                 |
004FBA6E | 2D 48114000              | sub eax,<crackme_dec.sub_401148>        |
004FBA73 | 05 4FAD4000              | add eax,<crackme_dec.sub_40AD4F>        |
004FBA78 | 8B28                     | mov ebp,dword ptr ds:[eax]              |
004FBA7A | E8 00000000              | call <crackme_dec.sub_4FBA7F>           | call $0
004FBA7F | 58                       | pop eax                                 |
004FBA80 | 2D 5A114000              | sub eax,crackme_dec.40115A              |
004FBA85 | 05 4BAD4000              | add eax,crackme_dec.40AD4B              |
004FBA8A | 8B20                     | mov esp,dword ptr ds:[eax]              | UINT uExitCode
004FBA8C | 6A 00                    | push 0                                  |
004FBA8E | FF15 50114000            | call dword ptr ds:[<&ExitProcess>]      | ExitProcess
004FBA94 | 83C4 04                  | add esp,4                               |
004FBA97 | C3                       | ret                                     |
004FBA98 | E8 00000000              | call crackme_dec.4FBA9D                 | call $0
004FBA9D | 58                       | pop eax                                 |
004FBA9E | 2D 78114000              | sub eax,crackme_dec.401178              |
004FBAA3 | 05 1C4B4000              | add eax,<crackme_dec.sub_404B1C>        |
004FBAA8 | 50                       | push eax                                | const char* format
004FBAA9 | FF15 00114000            | call dword ptr ds:[<&printf>]           | printf
004FBAAF | 83C4 04                  | add esp,4                               |
004FBAB2 | FF15 04114000            | call dword ptr ds:[<&getchar>]          |
004FBAB8 | FF15 04114000            | call dword ptr ds:[<&getchar>]          |
004FBABE | E8 00000000              | call crackme_dec.4FBAC3                 | call $0
004FBAC3 | 58                       | pop eax                                 |
004FBAC4 | 2D 9E114000              | sub eax,crackme_dec.40119E              |
004FBAC9 | 05 4FAD4000              | add eax,<crackme_dec.sub_40AD4F>        |
004FBACE | 8B28                     | mov ebp,dword ptr ds:[eax]              |
004FBAD0 | E8 00000000              | call <crackme_dec.sub_4FBAD5>           | call $0
004FBAD5 | 58                       | pop eax                                 |
004FBAD6 | 2D B0114000              | sub eax,crackme_dec.4011B0              |
004FBADB | 05 4BAD4000              | add eax,crackme_dec.40AD4B              |
004FBAE0 | 8B20                     | mov esp,dword ptr ds:[eax]              | UINT uExitCode
004FBAE2 | 6A 00                    | push 0                                  |
004FBAE4 | FF15 50114000            | call dword ptr ds:[<&ExitProcess>]      | ExitProcess
004FBAEA | 83C4 04                  | add esp,4                               |
004FBAED | C3                       | ret                                     |

其中printf字符串用的是相对地址,计算一下真实地址:4FBA47-401122+404B47=4FF46C,在内存中查看一下:(x64dbg自带计算器可以直接计算,还可以对计算结果在内存或反汇编中查看)
004FF46C  43 6F 6E 67 72 61 74 75 6C 61 74 69 6F 6E 73 2C  Congratulations,  
004FF47C  20 73 65 72 69 61 6C 20 6E 75 6D 62 65 72 20 69   serial number i  
004FF48C  73 20 63 6F 72 72 65 63 74 20 20 5E 2D 5E 1F 1F  s correct  ^-^

实际检测SN的代码位于4FBAEE处,对算法的分析不在本文之列,别人已经做的很完善了,我就不献丑了,下面对这个“多重壳”做一点分析。

1.复制到随机地址后,后续代码的入口在哪里确定?

在使用rdstc生成随机地址之前有一段代码:
004016AC | E8 00000000              | call crackme_dec.4016B1                 | call $0
004016B1 | 59                       | pop ecx                                 |
004016B2 | 81E9 48104000            | sub ecx,crackme_dec.401048              |
004016B8 | 81C1 43114000            | add ecx,crackme_dec.401143              |实际入口
004016BE | E8 00000000              | call crackme_dec.4016C3                 | call $0
004016C3 | 5F                       | pop edi                                 |
004016C4 | 81EF 5A104000            | sub edi,crackme_dec.40105A              |
004016CA | 81C7 10104000            | add edi,crackme_dec.401010              |复制起始位置
004016D0 | 51                       | push ecx                                |
004016D1 | 0F31                     | rdtsc                                   |
……
0040179A | 2BE6                     | sub esp,esi                             |修改堆栈
0040179C | 03E7                     | add esp,edi                             |
0040179E | 2BEE                     | sub ebp,esi                             |
004017A0 | 03EF                     | add ebp,edi                             |
004017A2 | 896C24 0C                | mov dword ptr ss:[esp+C],ebp            |
004017A6 | 9D                       | popfd                                   |
004017A7 | 61                       | popad                                   |
004017A8 | 5F                       | pop edi                                 |
004017A9 | FFD0                     | call eax                                |跳到新地址执行

在rdstc前计算出了2个相对地址,其中第一个就是真实入口,第二个是复制的起始位置,后面再附上头部相关数据,最后用call eax跳过去执行。
再观察一下可以发现,这个真实入口实际就在call eax后面一字节或二字节,完全可以用一个jmp跳过去。

2.每一层壳都做了什么?

由于我们已经把每一层壳都解码到原位置了,每一层的代码只有大约0x200字节,对比一下可以发现,是真的什么都没有做。当然我只是随便看了看,还需要验证一下。怎么验证呢?把上面完全解密的数据DUMP出来,直接在第一层壳的代码前jmp到最终的验证过程,看验证过程是否有问题!
修改位置有2处:
第一处:SN复制到指定位置后,直接跳到最终验证过程:
004013F2 | E8 F0000000              | call <crackme_dec.sub_4014E7>           |
004013F7 | 51                       | push ecx                                |
改为:
004013F2 | E8 F0000000              | call <crackme_dec.sub_4014E7>           |
004013F7 | E9 06A70F00              | jmp crackme_dec.4FBB02                  |
第二处:验证完毕后直接跳到校验结果的位置,避开销毁代码
004FF43B | E8 DBC5FFFF              | call crackme_dec.4FBA1B                 |
改为:
004FF43B | E8 00C6FFFF              | call crackme_dec.4FBA40                 |
然后保存执行,运行无误。说明中间所有过程都是混淆代码,可以直接跳过!

3.个人改进设想

设想一:混淆解密循环的入口和出口

修改难度:++
破解难度:+++
说明:可以考虑不使用固定寄存器,入口代码改为call $+XX,sub esi/add esi改为随机使用ror/xor之类的指令,出口代码附近添加形如jz/jnz之类的容易误判的花指令
设想二:验证代码拆解到每一层壳中
修改难度:+++
破解难度:+++
说明:看程序的验证代码已经扁平化了,估计作者是有这个想法的,在保存现场、恢复现场方面实现起来太麻烦,如果实现了,仅仅把这些碎片拼成完整代码就是一个巨大的工程
设想三:解密代码不固定在头部,随机放到尾部甚至中间,隐藏后续代码的入口
修改难度:++
破解难度:++
说明:后续代码的入口紧跟在解密代码的后面太显眼了,如果隐藏得好一点,再把入口藏好点,一层一层地脱壳是件很头疼的事。
设想四:解密数据不放回原位置
修改难度:+++
破解难度:++
说明:使用压缩算法,让解密数据在原位置放不下,必须放到新申请的位置


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

最后于 2019-12-31 15:50 被kanxue编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 22979
活跃值: (3337)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
KevinsBobo 8 2019-12-20 15:58
2
0
在命令栏输入变量名,回车,就可以在下面看到变量值了
雪    币: 6051
活跃值: (1441)
能力值: ( LV15,RANK:1473 )
在线值:
发帖
回帖
粉丝
lelfei 23 2019-12-20 16:11
3
0
KevinsBobo 在命令栏输入变量名,回车,就可以在下面看到变量值了
感谢,原来是直接输入变量名,试了N次没试出来
游客
登录 | 注册 方可回帖
返回