Worthy_Knight
By DKvenom, 0-Day Aarhus
Description
Author : NomanProdhan
The gates of the Crimson Keep stand locked, sealed by cryptic runes from ages past. Many challengers have tested their might against these ancient wards—yet all were found wanting. Will you speak the correct incantation and earn the Keep’s hidden treasures? Prove your valor and stand among legends… if you truly are a Worthy Knight.
Links to Files
github.com/sajjadium/KnightCTF
Solve
┌──(DKvenom㉿)-[~/CTF/KnightCTF/Worthy_Knight]
└─$ file worthy.knight
worthy.knight: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=777ca166efbe8a9cbfdf18c865bad856843d9318, for GNU/Linux 4.4.0, stripped
Its a ELF executable so I open up Ghidra and begin reversing (I have changed some var's names). This function stood the most out because of all the ASCII art. Reversing this function, we find a lot of if statements. We see that only letters are accepted if (((uVar2 & 0x400) == 0) || (uVar3 = (*ppuVar5)[pbVar6[1]], (uVar3 & 0x400) == 0)) {. The enchantment is 10 characters long if (Input_Length == 10) {. And then we have the multiple if statements to check whether the input contains the right letters, as well as some XOR that we can calculate by mathematically isolating, e.g. (Input[1] ^ Input[0]) == 0x24 -> Input[1] ^ 0x24 == Input[0]. With all of this, we can already calculate 8 out of the 10 letters. The only 2 letters we can’t calculate using this method are Input[4] & Input[5]. But we can indeed brute-force the last 2, because we have here:local_10c = Input._4_2_ << 8 | (ushort)Input._4_2_ >> 8; and flag_compare = strcmp(local_f8,"33a3192ba92b5a4803c9a9ed70ea5a9c");. In Ghidra, _4_2_ means start with offset 4 and read 2 bytes forward (Input[4] & Input[5]). The way we can brute-force this is by using our target value and then swapping the 2 values as the C code suggests, and then we just try to find hex values that will match the target. But we can brute-force the last two unknown bytes because the binary transforms Input[4] and Input[5] and then verifies their MD5 hash. With the target:local_10c = Input._4_2_ << 8 | (ushort)Input._4_2_ >> 8;. In Ghidra, the notation _4_2_ means start at offset 4 and read 2 bytes — in other words, Input[4] and Input[5]. This expression performs a byte swap on those two bytes. After swapping the two, the program computes the MD5 hash of these two bytes and compares it against a fixed value flag_compare = strcmp(local_f8, "33a3192ba92b5a4803c9a9ed70ea5a9c"); Because we know the target MD5 hash, we can brute-force Input[4] and Input[5]. (Python script attached below)
undefined4 Main(void)
{
byte bVar1;
ushort uVar2;
ushort uVar3;
int flag_compare;
char *pcVar4;
size_t Input_Length;
ushort **ppuVar5;
byte *pbVar6;
undefined4 uVar7;
char *pcVar8;
long in_FS_OFFSET;
ushort local_10c;
undefined local_10a;
byte local_108 [16];
char local_f8 [32];
char local_d8 [16];
undefined Input [16];
undefined local_b8 [16];
undefined local_a8 [16];
undefined local_98 [16];
undefined local_88 [16];
undefined local_78 [16];
undefined local_68 [16];
undefined local_58 [16];
long local_40;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
puts(
" (Knight\'s Adventure) \n\n O \n <M> .---. \n /W\\ ( -.- )--------. \n ^ \\|/ \\_o_/ ) ^ \n /|\\ | * ~~~~~~~ / /|\\ \n / \\ / \\ / |\\ / / \\ \n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~\nWelcome, traveler. A mighty dragon blocks the gate.\nSpeak the secret incantation ( 10 runic letters) to continue.\n"
);
Input = (undefined [16])0x0;
local_b8 = (undefined [16])0x0;
local_a8 = (undefined [16])0x0;
local_98 = (undefined [16])0x0;
local_88 = (undefined [16])0x0;
local_78 = (undefined [16])0x0;
local_68 = (undefined [16])0x0;
local_58 = (undefined [16])0x0;
printf("Enter your incantation: ");
pcVar4 = fgets(Input,128,stdin);
if (pcVar4 == (char *)0x0) {
puts("\nSomething went awry. Fare thee well...");
}
else {
Input_Length = strcspn(Input,"\n");
Input[Input_Length] = 0;
Input_Length = strlen(Input);
if (Input_Length == 10) {
ppuVar5 = __ctype_b_loc();
pbVar6 = Input;
do {
uVar2 = (*ppuVar5)[*pbVar6];
if (((uVar2 & 0x400) == 0) || (uVar3 = (*ppuVar5)[pbVar6[1]], (uVar3 & 0x400) == 0)) {
puts("\nThe runes fail to align. The incantation is impure.");
puts(&DAT_001022b8);
goto LAB_0010124c;
}
if ((((uVar2 & 0x100) != 0) && ((uVar3 & 0x100) != 0)) ||
(((uVar2 & 0x200) != 0 && ((uVar3 & 0x200) != 0)))) {
puts("\nThe ancient seals do not resonate with your runes.");
puts(&DAT_001022b8);
goto LAB_0010124c;
}
pbVar6 = pbVar6 + 2;
} while (pbVar6 != Input + 10);
if ((byte)(Input[1] ^ Input[0]) == 0x24) {
if (Input[1] == 'j') {
if ((Input[2] ^ Input[3]) == 0x38) {
if (Input[3] == 'S') {
local_10a = 0;
pbVar6 = local_108;
local_10c = Input._4_2_ << 8 | (ushort)Input._4_2_ >> 8;
Input_Length = strlen((char *)&local_10c);
MD5((uchar *)&local_10c,Input_Length,pbVar6);
pcVar4 = local_f8;
do {
bVar1 = *pbVar6;
pcVar8 = pcVar4 + 2;
pbVar6 = pbVar6 + 1;
sprintf(pcVar4,"%02x",(ulong)bVar1);
pcVar4 = pcVar8;
} while (local_d8 != pcVar8);
local_d8[0] = '\0';
flag_compare = strcmp(local_f8,"33a3192ba92b5a4803c9a9ed70ea5a9c");
if (flag_compare == 0) {
if ((Input[6] ^ Input[7]) == 0x38) {
if (Input[7] == 'a') {
if ((byte)(Input[9] ^ Input[8]) == 0x20) {
if (Input[9] == 'i') {
printf("\n%s\n",
" The kingdom\'s gates open, revealing the hidden realm... \n ( ( \n \\ \\ \n .--. ) ) .--. \n ( )/_/ ( ) \n \'--\' \'--\' \n \"Huzzah! Thy incantation is true. Onward, brave knight!\" \n"
);
printf("The final scroll reveals your reward: KCTF{%s}\n\n",Input);
uVar7 = 0;
goto LAB_00101251;
}
puts("\nThe wards reject your Pair 5 second char.");
puts(&DAT_001022b8);
}
else {
puts("\nThe wards reject your Pair 5.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 4 second char.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 4.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe dragon\'s eyes glow red... The final seal remains locked.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 2 second char.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 2.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 1 second char.");
puts(&DAT_001022b8);
}
}
else {
puts("\nThe wards reject your Pair 1.");
puts(&DAT_001022b8);
}
}
else {
puts("\nScribe\'s note: The incantation must be exactly 10 runic symbols.");
puts(&DAT_001022b8);
}
}
LAB_0010124c:
uVar7 = 1;
LAB_00101251:
if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
return uVar7;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Solve Script
import hashlib
import string
# Target MD5 hash to match
target = "33a3192ba92b5a4803c9a9ed70ea5a9c"
# What we know from ghidra :
input_0 = ord('j') ^ 0x24
input_1 = ord('j')
input_2 = ord('S') ^ 0x38
input_3 = ord('S')
input_4 = None # Some 8 byte value
input_5 = None # Some 8 byte value
input_6 = 0x38 ^ ord('a')
input_7 = ord('a')
input_8 = 0x20 ^ ord('i')
input_9 = ord('i')
def xor(x, y):
return x ^ y
chars = string.printable
found = False
for a in chars:
for b in chars:
swapped = (b + a).encode() # Input[5], Input[4]
h = hashlib.md5(swapped).hexdigest()
if h == target:
input_4 = a
input_5 = b
found = True
break
if found:
break
final_Enchantment = (
chr(input_0) +
chr(input_1) +
chr(input_2) +
chr(input_3) +
input_4 +
input_5 +
chr(input_6) +
chr(input_7) +
chr(input_8) +
chr(input_9)
)
print(final_Enchantment)
NjkSfTYaIi
┌──(DKvenom㉿)-[~/CTF/KnightCTF/Worthy_Knight]
└─$ ./worthy.knight
(Knight's Adventure)
O
<M> .---.
/W\ ( -.- )--------.
^ \|/ \_o_/ ) ^
/|\ | * ~~~~~~~ / /|\
/ \ / \ /|\ / / \
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Welcome, traveler. A mighty dragon blocks the gate.
Speak the secret incantation (10 runic letters) to continue.
Enter your incantation: NjkSfTYaIi
The kingdom's gates open, revealing the hidden realm...
( (
\ \
.--. ) ) .--.
( )/_/ ( )
'--' '--'
"Huzzah! Thy incantation is true. Onward, brave knight!"
The final scroll reveals your reward: KCTF{NjkSfTYaIi}