Skip to content

Instantly share code, notes, and snippets.

@lordidiot
Last active March 17, 2021 10:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lordidiot/05692aa8e2661e9b93b7d5226d48240c to your computer and use it in GitHub Desktop.
Save lordidiot/05692aa8e2661e9b93b7d5226d48240c to your computer and use it in GitHub Desktop.
CTF.SG CTF 2021

11B, Please [Misc]: Author Writeup

Overview

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.

TL;DR

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.

Code Analysis

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.

days_left

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.

money_left

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.

Dead-end

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.

XMM [Pwn]: Author Writeup

Got a bit lazy to writeup this one after writing the 11B, Please one. Maybe I'll update this gist when I'm back from camp.

TL;DR solution.

You are allowed to write shellcode, write a simple shellcode to do the write syscall to leak either

0x495060: The flag

.rodata:0000000000495060 aCtfsgYyyyyyyyy db 'CTFSG{xmm_hunter_1337}

OR

0x49501D: The secret food you need to get the flag

.rodata:000000000049501D aM0nsterCurr7   db 'm0nster_curr7',0

The challenge actually meant to do something else. But I'm not called Lord_Idiot for no reason so I made a mistake. Maybe I'll redeploy the fixed one in a future edition of the CTF!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment