PicoCTF 2022

Category: Reverse Engineering

Wizardlike (500 points)

Do you seek your destiny in these deplorable dungeons? If so, you may want to look elsewhere. Many have gone before you and honestly, they’ve cleared out the place of all monsters, ne’erdowells, bandits and every other sort of evil foe. The dungeons themselves have seen better days too. There’s a lot of missing floors and key passages blocked off. You’d have to be a real wizard to make any progress in this sorry excuse for a dungeon!

Again, surprisingly, this challenge really was super simple. It seemed a bit terrifying at first, but once I understood it, it was really not that bad.

Essentially, what we’re given is a binary that is a sort of game. We can walk around using wasd. The > and < signs signify stairs. Although, the game looks pretty barebones. There’s nothing really to do besides just walk around and bump into walls or climb the single staircase we’re welcomed into.

That’s because there’s more to the game than it tells us (this is picoCTF, not GameJolt). In short, in order to get the flag, we have to somehow exploit the game so that we are able to bypass the rules of physics and teleport wherever we want by modifying our x and y values, and (optionally) the elevation we’re currently at.

The tool I used for this is known as GameConqueror. Basically, it’s Cheat Engine, but for Linux. It’s about the same too. Change a value, scan for it, change it again, scan with that value, until you get a match. The hint recommended Radare2, but, y’know, i’m 2cool4radare2 😎

But in all seriousness, in order to actually know what to check for, we need to first understand what the binary is actually doing. What I did was open it through ghidra to look at the decompiled C code. I had to manually prettify it since the binary was stripped of all debug symbols (I even had to locate the main function via _libc_start_main), but after some work, this is what I ended up with:

The Decompilation


