This page has been machine-translated from the original page.
I participated in the 1337UP CTF, which started on November 17, 2023, as part of 0nePadding, and we placed 32nd overall.
There were so many challenges that we could not solve them all, but I will briefly write up the ones we did solve.
Table of Contents
- Obfuscation(Rev)
- FlagChecker(Rev)
- Anonymous(Rev)
- imPACKful(Rev)
- Can We Fix It(Rev)
- Virtual RAM(Rev)
- Crack Me If You Can(Rev)
- Escape(Game Hacking)
- Over the Wire 1(Warmup)
- Over the Wire 2(Warmup)
- Summary
Obfuscation(Rev)
I think I made my code harder to read. Can you let me know if that’s true?
After unpacking the challenge binary, I found the source code for the obfuscated challenge binary and an encrypted data file named output.
After compiling the obfuscated source once and decompiling it with Ghidra, I found that the main function performs the following processing.
undefined8 main(int param_1,long param_2)
{
int __n;
FILE *pFVar1;
undefined8 uVar2;
char *__s;
if (param_1 != 2) {
printf("Not enough arguments provided!");
/* WARNING: Subroutine does not return */
exit(-1);
}
pFVar1 = fopen(*(char **)(param_2 + 8),"r");
if (pFVar1 == (FILE *)0x0) {
perror("Error opening file");
uVar2 = 0xffffffff;
}
else {
__n = o_0b97aabd0b9aa9e13aa47794b5f2236f(pFVar1);
__s = (char *)malloc((long)(__n + 1));
if (__s == (char *)0x0) {
perror("Memory allocation error");
fclose(pFVar1);
uVar2 = 0xffffffff;
}
else {
fgets(__s,__n,pFVar1);
fclose(pFVar1);
o_e5c0d3fd217ec5a6cd022874d7ffe0b9(__s,__n);
pFVar1 = fopen("output","wb");
if (pFVar1 == (FILE *)0x0) {
perror("Error opening file");
uVar2 = 0xffffffff;
}
else {
fwrite(__s,(long)__n,1,pFVar1);
fclose(pFVar1);
free(__s);
uVar2 = 0;
}
}
}
return uVar2;
}The o_e5c0d3fd217ec5a6cd022874d7ffe0b9 function called from there looked like this.
void o_e5c0d3fd217ec5a6cd022874d7ffe0b9(long param_1,int param_2)
{
int i;
if (param_2 != 0x18) {
/* WARNING: Subroutine does not return */
__assert_fail("o_8ce986b6b3a519615b6244d7fb2b62f8 == 24","chall.c",5,__PRETTY_FUNCTION__.0);
}
for (i = 0; i < 0x18; i = i + 1) {
*(byte *)(param_1 + i) =
*(byte *)(param_1 + i) ^
(byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^
0x37;
}
return;
}From this, we can see that the result of encrypting the flag in the *(byte *)(param_1 + i) ^
(byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^
0x37 part is written to the output file.
Therefore, I was able to obtain the flag with the following solver.
import struct
obs = b'\x2a\x00\x00\x00\x4d\x00\x00\x00\x03\x00\x00\x00\x08\x00\x00\x00\x45\x00\x00\x00\x56\x00\x00\x00\x3c\x00\x00\x00\x63\x00\x00\x00\x32\x00\x00\x00\x4c\x00\x00\x00\x0f\x00\x00\x00\x0e\x00\x00\x00\x29\x00\x00\x00\x57\x00\x00\x00\x2d\x00\x00\x00\x3d\x00\x00\x00\x10\x00\x00\x00\x32\x00\x00\x00\x14\x00\x00\x00\x05\x00\x00\x00\x0d\x00\x00\x00\x21\x00\x00\x00\x3e\x00\x00\x00\x46\x00\x00\x00\x46\x00\x00\x00\x4d\x00\x00\x00\x1c\x00\x00\x00\x55\x00\x00\x00\x52\x00\x00\x00\x1a\x00\x00\x00\x1c\x00\x00\x00\x20\x00\x00\x00\x38\x00\x00\x00\x16\x00\x00\x00\x15\x00\x00\x00\x30\x00\x00\x00\x26\x00\x00\x00\x2a\x00\x00\x00\x62\x00\x00\x00\x14\x00\x00\x00\x2c\x00\x00\x00\x42\x00\x00\x00\x15\x00\x00\x00\x37\x00\x00\x00\x62\x00\x00\x00\x11\x00\x00\x00\x14\x00\x00\x00\x5d\x00\x00\x00\x63\x00\x00\x00\x36\x00\x00\x00\x15\x00\x00\x00\x2b\x00\x00\x00\x50\x00\x00\x00\x63\x00\x00\x00\x40\x00\x00\x00\x62\x00\x00\x00\x37\x00\x00\x00\x03\x00\x00\x00\x5f\x00\x00\x00\x10\x00\x00\x00\x38\x00\x00\x00\x3e\x00\x00\x00\x2a\x00\x00\x00\x53\x00\x00\x00\x48\x00\x00\x00\x17\x00\x00\x00\x47\x00\x00\x00\x3d\x00\x00\x00\x5a\x00\x00\x00\x0e\x00\x00\x00\x21\x00\x00\x00\x2d\x00\x00\x00\x54\x00\x00\x00\x19\x00\x00\x00\x18\x00\x00\x00\x60\x00\x00\x00\x4a\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5c\x00\x00\x00\x19\x00\x00\x00\x21\x00\x00\x00\x24\x00\x00\x00\x06\x00\x00\x00\x1a\x00\x00\x00\x0e\x00\x00\x00\x25\x00\x00\x00\x21\x00\x00\x00\x64\x00\x00\x00\x03\x00\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x00\x1f\x00\x00\x00\x1f\x00\x00\x00\x56\x00\x00\x00\x5c\x00\x00\x00\x3d\x00\x00\x00\x56\x00\x00\x00\x51\x00\x00\x00\x26\x00\x00\x00'
obs = [struct.unpack("<I", obs[i:i+4])[0] for i in range(0,len(obs),4)]
with open("output", "rb") as f:
data = f.read()
for i in range(0x18):
print(chr(data[i]^0x37^obs[i]),end="")
# INTIGRITI{Z29vZGpvYg==}FlagChecker(Rev)
Can you beat this FlagChecker?
The Rust source code given as the challenge binary was as follows.
use std::io;
fn check_flag(flag: &str) -> bool {
flag.as_bytes()[18] as i32 * flag.as_bytes()[7] as i32 & flag.as_bytes()[12] as i32 ^ flag.as_bytes()[2] as i32 == 36 &&
flag.as_bytes()[1] as i32 % flag.as_bytes()[14] as i32 - flag.as_bytes()[21] as i32 % flag.as_bytes()[15] as i32 == -3 &&
flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 * flag.as_bytes()[11] as i32 - flag.as_bytes()[20] as i32 == 5141 &&
flag.as_bytes()[19] as i32 + flag.as_bytes()[12] as i32 * flag.as_bytes()[0] as i32 ^ flag.as_bytes()[16] as i32 == 8332 &&
flag.as_bytes()[9] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[8] as i32 & flag.as_bytes()[16] as i32 == 113 &&
flag.as_bytes()[3] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[5] as i32 + flag.as_bytes()[6] as i32 == 7090 &&
flag.as_bytes()[21] as i32 * flag.as_bytes()[2] as i32 ^ flag.as_bytes()[3] as i32 ^ flag.as_bytes()[19] as i32 == 10521 &&
flag.as_bytes()[11] as i32 ^ flag.as_bytes()[20] as i32 * flag.as_bytes()[1] as i32 + flag.as_bytes()[6] as i32 == 6787 &&
flag.as_bytes()[7] as i32 + flag.as_bytes()[5] as i32 - flag.as_bytes()[18] as i32 & flag.as_bytes()[9] as i32 == 96 &&
flag.as_bytes()[12] as i32 * flag.as_bytes()[8] as i32 - flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 == 8277 &&
flag.as_bytes()[16] as i32 ^ flag.as_bytes()[17] as i32 * flag.as_bytes()[13] as i32 + flag.as_bytes()[14] as i32 == 4986 &&
flag.as_bytes()[0] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[3] as i32 == 7008 &&
flag.as_bytes()[13] as i32 + flag.as_bytes()[18] as i32 * flag.as_bytes()[2] as i32 & flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 == 118 &&
flag.as_bytes()[0] as i32 % flag.as_bytes()[12] as i32 - flag.as_bytes()[19] as i32 % flag.as_bytes()[7] as i32 == 73 &&
flag.as_bytes()[14] as i32 + flag.as_bytes()[21] as i32 * flag.as_bytes()[16] as i32 - flag.as_bytes()[8] as i32 == 11228 &&
flag.as_bytes()[3] as i32 + flag.as_bytes()[17] as i32 * flag.as_bytes()[9] as i32 ^ flag.as_bytes()[11] as i32 == 11686 &&
flag.as_bytes()[15] as i32 ^ flag.as_bytes()[4] as i32 * flag.as_bytes()[20] as i32 & flag.as_bytes()[1] as i32 == 95 &&
flag.as_bytes()[6] as i32 * flag.as_bytes()[12] as i32 + flag.as_bytes()[19] as i32 + flag.as_bytes()[2] as i32 == 8490 &&
flag.as_bytes()[7] as i32 * flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 ^ flag.as_bytes()[0] as i32 == 6869 &&
flag.as_bytes()[21] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[11] as i32 == 4936 &&
flag.as_bytes()[16] as i32 + flag.as_bytes()[20] as i32 - flag.as_bytes()[3] as i32 & flag.as_bytes()[9] as i32 == 104 &&
flag.as_bytes()[18] as i32 * flag.as_bytes()[1] as i32 - flag.as_bytes()[4] as i32 + flag.as_bytes()[14] as i32 == 5440 &&
flag.as_bytes()[8] as i32 ^ flag.as_bytes()[6] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[12] as i32 == 7104 &&
flag.as_bytes()[11] as i32 * flag.as_bytes()[2] as i32 + flag.as_bytes()[15] as i32 == 6143
}
fn main() {
let mut flag = String::new();
println!("Enter the flag: ");
io::stdin().read_line(&mut flag).expect("Failed to read line");
let flag = flag.trim();
if check_flag(flag) {
println!("Correct flag");
} else {
println!("Wrong flag");
}
}It seemed to be validating whether the input string matched the flag, and from the implementation it was pretty clear that it was intended to be solved with Z3.
So I used the following solver to obtain the flag.
from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(22)]
s = Solver()
for i in range(22):
s.add(And(
(flag[i] >= 0x21),
(flag[i] <= 0x7e)
))
# INTIGRITI{
s.add(flag[0] == ord("I"))
s.add(flag[1] == ord("N"))
s.add(flag[2] == ord("T"))
s.add(flag[3] == ord("I"))
s.add(flag[4] == ord("G"))
s.add(flag[5] == ord("R"))
s.add(flag[6] == ord("I"))
s.add(flag[7] == ord("T"))
s.add(flag[8] == ord("I"))
s.add(flag[9] == ord("{"))
s.add(flag[21] == ord("}"))
s.add(flag[18] * flag[7] & flag[12] ^ flag[2] == 36)
s.add(flag[1] % flag[14] - flag[21] % flag[15] == -3)
s.add(flag[10] + flag[4] * flag[11] - flag[20] == 5141)
s.add(flag[19] + flag[12] * flag[0] ^ flag[16] == 8332)
s.add(flag[9] ^ flag[13] * flag[8] & flag[16] == 113)
s.add(flag[3] * flag[17] + flag[5] + flag[6] == 7090)
s.add(flag[21] * flag[2] ^ flag[3] ^ flag[19] == 10521)
s.add(flag[11] ^ flag[20] * flag[1] + flag[6] == 6787)
s.add(flag[7] + flag[5] - flag[18] & flag[9] == 96)
s.add(flag[12] * flag[8] - flag[10] + flag[4] == 8277)
s.add(flag[16] ^ flag[17] * flag[13] + flag[14] == 4986)
s.add(flag[0] * flag[15] + flag[3] == 7008)
s.add(flag[13] + flag[18] * flag[2] & flag[5] ^ flag[10] == 118)
s.add(flag[0] % flag[12] - flag[19] % flag[7] == 73)
s.add(flag[14] + flag[21] * flag[16] - flag[8] == 11228)
s.add(flag[3] + flag[17] * flag[9] ^ flag[11] == 11686)
s.add(flag[15] ^ flag[4] * flag[20] & flag[1] == 95)
s.add(flag[6] * flag[12] + flag[19] + flag[2] == 8490)
s.add(flag[7] * flag[5] ^ flag[10] ^ flag[0] == 6869)
s.add(flag[21] ^ flag[13] * flag[15] + flag[11] == 4936)
s.add(flag[16] + flag[20] - flag[3] & flag[9] == 104)
s.add(flag[18] * flag[1] - flag[4] + flag[14] == 5440)
s.add(flag[8] ^ flag[6] * flag[17] + flag[12] == 7104)
s.add(flag[11] * flag[2] + flag[15] == 6143)
while s.check() == sat:
m = s.model()
for c in flag:
print(chr(m[c].as_long()),end="")
print("")
break
# INTIGRITI{tHr33_Z_FTW}Anonymous(Rev)
Anonymous has hidden a message inside this exe, can you extract it?
Analyzing the file given as the challenge binary with ILSpy produced the following code.
In this program, it extracts from an icon hardcoded inside the program a string that satisfies specific conditions and then Base64-decodes it.
So I copied a few suitable strings from around the middle of the icon and Base64-decoded them, which gave me the flag.
imPACKful(Rev)
This program seems to be compressed but still can be executed, I wonder what could cause that..
The challenge binary was packed with UPX, so I unpacked it first.
However, even after decompiling the unpacked program, the program itself did not seem to do anything at all.
So I examined the sections of the unpacked program and found that a section named {N3v3R} had been registered there, and that turned out to be the flag.
Can We Fix It(Rev)
We have extracted this payload that some malware tried to dump.. Seems dumping it directly from memory debased it so it can’t run. Can you fix it?
The file given as the challenge binary was a PE file that could not be executed for some reason.
Since Windows no longer seemed able to load it as a PE file, I proceeded with the analysis while comparing it with the image below and the results from CFF Explorer.
Reference: PE Format - Win32 apps | Microsoft Learn
In the end, I found that incorrect alignment of the .text section was the reason the PE binary could not be executed.
So I changed the .text section’s VA to 0x1000, saved the file, and successfully obtained the correct flag.
Virtual RAM(Rev)
I wonder what the old man is talking about
The challenge provides a Game Boy ROM image as the binary.
This ROM image can be run, debugged, and cheated with using the BGB GameBoy Emulator.
Reference: BGB GameBoy Emulator (current version: BGB 1.5.10)
Reference: [Reverse] WPI CTF 2022 - PokemonRematch | TeamRocketIST - Portuguese CTF Team
When you talk to the old man on the game’s start screen, he tells you to check VRAM.
So I checked the VRAM information and found that the flag string was embedded in tiles outside the visible screen.
After cutting and pasting those together, I obtained the following string.
At first I could only read this as INTIGRITI{H3r0_0F_tIM3}, so I had a hard time getting it accepted. Then a teammate made the sharp observation that all of the vowels might be written as digits, which led us to identify the correct flag as INTIGRITI{H3r0_0F_t1M3}.
Crack Me If You Can(Rev)
Can you slay goliath?
Looking at the EXE given as the challenge binary, I found that it was a .Net program.
When I ran it, a GUI asking for some mysterious input appeared.
When I decompiled it with ILSpy, I found that several fairly complicated-looking functions had been defined.
The Main function decrypts encrypted byte data and retrieves methods via executingAssembly, so analysis seemed difficult in that form.
So I used ExtremeDumper to extract the decrypted binary from the running process.
Analyzing the extracted binary with ILSpy then made it possible to inspect the code for the GUI shown when the program starts.
Reading the code in this Main class, I found that it checks whether the output obtained by passing the input string and the predefined byte array enc to the unkownMethod function matches the string WhatAreYouDoingToChallenge.
private void Button1_Click(object sender, EventArgs e)
{
string @string = Encoding.UTF8.GetString(Resources.enc);
if (Operators.CompareString(unkownMethod(TextBox1.Text, @string), "WhatAreYouDoingToChallenge", TextCompare: false) == 0)
{
Interaction.MsgBox("You Solve It", MsgBoxStyle.Information, "Nice");
}
}
public string unkownMethod(string textToScramble, string password)
{
StringBuilder stringBuilder = new StringBuilder(textToScramble.Length);
int num = checked(textToScramble.Length - 1);
for (int i = 0; i <= num; i = checked(i + 1))
{
int index = i % password.Length;
char c = textToScramble[i];
c = Strings.ChrW(c ^ password[index]);
stringBuilder.Append(c);
}
return stringBuilder.ToString();
}The unkownMethod function in turn calls another function named checked.
The code for the checked function was not included in ILSpy’s decompilation result, but judging from the later processing, it just XORs the input characters with enc, so it seemed safe to ignore.
I was able to obtain the flag with the following solver.
ans = "WhatAreYouDoingToChallenge"
with open("enc", "rb") as f:
enc = f.read()
for i in range(len(ans)):
print(chr(ord(ans[i])^enc[i%len(enc)]),end="")
# intigriti{You_Are_Amazing}Escape(Game Hacking)
Your trapped inside a box. Can you escape it and do the reverse to get the flag?
The file given as the challenge binary was a Unity-made game program.
Launching it shows a mysterious object surrounded by walls on all four sides.
For now, it seemed that escaping from this walled area would allow me to get the flag.
At first, I thought I just needed to use CheatEngine to locate and tamper with the memory region that stores the coordinates while moving, but after repeated trial and error I unfortunately could not find the target memory region.
Next, as another approach, I considered modifying the game’s objects.
Reference: Reverse engineering unity game | tripoloski blog
Under Unity’s default settings, the compiled .Net program is placed at <Project>\Managed\Assembly-CSharp.dll, so I analyzed that with ILSpy.
Looking through the modules, I found a class named MyCharacterController.
By patching this module, it seemed likely that I could obtain the flag by cheating in one of several ways: moving the object’s starting position outside the wall, deleting the wall objects, removing wall collision so I could pass through them, or making the character jump over the walls.
To tamper with the code, I reopened the program in dnSpy instead of ILSpy. (ILSpy might also be able to patch it, but I could not find the menu, so I used dnSpy.)
Looking at the character object’s UpdateVelocity method here, I found that the jump height is controlled there.
So I added the following line to force an extremely high jump and then compiled the code.
Finally, I saved the file from Save All and launched the game.
This made an ordinary jump turn into a super-high jump, letting me clear the wall.
After getting over the wall, I was able to obtain the flag on the map.
Over the Wire 1(Warmup)
I’m not sure how secure this protocol is but as long as we update the password, I’m sure everything will be fine
When I opened the provided pcap in Wireshark, I found that files were being transferred over FTP.
So I first saved the file as binary data.
The file transferred over FTP (flag.zip) was a password-protected ZIP file, so next I looked for the password needed to extract it.
The password used during the FTP transfer was 5up3r_53cur3_p455w0rd_2022, so I tried extracting it with that password, but it failed.
220 pyftpdlib 1.5.9 ready.
USER cat
331 Username ok, send password.
PASS 5up3r_53cur3_p455w0rd_2022
230 Login successful.
SYST
215 UNIX Type: L8
PORT 192,168,16,131,179,47
200 Active data connection established.
LIST
125 Data connection already open. Transfer starting.
226 Transfer complete.
TYPE I
200 Type set to: Binary.
PORT 192,168,16,131,203,181
200 Active data connection established.
RETR flag.zip
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,132,11
200 Active data connection established.
RETR reminder.txt
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,162,139
200 Active data connection established.
RETR README.md
125 Data connection already open. Transfer starting.
226 Transfer complete.
QUIT
221 Goodbye.Reading the packets further, I found that a file containing the following message had also been received over FTP.
Using this hint, I changed the password to 5up3r_53cur3_p455w0rd_2023, which allowed me to extract the ZIP file and obtain the flag.
Over the Wire 2(Warmup)
When I examined the provided pcap file, I found that images were being exchanged over SMTP.
The first image was the following, but after checking it thoroughly I could not find anything suspicious.
So I followed the packets further and found that another email was also sending an image.
So I investigated this image next.
Analyzing the file with zsteg gave me the flag.
Summary
It was fun to be able to solve a lot of challenges again after a while.
It was also great to learn new things, such as dealing with packed .Net binaries and analyzing game programs.