Flag Pointer Register
- Category: rev
- Difficulty: Beginner
- Author: hoover
“The provided x86-64 .exe decodes the flag successfully, yet it still prints the wrong message. Somewhere between the decoder and WriteFile, a perfectly good pointer ends up in the wrong place.”
A one-instruction bug, used as an excuse to learn where the Windows x64 calling convention puts each argument.
Solution:
1. The decoder works fine
0x140001000 XORs 0x39 bytes at 0x140003000 against an 8-byte repeating key at 0x140002000, then returns the decoded buffer in rax:
140001010: mov r8d,eax
140001013: and r8d,0x7 ; key index = i & 7
140001017: movzx r8d,BYTE PTR [r8+rcx*1]
14000101c: xor BYTE PTR [rax+rdx*1],r8b
140001023: cmp rax,0x39
140001029: lea rax,[rip+0x1fd0] ; return <flag>
140001030: ret
Nothing is wrong here — by the time this returns, the plaintext is sitting in .data.
2. rdx gets the wrong buffer
Windows x64 passes the first four integer arguments in rcx, rdx, r8, r9, so for WriteFile(hFile, lpBuffer, nBytes, lpWritten, NULL) the buffer belongs in rdx. Immediately after the decoder returns:
1400010b8: call 0x140001000 ; rax = decoded flag @ 0x140003000
1400010bd: mov rcx,QWORD PTR [rip+0x1f7c] ; hFile = stdout
1400010c4: mov QWORD PTR [rsp+0x20],0x0 ; 5th arg into shadow space
1400010cd: lea rdx,[rip+0xf6c] ; 0x140002040 <-- wrong buffer
1400010d4: mov r8d,0x39 ; ...but the decoder's length
1400010da: mov r9,rdi
1400010dd: call rbx ; WriteFile
rax is simply discarded and rdx is loaded with a static string instead. The length is the tell: r8d = 0x39 is the decoded buffer’s size, not the size of the message that actually gets printed.
$ echo | wine flag-pointer-register.exe
Press ENTER to receive the flag.
Access denied: RDX points to the wrong output buffer.
3. Put the pointer where WriteFile expects it
Replace the 7-byte lea rdx,[rip+0xf6c] with mov rdx,rax plus padding. .text is VA 0x1000 at raw 0x400, so the file offset is 0x4cd:
48 8d 15 6c 0f 00 00 -> 48 89 c2 90 90 90 90
$ echo | wine flag-pointer-register-patched.exe
Press ENTER to receive the flag.
NNS{r4x_h4d_7h3_fl4g_bu7_rdx_p01n73d_70_7h3_wr0ng_buff3r}
Live in a debugger the fix is the same instruction by hand: break at 0x1400010dd, set RDX = RAX, continue.
4. Static route
The key and ciphertext are both static, so the patch is only needed to make the program cooperate — not to learn the flag:
key = data[0x600:0x600 + 8] # .rdata VA 0x2000 -> raw 0x600
enc = data[0x800:0x800 + 0x39] # .data VA 0x3000 -> raw 0x800
print(bytes(enc[i] ^ key[i & 7] for i in range(0x39)).decode())
Flag: NNS{r4x_h4d_7h3_fl4g_bu7_rdx_p01n73d_70_7h3_wr0ng_buff3r}
Takeaways
- A length/buffer mismatch is a reliable tell for a swapped pointer.
r8d = 0x39sitting next to aleaof a buffer that clearly isn’t0x39bytes long says the pointer is wrong, not the length — and that reasoning works without ever running the binary. - Windows x64 is
rcx, rdx, r8, r9then stack, and the caller reserves 32 bytes of shadow space, which is why the fifth argument here is written to[rsp+0x20]rather than pushed. Reading Win32 call sites is mostly just holding that ordering in mind.