CTF write-up: Rob's Admin Program
2019-10-06
Last thursday I was participating in a CTF which had challenges in different categories of difficulty. This challenge was in the ‘ARGH’ category and labelled as very hard. I jumped right into it from the start of the CTF but unfortunately didn’t made it in time due to some stupid mistakes I made. Lucky for me I had a team which was performing exceptionally well and we still won the CTF!
I finished the binary about 2 minutes late and decided I would do a write-up about it.
Rob's admin program
To do the actual challenge I had to SSH into a Linux machine with guest credentials that were provided with the challenge. This guest user had very limited rights, and I started to do the usual enumeration using the linux privilege escalation guide that was written by g0tmi1k.
In about a minute or two I found the binary that had a SUID set for the user rob and since this user also had access to the ‘flag.txt’ the path to take was pretty clear. There was a file in rob’s home folder called TODO.txt
, this file made it clear that the whoami command was implemented in the binary and that the /bin/sh
was still a work in progress.
When executing the binary without any arguments it displays the following output:
For this challenge I mainly used radare2 for the initial binary information, reverse engineering, and debugging. I used the ropper tool to find the ROP gadgets that I could use to craft the final payload.
Reverse Engineering
I don’t have a workflow yet since I’m still new to reverse engineering and exploit development, but I like to take the same approach as most things in security; what is it that I am looking at?
Binary Information
To answser this question I used rabin2 -I
command for some general information about the binary:
arch x86
baddr 0x8048000
binsz 6460
bintype elf
bits 32
canary false
class ELF32compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609
...
nx true
os linux
The above output confirms some of the assumptions I already made, the most notable things of the above output are:
It’s a 32-bit ELF binary;
It’s base address is located at
0x8048000
;It has the No eXecute bit set;
Analyzing the functions
Without dwelling too much on the information above I fired up radare to see if I could make something from the function calls. This can be done by executing the following commands:
r2 pvib_bin // Load the binary with radare
aaa // Analyze the binary
afl // List the functions of the binary
The above commands display the following functions being present in the binary:
I highlighted the functions that stood out to me and proved to be useful in a later stage.
Disassembly
To get a little bit of a better understanding of the execution flow of the program I disassembled some of the functions in the binary, starting with the main function:
In the above disassembly view it is shown that the function sym.parser is being called after the initial message is printed. It seems that most of the inner workings are present in the sym.parser
function. This can be verified by printing the disassembly view of this function:
The function sym.parser is the parser for the commandline arguments given when executing the binary. In the first part of this function a compare is being done between the string whoami
and the input that was given by the user. If the input of the user matches the string ‘whoami’ it will not jump to address 0x8048610
and execute the call to sym.whoami.
After looking through the binary it was clear that the functions sym.add_bin_str
and sym.add_sh_str
are the functions needed to execute the /bin/sh
.
But just executing /bin/sh
is not the goal of the challenge, the actual goal of the challenge is to execute this as the user rob. To execute the /bin/sh
command as the user rob, one extra function is needed which is the sym.exec_str
function:
The above function makes sure that the SGID and SUID are set to the value of rob (1001) before calling the system function to execute the /bin/sh
command.
At this point it is time to look at the functions that are not yet implemented, starting with sym.add_bin_str
:
The sym.add_bin_str
function takes 2 arguments, the first one (0xdeadc0de
) is stored in arg_8h
and the second one (0xbadfeed
) is stored in arg_ch
. This information is important for building the eventual ROP chain since this would likely mean that a POP,POP,RET gadget is needed to pull this off in the right order.
The next function that is need is the sym.add_sh_str
:
The function takes 1 argument (0xbaaaaaad
) which is stored in arg_8h
and will add the /sh string if it matches.
Exploiting the binary
With most of the important information in hand I started to fuzz the binary with the input that it takes. I like to use the cyclic function of pwntools for this. With cyclic you can generate a random pattern of a given length, if you manage to crash the program you can take a look at the registers to find the offset of the overflow.
Finding the right offset
I started with a buffer of 150 characters and managed to trigger a crash, with this information I fired up radare in debugging mode with the 150 characters as an argument:
r2 -d pvib_bin [150 random char] // Start radare with the random pattern as argumentdcu sym.parser // Continue running until the sym.parser functiondsu 0x08048634 // Step until the return address
Now we can look at the register values using the dr
command:
eax = 0x0000001b
ebx = 0xffcebac0
ecx = 0x093ff160
edx = 0xf7f43890
esi = 0xf7f42000
edi = 0x00000000
esp = 0xffceba8c
ebp = 0x62616163
eip = 0x08048634
eflags = 0x00000282o
eax = 0xffffffff
At this point EBP is overwritten with the values 0x62616163
, this translates to ASCII baac
. Be aware that values stored in the registries are little-endian, to find the value with cyclic_find
it needs to be read as caab
. According to pwntool’s cyclic_find
function this value is located at offset 108, this would mean that EIP gets overwritten at offset 112. We can verify this assumption by stepping once in the radare debugger with ds
and reading the register values again:
eax = 0x0000001b
ebx = 0xffcebac0
ecx = 0x093ff160
edx = 0xf7f43890
esi = 0xf7f42000
edi = 0x00000000
esp = 0xffceba90
ebp = 0x62616163
eip = 0x62616164
eflags = 0x00000282o
eax = 0xffffffff
EIP is indeed overwritten with some of the random characters; 0x62616164
, which translates to ASCII baad
(again little-endian), at offset 112.
The reason that this vulnerability exists is because there are no checks being done on the actual length of the argument before the strcpy
is called. This results in the buffer being too big to be copied and thus overflowing the stack.
Finding gadgets
Since the NX bit set we cannot directly execute code written to the stack. To get around this I can use ROP gadgets to return to the functions holding the strings to form /bin/sh and execute these with the sym.exec_str
function.
To find the right ROP gadgets I need to look at the arguments passed to the functions sym.add_bin_str
and sym.add_sh_str
. The sym.add_bin_str
takes two arguments and thus needs a ROP gadget containing POP,POP,RET. The sym.add_sh_str
takes one argument and will need a ROP gadget containing POP,RET. The gadgets can be found by using ropper.
Running ropper with the argument to find the right gadgets reveals the following:
./Ropper.py --file pvib_bin --search "pop"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop
[INFO] File: ../rob
0x080486a2: pop ebp; lea esp, [ecx - 4]; ret;
0x0804896b: pop ebp; ror dword ptr [ecx + eax], 0; inc ecx; ret;
0x08048587: pop ebp; ret;
0x080486a1: pop ebx; pop ebp; lea esp, [ecx - 4]; ret;
0x08048708: pop ebx; pop esi; pop edi; pop ebp; ret;
0x08048371: pop ebx; ret;
0x080486a0: pop ecx; pop ebx; pop ebp; lea esp, [ecx - 4]; ret;
0x08048586: pop edi; pop ebp; ret;
0x08048709: pop esi; pop edi; pop ebp; ret;
0x080486a4: popal; cld; ret;
Ropper shows a couple of gadgets that can be used to create the ROP chain:
The gadgets at address
0x08048586
can be used forsym.add_bin_str
;The gadgets at address
0x08048371
can be used forsym.add_sh_str
;
Putting the pieces together
With all the needed information an exploit can be developed that will use the ROP chain to do the following:
Create a buffer as initial overflow to replace the value with EIP with the address to
sym.add_bin_str
;Add the ROP gadget address with POP EDI, POP EBP, RET instructions for the 2 arguments that
sym.add_bin_str
takes;Add the values of the 2 arguments of
sym.add_bin_str
;Add the address to function
sym.add_sh_str
;Add the ROP gadget address with POP EBX, RET instructions for the argument that
sym.add_sh_str
takes;Add the value of the argument of
sym.add_sh_str
;Add the address of the function
sym.exec_str
to return to after pushing the /bin/sh value into memory;
I put the following Python script together for the exploit, this version is provided with comments and looks a lot better than the initial script I had running during the CTF:
Conclusion
I definitely learned a lot by doing this again and actually writing down the steps I took to get to the end goal. During this second run on the binary I used radare instead of gdb-peda because I wanted to learn how to this with radare.
The next challenge I will definitely use the r2pipe module that will let me interact with the binary using Python. This will allow me to automate more of the tasks that I would now do by hand but are in fact quite repetitive.
Last updated