Hi there,

Last weekend, I played a CTF for fun. Here are the writeups of the tasks that I solved (Reverse Engineering category)

[RE 100] Help Jimmy

It was a 64-bit Linux elf binary having a simple implementation where the player has to choose between going to Jungle or Sea and regardless of any decision, it ended up being attacked by tigers or pirates or an invalid choice and resulting in not getting the flag.

This challenge wants to examine the assembly code instead of blindly following the decompiled code from Ghidra or any decompiler, so there is a missing CALL instrn after the assignments and that function call is responsible for the flag.

Load the binary into radare2 and decompile it with the r2ghidra plugin which is more beneficial here as compared to traditional gdb to decompile and debug it.

radare2 disassembly: radare2 disassembly

radare2 - r2ghidra decompilation radare2 - r2ghidra decompilation

Pseudo code:

1
2
3
4
5
var_10h._0_4_ = 5;
var_10h._4_4_ = 5;
if (var_10h._0_4_ != var_10h._4_4_) {
    call_function();
}

The solution for this challenge would be just modifying any variable’s value to get into the check and call the function or directly skipping the check and setting rip to call_function would also work.

[0x55bfcefac629]> dr eax
0x00000005
[0x55bfcefac629]> dr eax=4
0x00000005 ->0x00000004
[0x55bfcefac629]> dc

Well Done!
You have found an alternate way for Jimmy!

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

This challenge is somewhat similar to first challenge, that is, Help Jimmy. I have followed the same approach as internally it has similar pattern.

There are lots of tools available to choose from and solve them, best way to learn is by doing manually then we can switch to tools. I have used radare2 and ghidra most of the time. In gdb/pwngdb/gef, we have to set the piebase and then look up the address from ghidra or ida then add them to piebase to get the address of the respective function. For example, to get the address of main(), load ghidra, set the base address to something like 0x555555554000 then go to the main() and note the address then set the same base with gdb then address mapping would be similar then put breakpoint and play with it.

In radare2, we don’t have to go through these processes manually, directly load the binary and analyze it then switch to main() and then pdf to get the disassembly. So, the following is the disassembly and decompilation of the code from the r2ghidra plugin.

radare2 disassembly:- call function which prints the flag after comparision activator_1

So, the approach would be nop out the check or after the input modify the rip value to the call instrn which will directly invoke the function and prints the flag.

[0x557615132860]> db 0x557615132bc5
[0x557615132860]> dc

           -KOS-

KnightOS License Checker.
Enter KnightOS Activation Key: 1234
INFO: hit breakpoint at: 0x557615132bc5
[0x557615132bc5]> dr rip=0x557615132bce
0x557615132bc5 ->0x557615132bce
[0x557615132bc5]> dc

KCTF{Th47_License_ch3cker_w4S_similar_t0_Wind0ws_95_OSR_Activator_Right?}
(3627) Process exited with status=0x0
[0x7f70ba718ca1]>

[RE 150] The Defuser

This challenge is about the bomb that has to be defused within some time frame. While executing this challenge it has signals which kill the process after some timeframe.

Following is the execution flow:

bsdb0y@test:~/challs/knight/reverse$ ./defuser

Time is ticking...

Defuse the bomb real quick if you want to save your Imaginarica!
I bet you won't be able to save it...
Hahahahaha...

Convince me to stop the explosion:




Hahahahaha LOSER!
The bomb has exploded.
You shouldn't have waste your time.
Remember, every second is valueable.



Terminated

So, after examining the disassembly and decompilation code, we can see that there are some things given below:

  • signals call
  • alarm call
  • setting parameter to 0x17b447b
  • env variables
    • LAB_HOSTNAME=DEFUSER_PC
    • LAB_USERNAME=FRITZ

Load the binary in radare2/gdb, set the environment variables to the value mentioned above and skip the signal/alarm calls then take the input from the user and step into the function at 0x2760. It expects the 1st argument to be 0x17b447b, then continues and it prints the flag.

[0x5610de303596]> dc



You have saved the country from a destruction.

Here is your reward:
KCTF{st0p_war_spr34d_10v3_and_p34c3}

(3995) Process exited with status=0x0

[RE 200] KrackMe 1.0

In this challenge, the flag was split into some strings then XORed with constant bytes then compared with static hardcoded strings. It had some checks on the command line argument, so provide 4 arguments to successfully get into the flag input state. So, for this, I had some approaches like:

  • either extract all the strings and then write a python code to do the same operation as manually examined from the assembly.
  • use angr to automate the flag-finding process and this binary was a perfect fit for the same.

I have decided to use angr and the following is the script:

#!/usr/bin/python3
 
import angr
import pdb
import sys
import claripy
 
FLAG_LEN = 40

