Down to the Wire

Toaster Week Part 2: TW_GR_E3_GtI and TW_GR_E4_STW

It occurred to me about a month ago that I never published author writeups for the “Toaster Wars: Going Rogue” CTF problems from PicoCTF 2017 and PlaidCTF 2017, which is particularly troubling since one of them wasn’t actually solved in contest! To remedy this, and in celebration of the release of a new game in the series that inspired these problems later this week, I’ll be posting writeups for all of the Toaster Wars problems over the course of the week.

For day two, I’ll be talking about the harder Toaster Wars problems from PicoCTF 2017: TW_GR_E3_GtI and TW_GR_E4_STW.

Toaster Wars: Going Rogue, Episode 3 – Garnish to Infinity

180 points (Level 3)

Many think the third entry in the Toaster Wars: Going Rogue series didn’t live up to Explorers due to its simplified gameplay, but I liked the new plot and ensemble cast better. Anyway, this one also has a flag that I can’t figure out how to get…

Hint: Clearly, you can’t get to the flag. But perhaps you don’t need to get over to it at all?

This version of the problem is pretty much the same game as before until you get to the fifth floor, when you come across this:

The flag is behind a wall

Even though the hint indicates we probably won’t need to, let’s consider possible ways to pick up the flag by simply stepping on it as usual.

The easiest would be to spawn on the same side of the wall as the flag, but unfortunately the config for 5F fixes the player’s start location and the location of the flag:

JavaScript content_copy /server/config.js (excerpt)
		{
			range: [5, 5],
			timeLimit: 500,
			generate: false,
			bgm: "at-the-end-of-the-road",
			description: {
				map: {
					width: 7,
					height: 14,
					grid: [
						// (snip)
					],
					stairs: {
						r: 11,
						c: 3
					},
					known: {
						grid: [
							// (snip)
						],
						stairs: {
							r: 11,
							c: 3
						}
					}
				},
				enemies: [],
				items: [
					createFlag({ r: 2, c: 3 })
				],
				player: {
					location: {
						r: 7,
						c: 3
					}
				}
			}
		}

If we don’t start near the flag, moving to it could also be an option. The generic movement action can only move you one tile at a time in any direction (this can be verified in /server/game.js), so simply walking there won’t work. No attack in the game has a movement effect either. This leaves only items, and conveniently, one of them does have a movement effect:

JavaScript content_copy /server/config.js (excerpt)
		turmeric: {
			name: "Turmeric",
			description: "Warps the user to a random location.",
			sprite: "turmeric",
			effects: [
				{
					type: "move",
					location: "random"
				}
			]
		},

However, the configuration unfortunately doesn’t allow this item to appear randomly as it did in the first two versions of the problem:

JavaScript content_copy /server/config.js (excerpt)
				items: {
					density: 30,
					distribution: {
						screwdriver: .4,
						battery: .3,
						paprika: .05,
						cayenne: .05,
						// turmeric: .05, // Nope :)
						oregano: .05,
						cinnamon: .05,
						peppercorn: .05
					}
				}

With this plan of attack exhausted, let’s instead follow the hint and try grabbing the flag without moving to it. A good place to start here is a careful inspection of how the logic around picking up items works, which can be found in /server/game.js:

JavaScript content_copy /server/game.js
function executeMove(state, entity, action, log){
	/* ...a bunch of logic unrelated to items... */

	// item check
	if(entity.items.length < entity.stats.maxItems){
		var itId = -1;
		var name = "";
		for(var i = 0; i < state.items.length; i++){
			if(state.items[i].location.r == entity.location.r && state.items[i].location.c == entity.location.c){
				itId = state.items[i].id;
				msg.outcome.push({
					type: "item/get",
					id: state.items[i].id,
					item: state.items[i].name
				});
				break;
			}
		}
	}

	entity.items = entity.items.concat(state.items.filter(function(it){ return it.id == itId; }));
	state.items = state.items.filter(function(it){ return it.id != itId; });

	log.push(msg);

	return true;
}

So, in short, when we move to a location and have open space in the bag, we iterate over all of the items, find the id corresponding to the item on that tile if one exists, and then add all items with that id to our bag. So if we can get an item on the ground with the same id as the flag, we can pick up the flag without moving over to it!

But how can we make that happen? Perhaps there’s some way to modify the ids of items in our bag? A search for the string "id = " turns up this interesting result:

JavaScript content_copy /server/game.js
		socket.on("resortItems", function(){
			// logger.warn("[sort]", socket.id);

			db.getState(socket.id)
				.then(function(state){

				if (state.done) {
					return;
				}

				var oldState = censor(state);

				state.player.items.sort(function(a, b){
					if(a.name < b.name){
						return -1;
					}

					if(a.name > b.name){
						return 1;
					}

					return 0;
				});

				for(var i = 0; i < state.player.items.length; i++){
					state.player.items[i].id = i;
				}

				state.log = [];

				return db.commit(socket.id, state)
					.then(sendState(socket, oldState, false));
			});
		})

This is the implementation of the item sorting feature available in the client; so when we sort our items, it also sets their ids to their indices in the bag. For example, if we have a full bag of 8 items, after sorting they will have ids 0, 1, …, 7. Unfortunately, we can see in /server/config.js that none of these match the id of the flag:

