Reverse Engineering Writeup - 2026 Edition
Table of contents
In this article, we will present the various solutions to the challenges of the Jeanne d’Hack CTF 2026 for the Reverse category.
Reverse Intro
The source files for the challenge are available here.
Introduction to Reverse Engineering Challenge! Will you be able to conquer this devilish level?
This challenge is the first in the Reverse Engineering category. The goal of the challenge is to introduce beginners to this category.
To do this, a single HTML file is provided (index.html). This file contains a checkFlag function with the following code:
// The flag check is not straightforward. Reverse engineering required!
function checkFlag() {
const input = document.getElementById('flagInput').value.trim();
const errorMsg = document.getElementById('errorMsg');
// Aahah nobody will ever guess this!
if (btoa(input.split('').reverse().join('')) === 'b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK') {
errorMsg.textContent = '';
document.getElementById('congratsModal').style.display = 'flex';
document.getElementById('flag').textContent = 'JDHACK{' + input + '}';
} else {
errorMsg.textContent = 'Incorrect flag. Try again!';
}
}
The btoa function converts a string into base64. All operations are reversible, so we can retrieve the flag by applying
them to the string “b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK” in reverse order. Thus, we get:
>>> atob("b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK").split("").reverse().join("")
"JEaNn3_H4cK_2026_reVerse_iNtro"
Snake Game
The source files for the challenge are available here.
Une petite partie de Snake, ça vous tente ?
This challenge is the second in the Reverse Engineering category. This time, we are provided with a Python file whose code is as follows:
def check_flag(code):
# SSSssuper SSssecure!!!
return base64.b64encode(''.join(chr(((ord(c) + 2) % 256) ^ 0x23) for c in code)[::-1].swapcase().encode()).decode() == "dhpEd2YWFGJxRGRuYlIaYlpyaWJaclNOYlUKcxFlYlZETkFTVmIAUUBXZg=="
flag = input(">>> ")
if check_flag(flag):
print("Ssss Congratulations! You found the Sssecret flag! You can validate with: ")
print("JDHACK{" + flag + "}")
else:
print("\nGame Over! Try again.")
The idea of the challenge is the same as before, but with more operations and added complexity, in order to increase the
difficulty while remaining accessible for someone who has never done reverse engineering. We can break down
the check_flag function as follows:
- Start by shifting by 2 (Caesar cipher).
- XOR the result with 0x23.
- Reverse the string using
[::-1]. - Use
swapcaseto change the case of the resulting characters. - Encode the result in base64 and compare it to a static string.
By performing these operations in reverse order, we get:
import base64
encoded = "dhpEd2YWFGJxRGRuYlIaYlpyaWJaclNOYlUKcxFlYlZETkFTVmIAUUBXZg=="
decoded = base64.b64decode(encoded).decode()
swaped = decoded.swapcase()
reversed = swaped[::-1]
xored = ''.join(chr(ord(c) ^ 0x23) for c in reversed)
flag = ''.join(chr((ord(c) - 2) % 256) for c in xored)
print("Flag: JDHACK{" + flag + "}")
When running the previous script, we obtain:
Flag: JDHACK{cRaP!_SN@KES_d0n'T_KNoW_hoW_7O_keEp_53crE7s}
LoL
Since your last game of LoL, your account has been banned. Will you find a way to reactivate it?
The source files for the challenge are available here.
This challenge is the third in the Reverse Engineering category and the first to contain executable code. We are provided with a compiled program for Linux without symbols. We can decompile the program using software like Ghidra.
The code for the main function is as follows:
int main(void) {
fputs(&DAT_00104080,stdout);
fwrite("[League Of Losers] Game is starting\n",1,0x24,stdout);
fwrite("[League Of Losers] Error: cannot start game, you are BANNED!\n",1,0x3d,stdout);
do {
char *__s1 = (char *)FUN_001011c9();
if (__s1 == (char *)0x0) {
fwrite("[League Of Losers] Error: Fail to read input\n",1,0x2d,stdout);
return -1;
}
int iVar1 = strcmp(__s1,"help");
if (iVar1 == 0) {
FUN_00101624();
}
else {
iVar1 = strcmp(__s1,"play");
if (iVar1 == 0) {
iVar1 = FUN_00101278();
if (iVar1 == 0) {
fwrite("[League Of Losers] You are not banned anymore! But there are still other challenge s to solve ;)\n"
,1,0x60,stdout);
}
else {
fwrite("[League Of Losers] Error: cannot start game, you are BANNED!\n",1,0x3d,stdout);
}
}
else {
iVar1 = strcmp(__s1,"rank");
if (iVar1 == 0) {
fwrite("[League Of Losers] You current rank is: Dirt-III\n",1,0x31,stdout);
}
else {
iVar1 = strcmp(__s1,"unban");
if (iVar1 == 0) {
FUN_001014bd();
}
else {
iVar1 = strcmp(__s1,"quit");
if (iVar1 == 0) {
fwrite("[League Of Losers] Game shutting down\n",1,0x26,stdout);
return 0;
}
fprintf(stdout,"[League Of Losers] Error: Unknown command \'%s\'\n",__s1);
fwrite("[League Of Losers] Try \'help\' to see available commands\n",1,0x38,stdout);
}
}
}
}
free(__s1);
} while( true );
}
We can see that the program offers a minimalist interpreter with several commands. Among them, there is the command “unban” which, based on its name, can allow us to reactivate our account, as indicated in the description. Once renamed, the function has the following code:
void unban(void) {
int iVar1 = FUN_00101278();
if (iVar1 == 0) {
fwrite("[League Of Losers] Error: Account is not banned\n",1,0x30,stdout);
}
fwrite("[League Of Losers] Attempting to unban account\n",1,0x2f,stdout);
fwrite("[League Of Losers] Need authentication, enter Admin password:\n",1,0x3e,stdout);
void *__ptr = (void *)FUN_001011c9();
if (__ptr == (void *)0x0) {
fwrite("[League Of Losers] Error: Fail to read input\n",1,0x2d,stdout);
}
else {
iVar1 = FUN_00101284(__ptr);
if (iVar1 == 0) {
fwrite("[League Of Losers] Error: Authentication failed\n",1,0x30,stdout);
}
else {
fwrite("[League Of Losers] Authentication successful\n",1,0x2d,stdout);
fprintf(stdout,
"[League Of Losers] Welcome back Admin! You left this last time you connected \'JDHACK {%s}\'\n"
,__ptr);
fwrite("[League Of Losers] Account UNBANNED!\n",1,0x25,stdout);
DAT_00104320 = 0;
}
free(__ptr);
}
return;
}
From the many strings displayed on the screen via fwrite, we understand that the function FUN_001011c9 allows
reading user input, and the function FUN_00101284 handles authentication. To validate the challenge, it is
necessary to ensure that the latter returns a non-zero value. The code is as follows:
int FUN_00101284(char *param_1) {
int result;
char *ptr;
for (ptr = param_1; *ptr != '\0'; ptr = ptr + 1) { }
if ((long)ptr - (long)param_1 == 0x1c) {
if (((((((param_1[10] == '7') && (param_1[5] == 'r')) && (param_1[0x19] == 'n')) &&
((*param_1 == 'y' && (param_1[0xe] == 'N')))) && (param_1[0x15] == 'l')) &&
(((((param_1[0xc] == 'r' && (param_1[0x12] == 'H')) &&
((param_1[0xb] == 'e' &&
(((param_1[0x1b] == '!' && (param_1[4] == '4')) && (param_1[1] == '0')))))) &&
((param_1[0x14] == '_' && (param_1[0xd] == 'I')))) && (param_1[3] == '_')))) &&
((((param_1[0x11] == '7' && (param_1[9] == 'n')) &&
((param_1[7] == '_' &&
(((param_1[2] == 'u' && (param_1[0x16] == 'E')) && (param_1[0x13] == 'e')))))) &&
(((param_1[0x10] == '_' && (param_1[0x17] == 'G')) &&
((param_1[8] == 'e' &&
(((param_1[6] == '3' && (param_1[0xf] == 'g')) &&
((param_1[0x1a] == 'D' && (param_1[0x18] == 'E')))))))))))) {
result = 1;
}
else {
result = 0;
}
}
else {
result = 0;
}
return result;
}
The function starts by checking that the input size is 28 (0x1c) characters, then compares
each character one by one. If we rearrange the characters, we obtain the following flag: y0u_4r3_en7erINg_7He_lEGEnD!
CTF:Go
The source files for the challenge are available here.
You are an elite agent on a mission to diffuse a bomb planted by a terrorist group. Your objective: infiltrate the bomb’s security system and retrieve the PIN code needed to neutralize it before it’s too late.
The terrorists only had time to arm the bomb this morning and left behind a mysterious USB key containing the file
libpasscode.so. This file could hold valuable clues to help you diffuse the bomb. Your mission, should you choose to accept it: use your reverse engineering skills to decipher the contents of this file and diffuse the bomb before it’s too late.
For this challenge, we have a C library (libpasscode.so) that generates and verifies the PIN codes for a
bomb to be diffused. The objective is to find the current PIN code to diffuse the bomb.
From the statement, we know that the bomb has been armed only once and that this action took place during the day.
The library libpasscode.so contains the following three exported functions:
arm_bombget_error_messagecheck_flag
By analyzing the check_flag function, we find the following code:
int check_flag(char *pin) {
int iVar1;
uint uVar2;
time_t now;
char local_24 [20];
printf("[LibPasscode] Checking PIN: %s\n",pin);
iVar1 = strcmp(pin,"1337");
if (iVar1 == 0) {
uVar2 = 0xfffffac7; // -1337 as integer
}
else {
now = time((time_t *)0x0);
iVar1 = FUN_001011a0(((ulong)((SUB168(SEXT816(-0x7777777777777777) * SEXT816(now),8) + now >> 5)
- (now >> 0x3f)) % 0x5a0) / 10,local_24);
if (iVar1 == 0) {
iVar1 = strcmp(pin,local_24);
uVar2 = (uint)(iVar1 == 0);
}
else {
uVar2 = 0xffffffff;
}
}
return uVar2;
}
The function starts by comparing the PIN with “1337”. If they match, the value 0xfffffac7 is returned. Otherwise,
the current time is retrieved and passed as a parameter to the FUN_001011a0 function after undergoing a
series of arithmetic operations. This function has the following code:
int FUN_001011a0(int param_1,long param_2) {
int fd;
ssize_t r;
int value;
ulong uVar1;
char c [9];
fd = open("pincode.txt",0);
value = fd;
if (fd != -1) {
uVar1 = 0;
value = 0;
while( true ) {
r = read(fd,c,1);
if (r != 1) break;
if ((c[0] == '\n') || (c[0] == '\r')) {
if (value == param_1) goto end;
value = value + 1;
uVar1 = 0;
}
else if ((value == param_1) && (uVar1 < 0xb)) {
*(char *)(param_2 + uVar1) = c[0];
uVar1 = uVar1 + 1;
}
}
if (value == param_1) {
end:
*(undefined1 *)(param_2 + uVar1) = 0;
value = 0;
close(fd);
}
else {
value = -2;
close(fd);
}
}
return value;
}
The function opens the file “pincode.txt” and reads line by line until it reaches the line corresponding to the first
parameter (based on the current time). When the requested index is reached, the current line is copied into the param_2
variable. We can deduce that this function retrieves the line requested by the first parameter and stores the result in the second.
Returning to the check_flag function, we see that the result of reading the file is compared to our PIN.
If they match, the value 1 is returned; otherwise, 0.
To find the PIN, it’s necessary to understand how the “pincode.txt” file is generated. This generation is performed
in the arm_bomb function, whose decompiled code is as follows (after a reverse engineering pass):
int arm_bomb(void) {
int fd;
int random_value;
ulong now;
ssize_t sVar1;
int c;
uint hash;
char *ptr;
char *pcVar2;
char *local_50;
char line [10];
char local_39 [9];
fd = open("pincode.txt",0x241,0x1a4);
c = fd;
if (fd != -1) {
now = time((time_t *)0x0);
c = 0x4c;
ptr = "LibPasscode, A secure way to verify your PIN!";
hash = 0;
do {
ptr = ptr + 1;
hash = hash * 0x21 + c;
c = (int)*ptr;
} while (*ptr != '\0');
c = 0x90;
srand((int)((now & 0xffffffff) / 600) * 600 ^ hash);
ptr = line;
do {
do {
random_value = rand();
pcVar2 = ptr + 1;
*ptr = "0123456789"[random_value % 10];
ptr = pcVar2;
} while (local_39 != pcVar2);
local_39[0] = '\0';
sVar1 = write(fd,line,10);
if ((sVar1 == -1) || (sVar1 = write(fd,"\n",1), sVar1 == -1)) goto fail;
c = c + -1;
ptr = line;
local_50 = line;
} while (c != 0);
do {
c = rand();
ptr = local_50 + 1;
*local_50 = "0123456789"[c % 10];
local_50 = ptr;
} while (local_39 != ptr);
local_39[0] = '\0';
sVar1 = write(fd,line,10);
if ((sVar1 == -1) || (sVar1 = write(fd,"\n",1), sVar1 == -1)) {
fail:
c = -4;
close(fd);
}
else {
c = close(fd);
c = (uint)(c != -1) * 4 + -3;
}
}
return c;
}
We can make the following observations:
- The pseudo-random number generator (PRNG) used is from the standard C library (
srand/rand). - It is initialized with
srand(seed ^ hash(passphrase)), whereseedis a value based on the current time. - The PIN code is generated character by character with
rand() % len(letters).
The function then generates 0x90, or 144 values, followed by a last value, all written to the “pincode.txt” file. To find the PIN needed to diffuse the bomb, one must recover the seed used to initialize the random generator. Since the hashed value is constant, we can focus on the time. It is possible to brute-force all possible time values of the day, but how can we detect that we are testing the correct value?
We then recall the special code “1337” in the check_flag function. This could be a backdoor allowing the person who set the bomb to deactivate it in case of error. Testing this code on the web page or the physical version, we can see a sort of version displayed in the format LibPasscode v1.2.3-abcdef.
This version is returned by the third and last imported function: get_error_message:
char * get_error_message(int param_1) {
int iVar1;
undefined1 local_14 [12];
if (param_1 != -1337) {
switch(param_1) {
case -5:
return "Error generating pincode";
case -4:
return "Error writing pincode file";
case -3:
return "Error closing pincode file";
case -2:
return "Error reading pincode file";
case -1:
return "Error opening pincode file";
default:
return "Unknown error";
}
}
iVar1 = FUN_001011a0(0x90,local_14);
if (iVar1 == 0) {
snprintf((char *)string,0x40,"LibPasscode v1.2.3-%s",local_14);
} else {
snprintf((char *)string,0x40,"LibPasscode v1.2.3-ERROR");
}
return string;
}
In the case where the received error code is -1337, the FUN_001011a0 function is called to retrieve the last line of the “pincode.txt” file.
This is then used to construct the version displayed on the screen. Thus, when entering the code “1337”, we actually retrieve the last PIN
generated for the day. From this PIN, it is possible to brute-force the time at which the “pincode.txt” file was generated, generate it
ourselves, and calculate the current PIN based on the current time (rounded to the nearest 10 minutes).
The solution is as follows:
-
Retrieve the version: We send the special code
1337via the interface or API, allowing us to retrieve the version, for example:LibPasscode v1.2.3-2a1b3c. -
Brute-force the seed: We assume that the bomb was armed today (midnight UTC).
- For each possible timestamp from midnight until now:
- We initialize the PRNG as in the C library.
- We generate 145 PIN codes (144 slots + 1 version).
- If the hash matches the disclosed one, we have found the seed.
- For each possible timestamp from midnight until now:
-
Retrieve the current PIN code: We calculate the index of the current 10-minute slot, take the PIN code at that index from the generated list, and submit it via the interface or API to diffuse the bomb.
The script exploit.py implements this solution:
python exploit.py --url http://<IP>:5000
[*] Getting leaked version string from server...
[+] Leaked version string: LibPasscode v1.2.3-4763682466
[+] Extracted version hash: 4763682466
[*] Brute-forcing seed for today...
[Brute-force] |================================================= | 99% ETA: 0ss
[+] Found seed: 1752788400
[+] Current slot: 130, PIN: 6858607877
[*] Submitting PIN to get the flag...
[+] Server response: JDHACK{1ook_l!K3_tH3_anT!-TERR0r157_h@Ve_woN}
Jeanne d’Hack RPG - Level I
The source files for the challenge are available here.
You awaken in a dark forest, the crisp air filled with the haunting howls of wolves. Panic grips you as you realize you must flee from the shadows lurking just beyond the trees.
Will you survive the chase?
This challenge is the first level of a series of four challenges themed around RPGs (Role-Playing Games).
Each challenge is a shared library that must be analyzed to obtain a flag and progress to the next level. In this first level, the character wakes up in the middle of a forest, disturbed by sinister noises.
+==============================================================================+
| Awakening in the Forest |
+==============================================================================+
| |
| ( |
| ) |
| ( ( |
| ) |
| ( ( |
| ) /\ -( |
| ( // | (` |
| _ -.;_/ \\--._ |
| (_;-// | \ \-'.\ |
| ( `.__ _ ___,') |
| `'(_ )_)(_)_)' |
| |
+==============================================================================+
| You awaken to the sound of rustling leaves and distant howls echoing through |
| the dense forest. The flickering embers of your small campfire cast eerie |
| shadows around you, illuminating the rough bark of the trees that surround |
| your makeshift shelter. The night air is cool, and a sense of unease settles |
| in your stomach. |
| |
+==============================================================================+
| [0] You decide to investigate the source of the noise, feeling a mix of |
| curiosity and caution. |
| [1] You choose to ignore it and try to go back to sleep. |
+------------------------------------------------------------------------------+
At each step, one or more choices are available. For example, if you choose to go back to sleep, you end up eaten by wolves and die, which restarts the level.
To escape the wolves, you can analyze the decompiled code with an SRE like Ghidra, or more simply by trying all possible combinations. As choices are made, the character reaches the gate of a village. The gate is locked and requires a password. On failure, the wolves catch up and devour the character.
To find the password you must use Ghidra. The first level includes symbols, making its analysis relatively simple. Searching for strings related to the village gate leads to the function keep_moving_forward, whose code is as follows:
int keep_moving_forward(void) {
int iVar1;
undefined8 choices;
undefined8 img;
char *input;
char *dup;
int c;
choices = create_choices();
choices_add(choices,"Give the code to enter the village.");
c = 0;
while( true ) {
if ((c == L'0') || (c == L'1')) {
choices_dispose(choices);
return 0;
}
img = get_image("level1.wooden_door");
c = window_frame("The Village",img,
"You manage to get out of the forest and stumble accross a small village.\n
However the wolves are behind you so you run as quickly as you can to the\n
entry do or of the village which is closed. You knock at the door with all\n
of your rema ing strength until someone yield at you: \n\n
\"What is the secret code ?\"\n"
,choices);
if (c == L'0') {
window_clear();
input = (char *)window_prompt("What is the secret code: ");
dup = strdup(input);
dup = (char *)enc(dup);
iVar1 = strcmp("B1ofs@urX1t4tswhwDeM2w2m1od",dup);
if (iVar1 == 0) {
choices_dispose(choices);
free(dup);
iVar1 = enter_village(input);
return iVar1;
}
choices_dispose(choices);
free(dup);
iVar1 = attack_by_wolves(2);
return iVar1;
}
if ((c == 0x1b) || (c == 0x71)) break;
window_msg("I did not understand what you are saying");
}
choices_dispose(choices);
return -1;
}
The program obtains input via window_prompt, duplicates it with strdup, then passes that copy to the enc function.
Finally, strcmp checks whether the result of enc equals “B1ofs@urX1t4tswhwDeM2w2m1od”. Otherwise, the character
is attacked by wolves and loses. The enc function is as follows:
byte *enc(byte *input) {
for (byte *c = input; *c != 0; c = c + 1) {
*c = *c ^ 1;
}
return input;
}
This function XORs each character with the value 1. Since XOR is reversible, it is possible to recover the flag by applying enc
to the static string found in the program. The following Python code prints the password:
input_string = "B1ofs@urX1t4tswhwDeM2w2m1od"
xor_result = ''.join(chr(ord(c) ^ 1) for c in input_string)
print(xor_result)
The resulting string is C0ngrAtsY0u5urvivEdL3v3l0ne, which allows entry to the village and clears the level.
Jeanne d’Hack RPG - Level II
The source files for the challenge are available here.
Stumbling into the village, you find refuge among its warm lights. But as you explore, whispers of hidden treasures and forgotten tools beckon you.
What secrets will you uncover that could aid your journey?
For this second challenge, the player finds themself in the village in the middle of the night after escaping the wolves. To progress the story, the player must sleep in the stable to pass the night. Once morning comes, the player can explore the village: visit abandoned houses or talk with villagers. The villagers indicate that an item that could help on the journey is located in the tavern.
At the tavern, the player can play a card game against a drunk. Regardless of the choices made, the game always ends in defeat.
+==============================================================================+
| Tavern Challenge |
+==============================================================================+
| _____ |
| |A . | _____ |
| | /.\ ||A ^ | _____ |
| |(_._)|| / \ ||A _ | _____ |
| | | || \ / || ( ) ||A_ _ | |
| |____V|| . ||(_'_)||( v )| |
| |____V|| | || \ / | |
| |____V|| . | |
| |____V| |
| |
+==============================================================================+
| Welcome to the High Card Wins game! Draw a card and try to have the |
| highest card. You will play against the grizzled man. Good luck! |
| |
| You drew a Hearts (Value: 4). |
| |
+==============================================================================+
| [0] Discard you card to draw a new one |
| [1] Wait a turn |
| [2] End the game here |
+------------------------------------------------------------------------------+
Examining the card-game loop yields the following pseudocode (relevant excerpts):
int FUN_00101d95(void) {
undefined1 buffer [32];
char desc [128];
char game_choices [9];
char *cards [4];
cards[3] = "Diamonds";
cards[2] = "Hearts";
cards[1] = "Clubs";
cards[0] = "Spades";
memset(game_choices,L'0',9);
snprintf(desc,0x400,
"Welcome to the High Card Wins game! Draw a card and try to have the \n
highest card. You will play against the grizzled man. Good luck!\n\n
You drew a %s (Value: %d).\n"
,cards[(long)(d4() + -1) + 2], d6());
void *choices = create_choices();
choices_add(choices,"Discard you card to draw a new one");
choices_add(choices,"Wait a turn");
choices_add(choices,"End the game here");
int turns = 5;
int c = 0;
do {
while( true ) {
do {
if ((((c == L'0') || (c == L'1')) || (c == L'2')) || (turns < 1)) {
/* End the game here | 5 turns passed */
choices_dispose(choices);
if (DAT_00106190 == '\0') {
game_choices[5] = '0';
}
else {
game_choices[5] = '1';
}
if (DAT_00106192 == '\0') {
game_choices[6] = '0';
}
else {
game_choices[6] = '1';
}
if (DAT_00106191 == '\0') {
game_choices[7] = '0';
}
else {
game_choices[7] = '1';
}
game_choices[8] = '\0';
FUN_001016c0(game_choices,8,buffer);
iVar1 = memcmp(buffer,&DAT_00106170,20);
if (iVar1 == 0) {
uVar2 = FUN_00101c02(game_choices);
}
else {
uVar2 = FUN_00101c02(0);
}
return uVar2;
}
turns--;
c = window_frame("Tavern Challenge",get_image("level2.cards"),desc,choices);
game_choices[turns] = (char)c;
} while (c == 0x32);
if (c < 0x33) break;
LAB_00101faf:
if ((c == 0x1b) || (c == 0x71)) {
choices_dispose(choices);
return -1;
}
window_msg("I did not understand what you are saying");
}
if (c == 0x30) {
/* Discard you card to draw a new one */
...
}
else {
/* Wait a turn */
...
}
} while( true );
}
The player’s choices are stored in the array game_choices; additional bits (1 or 0) are appended based on
the values of global variables (DAT_00106190, DAT_00106191 and DAT_00106192). When the player chooses
“End the game here” or when five turns elapse, the program fills the array and calls FUN_001016c0.
void FUN_001016c0(undefined1 *param_1,uint param_2,long param_3) {
for (local_9 = 0; local_9 < 5; local_9 = local_9 + 1) {
*(undefined4 *)(param_3 + (ulong)local_9 * 4) =
*(undefined4 *)(&DAT_001060a0 + (long)(int)(uint)local_9 * 4);
}
local_20 = param_1 + (param_2 & 0xffffffc0);
local_18 = param_3;
for (local_80 = param_1; local_80 < local_20; local_80 = local_80 + 0x40) {
FUN_00101501(local_18,local_80);
}
local_21 = (byte)param_2 & 0x3f;
for (local_a = 0; local_a < local_21; local_a = local_a + 1) {
local_78[(int)(uint)local_a] = *local_80;
local_80 = local_80 + 1;
}
local_78[(int)(uint)local_21] = 0x80;
local_b = local_21;
while (local_b = local_b + 1, local_b < 0x40) {
local_78[(int)(uint)local_b] = 0;
}
if (0x37 < local_21) {
FUN_00101501(local_18,local_78);
for (local_c = 0; local_c < 0x38; local_c = local_c + 1) {
local_78[(int)(uint)local_c] = 0;
}
}
local_30 = &local_40;
local_40 = param_2 * 8;
local_38 = &local_3c;
local_3c = param_2 >> 0x1d;
FUN_00101501(local_18,local_78);
return;
}
void FUN_00101501(int *param_1,undefined8 param_2) {
for (local_9 = 0; local_9 < 0x10; local_9 = local_9 + 1) {
local_28[(int)(uint)local_9] = local_9;
}
FUN_001011f9(param_1,&local_48,param_2,local_28,&DAT_001060e0,&DAT_00106130,&DAT_00106164);
local_28[0] = 5;
for (local_a = 1; local_a < 0x10; local_a = local_a + 1) {
local_28[(int)(uint)local_a] = local_28[(int)(local_a - 1)] + 9 & 0xf;
}
FUN_001011f9(param_1,&local_68,param_2,local_28,&DAT_001060e0,&DAT_00106150,&DAT_00106169);
*param_1 = local_60 + local_44 + *param_1;
param_1[1] = param_1[1] + local_40 + local_5c;
param_1[2] = param_1[2] + local_3c + local_58;
param_1[3] = param_1[3] + local_38 + local_68;
param_1[4] = param_1[4] + local_48 + local_64;
iVar1 = *param_1;
*param_1 = param_1[1];
param_1[1] = param_1[2];
param_1[2] = param_1[3];
param_1[3] = param_1[4];
param_1[4] = iVar1;
return;
}
The code of this function and its subroutines performs many complex operations resembling a
cryptographic function. The array DAT_001060a0 contains the following values:
DAT_001060a0
001060a0 01 23 45 67 undefined4 67452301h
001060a4 89 ab cd ef undefined4 EFCDAB89h
001060a8 fe dc ba 98 undefined4 98BADCFEh
001060ac 76 54 32 10 undefined4 10325476h
001060b0 f0 e1 d2 c3 undefined4 C3D2E1F0h
These constants are typical of SHA-1 implementations,
which suggests that FUN_001016c0 computes a hash (SHA-1 or similar) of the first parameter
and stores it in the third. The result is then compared to a static array via memcmp.
The goal is therefore to find the sequence of choices that produces the expected hash:
e1 51 67 57 d5 87 9a 29 61 bf 5d fd 44 13 7e 75 ef 0f f5 fa
The first five bytes of the hash correspond to the five turn choices (characters ‘0’, ‘1’ or ‘2’) and the next three bytes depend on the global variables. Code analysis shows these variables are set to 1 when the player performs certain actions:
- $choices[5] = 1$ if the player slept in the stable, 0 otherwise.
- $choices[6] = 1$ if the player visited the abandoned house, 0 otherwise.
- $choices[7] = 1$ if the player spoke to the villager before going to the tavern, 0 otherwise.
It is possible to test all combinations manually or automatically with a tool like John the Ripper.
$ echo "e1516757d5879a2961bf5dfd44137e75ef0ff5fa" > hash.txt
$ john --mask='?1?1?1?1?1?2?2?2' -1='[012]' -2='[01]' --format=Raw-SHA1 hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA1 [SHA1 256/256 AVX2 8x])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
0g 0:00:00:00 N/A 0g/s 194400p/s 194400c/s 194400C/s 01022111..22222111
Session completed
Using the Raw-SHA1 format initially yields no matching combination, indicating either
a SHA-1 variant or a different algorithm. The presence of the constant 0x5C4DD124 identifies
RIPEMD-160 as an alternative candidate.
Running John with the ripemd-160 format finds a solution:
$ john --mask='?1?1?1?1?1?2?2?2' -1='[012]' -2='[01]' --format=ripemd-160 hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ripemd-160, RIPEMD 160 [32/64])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
02100101 (?)
1g 0:00:00:00 DONE (2025-11-07 20:17) 50.00g/s 64000p/s 64000c/s 64000C/s 10000101..10120101
Use the "--show" option to display all of the cracked passwords reliably
Session completed
The solution therefore corresponds to the following in-game actions:
- Sleep in the stable.
- Do not visit the abandoned house.
- Speak to the villager before going to the tavern.
- At the tavern turns: draw a new card on the first two turns, wait a turn on the third, then end the game on the fourth turn.
Doing so awards the player a sword usable later in the adventure, and yields the flag: JDHACK{02100101}.
Jeanne d’Hack RPG - Level III
The source files for the challenge are available here.
With a newfound prize in hand, your adventure takes you to the dark depths of the forest. Darkness looms ahead, stirring both excitement and dread…
What mysteries await within?
In this third challenge, the player returns to the forest from the first level. This time, a strange attraction pulls the player toward a dark cave. Whatever the initial choice, the player ends up in front of a dungeon door. Upon entering, the dungeon reveals itself: several options are then offered, for example moving to a neighboring room or searching the room for loot.
+==============================================================================+
| Dungeon |
+==============================================================================+
| -.._ | |-.._ | | |` .' |!____|_ |;' | _.! |
| | `-!._ | | `-!._ | `;;. . .-| | | | _!.i' |
| | |`-! | |`-! | ` | || |_._ __!_ !!.;' | |
| ``...__ | |``...__ | |`.;.i| |!-|| |' | | _|..- |
| |``--` |``--`_ | !| | ||____|_ _ `..!-' | |
| | | | | |`-_| |;' | ` | | | |
| ______|____!______|____!.!,.!| |!.,|._______ |_!..__|_____ |
| | | | | | | | | | || !' | | | |
| | | | | | |..!-i| |..||____|___ |. | | |
| | _!.-j | _!.-j | _.| || |' `| j `-!.._ | |
| _!.-'| | _!.-'| |..| ;| |`-.| __.___i |. |``-.. |
| ' | _.'' | _.' !-!| |. ||' ! ' ``._ | |
| |.| | |.| |.| | ||`!| __|____ |. | `.. |
| _.-' | . _.-' | . |.'/| |! |' | | ..| |-._ |
| .'| !.'|.'| !.'|.'| || | `.| ____-|_ | | `._ | |
| | .' | | .' | |//| |` ||. | |.! |.| |
| |_.'| .' |_.'| .' .' / \ || | ' `. | `._ |
| .' | .' | .' | .' |/| / \! | ___i__- | |`.| ` |
| ' !'| |' !'| | | / \|. | | | `. |
| |
+==============================================================================+
| Coordinate: (5, 5) |
| Facing: West |
| |
| You enter a flickering corridor. |
| |
| A chill runs down your spine as you step further inside. |
| |
+==============================================================================+
| [0] Right |
| [1] Forward |
| [2] Search the room for loot |
+------------------------------------------------------------------------------+
As the player advances through the dungeon, monsters appear that must be fought to survive. Searching for occurrences of the string “Dungeon” makes it possible to identify the function responsible for exploration (after some reverse engineering to name variables):
int main_dungeon_loop(long dungeon,undefined8 player) {
int fight_cooldown = d4();
do {
if (player_is_alive(player) == 0) {
return 0;
}
int open_room_count = 0;
void *choices = create_choices();
// Iterate over the 4 possible directions to see if there is a open room
for (i = 0; (int)i < 4; i = i + 1) {
local_36 = *(ushort *)
(&DAT_00125280 + ((long)(int)i + (long)(int)(uint)local_1a * 4) * 2);
currentRoom = get_room(dungeon->current_room);
has_room = FUN_0010e928(currentRoom,i & 0xffff);
if (has_room) {
choices_add(choices,currentRoom);
// ...
open_room_count = open_room_count + 1;
}
}
choices_add(choices,"Search the room for loot");
// Dynamically create description about where the player is and which direction he is facing
poVar6 = std::operator<<(aoStack_238,"Coordinate: (");
currentRoom = get_room(dungeon->current_room);
uVar3 = FUN_0010e620(currentRoom);
poVar6 = (ostream *)std::ostream::operator<<(poVar6,uVar3);
poVar6 = std::operator<<(poVar6,", ");
currentRoom = get_room(dungeon->current_room);
uVar3 = FUN_0010e632(currentRoom);
poVar6 = (ostream *)std::ostream::operator<<(poVar6,uVar3);
poVar6 = std::operator<<(poVar6,")");
std::ostream::operator<<(poVar6,std::endl<>);
poVar6 = std::operator<<(aoStack_238,"Facing: ");
// Check cooldown and trigger a fight if needed
if ((is_visited(currentRoom) == '\x01') || (fight_cooldown < d20())) {
should_fight = false;
}
else {
should_fight = true;
}
if (should_fight) {
fight(dungeon,player);
// ...
}
else {
fight_cooldown = fight_cooldown + d4();
description = std::__cxx11::string::c_str();
player_choice = window_frame("Dungeon",get_image(),description,choices);
currentRoom = get_room(dungeon->current_room);
if ((player_choice == 0x1b) || (player_choice == 0x71)) {
quit = false;
}
else {
if ((player_choice < 0x30) || (0x38 < player_choice)) {
window_msg("I did not understand what you are saying");
}
else {
if ((player_choice + -0x30 < list_length(choices)) && (player_choice + -0x2f <= open_room_count)) {
move_to_next_room(dungeon,local_1a);
}
else {
player_choice = (player_choice + -0x30) - (uint)open_room_count;
if (player_choice == 0) {
player_choice = loot_room(dungeon,player);
}
else if (player_choice == 3) {
player_choice = enter_weird_room(dungeon,player);
}
if ((player_choice == 0x1b) || (player_choice == 0x71)) {
quit = false;
}
}
}
LAB_0010f899:
choices_dispose(choices);
quit = true;
}
}
} while( !quit );
}
The functions were renamed from strings found in the subroutines. The game’s main loop performs the following steps:
- Check player state (alive/dead).
- Build the list of available choices according to the current room (iterate over 4 directions, add the “Search the room for loot” option).
- Dynamically generate the displayed description (coordinates and facing).
- Detect and manage a fight cooldown: if the room was not visited and the cooldown allows, trigger a fight; otherwise let the player choose an action through the interface.
- Handle the choice: change room (
move_to_next_room), search (loot_room) or enter a specific room (enter_weird_room). The option to enter the weird room does not appear among the offered choices.
During combat, the player fights a monster (goblin, skeleton or orc) until one dies. The player structure contains
the name and stats (Strength, Magic Energy, Hit Points, etc.).
gef> hexdump byte --size 34 $rsi
0x0000000012d1df70 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 test............
0x0000000012d1df80 00 00 00 00 00 00 03 00 0e 00 02 00 0f 00 10 00 ................
0x0000000012d1df90 12 00 ..
It is possible to modify these values in memory to increase the player’s HP or deal more damage. However, killing the monsters does not yield the flag. (Violence is therefore not the solution?)
Looking at loot_room, the code is:
int loot_room(Dungeon *dungeon,void *player) {
string loot [40];
void *room = get_room(&dungeon->current_room);
get_loot(loot,room);
bool isEmpty = std::__cxx11::string::empty((char *)loot);
if (isEmpty) {
window_msg("There is nothing in this room");
}
else {
void *msg = std::__cxx11::string::c_str();
/* try { // try from 00111c8b to 00111cd8 has its CatchHandler @ 00111cee */
window_msg(msg);
std::__cxx11::string::operator=(loot,"");
room = get_room(&dungeon->current_room);
set_loot(room,loot);
}
std::__cxx11::string::~string(loot);
return 0;
}
The loot_room function does the following:
- Retrieves the room’s loot.
- If the string is empty, displays “There is nothing in this room”.
- Otherwise, displays the loot and clears the room’s loot (to mark it as searched).
Unfortunately, even after searching the entire dungeon, no room appears to contain the flag.
The last interesting lead is enter_weird_room. This function calls a routine that
checks a condition; if true, the current room receives a message indicating a “weird room”,
the room’s loot is set, and a loop similar to the main loop offers to search the room or exit.
int enter_weird_room(Dungeon *dungeon,void *player) {
// ...
current_room = get_room(&dungeon->field_0x40);
uVar1 = FUN_0010e632(current_room);
current_room = get_room(&dungeon->field_0x40);
uVar2 = FUN_0010e620(current_room);
current_room = get_room(&dungeon->current_room);
uVar3 = FUN_0010e632(current_room);
current_room = get_room(&dungeon->current_room);
uVar4 = FUN_0010e620(current_room);
ret = FUN_0010ef98(uVar4,uVar3,uVar2,uVar1);
if (ret == 1) {
// ...
current_room = get_room(&dungeon->current_room);
set_loot(current_room,local_4e8);
current_room = get_room(&dungeon->current_room);
std::__cxx11::string::string<>
(local_98,"What a weird room! You wonder how you ended up here!",&local_69);
FUN_0010e644(current_room,local_98);
choices = create_choices();
choices_add(choices,"Search the room for loot");
while( true ) {
current_room = get_room(&dungeon->current_room);
FUN_0010e66e(local_68,current_room);
local_34 = window_frame("Weird Room",get_image("level3.wall_xxx"),current_room,choices);
std::__cxx11::string::~string(local_68);
if ((local_34 == 0x1b) || (local_34 == 0x71)) break;
if (local_34 == 0x30) {
loot_room(dungeon,player);
choices_dispose(choices);
goto end;
}
window_msg("I did not understand what you are saying");
}
choices_dispose(choices);
end:
ret = 0x71;
std::__cxx11::string::~string(local_4e8);
}
else {
ret = 0;
}
return ret;
}
The condition is determined by the result of FUN_0010ef98:
int FUN_0010ef98(ushort param_1,ushort param_2,ushort param_3,ushort param_4) {
double dVar1;
double dVar2;
dVar1 = (double)std::pow<int,int>((uint)param_1 - (uint)param_3,2);
dVar2 = (double)std::pow<int,int>((uint)param_2 - (uint)param_4,2);
dVar1 = sqrt(dVar2 + dVar1);
return (int)(dVar1 + 0.5);
}
Renamed to distance(x1,y1,x2,y2), the logic becomes clear: enter_weird_room checks whether the current room is
sufficiently close to another room (stored in field_0x40). If distance(...) returns 1, the weird room
activates and offers its content.
current_room = get_room(&dungeon->field_0x40);
y2 = get_y(current_room);
current_room = get_room(&dungeon->field_0x40);
x2 = get_x(current_room);
current_room = get_room(&dungeon->current_room);
y1 = get_y(current_room);
current_room = get_room(&dungeon->current_room);
x1 = get_x(current_room);
ret = distance(x1,y1,x2,y2);
By setting a breakpoint, the parameters passed to distance can be inspected, for example:
0x7f733110ff98 (
$rdi = 0x0000000000000005,
$rsi = 0x0000000000000004,
$rdx = 0x0000000000000005,
$rcx = 0x0000000000000002
)
In normal play, the mysterious room is not directly accessible from the menu. However, a hidden option magically leads to that room if the player is sufficiently close to the target room.
+==============================================================================+
| Weird Room |
+==============================================================================+
| -.._ | |-.._ | ___!_.__ | __._ |!____|___' | _.!|;' | _.! |
| | `-!._ | | `-!._ ! | ! | | _!.i' | | _!.i' |
| | |`-! | |`-! _.___|__ | __|_ _._ __!_._.;' | !!.;' | |
| ``...__ | |``...__ | | '|' | ' | _|..- | | _|..- |
| |``--` |``-- ___|_____|_____ ____|_ _ _!-' | `..!-' | |
| | | | | '| ! | | | | | |
| ______|____!______|_____ __..|..______|______-_____!..__|_____|_!..__|_____ |
| | | | | | | i !' | | | | | |
| | | | | | _ ____._!_______|_____|_____ | | |. | | |
| | _!.-j | _!.-` ' | | `|` '|' `-!.._ | j `-!.._ | |
| _!.-'| | _!.-'| _ _.__i____|____|_.___i___._ |``-..|. |``-.. |
| ' ┌──────────────────────────────────────────────────────────────────┐ |
| │ You found JDHACK{yOU_fOunD_ThE_$eCR3t_R0om} │ |
| _.│ │-._ |
| .'| │ Press any key to continue │ |
| | └──────────────────────────────────────────────────────────────────┘ |
| |_.'| .' |_.'| . ' | | | `. | `._ ' `. | `._ |
| .' | .' | .' | .' _i___ | __i__-___i___ | _ |`.| `| |`.| ` |
| ' !'| |' !'| .. | .. | `. | | `. |
| |
+==============================================================================+
| What a weird room! You wonder how you ended up here! |
| |
+==============================================================================+
| [0] Search the room for loot |
+------------------------------------------------------------------------------+
Other methods achieve the same result: modify distance’s return value in GDB or patch the conditional jump that depends on it.
Jeanne d’Hack RPG - Level IV
The source files for the challenge are available here.
Navigating the depths of the cave, you confront an inexplicable force. It’s a battle not just of strength, but of Will.
Can you harness the unknown to emerge victorious?
In this final level, the player awakens deep within a cave after the ground collapses beneath them in a secret maze room. While exploring the cave, the player encounters a dragon. Regardless of the choices made by the player, the dragon proves too powerful and defeats them.
+==============================================================================+
| The Dragon's Lair |
+==============================================================================+
| |===-~___ _,-' |
| -==\\ `//~\\ ~~~~`---.___.-~' |
| ______-==| | | \\ _-~` |
| __--~~~ ,-/-==\\ | | `\ ,' |
| _-~ /' | \\ / / \ / |
| .' / | \\ /' / \ /' |
| / ____ / | \`\.__/-~~ ~ \ _ _/' / \/' |
|/-'~ ~~~~~---__ | ~-/~ ( ) /' _--~` |
| \_| / _) ; ), __--~~ |
| '~~--_/ _-~/- / \ '-~ \ |
| {\__--_/} / \\_>- )<__\ \ |
| /' (_/ _-~ | |__>--<__| | |
| |0 0 _/) )-~ | |__>--<__| | |
| / /~ ,_/ / /__>---<__/ | |
| o o _// /-~_>---<__-~ / |
| (^(~ /~_>---<__- _-~ |
| /__>--<__/ _-~ |
| |__>--<__| / .---_ |
| |__>--<__| | /' _--_~\ |
| |__>--<__| | /' / ~\`\|
| \__>--<__\ \ /' // |||
| ~-__>--<_~-_ ~--____---~' _/'/ /' |
| ~-_~>--<_/-__ __-~ _/ |
| ~~-'_/_/ /~~~~~~~__--~ |
| ~~~~~~~~~~ |
| |
+==============================================================================+
| You venture deeper into the cave system. The air grows warmer |
| with each step, and a faint red glow appears ahead. |
| |
| As you round a corner, the tunnel opens into a massive cavern. |
| Your heart stops. In the center lies an enormous dragon, |
| its golden scales reflecting the dim light. |
| |
+==============================================================================+
| [0] Try to sneak past quietly. |
| [1] Attack while it's sleeping. |
| [2] Back away slowly. |
+------------------------------------------------------------------------------+
The function managing the battle against the dragon is defined as follows:
int dragon_battle(void *player)
{
int ret;
undefined8 uVar1;
undefined8 uVar2;
string local_48 [35];
allocator local_25;
int choice;
undefined8 choices;
bool won;
std::allocator<char>::allocator();
/* try { // try from 0010c92a to 0010c92e has its CatchHandler @ 0010cb0e */
std::__cxx11::string::string<>
(local_48,
"The dragon rises to its full height, towering above you."
"Its claws are like swords, i ts teeth like spears."
"This may be your final battle."
,&local_25);
std::allocator<char>::~allocator((allocator<char> *)&local_25);
/* try { // try from 0010c93b to 0010cafa has its CatchHandler @ 0010cb28 */
choices = create_choices();
choices_add(choices,"Strike at its heart.");
choices_add(choices,"Aim for its eyes.");
choices_add(choices,"Look for a weakness.");
ret = can_use_thuum(player);
if (ret == 0) {
choices_add(choices,"Use the Thu'um - the Voice.");
}
uVar1 = std::__cxx11::string::c_str();
uVar2 = get_image("level4.dragon_fire");
choice = window_frame("Dragon Battle",uVar2,uVar1,choices);
choices_dispose(choices);
if (choice == L'3') {
ret = can_use_thuum(player);
if ((ret == 0) && (ret = fight_dragon(), ret == 0)) {
won = true;
}
else {
won = false;
}
if (won) {
ret = show_win(player);
}
else {
ret = game_over_with_msg(player,"confused");
}
goto LAB_0010cafe;
}
if (choice < L'4') {
if (choice == L'2') {
ret = game_over_with_msg(player,"weakness");
goto LAB_0010cafe;
}
if (choice < L'3') {
if (choice == L'0') {
ret = game_over_with_msg(player,"heart");
goto LAB_0010cafe;
}
if (choice == L'1') {
ret = game_over_with_msg(player,"eyes");
goto LAB_0010cafe;
}
}
}
if ((choice == 0x1b) || (choice == 0x71)) {
ret = -1;
}
else {
ret = game_over_with_msg(player,"hesitation");
}
LAB_0010cafe:
std::__cxx11::string::~string(local_48);
return ret;
}
The program presents various choices that lead to the player’s death in different ways. However, one choice is different and
only appears if a certain condition is met. This condition is verified by the can_use_thuum function:
bool can_use_thuum(void *player) {
char string [19];
int i;
builtin_strncpy(string,"JDHACK",7);
i = 0;
while( true ) {
if (5 < i) {
return false;
}
if ((char)*(undefined2 *)((long)player + (long)i * 2 + 0x16) != string[i]) break;
i = i + 1;
}
return true;
}
This function checks if the player’s stats form the string “JDHACK”. Thus, it is necessary to create the following save file before starting the game:
$ echo $(echo -e "4a444841434b504c4a65616e6e6500000000000000000000000000004a0044004800410043004b0000000000" | sed 's/../\\x&/g') > player.save
$ xxd player.save
00000000: 4a44 4841 434b 504c 4a65 616e 6e65 0000 JDHACKPLJeanne..
00000010: 0000 0000 0000 0000 0000 0000 4a00 4400 ............J.D.
00000020: 4800 4100 4300 4b00 0000 0000 0a H.A.C.K......
Once this condition is met, the player can select the option “Use the Thu’um”, which then calls the function fight_dragon:
int fight_dragon(void) {
byte cVar1;
int ret;
size_t len;
void *s;
FILE *fp;
len = std::__cxx11::string::length(&DRAGON_WORDS);
s = std::__cxx11::string::c_str(&DRAGON_WORDS);
fp = fmemopen(s,len,"r");
if (fp == (FILE *)0x0) {
ret = 1;
}
else {
/* try { // try from 0010c71f to 0010c77a has its CatchHandler @ 0010c77e */
create_struct(&GLOBAL_STRUCT);
GLOBAL_FILE = fp;
parse_something();
fclose(fp);
do_loop(&GLOBAL_STRUCT);
cVar1 = get_flag(&GLOBAL_STRUCT);
if (cVar1 == 0) {
ret = get_top_value(&GLOBAL_STRUCT);
}
else {
ret = 1;
}
}
return ret;
}
The function starts by calling fmemopen, which allows the allocation of a file descriptor (FILE *) without creating a file on the system.
Once created, this descriptor is stored in a global variable. The pseudo-file contains a string initialized during the library
loading through initialization arrays.
// Called by INIT_1
void _INIT_1_1(int param_1,int param_2) {
if ((param_1 == 1) && (param_2 == 0xffff)) {
std::__cxx11::string::string<>
((string *)&DRAGON_WORDS,
"Dah Osos Ruvaak Oblaan Dah Osos Ruvaak Onik Oblaan Ahst Osos Qethsegol Tah Bormah Ob laan Dah..." /* TRUNCATED STRING LITERAL */
,local_19);
__cxa_atexit(std::__cxx11::string::~string,&DRAGON_WORDS,&PTR_LOOP_00129a30);
}
return;
}
This string appears to contain a series of words in an unknown language. Internet searches reveal that it is Dragon Language, as indicated on this page.
Returning to the fight_dragon function, it initializes another global variable via create_struct:
void create_struct(astruct *param_1) {
param_1->field27_0x30 = 0;
param_1->field28_0x38 = 1;
std::vector<>::clear((vector<> *)param_1);
std::vector<>::reserve((vector<> *)param_1,0x100);
return;
}
The create_struct function initializes a structure containing a C++ vector with an initial space for 100 elements. The fight_dragon
function then calls parse_something before closing the file descriptor. This function, along with its subroutines, performs many operations,
including encountering the error message: “fatal flex scanner internal error–no action found.” This indicates that the code utilizes
the parser flex, suggesting that parse_something reads the text written in the Dragon language.
The next function, initially called do_loop, contains the following code:
void do_loop(astruct *param_1) {
ulong uVar1;
undefined8 uVar2;
ulong uVar3;
bool run;
while( true ) {
uVar1 = param_1->field27_0x30;
uVar3 = FUN_00112bb6(¶m_1->field_0x18);
if ((uVar1 < uVar3) && (param_1->field28_0x38 != '\0')) {
run = true;
}
else {
run = false;
}
if (!run) break;
uVar2 = FUN_00112bda(¶m_1->field_0x18,param_1->field27_0x30);
FUN_0010d5fa(param_1,uVar2);
}
return;
}
This function contains an infinite loop that stops when certain conditions are met. The function FUN_0010d5fa
is called in each iteration and includes a switch-case statement that depends on the second parameter:
void FUN_0010d5fa(astruct *param_1, void *param_2) {
// ...
uVar1 = FUN_0011582a(param_2);
switch(uVar1) {
case 0:
uVar1 = FUN_0011583a(param_2);
FUN_0010dd76(param_1,uVar1);
break;
// ...
This structure is typical of a virtual machine. This technique is commonly used to obfuscate a program by creating a virtual processor with its own instruction set and operations. Therefore, functions and variables can be renamed to clarify the program’s intent, leading to the following function:
void run_vm(vm *vm) {
undefined8 instr;
ulong size;
ulong pc;
bool run;
while( true ) {
pc = vm->pc;
size = get_program_size(&vm->program);
if ((pc < size) && (vm->run != 0)) {
run = true;
}
else {
run = false;
}
if (!run) break;
instr = get_instruction(&vm->program,vm->pc);
execute_one_instruction(vm,instr);
}
return;
}
The vm->program field is a C++ vector, which has the following structure:
template <class T, class A = std::allocator<T>>
class vector {
public:
// public member functions
private:
T* data_; // points to first element
T* end_; // points to one past last element
T* end_capacity_; // points to one past internal storage
A allocator_;
};
It is possible to retrieve the code (often called Bytecode) of the virtual machine by setting a breakpoint
on the run_vm function and inspecting the contents of the vm structure:
(gdb) b *0x00007ff5519e5434 # Break at run_vm
(gdb) x/3gx ($rdi+0x18) # Inspect vector
0x7ff551a01b98: 0x00007ff55133cb50 0x00007ff55133de00
0x7ff551a01ba8: 0x00007ff55133eb50
(gdb) x/2wx 0x00007ff55133cb50 # Inspect program data
0x7ff55133cb50: 0x00000000 0x00000003
# Dump the whole program
(gdb) dump binary memory bytecode.bin 0x00007ff55133cb50 0x00007ff55133de00
After obtaining the program, it’s essential to analyze the switch-case structure to understand how the instructions are organized,
along with their respective operation codes (Opcode). The execute_one_instruction function contains the following pseudocode:
void execute_one_instruction(vm *vm,void *instr) {
int opcode = get_opcode(instr);
switch(opcode) {
case 0:
int value = get_operand(instr);
do_push(vm,value);
break;
case 1:
do_pop(vm);
break;
case 2:
...
}
}
Analyzing this function identifies the following opcodes:
| Value | Name | Description |
|---|---|---|
| 0 | PUSH | Adds a value onto the stack. |
| 1 | POP | Removes the value from the top of the stack. |
| 2 | LOAD | Loads a value from a specific memory address onto the stack. |
| 3 | STORE | Stores the value from the top of the stack at a specified address. |
| 4 | EXCH | Exchanges the two values at the top of the stack. |
| 5 | DUP | Duplicates the value at the top of the stack. |
| 6 | ADD | Adds the two values at the top of the stack. |
| 7 | SUB | Subtracts the top value from the one below it. |
| 8 | MUL | Multiplies the two values at the top of the stack. |
| 9 | DIV | Divides the top value by the one below it. |
| 10 | XOR | Performs a bitwise XOR on the two top values. |
| 11 | AND | Performs a bitwise AND on the two top values. |
| 12 | OR | Performs a bitwise OR on the two top values. |
| 13 | EQU | Compares the two top values for equality. |
| 14 | NOT | Performs a logical NOT operation on the top value. |
| 15 | LSS | Checks if the second top value is less than the first. |
| 16 | GTR | Checks if the second top value is greater than the first. |
| 17 | LEQ | Checks if the second top value is less than or equal to the first. |
| 18 | GEQ | Checks if the second top value is greater than or equal to the first. |
| 19 | NEG | Negates the top value. |
| 20 | JMP | Jumps to a specified instruction address unconditionally. |
| 21 | JFALSE | Jumps if the top value is false (zero). |
| 22 | JTRUE | Jumps if the top value is true (non-zero). |
| 23 | RET | Returns from a function call. |
| 24 | SYSCALL | Triggers a system “call”. |
| 25 | HALT | Stops program execution. |
Instructions are stored in the form of two integers: the opcode and the operand. From the opcode table above, it’s possible to write a disassembler that converts binary code into a textual representation.
Here’s a sample of the disassembler code:
class InstructionType:
PUSH = 0
POP = 1
...
INSTRUCTION_SET = {
InstructionType.PUSH: "push",
InstructionType.POP: "pop",
...
}
def instr_to_string(addr: int, opcode: int, operand: int = None) -> str:
instruction = "[{:04x}] ".format(addr)
instruction += INSTRUCTION_SET.get(opcode, "unknown")
if operand is not None:
instruction += " 0x{:x}".format(operand)
if 20 <= operand and operand <= 0x7f:
instruction += " '{}'".format(chr(operand))
return instruction
def disassemble(bytecode: list[int], start_addr: int = 0) -> str:
instructions = []
addr = start_addr
for i in range(0, len(bytecode), 2):
opcode = bytecode[i]
operand = bytecode[i + 1] if (i + 1) < len(bytecode) else None
instructions.append(instr_to_string(addr, opcode, operand=operand))
addr += 1
return "\n".join(instructions)
Running the disassembler give the following output:
$ python disassembler.py bytecode.bin
[0000] push 0x3
[0001] push 0x1e ''
[0002] jmp 0xfb
[0003] push 0x7
[0004] push 0x11
[0005] push 0xb9
[0006] jmp 0xca
[0007] push 0xa
[0008] push 0xb9
[0009] jmp 0x106
[000a] jfalse 0x16 ''
[000b] push 0xe
[000c] push 0x40 '@'
[000d] jmp 0xfb
[000e] push 0x11
[000f] push 0xb9
[0010] jmp 0xfb
[0011] push 0x7d '}'
[0012] syscall 0x2
[0013] pop 0x0
[0014] push 0x0
[0015] jmp 0x1a ''
[0016] push 0x1
[0017] push 0x1a ''
[0018] push 0x70 'p'
[0019] jmp 0xfb
[001a] push 0xa
[001b] syscall 0x2
[001c] pop 0x0
[001d] halt 0x0
[001e] unknown 0x43 'C'
[001f] unknown 0x68 'h'
[0020] unknown 0x6f 'o'
[0021] unknown 0x6f 'o'
[0022] unknown 0x73 's'
[0023] unknown 0x65 'e'
[0024] unknown 0x20 ' '
[0025] unknown 0x79 'y'
[0026] unknown 0x6f 'o'
[0027] unknown 0x75 'u'
[0028] unknown 0x72 'r'
[0029] unknown 0x20 ' '
Once the bytecode is disassembled, analysis can commence. Static analysis alone can be complex; thus, a dynamic analysis combined with static analysis is often helpful to save time.
The disassembler can be expanded to enable code emulation. The emulate function allows for executing the virtual machine’s bytecode:
def emulate(bytecode: list[int], start_addr: int = 0) -> None:
stack = []
pc = start_addr # Program counter
while pc < len(bytecode):
opcode = bytecode[pc]
operand = bytecode[pc + 1]
# Execute the instruction
if opcode == InstructionType.PUSH:
stack.append(operand)
elif opcode == InstructionType.POP:
stack.pop()
elif ...:
else:
raise Exception("Unknown instruction: 0x{:x}".format(opcode))
pc += 2
When the code is executed, we can see outputs that resemble the actual program behavior:
$ python disassembler.py bytecode.bin
Choose your words carefully Dovah
AAAAAAAAAAAAAAAAAAAAAA
Language is Knowledge, Knowledge is Power.
You are not powerfull enough!
It is possible to add ability to display executed instructions during emulation allows for tracing execution:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[01aa] dup 0x0
...
[01f0] ret 0x0
[000e] push 0xa
[0010] push 0xb9
[0012] jmp 0x106
[020c] push 0x254
[020e] exch 0x0
[0210] store 0x0
[0212] push 0x255
[0214] push 0x0
[0216] store 0x0
[0218] push 0x254
[021a] load 0x0
[021c] load 0x0
[021e] jfalse 0x24d
[0220] push 0x54 'T'
[0222] xor 0x0
[0224] push 0x37 '7'
[0226] equ 0x0
...
Examining the execution trace reveals that the program performs a series of comparisons, all preceded by arithmetic operations (add, sub, mul, xor, etc.). It can be beneficial to modify the emulator to display the manipulated values during these operations.
By showing values worked on during the trace, we can refine our analysis:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...
[0222] xor a: 65, b: 84
[0226] equ a: 21, b: 55
[022c] add a: 0, b: 0
[023c] add a: 185, b: 1
[024a] mul a: 65, b: 89
[024e] equ a: 5785, b: 4272
[0254] add a: 0, b: 0
[0264] add a: 186, b: 1
[0272] xor a: 65, b: 109
[0276] equ a: 44, b: 35
...
Focusing on the first few operations reveals that the xor value corresponds to the ASCII code of ‘A’. This value, when XORed with 85, gives the result (21), which is then compared to 55. The following repeated “add” operations can be ignored, refining the relevant output:
[0222] xor a: 65, b: 84
[0226] equ a: 21, b: 55
[024a] mul a: 65, b: 89
[024e] equ a: 5785, b: 4272
[0272] xor a: 65, b: 109
[0276] equ a: 44, b: 35
[029a] sub a: 65, b: 58
[029e] equ a: 7, b: 45
[02c2] div a: 65, b: 38
[02c6] equ a: 1, b: 3
[02ea] sub a: 65, b: 73
[02ee] equ a: -8, b: 24
[0312] xor a: 65, b: 56
[0316] equ a: 121, b: 108
[033a] add a: 65, b: 117
[033e] equ a: 182, b: 200
[0362] xor a: 65, b: 58
[0366] equ a: 123, b: 101
[038a] div a: 65, b: 68
[038e] equ a: 0, b: 1
[03b2] div a: 65, b: 48
[03b6] equ a: 1, b: 1
[03da] mul a: 65, b: 110
[03de] equ a: 7150, b: 12980
[0402] xor a: 65, b: 118
[0406] equ a: 55, b: 54
[042a] xor a: 65, b: 98
[042e] equ a: 35, b: 41
[0452] mul a: 65, b: 49
[0456] equ a: 3185, b: 5145
[047a] add a: 65, b: 106
[047e] equ a: 171, b: 184
From the observed operation, we can derive constraints to validate against our inputs:
a ^ 84 == 55 => a == 55 ^ 84 => a == 99 ('c')
By solving each of these constraints, we arrive at the input “c0NgraTS_D0v@KiN” which can be validated within the emulator:
$ python disassembler.py bytecode.bin
Choose your words carefully Dovah
c0NgraTS_D0v@KiN
Congratulations, you can validate with:
JDHACK{c0NgraTS_D0v@KiN}