CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令

厌倦了在 gdb 中一步步顺序执行 bomb 可执行程序。为什么不能自行控制程序的执行呢?跳到特定的函数去执行,又或者把原本要执行的指令改掉,gdb 里能做到吗?

这一篇依然不是正经的 bomblab 解题记录, 而是基于bomblab的实验,练习 gdb 命令的使用,以及基于 gdb 中人工干预的代码执行流程, 在二进制编辑器中魔改二进制文件,修改后 bomb 可执行程序,无论输入什么,都能顺利过关。

CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令-LMLPHP

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 前:
CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令-LMLPHP
call phase_1 修改为 nop 后:
CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令-LMLPHP

执行结果: 随便怎么输入,都能到达最终结果:

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!

CSAPP - bomblab 作弊方式2: gdb jump 命令, 以及修改 jne 为 nop 指令-LMLPHP

6. 总结

  1. 在 gdb 中, 你不必循规蹈矩的从头执行到尾:
    • 可以用 jump 命令跳转到希望之星的地方,例如 (gdb) jump *0x0000555555555283
    • 可以用 set 命令修改内存,把即将执行的指令改为想执行的指令, 例如修改 call phase_x 为 nop 指令,也就是把 e8 开头的5个字节,全都改为0x90
  2. 为了运行修改后的程序
    • 可以在 gdb 里修改, 也就是前面提到的做法
    • 你也可以在二进制文件中修改, 但这往往还是需要搭配 gdb 反汇编,先获得要修改的指令的地址
  3. 为了获得需要修改的指令的地址
    • 我目前的经验,是在 start 之前执行 disas xxx_fucntion, 对于自己单独写的代码的小实验,有效
    • 但是 CSAPP 官方的 bomb 可执行程序, start 前和start 后,地址一样,并且在二进制编辑的时候,地址又减低
  4. 为了完成逆向工程的一些想法,不必拘泥于原始的可执行文件
    • 通过自行构造 C 代码和可执行程序,完成了原型,很容易扩展到原始的 bomb 文件上
01-14 16:19