def solve(elf_binary):
    project = angr.Project(elf_binary, load_options={'auto_load_libs': False}) #load up binary
    arg_chrs = [claripy.BVS('arg_%d' % i ,8) for i in range(FLAG_LEN)] #set a bit vector for argv[1]
    arg = claripy.Concat(*arg_chrs) 
    initial_state = project.factory.entry_state(args=[elf_binary, "q", "q", "q", "q"], stdin=arg) #set entry state for execution

    # To debug - insert the breakpoints at runtime
    '''
    def stoca(state):
        print("GOT HERE")
        import pdb; pdb.set_trace()
 
    initial_state.inspect.b('instruction', instruction=0x401613, when=angr.BP_AFTER, action=stoca)
    initial_state.inspect.b('instruction', instruction=0x401667, when=angr.BP_AFTER, action=stoca)
    initial_state.inspect.b('instruction', instruction=0x401311, when=angr.BP_AFTER, action=stoca)
    #initial_state.inspect.b('instruction', instruction=0x4018d9, when=angr.BP_AFTER, action=stoca)
    '''

    for byte in arg.chop(8):
        initial_state.solver.add(byte < 0x7f)
        initial_state.solver.add(byte >= 0x20)
 
    simulation = project.factory.simgr(initial_state) #get a simulation manager object under entry state
    simulation.explore(find=is_successful)
    
    if len(simulation.found) > 0: #if we find that our 'found' array is not empty then we have a solution!
        for solution_state in simulation.found: #loop over each solution just for interest sake
            print("[>>] {!r}".format(solution_state.solver.eval(arg,cast_to=bytes))) #print the goodness!
 

def is_successful(state):
    output = state.posix.dumps(sys.stdout.fileno()) #grab the screen output everytime Angr thinks we have a solution
    if b'Congratulations !!' in output: #make sure it says 'Congratulations'!
        return True
    return False
 

if __name__=="__main__":
    if len(sys.argv) < 2:
        print("[*] need 1 arguments\nUsage: %s [binary path]")
        sys.exit()
 
    solve(sys.argv[1]) #solve!

After executing the above angr script, here’s the output:

bsdb0y@test:~/challs/knight/reverse$ ./reverse.py krackme_1.out
WARNING  | 2023-01-30 14:35:45,601 | cle.loader     | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
WARNING  | 2023-01-30 14:35:45,643 | angr.simos.simos | stdin is constrained to 40 bytes (has_end=True). If you are only providing the first 40 bytes instead of the entire stdin, please use stdin=SimFileStream(name='stdin', content=your_first_n_bytes, has_end=False).
WARNING  | 2023-01-30 14:36:03,538 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory with an unspecified value. This could indicate unwanted behavior.
WARNING  | 2023-01-30 14:36:03,542 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING  | 2023-01-30 14:36:03,543 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING  | 2023-01-30 14:36:03,545 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING  | 2023-01-30 14:36:03,547 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING  | 2023-01-30 14:36:03,550 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0xffffffffffffffc0 with 64 unconstrained bytes referenced from 0x500038 (printf+0x0 in extern-address space (0x38))
WARNING  | 2023-01-30 14:36:03,555 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x0 with 65 unconstrained bytes referenced from 0x500038 (printf+0x0 in extern-address space (0x38))
WARNING  | 2023-01-30 14:36:26,106 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffefec9 with 23 unconstrained bytes referenced from 0x500028 (strlen+0x0 in extern-address space (0x28))
WARNING  | 2023-01-30 14:36:26,111 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff1b with 6 unconstrained bytes referenced from 0x500028 (strlen+0x0 in extern-address space (0x28))
WARNING  | 2023-01-30 14:36:28,008 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffefe19 with 7 unconstrained bytes referenced from 0x500028 (strlen+0x0 in extern-address space (0x28))
WARNING  | 2023-01-30 14:36:28,012 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffefe3b with 5 unconstrained bytes referenced from 0x500028 (strlen+0x0 in extern-address space (0x28))
[>>] b'KCTF{kRaCk_M3_oNe_0_fLaG_c0xs_bAzar}'

[RE 250] Fan

I liked this challenge, we are provided with python’s dis() output bytecode in the text file. For this challenge, I decided to manually re-implement the code from bytecode to python.

Following the re-implementation of python bytecode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 define_both(p):
    unpacked = []
    for i in p:
        packed = i.split(')')
        char = ''
        for j in packed:
            if j == '':
                break
            j += ')'
            char += eval(j)

        unpacked.append(char)
    return unpacked


if __name__ == '__main__':
  s = [
    'chr(75)chr(67)chr(84)chr(70)chr(123)',
    'chr(115)chr(105)chr(85)chr(85)chr(85)',
    'chr(109)chr(69)chr(51)chr(115)chr(115)chr(105)',
    'chr(105)chr(115)',
    'chr(103)chr(48)chr(79)chr(97)chr(116)chr(125)'
  ]

  print(define_false(define_true(define_both(s))))
bsdb0y@test:~/challs/knight/reverse$ python3 bytecode_reverse.py
:::::KCTF{:::::siUUU::::::mEssi::::::::::::::::::::::::::::::::is::::::gOat}

[RE 250] Take RISC five times

This challenge has provided an assembly file for riscv, the assembly function fun_risc_v calling reverse_str function multiple times. So, modified the asm file a little like replace; with # and put .globl in front of the fun_risc_v label then called this function from c code.

C code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
extern void fun_risc_v();
 
