第十课——x64平台脱壳与破解实战

由于吾爱虚拟机是32位的,运行不了64位程序,所以这节课用物理机来操作。

1. 课程例子

拿到程序,先运行一下熟悉流程,再查壳,发现有MPRESS壳(压缩壳)。压缩壳在壳段开始的时候做的第一件事往往是pushad(保存寄存器),在壳段结束的时候做的最后一件事是popad(还原寄存器),所以我们可以用ESP定律脱压缩壳。但在64位的程序下,一般不会这么做。

将程序载入x64dbg,F9运行,去到标准的MPRESS的入口地址

1
2
3
4
5
6
000000014001B0C0 | 57                       | push rdi                                |
000000014001B0C1 | 56 | push rsi |
000000014001B0C2 | 53 | push rbx |
000000014001B0C3 | 51 | push rcx |
000000014001B0C4 | 52 | push rdx | rdx:EntryPoint
000000014001B0C5 | 41:50 | push r8 |

既然有push,那肯定也有pop,找到下面这几句指令,也就找到出口了。

1
2
3
4
5
6
pop r8
pop rdx
pop rcx
pop rbx
pop rsi
pop rdi

但现在找不到,因为MPRESS把壳段跳到OEP的那部分代码给压缩了,要等它解压到那部分代码才能找到。

暂时先不管,单步跟踪试一下,到达一个跨区段的跳转14001BBA7。下面是空代码,上面有pop指令(但不是pop上面那几句),很像UPX。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
000000014001BB9A | 41:5F                    | pop r15                                 |
000000014001BB9C | 41:5E | pop r14 |
000000014001BB9E | 41:5D | pop r13 |
000000014001BBA0 | 41:5C | pop r12 |
000000014001BBA2 | 5F | pop rdi |
000000014001BBA3 | 5E | pop rsi |
000000014001BBA4 | 5D | pop rbp |
000000014001BBA5 | 5B | pop rbx |
000000014001BBA6 | C3 | ret |
000000014001BBA7 | E9 1F00FFFF | jmp demo.14000BBCB |
000000014001BBAC | 54 | push rsp |
000000014001BBAD | 54 | push rsp |
000000014001BBAE | FE | ??? |
000000014001BBAF | FF | ??? |
000000014001BBB0 | FF | ??? |
000000014001BBB1 | FF | ??? |
000000014001BBB2 | FF | ??? |
000000014001BBB3 | FF00 | inc dword ptr ds:[rax] |
000000014001BBB5 | 0000 | add byte ptr ds:[rax],al |
000000014001BBB7 | 004D 73 | add byte ptr ss:[rbp+73],cl |
000000014001BBBA | 0000 | add byte ptr ds:[rax],al |
000000014001BBBC | 0000 | add byte ptr ds:[rax],al |
1
41 5F 41 5E 41 5D 41 5C 5F 5E 5D 5B C3

执行完jmp后,MPRESS完成了第一次解压。

1
2
3
4
5
000000014000BBCB | 48:83EC 28               | sub rsp,28                              |
000000014000BBCF | 48:0300 | add rax,qword ptr ds:[rax] |
000000014000BBD2 | 0AC0 | or al,al |
000000014000BBD4 | 0F85 85000000 | jne demo.14000BC5F |
000000014000BBDA | 48:2D 00100000 | sub rax,1000 |

继续单步,到下面这一步跑飞,重载,再次运行到这里时F7进去这个call指令。

1
000000014000BBFB | E8 09000000              | call demo.14000BC09                     |;进去

再次跑飞,再进去单步

1
000000014000BC14 | E8 0F000000              | call demo.14000BC28                     |;进去

运行到此处,这几条pop与开头几条push对应,并且jmp是个大跳转。

1
2
3
4
5
6
7
000000014000BCD5 | 41:58                    | pop r8                                  |
000000014000BCD7 | 5A | pop rdx |
000000014000BCD8 | 59 | pop rcx |
000000014000BCD9 | 5B | pop rbx |
000000014000BCDA | 5E | pop rsi |
000000014000BCDB | 5F | pop rdi |
000000014000BCDC | E9 4F54FFFF | jmp demo.140001130 |
1
41 58 5A 59 5B 5E 5F

执行这个跳转,这里就是64位的VS2013编译出来的OEP。

1
2
3
4
5
6
0000000140001130 | 48:83EC 28               | sub rsp,28                              | OEP
0000000140001134 | E8 7B180000 | call demo.1400029B4 |
0000000140001139 | 48:83C4 28 | add rsp,28 |
000000014000113D | E9 02000000 | jmp demo.140001144 |
0000000140001142 | CC | int3 |
0000000140001143 | CC | int3 |

