BKCTF 2023

BKCTF was the first CTF I attended on-site. It was hosted by BKSEC, a club at Hanoi University of Science and Technology (HUST), a leading engineering school in Vietnam. HUST is the dream of many students across the country. I remember BKSEC because I know some very skilled peoples such as chung96vn, lanleft, and hacmao.
rev/BabyStack (Hard)
- Given files: BabyStack.zip
- Difficulty: Hard
- Description: Stack up to the moon. Flag format:
BKSEC{}
0x00 My opinion
In my opinion, this challenge is not very hard. If you have a bit of experience with StackVM-style problems, you will find it quite easy. I will try to explain every small step so beginners can follow along. Happy hacking…
0x01 Overview & Clean code
The challenge gives us a 64-bit PE file named StackVM.exe
with more than 300 lines of pseudocode, mostly variable declarations and assignments.
At a glance, we can see the program initializes a series of bytecode for the VM like this:

Next, the program reads your input into Buffer
and checks whether its length is exactly 20.
|
|
We should redefine the sizes of the bytecodes[]
and Buffer[]
arrays to make the code cleaner.
Set Buffer[]
to 20 bytes and rename it to input[]
.

and set the bytecodes[]
array to 400 bytes.

Why did I calculate the size as 400 bytes?
Because bytecodes starts at
v24 [rsp+60h]
and ends atv131 [rsp+1E8h]
. So, 0x1E8 - 0x60 + 8 = 400.
Okay, the program is a bit shorter now. Looking at the code below, we can see it uses a vtable. It is a table of functions when the program needs a function, it jumps to the corresponding entry in that table and calls it.

I will create a struct for the vtable with a size of 40 bytes, which matches the size of v19
. Double-click vtable
, select all the functions, right-click, and choose Create struct …, name it struct_vtable
. I’ll keep the field names as they are for now and rename them later when the analysis is more detailed.

In StackVM challenges, almost everything happens on a single stack. Two things are essential:
stack_base
: the base address of the stackstack_esp
: the top-of-stack pointer
From the pseudocode, I’m quite sure v19[4]
is stack_base
and v19[3]
is stack_esp
, v19[1]
and v19[2]
are still unclear, so I won’t define them yet. Next, create a struct_vm
struct like this:

And cast the first field as a *struct_vtable
(the one we defined above).
Right-click v19
, choose Convert to Struct …, and select struct_vm
to apply the new structure to v19
.
0x02 VM Analysis
We can see that the input
is loaded into the bytecodes[]
array as follows:
|
|
If you look closely, the bytecodes
that hold the input appear in adjacent pairs. So it’s very likely the program processes the input
two bytes at a time.
The main processing routine is here:
|
|
Summary of the code above:
If [idx + 1] == 6
:
- The instruction is 4 bytes long, from
[idx]
to[idx + 3]
. - The value is the combination of
[idx + 2]
and[idx + 3]
. - It’s handled by the
PUSH
function.
If [idx + 1] != 6
:
- The instruction is 2 bytes long, from
[idx]
to[idx + 1]
. - Based on
[idx + 1]
, there are 8 different handler functions to call.- CMP = 0
- XOR = 1
- ADD = 2
- SUB = 3
- SHL = 4
- SHR = 5
- POP = 7
- AND = 8
Below is an example of how I renamed and re-typed the PUSH
function.
|
|
0x03 VM Emulator
After understanding how it works, I extracted all the values from the bytecodes[]
array and wrote a small Python script to see what operations the program performs.
|
|
I’ll start by analyzing a small part of the first results using the input abcdefghiklm12345678.
|
|
PUSH
3 numbers 0x1, 0xC0D, 0x8 onto the stack.ESP
will point to the value0x8
.SHR
is shift-right top 2 values on the stack: 0xC0D » 0x8 = 0xC.ESP
becomes 0xC.PUSH
2 numbers 0x2238 and 0xFF00, then doAND
. Result: 0x2238 & 0xFF00 = 0x2200.ADD
the top two values on the stack: 0x2200 + 0xC = 0x220C.PUSH
0x6261, which is the first 2 bytes of theinput
.XOR
the values: 0x220C ^ 0x6261 = 0x406D.CMP
that result with 0x694E.
The above is just my guess. To verify it, I’ll debug and check at the CMP
function to see if the logic is actually correct.

The result is completely correct.
Since we’ve dumped all the instructions, we could solve the flag by hand. But to save time, I’ll set breakpoints at the XOR
and CMP
functions to capture the final results.
|
|
rev/Reality (Medium)
- Given files: reality.zip
- Difficulty: Medium
- Description: A simple reversing challenge… Flag format:
BKSEC{}
The challenge gives us a PE32 file named reality.exe
. When I decompile it, IDA doesn’t show any pseudocode. I’ll read the assembly and debug it to understand what the program is doing.

Overall, the program reads the input and, when we debug it, it always jumps into the exception handler block.

In the neighboring block, I see a suspicious string: BKSEECCCC!!!. Checking the function sub_401220()
called there, it’s just XORing the input with that string as the key.
|
|
I debugged the program and changed EIP
to point to this block. Below it, there are lots of bytecodes. After using Make Code (in IDA) to disassemble them, I can see they perform very complex transformations.

At loc_40131F
, there is the instruction
|
|
This means it would loop forever at this point. I realized something was wrong, so I pressed d
to split everything into individual bytecodes, then pressed c
to make code again. The result is as follows.

Yeah, now it’s clear. The program checks if we are debugging. If yes, it jumps into that complex calculation block; otherwise, it jumps to loc_401AD5
.
Why do I know this is anti-debugging? You can see the answer in the reference below. stackoverflow.
At loc_401AD5
, the code only assigns values to cipher[]
. So we just need to take this array and XOR it with the key above to recover the flag.
|
|
Flag: BKSEC{e4sy_ch4ll_but_th3r3_must_b3_som3_ant1_debug??}
pwn/File Scanner (Medium)
- Given files: bkctf2023-file-scanner.zip
- Difficulty: Medium
- Description: The most powerful tool maybe the worst :(. Flag format:
BKSEC{}
0x01 Finding the bug
You might get a “permission denied” error even after chmod +x ./file_scanner
. I fixed it by creating a symlink with the correct, expected name so the loader can find it.
|
|
The program generates a 16-byte random value and requires us to enter that exact value.
|
|
We can fully bypass the strncmp
function by using the input \n
.
It’s clear the author intends us to use a File Structure Attack. In option 4, the program has a buffer overflow (BOF) bug as follows:
|
|
0x02 Exploiting BOF bug
From the name
variable, we can fully overwrite both filePtr
and fileContent
.

My idea is to create a fake file structure, then overwrite filePtr
so it points to this fake file. Every function in the vtable
is system()
from libc, and fakeFile.flags
points to the string /bin/sh\x00
. When we call fclose(filePtr)
, it will actually call system("/bin/sh")
.
To leak the libc base, we can open /proc/self/maps
or /proc/self/syscall
.
0x03 Final script
|
|
Flag: BKSEC{fSoP_1s_n0t_2_hArd_4_u_1fac8554f8eb55a103be3e34c9cf6940}