BoxCutter Writeup - Cyber Apocalypse 2024
→ 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.
→ 2 Key Techniques
The key techniques employed in this writeup are:
→ 3 Artifacts Summary
The downloaded artifact had the following hash:
$ shasum -a256 rev_boxcutter.zip
1549178544802894206f6b91bb8f9114f168201e0c9e7dcaaec88deca410ab6d rev_boxcutter.zip
The zip file contained a single file, cutter
:
$ unzip rev_boxcutter.zip
Archive: rev_boxcutter.zip
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:
- ELF: The binary is in ELF format, commonly used on Linux systems.
- x86-64: The binary is compiled for x86-64 bit machines.
- not stripped: Symbols have not been stripped so they will be visible when reverse engineering the binary.
$ file cutter
cutter: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, 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:
- The main function does not read any user input.
-
Lines 16-19 transform the 22 bytes starting from
local_28
by xor’ing each byte with0x37
-
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.
→ 5 Dynamic analysis
Since the main function appeared rather innocuous, the fastest way to solve the challenge was with a debugger.
-
The binary was loaded into
gdb
1$ gdb ./cutter
-
A breakpoint was added to the main function:
$ (gdb) b main
-
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/libthread_db.so.1". Breakpoint 3, 0x000055555555515d in main () => 0x000055555555515d <main+4>: 48 83 ec 20 sub rsp,0x20
-
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.
-
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
-
Program execution was continued:
(gdb) c Continuing. Breakpoint 2, 0x00005555555551c9 in main () => 0x00005555555551c9 <main+112>: e8 82 fe ff ff call 0x555555555050 <open@plt>
-
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])
b'HTB{tr4c1ng_th3_c4ll5}\x00'
→ 6 Conclusion
The flag was submitted and the challenge was marked as pwned