用Scylla将程序dump再fix dump,完成脱壳。

用脚本找OEP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
erun
find rip,"415F415E415D415C5F5E5D5BC3" //搜索二进制
mov first_jmp,$result //first_jmp=41(first_jmp指向pop r15那一行)
add first_jmp,D //first_jmp=first_jmp+D=E9(加上0xD个偏移到达jmp)
bp first_jmp //在jmp下断
erun //运行
bc //取消断点
sti //F8
find rip,"41585A595B5E5F"
mov second_jmp,$result
add second_jmp,7
bp second_jmp
erun
bc
sti //OEP
ret

在脚本窗口中,右键->载入脚本,按空格直接运行脚本。

很简单的一个破解,搜索字符串改个跳转即可,右键->补丁->修补文件,后缀名自己加上。

如果是追码也很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
000000014000104C | FF15 CEB10000            | call qword ptr ds:[<&GetDlgItemTextA>]  |
0000000140001052 | 48:8D15 C7470100 | lea rdx,qword ptr ds:[140015820] |
0000000140001059 | 48:8D0D 88020100 | lea rcx,qword ptr ds:[1400112E8] | 00000001400112E8:"Fuck L4Nce"
0000000140001060 | E8 9B040000 | call demo_dump_scy.140001500 |
0000000140001065 | 48:8BCB | mov rcx,rbx |
0000000140001068 | 85C0 | test eax,eax |
000000014000106A | 75 13 | jne demo_dump_scy.14000107F |
000000014000106C | 45:33C9 | xor r9d,r9d |
000000014000106F | 4C:8D05 7E020100 | lea r8,qword ptr ds:[1400112F4] | r8:&"吚x\n€|$@", 00000001400112F4:"Boom!"
0000000140001076 | 48:8D15 83020100 | lea rdx,qword ptr ds:[140011300] | 0000000140011300:"Congratulations! You have successfully fucked L4Nce"
000000014000107D | EB 14 | jmp demo_dump_scy.140001093 |
000000014000107F | 41:B9 10000000 | mov r9d,10 |
0000000140001085 | 4C:8D05 AC020100 | lea r8,qword ptr ds:[140011338] | r8:&"吚x\n€|$@", 0000000140011338:"Boomshakalaka"
000000014000108C | 48:8D15 B5020100 | lea rdx,qword ptr ds:[140011348] | 0000000140011348:"You Failed!"

GetDlgItemTextA函数下断,这个是获取我们输入的字符串。两次F9运行至主程序,输入假码提交,停在断点处。接下来单步

1
2
3
4
5
6
7
8
9
10
000000014000104C | FF15 CEB10000            | call qword ptr ds:[<&GetDlgItemTextA>]  |
0000000140001052 | 48:8D15 C7470100 | lea rdx,qword ptr ds:[140015820] | rdx:"hhhhhhh", 0000000140015820:"hhhhhhh"
0000000140001059 | 48:8D0D 88020100 | lea rcx,qword ptr ds:[1400112E8] | rcx:"Fuck L4Nce", 00000001400112E8:"Fuck L4Nce"
0000000140001060 | E8 9B040000 | call demo_dump_scy.140001500 |
0000000140001065 | 48:8BCB | mov rcx,rbx | rcx:"Fuck L4Nce"
0000000140001068 | 85C0 | test eax,eax |
000000014000106A | 75 13 | jne demo_dump_scy.14000107F |
000000014000106C | 45:33C9 | xor r9d,r9d |
000000014000106F | 4C:8D05 7E020100 | lea r8,qword ptr ds:[1400112F4] | 00000001400112F4:"Boom!"
0000000140001076 | 48:8D15 83020100 | lea rdx,qword ptr ds:[140011300] | rdx:"hhhhhhh", 0000000140011300:"Congratulations! You have successfully fucked L4Nce"

很明显就是我们输入的字符串与Fuck L4Nce比较。

如果想在64位下做补丁工具,推荐用IDA。将脱完壳的程序载入IDA,在Options->General勾选地址前缀和填上要显示的字节个数。

在汇编窗口找到成功与失败的分岔路。

选中jnz指令,在界面上Edit->Patch program->Assemble,将jnz改为nop,由于jnz有两个字节,所以要连续修改两个nop。或者Edit->Patch program->Change word,写入0x9090。

现在可以看到,上面那块与左边那块合并了,而右边那块被独立出来,没有箭头指向右边那块了。

然后Edit->Patch program->Apply patches to input file,输出窗口提示应用成功。

课后作业几乎与课程例子一模一样,所以不记笔记了。