CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令
厌倦了在 gdb 中一步步顺序执行 bomb
可执行程序。为什么不能自行控制程序的执行呢?跳到特定的函数去执行,又或者把原本要执行的指令改掉,gdb 里能做到吗?
这一篇依然不是正经的 bomblab
解题记录, 而是基于bomblab的实验,练习 gdb 命令的使用,以及基于 gdb 中人工干预的代码执行流程, 在二进制编辑器中魔改二进制文件,修改后 bomb 可执行程序,无论输入什么,都能顺利过关。
文章目录
0. 从 jump 到作弊
使用 gdb 中的 jump 命令, 可以改变可执行程序原本的执行流程,跳转到我们希望执行的代码,然后继续执行。
例如, 在 bomblab 中, 跳过所有的 explode_bomb() 函数, 或跳过所有的 phase_x() 函数。
这种“作弊“有什么好处呢?假设某个测试程序只接受注册用户使用, 未注册用户会被直接结束,在未使用合法用户名字作为输入的情况下,如果仍然想要运行程序,并且可以接受在 gdb 里运行的方式, 那么在 gdb 中可以使用 jump 命令, 跳过用户名是否合法的检查。
1. 最简例子
准备,以及问题描述
test3.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
printf("Please input your name: ");
char name[50] = {0};
scanf("%s", name);
if (strcmp(name, "admin") == 0)
{
printf("Welcome, you are valid user\n");
}
else
{
printf("Error: you are not valid user\n");
exit(1);
}
printf("Every feature is avaialble for valid user\n");
printf("Bye~\n");
return 0;
}
第一种运行结果:
zz@Legion-R7000P% ./a.out
Please input your name: admin
Welcome, you are valid user
Every feature is avaialble for valid user
Bye~
第二种运行结果:
zz@Legion-R7000P% ./a.out
Please input your name: hello
Error: you are not valid user
我们希望在 gdb 中执行程序, 并且在输入 “hello” 的情况下,看到如下的输出:
Welcome, you are valid user
Every feature is avaialble for valid user
Bye~
解法
C代码的关键是 strcmp, 对应到汇编指令是:
0x000055555555527a <+145>: call 0x5555555550d0 <strcmp@plt>
0x0000555555555283 <+154>: lea rax,[rip+0xda0] # 0x55555555602a
因此在 0x000055555555527a 处设置断点, 并跳转到 偏移地址为154 的汇编语句,继续执行。
完整的 gdb 命令:
gdb
(gdb) file a.check_output
(gdb) start
(gdb) disas main
(gdb) b *0x000055555555527a
(gdb) c
(gdb) jump *0x0000555555555283
完整的gdb命令运行记录
(gdb) start
Temporary breakpoint 1 at 0x11f1
Starting program: /home/zz/work/zcnn/csapp/bomblab/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Temporary breakpoint 1, 0x00005555555551f1 in main ()
(gdb) disassemble main
Dump of assembler code for function main:
0x00005555555551e9 <+0>: endbr64
0x00005555555551ed <+4>: push rbp
0x00005555555551ee <+5>: mov rbp,rsp
=> 0x00005555555551f1 <+8>: sub rsp,0x40
0x00005555555551f5 <+12>: mov rax,QWORD PTR fs:0x28
0x00005555555551fe <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000555555555202 <+25>: xor eax,eax
0x0000555555555204 <+27>: lea rax,[rip+0xdfd] # 0x555555556008
0x000055555555520b <+34>: mov rdi,rax
0x000055555555520e <+37>: mov eax,0x0
0x0000555555555213 <+42>: call 0x5555555550c0 <printf@plt>
0x0000555555555218 <+47>: mov QWORD PTR [rbp-0x40],0x0
0x0000555555555220 <+55>: mov QWORD PTR [rbp-0x38],0x0
0x0000555555555228 <+63>: mov QWORD PTR [rbp-0x30],0x0
0x0000555555555230 <+71>: mov QWORD PTR [rbp-0x28],0x0
0x0000555555555238 <+79>: mov QWORD PTR [rbp-0x20],0x0
0x0000555555555240 <+87>: mov QWORD PTR [rbp-0x18],0x0
0x0000555555555248 <+95>: mov WORD PTR [rbp-0x10],0x0
0x000055555555524e <+101>: lea rax,[rbp-0x40]
0x0000555555555252 <+105>: mov rsi,rax
0x0000555555555255 <+108>: lea rax,[rip+0xdc5] # 0x555555556021
0x000055555555525c <+115>: mov rdi,rax
0x000055555555525f <+118>: mov eax,0x0
0x0000555555555264 <+123>: call 0x5555555550e0 <__isoc99_scanf@plt>
0x0000555555555269 <+128>: lea rax,[rbp-0x40]
0x000055555555526d <+132>: lea rdx,[rip+0xdb0] # 0x555555556024
0x0000555555555274 <+139>: mov rsi,rdx
0x0000555555555277 <+142>: mov rdi,rax
0x000055555555527a <+145>: call 0x5555555550d0 <strcmp@plt>
0x000055555555527f <+150>: test eax,eax
0x0000555555555281 <+152>: jne 0x5555555552c6 <main+221>
0x0000555555555283 <+154>: lea rax,[rip+0xda0] # 0x55555555602a
0x000055555555528a <+161>: mov rdi,rax
0x000055555555528d <+164>: call 0x5555555550a0 <puts@plt>
0x0000555555555292 <+169>: lea rax,[rip+0xdcf] # 0x555555556068
0x0000555555555299 <+176>: mov rdi,rax
0x000055555555529c <+179>: call 0x5555555550a0 <puts@plt>
0x00005555555552a1 <+184>: lea rax,[rip+0xdea] # 0x555555556092
0x00005555555552a8 <+191>: mov rdi,rax
0x00005555555552ab <+194>: call 0x5555555550a0 <puts@plt>
0x00005555555552b0 <+199>: mov eax,0x0
0x00005555555552b5 <+204>: mov rdx,QWORD PTR [rbp-0x8]
0x00005555555552b9 <+208>: sub rdx,QWORD PTR fs:0x28
0x00005555555552c2 <+217>: je 0x5555555552e4 <main+251>
0x00005555555552c4 <+219>: jmp 0x5555555552df <main+246>
0x00005555555552c6 <+221>: lea rax,[rip+0xd79] # 0x555555556046
0x00005555555552cd <+228>: mov rdi,rax
0x00005555555552d0 <+231>: call 0x5555555550a0 <puts@plt>
0x00005555555552d5 <+236>: mov edi,0x1
0x00005555555552da <+241>: call 0x5555555550f0 <exit@plt>
0x00005555555552df <+246>: call 0x5555555550b0 <__stack_chk_fail@plt>
0x00005555555552e4 <+251>: leave
0x00005555555552e5 <+252>: ret
End of assembler dump.
(gdb) b *0x000055555555527a
Breakpoint 2 at 0x55555555527a
(gdb) c
Continuing.
Please input your name: hello
Breakpoint 2, 0x000055555555527a in main ()
(gdb) jump 0x0000555555555283
Function "0x0000555555555283" not defined.
(gdb) jump *0x0000555555555283
Continuing at 0x555555555283.
Welcome, you are valid user
Every feature is avaialble for valid user
Bye~
[Inferior 1 (process 121417) exited normally]
(gdb)
2. 替换 jne 指令为 nop
相比于 jump 到一个地址, 改为 nop 命令的做法更简单和通用:
- jump 到的新地址,不好判断
- nop 指令是 0x90, 好改
如下是关键命令和gdb运行结果:
(gdb) start
(gdb) disas main
(gdb) set {char[2]}0x0000555555555281 = {0x90, 0x90}
(gdb) c
Continuing.
Please input your name: hello
Welcome, you are valid user
Every feature is avaialble for valid user
Bye~
[Inferior 1 (process 144989) exited normally]
其中 set 修改地址之前, disas 看到的关键代码:
0x0000555555555274 <+139>: mov rsi,rdx
0x0000555555555277 <+142>: mov rdi,rax
0x000055555555527a <+145>: call 0x5555555550d0 <strcmp@plt>
0x000055555555527f <+150>: test eax,eax
0x0000555555555281 <+152>: jne 0x5555555552c6 <main+221>
0x0000555555555283 <+154>: lea rax,[rip+0xda0] # 0x55555555602a
0x000055555555528a <+161>: mov rdi,rax
其中 jne 这行的地址 0x0000555555555281 被 set 拿去修改来着。
在 set 前、 set 后,可以分别检查 jne 和 nop 的代码的数值. 执行前:
(gdb) x /2b 0x0000555555555281
0x555555555281 <main+152>: 0x75 0x43
(gdb)
(gdb) disassemble 0x0000555555555281,0x0000555555555281+2
Dump of assembler code from 0x555555555281 to 0x555555555283:
0x0000555555555281 <main+152>: jne 0x5555555552c6 <main+221>
执行后:
(gdb) x /2b 0x0000555555555281
0x555555555281 <main+152>: 0xffffffffffffff90 0xffffffffffffff90
(gdb)
(gdb) disassemble 0x0000555555555281,0x0000555555555281+2
Dump of assembler code from 0x555555555281 to 0x555555555283:
0x0000555555555281 <main+152>: nop
0x0000555555555282 <main+153>: nop
End of assembler dump.
3. 用二进制编辑器,修改 jne 为 nop 指令
首先要确定 jne 指令的位置。 在 gdb 中, 如果没有输入 start 命令, 此时 disas main 看到的结果, 和在执行 start 命令后再执行 disas main, 是有一个偏移地址的。
start 之前,执行 disas main:
0x0000000000001274 <+139>: mov rsi,rdx
0x0000000000001277 <+142>: mov rdi,rax
0x000000000000127a <+145>: call 0x10d0 <strcmp@plt>
0x000000000000127f <+150>: test eax,eax
0x0000000000001281 <+152>: jne 0x12c6 <main+221>
start 之后, 执行 disas main:
0x0000555555555274 <+139>: mov rsi,rdx
0x0000555555555277 <+142>: mov rdi,rax
0x000055555555527a <+145>: call 0x5555555550d0 <strcmp@plt>
0x000055555555527f <+150>: test eax,eax
0x0000555555555281 <+152>: jne 0x5555555552c6 <main+221>
其中第一列有差别。而当我们在二进制编辑器中, 修改 jne 汇编指令为 nop 时, 显然程序没有运行, 是要根据 start 之前的 disas main 得到的地址, 去修改。
因此,需要把地址为 0x1281, 0x1282 的内容, 从原来的 0x75, 0x43, 修改为 0x90, 0x90.
使用 vim 修改二进制的步骤
cp a.out aa.out
vim aa.out
:%!xxd
找到 1280 行, 修改75 43 为90 90
:%!xxd -r # 转换回到二进制,否则结果不对
:wq # 保存
其中 %!xxd -r
是关键。
使用https://hexed.it
成功了,很简单。
zz@Legion-R7000P% ./aa.out
Please input your name: he
Welcome, you are valid user
Every feature is avaialble for valid user
Bye~
4. 使用 gdb jump 命令,在 bomb lab 上作弊
跳过每个 phase_x() 函数的执行, 直接运行 phase_x() 之后的“恭喜通关”汇编指令.
需要收集 call phase_x 的地址, 然后设置断点。以 phase_1 为例:
(gdb) disas main
Dump of assembler code for function main:
=> 0x0000000000400da0 <+0>: push rbx
0x0000000000400da1 <+1>: cmp edi,0x1
0x0000000000400da4 <+4>: jne 0x400db6 <main+22>
0x0000000000400da6 <+6>: mov rax,QWORD PTR [rip+0x20299b] # 0x603748 <stdin@@GLIBC_2.2.5>
0x0000000000400dad <+13>: mov QWORD PTR [rip+0x2029b4],rax # 0x603768 <infile>
0x0000000000400db4 <+20>: jmp 0x400e19 <main+121>
0x0000000000400db6 <+22>: mov rbx,rsi
0x0000000000400db9 <+25>: cmp edi,0x2
0x0000000000400dbc <+28>: jne 0x400df8 <main+88>
0x0000000000400dbe <+30>: mov rdi,QWORD PTR [rsi+0x8]
0x0000000000400dc2 <+34>: mov esi,0x4022b4
0x0000000000400dc7 <+39>: call 0x400c10 <fopen@plt>
0x0000000000400dcc <+44>: mov QWORD PTR [rip+0x202995],rax # 0x603768 <infile>
0x0000000000400dd3 <+51>: test rax,rax
0x0000000000400dd6 <+54>: jne 0x400e19 <main+121>
0x0000000000400dd8 <+56>: mov rcx,QWORD PTR [rbx+0x8]
0x0000000000400ddc <+60>: mov rdx,QWORD PTR [rbx]
0x0000000000400ddf <+63>: mov esi,0x4022b6
0x0000000000400de4 <+68>: mov edi,0x1
0x0000000000400de9 <+73>: call 0x400c00 <__printf_chk@plt>
0x0000000000400dee <+78>: mov edi,0x8
0x0000000000400df3 <+83>: call 0x400c20 <exit@plt>
0x0000000000400df8 <+88>: mov rdx,QWORD PTR [rsi]
0x0000000000400dfb <+91>: mov esi,0x4022d3
0x0000000000400e00 <+96>: mov edi,0x1
0x0000000000400e05 <+101>: mov eax,0x0
0x0000000000400e0a <+106>: call 0x400c00 <__printf_chk@plt>
0x0000000000400e0f <+111>: mov edi,0x8
0x0000000000400e14 <+116>: call 0x400c20 <exit@plt>
0x0000000000400e19 <+121>: call 0x4013a2 <initialize_bomb>
0x0000000000400e1e <+126>: mov edi,0x402338
0x0000000000400e23 <+131>: call 0x400b10 <puts@plt>
0x0000000000400e28 <+136>: mov edi,0x402378
0x0000000000400e2d <+141>: call 0x400b10 <puts@plt>
0x0000000000400e32 <+146>: call 0x40149e <read_line>
0x0000000000400e37 <+151>: mov rdi,rax
0x0000000000400e3a <+154>: call 0x400ee0 <phase_1>
0x0000000000400e3f <+159>: call 0x4015c4 <phase_defused>
0x0000000000400e44 <+164>: mov edi,0x4023a8
0x0000000000400e49 <+169>: call 0x400b10 <puts@plt>
0x0000000000400e4e <+174>: call 0x40149e <read_line>
0x0000000000400e53 <+179>: mov rdi,rax
0x0000000000400e56 <+182>: call 0x400efc <phase_2>
0x0000000000400e5b <+187>: call 0x4015c4 <phase_defused>
0x0000000000400e60 <+192>: mov edi,0x4022ed
0x0000000000400e65 <+197>: call 0x400b10 <puts@plt>
0x0000000000400e6a <+202>: call 0x40149e <read_line>
0x0000000000400e6f <+207>: mov rdi,rax
0x0000000000400e72 <+210>: call 0x400f43 <phase_3>
0x0000000000400e77 <+215>: call 0x4015c4 <phase_defused>
0x0000000000400e7c <+220>: mov edi,0x40230b
0x0000000000400e81 <+225>: call 0x400b10 <puts@plt>
0x0000000000400e86 <+230>: call 0x40149e <read_line>
0x0000000000400e8b <+235>: mov rdi,rax
0x0000000000400e8e <+238>: call 0x40100c <phase_4>
0x0000000000400e93 <+243>: call 0x4015c4 <phase_defused>
0x0000000000400e98 <+248>: mov edi,0x4023d8
0x0000000000400e9d <+253>: call 0x400b10 <puts@plt>
0x0000000000400ea2 <+258>: call 0x40149e <read_line>
0x0000000000400ea7 <+263>: mov rdi,rax
0x0000000000400eaa <+266>: call 0x401062 <phase_5>
0x0000000000400eaf <+271>: call 0x4015c4 <phase_defused>
0x0000000000400eb4 <+276>: mov edi,0x40231a
0x0000000000400eb9 <+281>: call 0x400b10 <puts@plt>
0x0000000000400ebe <+286>: call 0x40149e <read_line>
0x0000000000400ec3 <+291>: mov rdi,rax
0x0000000000400ec6 <+294>: call 0x4010f4 <phase_6>
0x0000000000400ecb <+299>: call 0x4015c4 <phase_defused>
0x0000000000400ed0 <+304>: mov eax,0x0
0x0000000000400ed5 <+309>: pop rbx
0x0000000000400ed6 <+310>: ret
End of assembler dump.
(gdb) b *0x0000000000400e3a
Breakpoint 2 at 0x400e3a: file bomb.c, line 74.
(gdb) c
Continuing.
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
hello
Breakpoint 2, 0x0000000000400e3a in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:74
74 phase_1(input); /* Run the phase */
(gdb) jump *0x0000000000400e3f
Continuing at 0x400e3f.
Phase 1 defused. How about the next one?
完整gdb命令:
start
b *0x0000000000400e3a
b *0x0000000000400e56
b *0x0000000000400e72
b *0x0000000000400e8e
b *0x0000000000400eaa
b *0x0000000000400ec6
c
1
jump *($rip+5)
2
jump *($rip+5)
3
jump *($rip+5)
4
jump *($rip+5)
5
jump *($rip+5)
6
jump *($rip+5)
其中 jump *($rip+5) 是跳过5个字节,也就是 call phase_x 。 call phase_x 对应了5个字节。
执行的部分记录:
Breakpoint 3, 0x0000000000400e56 in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:82
82 phase_2(input);
(gdb) jump *($rip + 5)
Continuing at 0x400e5b.
That's number 2. Keep going!
hello
Breakpoint 4, 0x0000000000400e72 in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:89
89 phase_3(input);
(gdb) sd
Undefined command: "sd". Try "help".
(gdb) jump *($rip + 5)
Continuing at 0x400e77.
Halfway there!
hello
Breakpoint 5, 0x0000000000400e8e in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:95
95 phase_4(input);
(gdb) jump *($rip + 5)
Continuing at 0x400e93.
So you got that one. Try this one.
hello
Breakpoint 6, 0x0000000000400eaa in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:101
101 phase_5(input);
(gdb) jump *($rip + 5)
Continuing at 0x400eaf.
Good work! On to the next...
hello
Breakpoint 7, 0x0000000000400ec6 in main (argc=<optimized out>, argv=<optimized out>) at bomb.c:108
108 phase_6(input);
(gdb) jump *($rip + 5)
Continuing at 0x400ecb.
Congratulations! You've defused the bomb!
[Inferior 1 (process 165752) exited normally]
5. 修改二进制文件,把 call phase_x 为 nop
首先进入gdb, 在执行 start 命令之前, 从反汇编结果中获取所有的 call phase_x 指令的地址:
0x0000000000400e3a <+154>: call 0x400ee0 <phase_1>
0x0000000000400e56 <+182>: call 0x400efc <phase_2>
0x0000000000400e72 <+210>: call 0x400f43 <phase_3>
0x0000000000400e8e <+238>: call 0x40100c <phase_4>
0x0000000000400eaa <+266>: call 0x401062 <phase_5>
0x0000000000400ec6 <+294>: call 0x4010f4 <phase_6>
分别查看每个地址开头的5个字节的内容。这一步可选,作为后续在二进制编辑器中的对照
(gdb) x /5xb 0x0000000000400e3a
0x400e3a <main+154>: 0xe8 0xa1 0x00 0x00 0x00
(gdb) x /5xb 0x0000000000400e56
0x400e56 <main+182>: 0xe8 0xa1 0x00 0x00 0x00
(gdb) x /5xb 0x0000000000400e72
0x400e72 <main+210>: 0xe8 0xcc 0x00 0x00 0x00
(gdb) x /5xb 0x0000000000400e8e
0x400e8e <main+238>: 0xe8 0x79 0x01 0x00 0x00
(gdb) x /5xb 0x0000000000400eaa
0x400eaa <main+266>: 0xe8 0xb3 0x01 0x00 0x00
(gdb) x /5xb 0x0000000000400ec6
0x400ec6 <main+294>: 0xe8 0x29 0x02 0x00 0x00
然后使用 vim 或 hexed.it, 找到上述每个地址,每个地址处连续5个字节,统统改为 0x90 (nop指令).
这时候的坑是, 并没有找到 0x400e3a 的地址, 只找到 0x00000e3a 这样的地址。大概估计一下,应该是减掉了固定的地址偏移
call phase_1 修改为 nop 前:
call phase_1 修改为 nop 后:
执行结果: 随便怎么输入,都能到达最终结果:
zz@Legion-R7000P% ./bomb2
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
hello
Phase 1 defused. How about the next one?
yes
That's number 2. Keep going!
s
Halfway there!
s
So you got that one. Try this one.
s
Good work! On to the next...
ff
Congratulations! You've defused the bomb!
6. 总结
- 在 gdb 中, 你不必循规蹈矩的从头执行到尾:
- 可以用 jump 命令跳转到希望之星的地方,例如
(gdb) jump *0x0000555555555283
- 可以用 set 命令修改内存,把即将执行的指令改为想执行的指令, 例如修改 call phase_x 为 nop 指令,也就是把 e8 开头的5个字节,全都改为0x90
- 可以用 jump 命令跳转到希望之星的地方,例如
- 为了运行修改后的程序
- 可以在 gdb 里修改, 也就是前面提到的做法
- 你也可以在二进制文件中修改, 但这往往还是需要搭配 gdb 反汇编,先获得要修改的指令的地址
- 为了获得需要修改的指令的地址
- 我目前的经验,是在 start 之前执行 disas xxx_fucntion, 对于自己单独写的代码的小实验,有效
- 但是 CSAPP 官方的 bomb 可执行程序, start 前和start 后,地址一样,并且在二进制编辑的时候,地址又减低
- 为了完成逆向工程的一些想法,不必拘泥于原始的可执行文件
- 通过自行构造 C 代码和可执行程序,完成了原型,很容易扩展到原始的 bomb 文件上