On Friday/Saturday (yeah, right about when layoff news hit) I played a CTF for fun/to relax with some friends. Our choice was KnightCTF 2023and here are some writeups of the tasks I solved or helped solve (Reverse Engineering category only for now).
Update: Added a guest write-up for the Stegorev challenge by Sir P. Gently.
- [RE 100] Help Jimmy
- [RE 150] The Activator
- [RE 200] KrackMe 1.0
- [RE 250] Fan
- [RE 250] Take RISC five times
- [RE 400] Stegorev
[RE 100] Help Jimmy
Help Jimmy was a 64-bit Linux ELF binary, with a simple "game" implementation where the player had the choice to venture either into the Jungle or into the Sea. Regardless of the chosen path, it ended with Jimmy getting attacked by tigers or pirates and not getting the flag.
The game part was actually a distraction, as the actual purpose of the task was to notice that on the very top of the main() function there is something like this:
int x = 5;
int y = 5;
if (x != y) {
some_function();
}
While this is quite easy to spot in assembly, some decompilers (e.g. Ghidra) optimized out that branch and never showed it, thus making it easy to be missed.
Anyway, since this is a "just NOP the check" type of reverse engineering challenge (basically a classical crackme), the easiest way to solve is to either swap the comparison or NOP it out. This time around I've done the latter using GDB:
break *0x0000555555555629
r
set *(unsigned char*)0x555555555653 = 0x90
set *(unsigned char*)0x555555555654 = 0x90
c
Flag: KCTF{y0u_may_ch00s3_to_look_7h3_other_way_but_y0u_can_n3v3r_say_4gain_that_y0u_did_n0t_know}
[RE 150] The Activator
Actually this was a super similar task to Help Jimmy – i.e. it was also a classic crackme. In this case we were greeted in the main() function with with a whole set of complicated checks, which eventually just set one single flag (as in a boolean value denoting whether the input was deemed acceptable). This flag was then checked, and when true, the CTF flag was displayed.
if (checks_ok) {
show_flag();
} else {
std::cout << "Invalid Key." << std::endl;
}
This time around, instead of NOPing out the check, I just set RIP just before the show_flag() call:
KnightOS License Checker.
Enter KnightOS Activation Key: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b52992 in __GI___libc_read ...
(gdb) finish
Run till exit from #0 0x00007ffff7b52992 in __GI___libc_read ...
asdf
...
Value returned is $1 = 5
(gdb) set $rip=0x555555555bce
(gdb) c
Continuing.
KCTF{Th47_License_ch3cker_w4S_similar_t0_Wind0ws_95_OSR_Activator_Right?}
munmap_chunk(): invalid pointer
Program received signal SIGABRT, Aborted.
[RE 200] KrackMe 1.0
KrackMe 1.0 was actually solved in 99% by my friends and I just jumped in in the last moment to help with the final step. In general the task consists of a flag being split into four 9-character parts. Then each part was XORed with – effectively – a constant byte, and compared to static hardcoded values.
The issue we faced was with the 3rd part of the flag – for some reason we were getting the constant wrong. But, since the constant was just one byte, we eventually just brute forced it. Here's the script:
v13 = "mer`]MtGeaUG9UeDoU"
v14 = "(G~Ty_G{(v}QlOto|s"
v17 = "You don't have access to KrackMe 1.0 !"
v18 = "Since you are here let me ask you something..."
v15 = "Please enter the flag : "
v16 = "Oh My God ! What is that ?"
v20 = "Did you know, Bangladesh has the longest natural beach?..."
v13r = "mer`]MtGeaUG9UeDoU"
v14r = "(G~Ty_G{(v}QlOto|s"
for m in range(0,255):
flag = ""
for i in range(9):
flag += (chr((ord(v13r[i]) ^ ord(v20[14]) ^ ord(v16[8])) & 0xff))
for i in range(9):
flag += (chr((ord(v13r[i + 9]) ^ ord(v17[1]) ^ ord(v13r[1])) & 0xff))
mid = ""
for i in range(9):
mid += (chr((ord(v14[i]) ^ m) & 0xff))
flag += mid
for g in range(9):
flag += (chr((ord(v14r[g + 9]) ^ ord(v17[11]) ^ ord(v17[1])) & 0xff))
if "_" in mid: # Output filtering by guessing there will be _ in this part.
print(flag)
And the flag:
$ python go.py
KCTF{kRaCk_M3_oNe_(G~Ty_G{(xs_bAzar}
KCTF{kRaCk_M3_oNe_#Lu_rTLp#xs_bAzar}
KCTF{kRaCk_M3_oNe_0_fLaG_c0xs_bAzar}
KCTF{kRaCk_M3_oNe_ f_uX~fZ xs_bAzar}
KCTF{kRaCk_M3_oNe_
cZp]{c_
xs_bAzar}
KCTF{kRaCk_M3_oNe_aXr_ya]xs_bAzar}
KCTF{kRaCk_M3_oNe__0 #(0
_xs_bAzar}
[RE 250] Fan
In Fan we got the text output of Python's dis run on a program that outputted the flag. Given that I know Python bytecode pretty well (in the past I've even written a chapter for a reverse-engineering book about Python bytecode-level obfuscation), I immediately liked the task. Initially I thought about writing a small "compiler" for the text bytecode, but rejected this idea in favor of just re-implementing the function in Python. To keep myself honest I've done this while having the provided text file side-by-side with a console running python -m dis on my re-implementation. I do have to note that since I was using a different Python version, compilation artifacts were a little different – e.g. my version of Python didn't use the SETUP_LOOP opcode to setup for loops.
The whole thing took maybe 20 minutes. One place I initially changed however were calls to eval() – I replaced them with calls to my own fake_eval() which basically just printed the code to be executed. There turned out to be nothing interesting there however, but it's always good to check.
Re-implementation:
def define_false(s):
lstr = []
u = 0
packed = ''
for c in s:
if c == '[':
lstr.append((u, packed))
packed = ''
u = 0
continue
if c == ']':
num, prev_string = lstr.pop()
packed = prev_string + packed * num
continue
if c.isdigit():
u = u * 10 + int(c)
continue
packed += c
return packed
def define_true(p):
res = ''
for packed in p:
res += str(len(packed)) + '[:]' + packed
return res
def fake_eval(x):
return eval(x)
def define_both(p):
unpacked = []
for i in p:
packed = i.split(')')
char = ''
for j in packed:
if j == '':
break
j += ')'
char += fake_eval(j)
unpacked.append(char)
return unpacked
if __name__ == '__main__':
s = [
'chr(103)chr(48)chr(79)chr(97)chr(116)chr(125)',
'chr(105)chr(115)',
'chr(109)chr(69)chr(51)chr(115)chr(115)chr(105)',
'chr(115)chr(105)chr(85)chr(85)chr(85)',
'chr(75)chr(67)chr(84)chr(70)chr(123)'
]
s = s[::-1] # Initially I got the array ordering wrong.
print(define_false(define_true(define_both(s))))
And the flag: KCTF{:::::siUUU::::::mEssi::::::::::::::::::::::::::::::::is::::::gOat}
[RE 250] Take RISC five times
In this task we basically got an RISC-V assembly file (as in: text file) with one function. While I've implemented a small RISC-V emulator in the past, I honestly remember nothing about the architecture. Thankfully, it turned out that's not needed!
Since the assembly file turned out to be pretty well formatted, I decided to try to assemble it and link it with a minimal C program. At the very least this would give me some debugging capability – as well as, as pointed out by my friends, the ability to feed it to a decompiler. It turned out that GNU as accepted the file with two minor changes (changing the comment style from ; to # and adding a function symbol export in form of .globl fun_risc_v). After that I could assemble it and link it with my code, which provided main() and the sole external dependency for the function (please don't laugh at my reverse_str() implementation).
My code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern void fun_risc_v();
void reverse_str(char *str, int len) {
fwrite(str, 1, len, stdout);
putchar('\n');
char buf[1024];
for(int i = 0; i < len; i++) {
buf[len - i - 1] = str[i];
}
memcpy(str, buf, len);
}
int main(void) {
puts("xxx"); fflush(stdout);
fun_risc_v();
puts("yyy"); fflush(stdout);
return 0;
}
Compilation and running (note: I had to locate the required libraries on my filesystem, since they weren't where they needed to be; that's why I ended up invoking the loader directly with --library-path explicitly set):
#!/bin/bash
set +xe
riscv64-linux-gnu-gcc-12 go.c take-risc-five-times.S
/usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 --library-path /usr/riscv64-linux-gnu/lib/ ./a.out
Actually just running the code outputted the flag: KCTF{t4k3_r1sc_0r_p3ri5h}
[RE 400] Stegorev
Guest write-up by Sir P. Gently.
In this task we are given a JPEG and told to look through it. This, combined with the task's title, makes it clear that something is hidden in the image using steganography. By looking at the hacktricks section on stego tricks I found a tool called stegseek.
I was able to extract an ELF from the JPEG as follows:
By the way...
There are more blog posts you might like on my company's blog: https://hexarcana.ch/b/
$ stegseek ./kctf-rng70.jpg
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek
[i] Found passphrase: "this.parentNode.offsetWidth) {this.width=this.parentNode.offsetWidth-10; this.style.cursor='hand';this.onload=null;}">"
[i] Original filename: "stegorev-rng70".
[i] Extracting to "kctf-rng70.jpg.out".
By running the ELF file that was extracted from by stegseek with strace you can see that it checks if a file called ckrIupRS782prsdsf exists in the cwd:
openat(AT_FDCWD, "ckrIupRS782prsdsf", O_RDONLY) = -1 ENOENT (No such file or directory)
If you create this file in the cwd the application now outputs:
$ ./kctf-rng70.jpg.out
Flag:
?==? JBVE~k_lRciOX*=(HG=,0KKEl
What do you want me to return? :)
If you put the text it prints on the right side of the pseudo expression, JBVE~k_lRciOX*=(HG=,0KKEl, into the file you created you get the flag:
$ ./kctf-rng70.jpg.out
Flag: JBVE~k_lRciOX*=(HG=,0KKEl
KCTF{cRypT0_1S_su_hArd:e} ?==? JBVE~k_lRciOX*=(HG=,0KKEl
What do you want me to return? :)
While there was one other tasks in this category – The Defuser – my friend solved it and I have no idea what it was about. So no writeup 🤷.
Comments:
Add a comment: