No Strings Attached
- Category: rev
- Difficulty: Beginner
- Author: hoover
“The passphrase is the flag, but it is not written down anywhere in the file.”
strings yields guess:, correct and rejected and nothing else. The passphrase is assembled in .data at runtime and handed straight to strcmp, so the whole challenge is about watching the arguments to a single libc call.
Solution:
1. Confirm the flag is never on disk
Three imports, one of which is the entire attack surface:
$ readelf -sW no-strings-attached | grep UND | awk '{print $8}'
write@GLIBC_2.2.5
read@GLIBC_2.2.5
strcmp@GLIBC_2.2.5
main reads 127 bytes into input, strips a trailing newline, then runs a decode loop over a 56-byte blob at secret (0x404040) before comparing:
40115e: mov DWORD PTR [rbp-0x10],0x6 ; LCG seed
4011e2: imul eax,eax,0x41c64e6d ; state = state*1103515245 + 12345
4011eb: add eax,0x3039
4011f6: shr eax,0x10 ; keystream byte = state >> 16
4011fe: lea rdx,[rip+0x2e3b] ; <secret>
40120b: xor eax,edx
401220: cmp DWORD PTR [rbp-0xc],0x37 ; 56 bytes
40123a: call 401050 <strcmp@plt>
The multiplier/addend pair 0x41c64e6d/0x3039 is glibc’s rand() LCG. The plaintext exists only in the window between that loop finishing and strcmp returning.
2. Read it out of strcmp
That window is exactly what a library-call tracer prints, and the decoded passphrase is strcmp’s second argument:
$ echo AAAA | ltrace -s 200 ./no-strings-attached
write(1, "guess: ", 7) = 7
read(0, "AAAA\n", 127) = 5
strcmp("AAAA", "NNS{n0_str1ngs_1n_7h3_b1n4ry_bu7_ltr4c3_s4w_7h3_c0mp4r3}") = -13
write(1, "rejected\n", 9) = 9
-s 200 is load-bearing — ltrace truncates strings at 32 characters by default, which would cut a 56-byte flag off at NNS{n0_str1ngs_1n_7h3_b1n4ry_bu.
3. Static cross-check
Replaying the same LCG over the blob offline gives an identical string, confirming nothing in the runtime environment fed into it:
s = 6
out = bytearray()
for b in blob: # 56 bytes at vaddr 0x404040
s = (s * 0x41c64e6d + 0x3039) & 0xffffffff
out.append(b ^ ((s >> 16) & 0xff))
Flag: NNS{n0_str1ngs_1n_7h3_b1n4ry_bu7_ltr4c3_s4w_7h3_c0mp4r3}
Takeaways
- Decode-then-
strcmpis the weakest way to hide a secret: the PLT boundary is a free breakpoint that no amount of encoding gets past. - Always raise
ltrace -swhen the expected secret is longer than 32 bytes, otherwise the flag arrives silently truncated and looks like a dead end.