This challenge was based on a behaviour I learnt from reading Attacking Network Protocols (James Forshaw). The bug has to do with some integer trickery and I thought it was pretty neat. Fun fact, I crafted the challenge idea while on security trooper duty (screw NS T.T), hence the security trooper theme of the challenge.
The writeup is a bit lengthy, here's the quick solution run through.
Bribe -2147483648 (INT_MIN) which won't be turned positive by positive
, causing money_left
to be negative and giving us the flag.
The code for the game looks quite daunting, but it's mostly due to the UI code I had to add to make the game look cool, the core logic of the game is quite simple. When analysing code, it can be useful to work backwards.
In main
, we can see quite clearly that game()
should return a non-zero value [1] for us to get the flag [2].
int main(){
curses_init();
main_menu();
instructions();
if(game()){ // [1]
mvwaddstr(mainwin, 0, 0, "You Win!");
mvwaddstr(mainwin, 1, 0, "CTFSG{XXXXXXXXXXXXXXXXXXX}"); // [2]
refresh();
sleep(20);
}
else{
mvwaddstr(mainwin, 0, 0, "You Lose :(");
refresh();
sleep(5);
}
curses_end();
return 0;
}
Thus we can scan quickly though the code in game
to understand the conditions we need to fulfill to return a non-zero output.
int game(){
...
for (;;) {
...
if(_days_left != days_left){
...
// Win Method #1
if(_days_left <= 0){ // [1]
DELWIN();
return 1; /* return 1 for win */
}
}
if(money_left != _money_left){
...
// Win Method #2
if(_money_left <= 0){ // [2]
DELWIN();
return 1; /* return 1 for win */
}
}
}
}
We can see that there are 2 ways for this to occur, when _days_left <= 0
[1] and when _money_left <= 0
[2]. There are even comments left by the author to show you that these are the win conditions (what a kind author!).
Let's explore both possibilities.
Perform a simple Cntrl-F
(find) in your favourite text editor for days_left
, and you'll see all operations concerning this variable.
int game(){
...
days_left = DAYS_TO_ORD;
...
int _days_left = -1; /* Temporary variable to reduce screen updates ( NOT IMPT ) */
...
for (;;) {
// Update Status
char buf[22];
days_left = ( (start_time+360*DAYS_TO_ORD) - time(0) ) / 360; /* 6 minute = 1 game day */
if(_days_left != days_left){
_days_left = days_left;
snprintf(buf, 22, "DAYS TO ORD: %d days", _days_left);
...
// Monthly pay
if(!(_days_left % 30))
money_left -= 630; // Wait for CTF.SG 2021 then you'll be LCP, for now enjoy PTE pay
// Win Method #1
if(_days_left <= 0){
DELWIN();
return 1; /* return 1 for win */
}
}
}
}
I've reduced the code to only show code that affects days_left
and _days_left
, and we can quite simply see that _days_left = days_left
and days_left
is purely controlled by the time since the start time of the game.
days_left = ( (start_time+360*DAYS_TO_ORD) - time(0) ) / 360; /* 6 minute = 1 game day */
Unless we have some remote 0day to affect the output of time(0)
, or we somehow manipulate the value of start_time
through memory corruption, this method for winning is not likely to work.
We can repeat the same analysis techniques with money_left
instead this time.
int money_left = 1000000;
...
int game(){
...
int _money_left = -1; /* Temporary variable to reduce screen updates ( NOT IMPT ) */
...
for (;;) {
...
if(_days_left != days_left){
...
// Monthly pay
if(!(_days_left % 30)) // [1]
money_left -= 630; // Wait for CTF.SG 2021 then you'll be LCP, for now enjoy PTE pay
...
}
if(money_left != _money_left){
_money_left = money_left;
snprintf(buf, 22, "$ TO GOAL: $%d", _money_left);
...
// Win Method #2
if(_money_left <= 0){
DELWIN();
return 1; /* return 1 for win */
}
}
...
// User Input
if ((ch = getch()) == ERR) {
}
else {
switch(ch){
...
case '\n':
...
else{/* selected == 2 */
bribe = positive(atoi(bribe_buf));
if(bribe > curr.bribe){ /* Offered to be bribed at too high a price: LOSE */
DELWIN();
return 0;
}
money_left -= bribe; // [2]
mvwaddstr(desk, 2 , 13, "TOOK BRIBE ($.$)");
}
}
}
}
}
The code is quite similar here, _money_left = money_left
. money_left
's value is only modified at two places [1] and [2].
// Monthly pay
if(!(_days_left % 30)) // [1]
money_left -= 630; // Wait for CTF.SG 2021 then you'll be LCP, for now enjoy PTE pay
...
money_left -= bribe; // [2]
[1] will deduct our $630 every month, which is 180min/3hr in real time. Just based on this pay, we'll not be ORD-ing anytime soon. There's even a comment I left behind when I made this (was still PTE then). Anyways, this obviously is not the route we can use to win, considering the CTF lasts for 24 hours.
[2], on the other hand, deducts it by bribe
amount. If we somehow get this bribe
to a large value, perhaps we could win. We should thus analyse the surrounding code to understand how our user-input controls the value of bribe
, and if there are limitations in the value it can hold.
else{/* selected == 2 */
bribe = positive(atoi(bribe_buf)); // [1]
if(bribe > curr.bribe){ /* Offered to be bribed at too high a price: LOSE */
DELWIN();
return 0;
}
money_left -= bribe;
mvwaddstr(desk, 2 , 13, "TOOK BRIBE ($.$)");
}
At [1], we observe that bribe = positive(atoi(bribe_buf))
, and if you do further analysis you will realise that bribe_buf
is controlled by the player, except that only (ch >= '0' && ch <= '9') || ch == '-' || ch == '+' )
characters are allowed. If you don't understand how we control bribe_buf
, do some code analysis yourself as an exercise.
As we analyse our control of the bribe
value, we must keep in mind our goal, to make bribe
a very large value to instantly win the game. atoi(bribe_buf)
is quite normal code, which just converts our string of characters into an integer. However, the positive
function is called on this output. Let's have a further look at it.
// Convert negative n to positive n
int positive(int n){
return (n < 0) ? -n : n;
}
The function is super small, and simply checks if the number is negative (n < 0)
, and return -n
if it is negative, which should turn it postive. Else, it just returns n
which is a positive number. Looks pretty straightforward. So far, looks like we can control the value of bribe
to be a high number. However, there is a check!
if(bribe > curr.bribe){ /* Offered to be bribed at too high a price: LOSE */
DELWIN();
return 0;
}
If our offered bribe (bribe
) is higher than curr.bribe
which is the maximum bribe that the current visitor is willing to fork out. Looking into the code that generates the curr.bribe
amount, we see this.
void generate_person(person * p){
...
if(p->auth)
p->bribe = -1; /* Authorised Personnel do not accept bribe offers */
else
p->bribe = rand()%100;
}
...
int game(){
...
// Game Logic
if(!curr.name[0]){
generate_person(&curr);
mvwaddstr(id, 10, 16, curr.name);
mvwaddstr(id, 13, 16, curr.ic);
wrefresh(id);
}
The visitor is created through the generate_person
function which creates the value through p->bribe = rand()%100
, limiting our bribes to $100! This is far too little to allow us to win the game in time, even if we offer bribes perfectly every time.
With this, it seems like we've exhausted all options. It's time to look deeper for bugs. We know that our bribes have to be lower than curr.bribe
which is maxed at $99, but what if we were to offer negative bribe amounts?
money_left -= bribe;
This would increase our money_left
value, that sounds pretty stupid, we're getting further away from the goal. However, let's continue with this train of thought, it's possible that maybe we could increase it repeatedly to cause an integer overflow of money_left
, or maybe some other ideas will come as we pursue this idea.
Let's look at what prevents us from providing a negative bribe
value, the positive
function.
// Convert negative n to positive n
int positive(int n){
return (n < 0) ? -n : n;
}
Do you spot any bugs? This function is deceptively simple, but it actually holds a small edge-case! Consulting some online resources, we can see that the range of an integer is from -2147483648 to 2147483647
. Notice anything interesting? The absolute value of the smallest negative number -2147483648
is 1 larger than the largest positive number 2147483647
. What impliciations does this have on our positive
function? If n=-2147483648
, and we take -n
, we should get 2147483648
, but this is higher than the largest positive integer value and cannot be represented, so what value do we get instead?
We can just compile a C program to test this out, but since this is an educational write-up let's do more exploration. You may have learnt from your computer science lessons that the two's complement representation is used for representing integers, and that converting between a negative and positive number simply requires you to flip the bits and add 1. This is the process that happens when we take -n
of n
, let's try it out.
32-bit integer n
= -2147483648
= 10000000 00000000 00000000 00000000 (binary)
Now we flip the bits
= 01111111 11111111 11111111 11111111
Now we add 1
01111111 11111111 11111111 11111111 + 1
= 10000000 00000000 00000000 00000000
= -2147483648
Wow! So taking the negative of -2147483648
gives us back -2147483648
. Really cool behaviour. So what happens if our bribe
value is now -2147483648
? Tracing the code, we'll get money_left -= bribe;
where money = 1000000
.
1000000 -= -2147483648
is equivalent to
1000000 += -2147483648
because when we flip the number it remains negative!
1000000 - 2147483648 = -2146483648
The resultant value of money_left
is -2146483648
which is within the range of -2147483648 - 2147483647
, so it's a valid value for an integer. This is a negative number which fulfills our win condition!
// Win Method #2
if(_money_left <= 0){
DELWIN();
return 1; /* return 1 for win */
}
With this, we've won! We can connect to the remote service, type in this number as the bribe and hopefully earn the flag.
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DAYS TO ORD: 659 days $ TO GOAL: $999370 │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Queue ─────────────────────────────────────────────────────────────────────────────────────────┐
│ +---------------+-----------+ [+++++++++] │
│ | 0 |@__ | [=========] │
│ | /|\ | | @ [ __@ ] │
│ | / \ / \ | @/ /|\ /| ] │
│ | + | | +----+/ \ [ / \ ] -__@ │
│ | 0 | \@ | /|\ | [=========] /| │
│ | /|> | |\ | | [ ] / \ │
│ | / \ | / \ | | [ ] │
│ | | + | [ ] │
│ | @ | 0 __@ | / ] │
│ | /|\ | /|\ /| | [ ] │
│ | / \ | / \ / \ | [ ] │
│ + +---------------+ [+++++++++] │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Personnel ──────────────────────┐┌ 11B ────────────────────────────────────────────────────────┐
│ ││ │
│ ││ ALLOW REJECT [BRIBE: $-2147483648] │
│ _______ ││ _________________________________________ │
│ __|_______| ││ | __ | │
│ //////^\\ ││ |--/**\-----------------------------------| │
│ | * * | ││ | |****| SINGAPORE LEGGED FORCES | │
│ \ o / ││ |--\__/-----------------------------------| │
│ ___ \ / ___ ││ | _________ | │
│ / \ ││ | | ___ | | │
│ ------------------------------ ││ | David | / \ | | │
│ ============================== ││ | | |* *| | | │
│ _______ ││ | | \ / | | │
│ / /, __ ││ | T0161988D | _/ \_ | | │
│ / // / / ││ | |_________| | │
│ /______// / / ││ | | │
│ (______(/ \/ ││ |_________________________________________| │
│ ││ │
│ ││ │
└─────────────────────────────────┘└─────────────────────────────────────────────────────────────┘
You Win!
CTFSG{336_d4ys_to_0RD_:'(}
This was a pretty long writeup, but hopefully it shows some of the thought processes that I hoped participants would have to derive a solution. The challenge boils down to having good understanding of the code provided, and applying some basic computer science concepts to exploit the code.
Hopefully you've learnt something from this challenge, do give me feedback on Discord if you liked the challenge/hate the challenge/see areas of improvement.