void main()
{
  bool continuePlaying;
  int iVar2;
  int iVar3;
  int iVar4;
  undefined8 uVar5;
  long in_FS_OFFSET;
  int local_30;
  int local_2c;
  int local_28;
  int local_24;
  ushort local_12;
  
  continuePlaying = true;
  load_map((long)s__________________________________00107740);
  clear_visibility();

  //screen initialization
  initscr(); //initialize the screen
  if (stdscr == 0) {
    iVar2 = -1;
    iVar3 = -1;
  }
  else {
    iVar2 = *(short *)(stdscr + 4) + 1;
    iVar3 = *(short *)(stdscr + 6) + 1;
  }

  //set the cursor and disable echo to make the game pretty to play
  DAT_0011fe9c = iVar2;
  noecho();
  curs_set();

  //start the main game loop
  while (continuePlaying) {
    //presumably to handle the player going up/down steps after "elevation" is set
    if (DAT_0011fe78 != elevation) {
      if (elevation == 1) {
        clear_visibility();
        load_map((long)s__________________________________00107740);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 1;
      }
      else if (elevation == 2) {
        clear_visibility();
        load_map((long)s__________________________________00109e60);
        current_x = 1;
        current_y = 2;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 2;
      }
      else if (elevation == 3) {
        clear_visibility();
        load_map((long)s__________________________________0010c580);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 3;
      }
      else if (elevation == 4) {
        clear_visibility();
        load_map((long)s__________________________________0010eca0);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 4;
      }
      else if (elevation == 5) {
        clear_visibility();
        load_map((long)s__________________________________001113c0);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 5;
      }
      else if (elevation == 6) {
        clear_visibility();
        load_map((long)s___________________________________00113ae0);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 6;
      }
      else if (elevation == 7) {
        clear_visibility();
        load_map((long)s___________________________________00116200);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 7;
      }
      else if (elevation == 8) {
        clear_visibility();
        load_map((long)s__________________________________00118920);
        current_x = 1;
        current_y = 2;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 8;
      }
      else if (elevation == 9) {
        clear_visibility();
        load_map((long)s__________________________________0011b040);
        current_x = 2;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 9;
      }
      else if (elevation == 10) {
        clear_visibility();
        load_map((long)&DAT_0011d760);
        current_x = 1;
        current_y = 1;
        DAT_0011fe90 = 0;
        DAT_0011fe94 = 0;
        DAT_0011fe78 = 10;
      }
    }
    for (int y = 0; y < iVar2; y += 1) {
      for (int x = 0; x < iVar3; x += 1) {
        mvprintw(y,x,&DAT_00103004);
      }
    }
    for (local_28 = 0; local_28 < iVar2; local_28 = local_28 + 1) {
      for (local_24 = 0; local_24 < iVar3; local_24 = local_24 + 1) {
        if ((((local_24 + DAT_0011fe90 < 100) && (local_28 + DAT_0011fe94 < 100)) &&
            (-1 < local_24 + DAT_0011fe90)) && (-1 < local_28 + DAT_0011fe94)) {
          uVar5 = FUN_00101332(current_x,current_y,DAT_0011fe90 + local_24,
                               DAT_0011fe94 + local_28);
          if (((char)uVar5 != '\0') ||
             (board_visible
              [(long)(DAT_0011fe94 + local_28) * 100 + (long)(local_24 + DAT_0011fe90)] != '\0')) {
            board_visible[(long)(DAT_0011fe94 + local_28) * 100 + (long)(local_24 + DAT_0011fe90)]
                 = 1;
            local_12 = (ushort)(byte)game_board
                                     [(long)(DAT_0011fe94 + local_28) * 100 +
                                      (long)(local_24 + DAT_0011fe90)];
            mvprintw(local_28,local_24,&local_12);
          }
        }
        else {
          mvprintw(local_28,local_24,&DAT_00103004);
        }
      }
    }
    mvprintw(current_y - DAT_0011fe94,current_x - DAT_0011fe90,&DAT_00103006);
    wrefresh(stdscr);

    //handle char input
    iVar4 = wgetch();
    if (iVar4 == 'Q') {
      continuePlaying = false;
    }
    else if (iVar4 == 'w') {
      move(0, -1, -1);
    }
    else if (iVar4 == 's') {
      move(0, 1, 1);
    }
    else if (iVar4 == 'a') {
      move(-1, 0, -1);
    }
    else if (iVar4 == 'd') {
      move(1, 0, 1);
    }

    //these are for if you go up/down the stairs
    if (game_board[current_y * 100 + current_x] == '>') {
      elevation += 1;
    }
    else if (game_board[current_y * 100 + current_x] == '<') {
      elevation -= 1;
    }
  }
//move function
void move(int x, int y, int unknown)
{
  canMove = can_move_in_direction(current_x + x, current_y + y);
  if (canMove) {
    if ((DAT_0011fe9c / 2 < current_y) && (current_y <= 100 - DAT_0011fe9c / 2)) {
      DAT_0011fe94 = DAT_0011fe94 + unknown;
    }
    current_y = current_y + unknown;
  }
}


//judging by the way it's called, I assume it's a function which loads in a map?
void load_map(long param_1)
{
  for (int y = 0; y < 100; y += 1) {
    for (int x = 0; x < 100; x += 1) {
      game_board[y * 100 + x] = *(undefined *)(y * 100 + param_1 + x);
    }
  }
}


//presumably clears the player's visible board
void clear_visibility()
{
  for (int y = 0; y < 100; y += 1) {
    for (int x = 0; x < 100; x += 1) {
      board_visible[y * 100 + x] = 0;
    }
  }
}


//function that checks if you are allowed to move in the given direction
//(so you don't noclip through walls or something)
bool can_move_in_direction(long x, long y)
{
  if (x > -1 && y > -1 && x < 100 && y < 100) {
    //check if there is a wall blocking the path
    if ((game_board[y * 100 + x] == '#') ||
       (game_board[y * 100 + x] == ' ')) {
      return false;
    }
    else {
      return true;
    }
  }
  else {
    return false;
  }
}

Findings


Hmmmm……alright. This is actually very useful.

Essentially, whenever we press W, our Y coordinate goes down. Whenever we press S, our Y coordinate goes up. Pressing A makes our X coordinate decrease, and pressing D makes our X coordinate increase. I’m unsure about what the third parameter is, but the first parameter is what we add to the X coordinate, and the second is what we add to the Y coordinate.

Makes sense. So, to have our Y coordinate be 0, we have to be at the top of the map. In order to have our X coordinate be 0, we have to be at the leftmost portion of the map. This means that the top left pixel is (0,0).

Soooo…..okay. Let’s try the Y coordinate first. We are going to be performing our test on the first level. The one that looks like this:

Now, KEEPING IN MIND the fact that the top left pixel is a wall, the position we’re currently at is around (1,1). Let’s first try to figure out what memory address the Y coordinate is stored in.

First, open GameConqueror, and link it to the current game process. Since we know our coordinate is 1, search for that value. Next, move down 1. Your Y position should now be 2. Search for 2. The list should be smaller than it was before. Now, move down 1 again. Your Y position should be 3. Rinse and repeat this process until you stumble upon a value that changes to the appropriate Y coordinate everytime you move up and down.

Do the same for the X coordinate. Start at the furthest left, move 1, search for the current X coordinate, until you find the X and Y memory addresses.

Great! You now have full control over your position and no longer need to adhere to the rules of physics. Congratulations Harry, you have passed your main wizardy exam (IDK I haven’t read Harry Potter).

So, when playing the level you may have noticed something strange. Some little portion of the map you couldn’t quite access, thanks to the walls blocking your path.

Let’s try “teleporting” to it, if you will. Set your X coordinate appropriately so that you are able to walk on that portion of the map. Walk around a bit to really get a view of it. And….

….aaaahhh

….I see….

But that’s how we basically get the flag. Teleport around until we get to portions of the map that aren’t visible to a normal user without access to your coordinates like how we do. Each elevation will have one letter (or string) that corresponds to a part of the flag. This will be in order, meaning the string we just found at elevation 1 will be the first part of the flag, the string at elevation 2 will be after that, then elevation 3 after, etc.

You can also set your elevation (I.E: what map gets loaded when you go up/down the stairs). Just do the same thing like you did with X and Y, except go up/down the stairs. Do note, that because of the decompiled C code shown earlier, your elevation was designed to be in a range between 1-10.

Also, make sure that your terminal window is large enough to fit the entire map in, especially for the last and 2nd-to-last elevations. I was able to figure out the 2nd-to-last elevation flag portion by increasing my terminal window sufficiently, but I was not able to get the last portion of the flag, so I ended up just guessing it (it was 3).

All in all, if you do things right, you’ll manage to spell out the flag completely and get your token to Hogwartz (god what am I even saying): picoCTF{ur_4_w1z4rd_DC74CBD3}

Leave a Reply