Impossible Password (HTB rev)
first check is a fixed strcmp, second compares against a runtime-random string, so patch the branch
The binary gates the flag behind two checks. The first compares input to a hardcoded key, the second compares to a 20-byte string generated at runtime that you cannot guess. I patched the conditional jump after the second strcmp to a NOP so the flag routine always runs.
the challenge
Impossible Password is a reversing challenge shipped as a single ELF. Running it prompts for input, prints what I typed, then prompts a second time. The name is the hint: stage two is built so no input can ever match. The goal is the flag the program would print if both checks passed.
impossible_password.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, ... GNU/Linux 2.6.32, ... stripped
It is 64-bit and stripped, so no function names. I worked off addresses and Ghidra’s auto-named FUN_ labels.
analysis
strings on the binary leaked exactly one useful token:
$ strings impossible_password.bin | grep -i super
SuperSeKretKey
That is the stage-one key. The main logic function (Ghidra called it FUN_0040085d) lines up the whole flow:
local_10 = "SuperSeKretKey";
local_48 = 0x41; local_47 = 0x5d; local_46 = 0x4b; local_45 = 0x72;
local_44 = 0x3d; local_43 = 0x39; local_42 = 0x6b; local_41 = 0x30;
local_40 = 0x3d; local_3f = 0x30; local_3e = 0x6f; local_3d = 0x30;
local_3c = 0x3b; local_3b = 0x6b; local_3a = 0x31; local_39 = 0x3f;
local_38 = 0x6b; local_37 = 0x38; local_36 = 0x31; local_35 = 0x74;
printf("* ");
__isoc99_scanf(&DAT_00400a82,local_28);
printf("[%s]\n",local_28);
local_14 = strcmp(local_28,local_10);
if (local_14 != 0) {
exit(1);
}
printf("** ");
__isoc99_scanf(&DAT_00400a82,local_28);
__s2 = (char *)FUN_0040078d(0x14);
iVar1 = strcmp(local_28,__s2);
if (iVar1 == 0) {
FUN_00400978(&local_48);
}
The block of 0x41, 0x5d, 0x4b ... bytes is a 20-byte array built on the stack with the success routine’s argument (&local_48). It is not the password, it is the buffer the flag routine works on.
Stage one is trivial: type SuperSeKretKey and the first strcmp returns 0, so the exit(1) is skipped. The disassembly around that first check:
40090d: call 400630 <strcmp@plt>
400912: mov DWORD PTR [rbp-0xc],eax
400915: cmp DWORD PTR [rbp-0xc],0x0
400919: je 400925 ; equal -> continue
40091b: mov edi,0x1
400920: call 400680 <exit@plt> ; not equal -> exit(1)
Stage two is the impossible part. My second input is compared against the return of FUN_0040078d(0x14). That function builds a fresh 20-byte (0x14) string at runtime. Disassembling it shows why guessing is hopeless:
40078d <gen>:
...
4007a0: mov DWORD PTR [rbp-0x14],0x7e ; upper bound 0x7e ('~')
4007a7: mov DWORD PTR [rbp-0x18],0x21 ; lower bound 0x21 ('!')
4007ae: mov edi,0x0
4007b3: call 400650 <time@plt> ; time(0)
4007b8: mov edx,eax
4007ba: mov eax,DWORD PTR [rbp-0x24] ; arg = 0x14
4007bd: imul edx,eax ; time(0) * 0x14
...
4007d9: call 400620 <srand@plt> ; srand(time-derived seed)
4007e1: add eax,0x1
4007e9: call 400660 <malloc@plt> ; malloc(0x14 + 1)
...
; loop body, 0x14 iterations:
400802: call 400690 <rand@plt> ; rand()
400807: mov edx,DWORD PTR [rbp-0x14] ; 0x7e
40080a: add edx,0x1 ; 0x7f
40080d: mov ecx,edx
40080f: sub ecx,DWORD PTR [rbp-0x18] ; 0x7f - 0x21 = 0x5e (range span)
400812: cdq
400813: idiv ecx ; rand() % 0x5e
400815: mov eax,DWORD PTR [rbp-0x18] ; 0x21
400818: add eax,edx ; 0x21 + (rand()%0x5e) -> printable byte
40081a: mov DWORD PTR [rbp-0x1c],eax
...
40082d: mov BYTE PTR [rdx],al ; store byte into buffer
...
400836: cmp eax,DWORD PTR [rbp-0x24] ; i < 0x14 ?
400839: jl 400802
400848: mov BYTE PTR [rax],0x0 ; NUL terminate
So the routine seeds srand with time(0) * 0x14 (plus a global counter), then fills 20 bytes, each 0x21 + rand() % 0x5e, landing in the printable range 0x21..0x7e. The string is reseeded from the clock on every run, so it changes second to second. There is no fixed value to type, which is the “impossible” in the name. The flag routine FUN_00400978 only runs when that second strcmp returns 0.
the solve
The comparison is unbeatable, but the success branch sits right after it. So instead of matching the random string, I made the program take the success branch unconditionally by killing the conditional jump.
After the second strcmp, the code tests the result and jumps over the flag call when the strings differ:
400966 85 c0 TEST EAX,EAX
400968 75 0c JNZ LAB_00400976 ; skip flag call if not equal
40096a 48 8d 45 c0 LEA RAX,[RBP-0x40] ; -> &local_48 (the buffer)
40096e 48 89 c7 MOV RDI,RAX
400971 e8 02 00 00 00 CALL FUN_00400978 ; the flag routine
400976 c9 LEAVE
400977 c3 RET
The JNZ at 0x00400968 (bytes 75 0c) is the gate. Since strcmp is essentially always non-zero, that jump always fires and always skips the flag call. I overwrote those two bytes with a two-byte NOP (66 90) so execution falls straight through into the LEA/MOV/CALL:
400966 85 c0 TEST EAX,EAX
400968 66 90 NOP ; was JNZ
40096a 48 8d 45 c0 LEA RAX,[RBP-0x40]
40096e 48 89 c7 MOV RDI,RAX
400971 e8 02 00 00 00 CALL FUN_00400978 ; now always reached
I used Ghidra’s assembly patcher, picked 66 90 so the patch is exactly two bytes (same width as 75 0c, no shifting), and exported the patched binary with Export Program. 66 90 is the canonical two-byte NOP (xchg ax,ax); 90 90 would work just as well since both bytes are still in the instruction stream.
the flag
I ran the patched binary, fed SuperSeKretKey at the first prompt and any junk at the second, and with the JNZ gone the second comparison no longer mattered. FUN_00400978 fired and printed the flag:
* SuperSeKretKey
[SuperSeKretKey]
** whatever
HTB{...}
Patching the file is not the only route. The success branch is taken whenever the result is zero, so under a debugger I could break at 0x00400966, run the program normally to that point, and clear EAX (or set the zero flag) right before the JNZ. The test eax,eax then sets ZF, the JNZ does not fire, and the same FUN_00400978 runs with no file edit at all. Either way the gate disappears.