JavaScript content_copy /server/config.js (excerpt)
function createFlag(location) {
	return {
		name: "Flag",
		description: "Gives you the flag.",
		location: location,
		use: 0,
		id: 12,
		sprite: "flag",
		effects: [
			{
				type: "revealFlag"
			}
		]
	};
}

However, this does mean that if we managed to get 13 items in our bag somehow, we would be able to pick up the flag remotely, since the final in the bag would end up with index 12 after sorting.

Fortunately, the bug we’ve already discovered already gives us a way to do just that! Suppose you have a full bag of 8 items, and there is one more item nearby. If you sort the bag, then drop the first item, then sort the bag again, then drop the first item again, and so on, then all of the items that were in your bag now have id 0. So you can pick up the ninth item, then touch any one of the other eight to pick all of them up at once!

Picking up more than 12 items at once

Armed with at least 13 items (not hard at all with how abundant items are in this dungeon), we can pick up the flag on the final floor!

Grabbing the flag on 5F

Toaster Wars: Going Rogue, Episode 4 – Super Toaster Wars

200 points (Level 4)

Many saw the fourth installment of Toaster Wars: Going Rogue as a return to grace after the relative mediocrity of the third. I’m just glad it was made at all. And hey, they added some nifty new online scoreboard features, too!

Hint: Ooh, what a nifty scoreboard! If we get a bunch of people playing at once, we can have a race through the dungeon!

It’s once again the same game, but this time with an extra scoreboard feature.

The TW4 scoreboard

If we try to play the game, everything goes fine until 4F, when suddenly we come across stairs surrounded by walls!

TW4 is unfair

Looking at /server/config.js, this happens because the fourth floor has an unfair flag set to true, which tells the dungeon generator to always surround the stairs with walls. If we can get past it, though, the flag is waiting for us on the fifth floor!

JavaScript content_copy /server/config.js (excerpt)
		{
			range: [4, 4],
			timeLimit: 500,
			bgm: "creaky-kitchen",
			generate: true,
			description: {
				unfair: true,
				map: {
					width: 55,
					height: 43
				},
				// ...
			}
		},
		{
			range: [5, 5],
			timeLimit: 500,
			generate: false,
			bgm: "at-the-end-of-the-road",
			description: {
				map: {
					width: 7,
					height: 14,
					grid: [
						// ...
					],
					stairs: {
						r: 11,
						c: 3
					},
					known: {
						grid: [
							// ...
						],
						stairs: {
							r: 11,
							c: 3
						}
					}
				},
				enemies: [],
				items: [createFlag({ r: 2, c: 3 })],
				player: {
					location: {
						r: 7,
						c: 3
					}
				}
			}
		}

Since the scoreboard was added for this problem specifically, we can probably safely assume it has something to do with the solution. Searching for "scoreboard" in the files we know about turns up a few results, but most notable is this piece of code in /server/game.js related to advancing to the next floor:

JavaScript content_copy /server/game.js (excerpt)
if(state.player.location.r == state.map.stairs.r && state.player.location.c == state.map.stairs.c){
	db.scoreboard[socket.id].floor++;
	prm = generator.generateFloor(db.scoreboard[socket.id].floor)
		.then((newState) => {
			if(newState.done){
				socket.emit("win");
				return Promise.all[db.commit(socket.id, {done: true, win: true}), initState];
			}

			newState.player.items = state.player.items;
			newState.player.stats = state.player.stats;
			newState.player.stats.modifiers = {};
			newState.player.attacks = state.player.attacks;
			state = newState;
			log[0].outcome.push({
				type: "floor",
				number: state.map.floor + "F"
			});
			state.enemies = state.enemies.filter(function(en){
				return !en.dead;
			});
		})
} else {
	// ...
}

So the operations taken, in order, are:

  1. Increment the floor number stored on the scoreboard
  2. Generate the next floor using the floor number on the scoreboard (asynchronously)
  3. Update the game state to the generated value

This presents an opportunity for a race! (And suddenly, the hint makes sense!) Suppose that we submit two back-to-back requests to step on the stairs on 3F, A and B, at about the same time; one possible order of steps would be the following:

  • A1: scoreboard floor number incremented to 4
  • A2: start generating 4F
  • B1: scoreboard floor number incremented to 5
  • B2: start generating 5F
  • A3: state set to the generated state for 4F
  • B3: state set to the generated state for 5F

If we could make this happen, we could skip 4F entirely! While this is technically possible without any further help as-is since generateFloor is asynchronous (so Node has the freedom to switch to a different asynchronous task before computing it), generateFloor also conveiently has delay built into it with a comment lampshading its fairly unnatural inclusion:

JavaScript content_copy server/generator.js (excerpt)
function generateFloor(floor){
	return new Promise((resolve, reject) => {
		setTimeout(() => { // Don't block the main thread
			// ...
		}, 10);
	});
}

So all we have to do is go to the stairs on 3F, submit two requests to move to the stairs at the same time, and hope for the best!

Sending a request to move twice skips 4F


Thanks for sticking around! These problems will get far more complex starting tomorrow, as we will be tackling the first of the three problems from PlaidCTF 2017!