KnightCTF 2025
Heyaa!!
KnightCTF is held by a team originated from Bangladesh,
but I didn’t know so I kinda confused at first from how many ‘Indians’ are there.
All the challenge are quite easier than most CTFs nowaday,
but still fun regardless!
I played this CTF with my lovely team, LastSeenIn2026 <33
In respect to reversing, I actually learn how ctype works,
some Ghidra nuances, and practicing with IDA Pro (I’m still new with it XD).
Do note that the variables in decompiled code are mostly renamed for your viewing convenience :>
This write-ups also include helpful things I learned in this CTF,
and I wanna share it with y’all!!
Okay, without further ado, here’s the write-ups!
Happy reading! ;)
Binary Quest
We’re given binary.quest
, an ELF 64-bit LSB pie executable, x86-64
binary.
I say it now: all rev binary in this CTF are just that.
When I try out strings
,
I could see the output of the program,
and unexpectedly the first part of the flag:
Ghidra vs IDA Pro is weird
Let me explain the binary flow:
- Load unencrypted flag
- Encrypt it in memory
- Take input flag
- Encrypt it
- Compare the encrypted input and flag
- Success if they’re the same, else fail!
Well, if it’s just like that, then just peek at the flag before it gets encrypted! We should be able to do this statically.
So, I first tried this with IDA Pro, BUT-
If you go to main
, you’ll see this static data:
That’s just the same as the strings
command.
There’s no other modification to the flag prior to encryption,
so is it really the flag?
Weird.
Then I retry this with Ghidra,
new project, import binary, auto-analyze, (you get the gist)
and immediately:
|
|
See that? That’s just the hex value of the flag before it gets encrypted!
Ghidra ftw :3
Flag: KCTF{W4s_i7_e4sY?}
Knight’s Droid
Droid for the Android. Actually, it’s not an ELF this time, but .apk!
I decompile it with jadx
, and look around for non-library packages in the output source.
In the sources
folder,
you’ll see com/knightctf/knights_droid/
,
and there there’s three file there:
MainActivity.java
- This is what I dub the C
main
equivalent in .apk. I started analyzing here. The only notable thing is that it takes input from an interactable EditText, and pass it to theverifyFlag
function inSecretKeyVerifier
class.
- This is what I dub the C
R.java
- Static id for strings value used in the application. Sometimes useful for reversers; Irrelevant here.
SecretKeyVerifier.java
- This is where the flag check happens. I’ll go into details below.
Here’s what inside the SecretKeyVerifier
class:
|
|
Since the called function is verifyFlag
,
we’ll start there.
It’s clear that it checks the encrypted input with the encrypted flag.
There’s shift
here,
so it seems like this is just a caesar cipher,
and I was correct.
- It first get the first 10 characters of the package name as a ‘Key’ for calculating the shift.
- It then calculate the shift based on the key.
- Caesar shift the input to compare it to the shifted flag.
Well, there’s only 26 possible shift here, and it’s easy to just brute force them!! :D
I did that with CyberChef,
and tadaa!! There’s the flag! ^-^
Flag: KCTF{congrat5_KNIGHT_y0u_g0t_yOuR_Kn1gh7_dR01d}
Easy Path to the Grail
We’re given another x86_64 ELF binary, so just like previously, let’s hop to Ghidra! (f Ida)
|
|
In short, it encrypt the input and check it to the encrypted flag.
I assume that encrypted string is hexes (yes it is).
You see, there’s just one encryption method here :D
I hope it isn’t non-sensical–
|
|
So?
transform_input
pass each bytes into flip_byte
and store it as hex.
As it works in pointer,
it also make sure the encrypted input ends with a zero byte,
if you’re wondering.
What’s important here is flip_byte
.
It just, symmetrically flip the bits of the character and return that.
For example: flip_byte(0b00110010)
becomes 0b01001100
.
That’s all, really!
This can be easily decrypted by just doing the same operation, and I do that in Python:
|
|
Yayy!!
Flag: KCTF{e4sy_p3asY_r3v_ch4ll3nge}
Worthy Knight
ANOTHER ELF binary challenge…
As per usual, put it to Ghidra, then go to main
.
I replaced the lengthy output strings with short ones here, so it won’t drown out the code.
|
|
Yeah, honestly, that looks so disgusting, sorry T-T
So nestedd D:
That’s because it checks conditions one by one,
and fail separately with different error output.
Eww.
Okay, okay, what conditions?
Quite a handful, actually.
If you see based on line 19,
our input length should be 10.
And it checks by pair of characters, not byte by bite. So, our input is basically 5 pair of character. On line 24-25, at least one of them must be an uppercase character– (cut)
C-type Hell
This section is irrelevant to the solution
It’s just my rambles aboutctype
:p
Wa– Wait, how so?
The program uses C’s ctype
function to check the character classification,
like uppercase, lowercase, numeric, etc.
Based on ctype.h,
a call into it would return a bit masks of the argument byte,
with, according to ChatGPT, these bitmask table:
Flag | Hex Value | Meaning | Function Equivalent |
---|---|---|---|
_ISupper | 0x400 | Character is uppercase | isupper() |
_ISlower | 0x800 | Character is lowercase | islower() |
_ISalpha | 0x100 | Character is alphabetic | isalpha() |
_ISdigit | 0x04 | Character is a digit | isdigit() |
_ISxdigit | 0x80 | Character is a hexadecimal digit | isxdigit() |
_ISspace | 0x08 | Character is a whitespace character | isspace() |
_ISprint | 0x10 | Character is printable | isprint() |
_ISgraph | 0x20 | Character has a graphical representation | isgraph() |
_IScntrl | 0x40 | Character is a control character | iscntrl() |
_ISpunct | 0x200 | Character is a punctuation character | ispunct() |
Understand?
There’s two usage of ctype
here, at line 24 and 29.
The first check if any of the pair character is uppercase.
The second check if both pair character are not alphabetic, or not a punctuation.
Yeah? I think that’s how it works.
BUT NO. It doesn’t. ChatGPT is dead wrong.
So I tested it myself and here’s what I got:
Pair combination | Output |
---|---|
AA | “not resonate” |
Aa | “fail 1” |
A; | “impure” |
aa | “not resonate” |
a; | “impure” |
;; | “impure” |
The pair combination is repeated 5 times to get input length of 10. So, as you can see, it seems like the correct pair is with a lowercase and an uppercase!! :o
Well now that we know, what does it means?
I get “impure” output when there’s any punctuation character.
So I deduct,
that the first ctype
checks if any of the pair character is a punctuation.
Based on the usage of or
operator,
it’s clear that bit 0
means true for a classification.
Aight, how about the second ctype
?
Instead, it checks if both of the pair character is uppercase or lowercase. Note the usage of and
operator here!
From all of that, the ctype
bitmasks here actually does not conform to the ctype.h
.
That or I’m just a dummy who get this wrong.
At least with this binary, the bitmask table is:
Flag | Hex | Value | Meaning | Function Equivalent |
---|---|---|---|---|
_ISupper | 0x100 | Character | is uppercase | isupper() |
_ISlower | 0x200 | Character | is lowercase | islower() |
_ISpunct | 0x400 | Character | is alphabetic | isalpha() |
Now this one actually matches the ctype.h! So, what the frost. Don’t trust ChatGPT, guys. At least fact check it. I wasted hours stuck on this problem T-T
Oh well, how would that helps us solve the challenge?
Nothing really :>
It’s just interesting, and I wanna share it.
XOR
So, if you notice from the code highlights, there’s a check for each pair of character, for example on the first pair:
|
|
Ohh, wait, this is– really??
The value for the second ([1]
) character is 0x6A,
and in ASCII, that’s j
.
Ehh, now it also checks first ([0]
) character,
by XOR-ing it with j
and see if the result is 0x24
.
XOR is XOR, and you can un-XOR a byte by XOR-ing the XOR result with the other byte!
a ^ b = c
and c ^ b = a
.
So here, we can calculate what the first character like this:
|
|
So, just like that,
I got the first character.
This kind of check is also performed on the 2nd, 4th, and 5th pairs.
Just like that,
we know the flag would be NjkS??YaIi
Wait, how about the 3rd pair?
MD5
The 3rd pair is checked for its MD5 hash value, instead of the XOR check for other pairs. It’s still trivial to brute-force though, since it’s only 2 character!
Now the flag is complete: NjkSfTYaIi
.
BUT– when I actually try it,
it fails??
What happened was I didn’t notice a swap operation,
local_10c = input._4_2_ << 8 | (ushort)input._4_2_ >> 8;
(line 42)
So for the input, just swap them beforehand,
so that swap reverse the swap I did :>
There’s the flag!
Flag: KCTF{NjkSfTYaIi}
Knight’s Enigma
So, I actually up-solve this challenge using a gdb solver script by @mohammadolimat
(Discord).
That’s mostly just because my laptop is an Mac M1,
and debugging x86_64 binaries have always been a tough things for me.
I asked for QnQSec’s help regarding this issue and someone mentioned Google’s Cloud Shell Editor.
It’s a free x86_64 shell that you have admin access to,
so you can install whatever things you like there!!
What’s cool is that it has gdb preinstalled!! :D
Although, the VM hard reset after 20 minutes of inactivities,
and your system modification doesn’t persist across sessions.
I’m sure I can just make a setup script whenever I start a session there ;)
Big thanks to @5alv1.py
!! <33
Instead of a forward write-up, let me explain why this solver is how it is; reverse write-up!
|
|
You might tried this script with python3 solve.py
,
but that won’t work, duh.
You use this script by getting into gdb and typing source solve.py
!
This is because import gdb
only exist inside of gdb, and not in your usual python interpreter.
Aight, so what’s the deal with this challange?
It just checks if our encrypted input flag matches the encrypted target flag.
But, it’s really long, and most annoyingly, cryptic.
Here’s just one part of it all:
|
|
There’s A LOT MORE before and after this part.
Can you even understand what this does?
Maybe you can, but it’ll be hard, for sure.
This is where dynamic analysis, or debugging,
becomes really useful (when you can actually do it).
Someone said, this encryption is a one-to-one mapping, and that the table generation is just, weird. I wonder how the author made this. With that info, you can assume that there’s not gonna be any character mapping collision!
Anyway, here’s how the solver retrieve the flag.
There’s this memcmp
instruction to check the encrypted flag:
isCorrect = memcmp(enc_input,&target,0x22);
,
where enc_input is the encrypted input flag.
You’ll notice the 0x22
there,
and yup!
The flag length is indeed 34.
Since we’re doing this gdb,
where we don’t know the exact address of the call to memcmp
,
we can find its offset like this:
memcmp@plt
is the call, because it calls memcmp
based on procedure linked table, to adjust addresses based on where code sits in virtual memory using dynamic loading and linking.
I put a breakpoint there, and run the binary until it hit the breakpoint.
Just above the command prompt, you can see the stack trace of the program.
What matters is the function address of text eax, eax
,
the next instruction after main
’s call to memcmp
.
I repeatedly type fin
(finish
) to return from current function,
until I got to text eax, eax
Just above test
is where the call instruction is located,
and you can see the address is 0x555555555d73
!
That’s the exact breakpoint that the solver set.
Now, you might asks, “why rsi and rdi?”
Simply put, that’s just how function calling convention works here.
The binary set the value of rsi
and rdi
because that’s the argument to the memcmp
call.
For memcmp@plt
,
RDI
is the first argument (enc_input
),
RSI
is the second argument (&target
), and
EDX
as the third argument (`0x22).
|
|
That’s equivalent to memcmp(enc_input, &target, 0x22)
!
Since this is one-to-one mapping,
the encrypted character value is independent of other characters.
And that’s what the solver exploit here.
For each character index,
the solver brute force its value,
until it matches the corresponding target character at the same index.
That’s simple in principle!
Now what’s left is the implementation,
since static and dynamic solver much different.
For simulating input, this script uses file redirection,
like you can see here:
|
|
Well, I think that covers most of the things I learned for this challenge.
I’m really amazed by how my shell view seems tearing with this script running XD
Seeing the flag get constructed one byte at a time is quite a sight to behold :>
Flag: KCTF{c0ngrat5_knight_y0u_g0t_1t}