void reverse_str(char *str, int len) {
        char *temp;
        int index;
        for (index = 0; index < len; index--) {
                temp[index] = str[index];
                str[index] = str[len - index - 1];
                str[len- index - 1] = temp[index];
        }
}
 
int main(void) {
  fun_risc_v();
  return 0;
}

Compilation and linking with the RISC-V toolchain:

riscv64-linux-gnu-gcc-12 main.c take-risc-five-times.S /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 -o main

After the compilation, I am not able to execute it. I have tried qemu-system-riscv64 with --machine targets but nothing works. So, instead of executing I have used ghidra to decompile it and check. So, from the code, I got the flag:

  local_20 = 0x22563d53;
  local_1c = 0x46;
  local_1b = 0x54;
  local_1a = 0x43;
  local_19 = 0x4b;
  reverse_str(&local_1c,4);
  local_18 = 0x7b;
  local_17 = 0;
  local_30 = 0x3c222325;
  local_2c = 0x33;
  local_2b = 0x6b;
  local_2a = 0x34;
  local_29 = 0x74;
  reverse_str(&local_2c,4);
  local_28 = 0x5f;
  local_27 = 0;
  local_40 = 0x1b581a0a;
  local_3c = 99;
  local_3b = 0x73;
  local_3a = 0x31;
  local_39 = 0x72;
  reverse_str(&local_3c,4);
  local_38 = 0x5f;
  local_37 = 0;
  local_50 = 0x3775;
  local_4c = 0x72;
  local_4b = 0x30;
  reverse_str(&local_4c,2);
  local_4a = 0x5f;
  local_49 = 0;
  local_5c = 0x68;
  local_5b = 0x35;
  local_5a = 0x69;
  local_59 = 0x72;
  local_58 = 0x33;
  local_57 = 0x70;
  reverse_str(&local_5c,6);
  trap();
  trap();
                    /* WARNING: Bad instruction - Truncating control flow here */
  halt_baddata();
}

Flag: KCTF{t4k3_r1sc_0r_p3ri5h}

[RE 400] Stegorev

For this challenge, an image is provided so I have used the stegseek tool with rockyou.txt file to crack the password and extract the hidden ELF file.

bsdb0y@ubuarm64:~/Downloads$ stegseek kctf-rng70.jpg rockyou.txt
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".

while analyzing the extracted ELF file from stegseek, we can see that it checks whether the file named ckrIupRS782prsdsf is present in the pwd.

[0x5620da9a980f]> pdg

// WARNING: Variable defined which should be unmapped: var_8h

ulong main(void)

{
    char cVar1;
    ulong uVar2;
    ulong var_a0h;
    ulong var_80h;
    ulong var_51h;
    ulong var_30h;
    ulong var_8h;

    sym.imp.std::allocator_char_::allocator__(&var_51h);
    sym.imp.std::__cxx11::basic_string_char__std::char_traits_char___std::allocator_char___::basic_string_char_const__std::allocator_char__const_
              (&var_80h, "ckrIupRS782prsdsf", &var_51h);
    fcn.5620da9a9486(&var_a0h, &var_80h);
    fcn.5620da9a90a0(&var_80h);
    fcn.5620da9a9140(&var_51h);
    cVar1 = fcn.5620da9a9c13(&var_a0h, "The file maybe lost in the matrix. Try hard to find it");
    if (cVar1 != '\0') {
    // WARNING: Subroutine does not return
        sym.imp.exit(0);
    }
    ...
    ...
}

decompilation of function fcn.5620da9a9486(&var_a0h, &var_80h);:

[0x5620da9a980f]> pdg @fcn.5620da9a9486

// WARNING: Variable defined which should be unmapped: var_8h

ulong fcn.5620da9a9486(ulong arg1, ulong arg2)

{
    char cVar1;
    ulong uVar2;
    ulong var_250h;
    ulong var_248h;
    ulong var_240h;
    ulong var_220h;
    ulong var_11h;
    ulong var_8h;

    sym.imp.std::basic_ifstream_char__std::char_traits_char___::basic_ifstream__(&var_220h);    // => Here, var_220 belongs to file 
                                                                                                // => "ckrIupRS782prsdsf"
    sym.imp.std::basic_ifstream_char__std::char_traits_char___::open_std::__cxx11::basic_string_char__std::char_traits_char___std::allocator_char____const__std::_Ios_Openmode_
              (&var_220h, arg2, 8);
    cVar1 = sym.imp.std::basic_ifstream_char__std::char_traits_char___::is_open__(&var_220h);
    if (cVar1 == '\0') {

Later realized it is easily found by executing strace

bsdb0y@test:~/challs/knight/reverse$ ./kctf-rng70.jpg.out
Flag:
 ?==? JBVE~k_lRciOX*=(HG=,0KKEl
What do you want me to return? :)

enter the JBVE~k_lRciOX*=(HG=,0KKEl string into the file ckrIupRS782prsdsf then it directly dumps the flag:

bsdb0y@test:~/challs/knight/reverse$ echo "JBVE~k_lRciOX*=(HG=,0KKEl" > ckrIupRS782prsdsf
bsdb0y@test:~/challs/knight/reverse$ ./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? :)
bsdb0y@test:~/challs/knight/reverse$