forbytten blogs

BoxCutter Writeup - Cyber Apocalypse 2024

Last update:

1 Introduction

This writeup covers the BoxCutter Reversing challenge from the Hack The Box Cyber Apocalypse 2024 CTF, which was rated as having a ‘very easy’ difficulty. The challenge involved disassembling and debugging an x86-64 binary.

The description of the challenge is shown below.

BoxCutter challenge description

2 Key Techniques

The key techniques employed in this writeup are:

3 Artifacts Summary

The downloaded artifact had the following hash:

$ shasum -a256

The zip file contained a single file, cutter:

$ unzip
   creating: rev_boxcutter/
  inflating: rev_boxcutter/cutter

$ shasum -a256 cutter
c08975de9d50b6e44e7e12296ba44fd51d31d5a6fb6a7fb5b904b0bfc33cb955  cutter

4 Static analysis

4.1 Basic file identification

The file command was used to identify the binary. For the purposes of the challenge, the key properties identified were:

$ file cutter
cutter: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, BuildID[sha1]=f76eb244685ad0c3b817caa99093531754fc84c8, for GNU/Linux 3.2.0, not stripped

4.2 Ghidra

The binary was imported into Ghidra using default settings and the main function located. The decompiled C code has the following notable features:

  1. The main function does not read any user input.
  2. Lines 16-19 transform the 22 bytes starting from local_28 by xor’ing each byte with 0x37
  3. Line 20 opens a file where the filename is given by the string stored in local_28. Potentially the name of the file is the flag.
Disassembled main function in Ghidra

5 Dynamic analysis

Since the main function appeared rather innocuous, the fastest way to solve the challenge was with a debugger.

  1. The binary was loaded into gdb 1

    $ gdb ./cutter
  2. A breakpoint was added to the main function:

    $ (gdb) b main
  3. The program was run:

    $ (gdb) run
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: REDACTED/cutter
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/".
    Breakpoint 3, 0x000055555555515d in main ()
    => 0x000055555555515d <main+4>: 48 83 ec 20             sub    rsp,0x20
  4. The main function was disassembled:

    $ (gdb) disass main
    Dump of assembler code for function main:
       0x0000555555555159 <+0>:     push   rbp
       0x000055555555515a <+1>:     mov    rbp,rsp
    => 0x000055555555515d <+4>:     sub    rsp,0x20
       0x0000555555555161 <+8>:     movabs rax,0x540345434c75637f
       0x000055555555516b <+18>:    movabs rdx,0x68045f4368505906
       0x0000555555555175 <+28>:    mov    QWORD PTR [rbp-0x20],rax
       0x0000555555555179 <+32>:    mov    QWORD PTR [rbp-0x18],rdx
       0x000055555555517d <+36>:    movabs rax,0x374a025b5b035468
       0x0000555555555187 <+46>:    mov    QWORD PTR [rbp-0x11],rax
       0x000055555555518b <+50>:    mov    DWORD PTR [rbp-0x4],0x0
       0x0000555555555192 <+57>:    jmp    0x5555555551b0 <main+87>
       0x0000555555555194 <+59>:    mov    eax,DWORD PTR [rbp-0x4]
       0x0000555555555197 <+62>:    cdqe
       0x0000555555555199 <+64>:    movzx  eax,BYTE PTR [rbp+rax*1-0x20]
       0x000055555555519e <+69>:    xor    eax,0x37
       0x00005555555551a1 <+72>:    mov    edx,eax
       0x00005555555551a3 <+74>:    mov    eax,DWORD PTR [rbp-0x4]
       0x00005555555551a6 <+77>:    cdqe
       0x00005555555551a8 <+79>:    mov    BYTE PTR [rbp+rax*1-0x20],dl
       0x00005555555551ac <+83>:    add    DWORD PTR [rbp-0x4],0x1
       0x00005555555551b0 <+87>:    mov    eax,DWORD PTR [rbp-0x4]
       0x00005555555551b3 <+90>:    cmp    eax,0x16
       0x00005555555551b6 <+93>:    jbe    0x555555555194 <main+59>
       0x00005555555551b8 <+95>:    lea    rax,[rbp-0x20]
       0x00005555555551bc <+99>:    mov    esi,0x0
       0x00005555555551c1 <+104>:   mov    rdi,rax
       0x00005555555551c4 <+107>:   mov    eax,0x0
       0x00005555555551c9 <+112>:   call   0x555555555050 <open@plt>
       0x00005555555551ce <+117>:   mov    DWORD PTR [rbp-0x8],eax
       0x00005555555551d1 <+120>:   cmp    DWORD PTR [rbp-0x8],0x0
       0x00005555555551d5 <+124>:   jg     0x5555555551e8 <main+143>
       0x00005555555551d7 <+126>:   lea    rax,[rip+0xe26]        # 0x555555556004
       0x00005555555551de <+133>:   mov    rdi,rax
       0x00005555555551e1 <+136>:   call   0x555555555030 <puts@plt>
       0x00005555555551e6 <+141>:   jmp    0x555555555201 <main+168>
       0x00005555555551e8 <+143>:   lea    rax,[rip+0xe2e]        # 0x55555555601d
       0x00005555555551ef <+150>:   mov    rdi,rax
       0x00005555555551f2 <+153>:   call   0x555555555030 <puts@plt>
       0x00005555555551f7 <+158>:   mov    eax,DWORD PTR [rbp-0x8]
       0x00005555555551fa <+161>:   mov    edi,eax
       0x00005555555551fc <+163>:   call   0x555555555040 <close@plt>
       0x0000555555555201 <+168>:   mov    eax,0x0
       0x0000555555555206 <+173>:   leave
       0x0000555555555207 <+174>:   ret
    End of assembler dump.
  5. In the disassembly, the call to the open function was located:

    0x00005555555551c9 <+112>:   call   0x555555555050 <open@plt>

    A breakpoint was added to the line:

    b *0x00005555555551c9
  6. Program execution was continued:

    (gdb) c
    Breakpoint 2, 0x00005555555551c9 in main ()
    => 0x00005555555551c9 <main+112>:       e8 82 fe ff ff          call   0x555555555050 <open@plt>
  7. In x86-64, the first argument to a function is in the rdi register. This register was examined as a string, revealing the flag:

    (gdb) x/s $rdi
    0x7fffffffd980: "HTB{tr4c1ng_th3_c4ll5}"

5.1 (Optional) Revisiting the disassembled binary

With the behavior of the program clarified after debugging, the disassembled code can be revisited.

Prior to the loop that transforms the bytes in local_28, data is moved onto the stack:

00101161 48 b8 7f        MOV        RAX,0x540345434c75637f
         63 75 4c
         43 45 03 54
0010116b 48 ba 06        MOV        RDX,0x68045f4368505906
         59 50 68
         43 5f 04 68
00101175 48 89 45 e0     MOV        qword ptr [RBP + local_28],RAX
00101179 48 89 55 e8     MOV        qword ptr [RBP + local_20],RDX
0010117d 48 b8 68        MOV        RAX,0x374a025b5b035468
         54 03 5b
         5b 02 4a 37
00101187 48 89 45 ef     MOV        qword ptr [RBP + local_20+0x7],RAX

0x540345434c75637f is moved into register RAX on line 1 above, then line 7 moves the bytes into local_28. However, since x86-64 is little-endian, the actual bytes stored on the stack from lowest to highest address are the reverse: 0x7f63754c43450354.

0x68045f4368505906 is moved into register RDX on line 4 above, then line 8 moves the bytes into local_20. local_20 is actually at an 8 byte higher address than local_28, since the stack grows from high to low addresses so the moved bytes are contiguously after the bytes moved into local_28 and also reversed due to little-endian: 0x06595068435f0468.

0x374a025b5b035468 is moved into register RAX on line 9 but then moved into local_20+0x7 on line 12. Since the reverse is 0x6854035b5b024a37 and the leading 68 is the same as the trailing 68 from local_20 above, this means the bytes after local_20 are 0x54035b5b024a37

The discovered sequence of bytes can then be placed into a Python3 session and the xor decoding performed to arrive at the same flag, where the trailing \x00 is the null byte that would terminate the string.

$ python3
Python 3.11.8 (main, Feb  7 2024, 21:52:08) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> encoded_name = bytes.fromhex('7f63754c43450354') + bytes.fromhex('06595068435f0468') + bytes.fromhex('54035b5b024a37')
>>> bytes([b ^ 0x37 for b in encoded_name])

6 Conclusion

The flag was submitted and the challenge was marked as pwned

Submission of the flag marked the challenge as pwned