Try   HackMD

Writeup RE TCP1P CTF

Minggu ini saya bermain TCP1P CTF bersama tim L3ak, alhamdulillahnya kami berhasil melakukan full clear beberapa category salah satunya reverse engineering

{0F19C71B-1B32-4437-AD00-ACCD474E7051}

saya sendiri melakukan solve pada 3 soal yaitu :

dan berhasil mendapat 3rd blood dan 2nd blood sayangnya saya tidak cukup cepat untuk mendapat 1st blood :crying_cat_face:.

nyann

image

dari link yang diberikan pada deskripsi soal kita tahu bahwa soal ini pasti berkaitan dengan NekoVM

{9F94DAAF-BBC8-4462-8A5E-22BA3C494EBD}

sebelum dijalankan jika mendownload neko melalui file zip, maka binary nyann perlu kita patch agar dapat mengenali libneko di folder hasil download.

patchelf --set-rpath <neko-folder-path> nyann

Preliminary analysis

Ketika file nyann dijalankan dia akan menampilkan sebuah pesan usage, awalnya saya mengira bahwa ada file yang tidak diberikan mengingat bagaimana biasanya soal vm bekerja, tetapi author berkata bahwa tidak ada file attachment yang tertinggal dan perilaku ini intended.

{77D856E2-23DE-44AA-BEE9-F8001C8042B1}

dapat dilihat bahwa binary nyann sangat mirip dengan interpreter neko sehingga kemungkinan ada module atau bytecode yang ter-embed ke binary nyann melihat ada perbedaan size yang cukup besar.

{4368ED9F-B4D4-4157-BD6B-3DA6AE656619}

kita akan coba melakukan compile dengan compilernya (nekoc) untuk melihat bagaimana struktur file dari module NekoVM, saya akan menggunakan code berikut

$print("hello neko world !\n");

setelah di compile kita dapat melakukan sedikit analisa dengan command file dan xxd

{343E8679-F12D-4E09-A723-0D0185F98BDF}

dapat dilihat bahwa file module akan dimulai dengan NEKO lalu beberapa informasi seperti berapa banyak symbol global, global fields dan berapa banyak opcode.

saya memutuskan melihat kembali dokumentasi dan menemukan bahwa kita bisa convert sebuah module menjadi standalone executable dengan command

nekotools boot <module>

dokumentasi tools dapat dilihat disini.

Extracting the module

setelah di compile kita dapat melihat bahwa file module yang ter-embed diawali dengan NEKO dan diakhiri dengan itu juga

>>> open('try', 'rb').read()[22904:22904+69]
b'NEKO\x02\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00\x03\x13\x00hello neko world !\n\x05\x01try.neko\x00\x07\x00\x00\x00\x0c\x1aprint\x001L/-X\x8b\xc8\xad'
>>> open('try', 'rb').read()[22904:22904+69+4]
b'NEKO\x02\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00\x03\x13\x00hello neko world !\n\x05\x01try.neko\x00\x07\x00\x00\x00\x0c\x1aprint\x001L/-X\x8b\xc8\xadNEKO'

sehingga kita bisa mendapat kembali module yang ter-embed dalam nyann.

sebenarnya executable ini seharusnya dapat langsung menjalankan module yang ter-embed tetapi kenapa nyann tidak? sejujurnya setelah saya bandingkan sekilas tidak ada perbedaan pasti sehingga saya tidak tau trik apa yang digunakan oleh author

tetapi sekilas ketika saya bandingkan module yang sudah berhasil saya extract lalu saya convert kembali menjadi executable banyak informasi yang awalnya sebuah byte biasa menjadi ZZZZZ... saya tidak tahu apakah ini karena perbedaan cara convert atau memang itu perbuatan author, karena file executable yang saya buat bisa langsung run module tadi.

Where the fun begin

setelah module atau bytecode berhasil kita dapatkan kita dapat melakukan disassembly dengan tools dari nekovm itu sendiri, sekali lagi hal tersebut memang disebutkan dalam dokumentasinya.

nekoc -d dump.n

akan menghasilkan file dump.dump yang berisi global symbol, opcode, dan beberapa hal lain.

file tersebut sangatlah besar dan well sangat melelahkan jika kita baca secara keseluruhan, tetapi kita bisa lihat ada format flag diantara variable global.

..snip.. global 157 : string "}" global 158 : string "_" global 159 : string "It was Inu food instead of Nyann's!\n" global 160 : string "Wrong cat food, Nyann is dead." global 161 : string "\227\133\164/\225\144\160 - \203\149 -\227\131\158 Hackerika greets you and here's your cat food -> TCP1P{" global 162 : string "}\n" global 163 : function 1978 nargs 0 global 164 : var @Math global 165 : var Math global 166 : function 3859 nargs 0 global 167 : function 3868 nargs 2 global 168 : function 3889 nargs 2 global 169 : function 3910 nargs 1 global 170 : function 3916 nargs 1 global 171 : string "random_int" global 172 : string "random_float" ..snip..

kita bisa langsung saja mencari dimana string tersebut digunakan dan mendapatkan fakta bahwa ternyata function yang berbeda akan dipisahkan dengan line kosong, sehingga langsung saja kita pindahkan function yang mengandung string tersebut yang ternyata semua logic checker ada disana.

agar lebih jelas dan mudah dibaca saya mengganti nama global variable dengan isinya, dengan bantu rekan setim saya menggunakan script berikut

import re with open('dump.dump', 'r') as file: data = file.readlines() global_dict = {} global_pattern = re.compile(r'global\s+(\d+)\s+:\s+(.*)') for line in data: match = global_pattern.match(line.strip()) if match: index = int(match.group(1)) value = match.group(2) global_dict[index] = value accglobal_pattern = re.compile(r'AccGlobal\s+(\d+)') def replace_accglobal(match): index = int(match.group(1)) return global_dict.get(index, match.group(0)) output_data = [] for line in data: new_line = accglobal_pattern.sub(replace_accglobal, line) output_data.append(new_line) with open('really_dude.dump', 'w') as file: file.writelines(output_data)

sederhananya pemanggilan variable global seperti AccGlobal 69 akan diubah menjadi nilai dari global variable itu sendiri contoh string "example string"

Before :

000B9D 1978 AccGlobal 88 000B9F 1979 Push 000BA0 1980 AccField args 000BA2 1981 ObjCall 0 000BA4 1982 AccField length 000BA6 1983 Push 000BA7 1984 AccInt 1 000BA9 1985 Neq 000BAA 1986 JumpIfNot 2004 000BAC 1987 AccGlobal 89 000BAE 1988 Push 000BAF 1989 AccGlobal 36 000BB1 1990 Push 000BB2 1991 AccField new 000BB4 1992 ObjCall 1 000BB6 1993 Push 000BB7 1994 AccGlobal 88 000BB9 1995 Push 000BBA 1996 AccField println 000BBC 1997 ObjCall 1

After :

000B9D 1978 var Sys 000B9F 1979 Push 000BA0 1980 AccField args 000BA2 1981 ObjCall 0 000BA4 1982 AccField length 000BA6 1983 Push 000BA7 1984 AccInt 1 000BA9 1985 Neq 000BAA 1986 JumpIfNot 2004 000BAC 1987 string "Usage: ./nyann food.txt" 000BAE 1988 Push 000BAF 1989 var String 000BB1 1990 Push 000BB2 1991 AccField new 000BB4 1992 ObjCall 1 000BB6 1993 Push 000BB7 1994 var Sys 000BB9 1995 Push 000BBA 1996 AccField println 000BBC 1997 ObjCall 1

dari snippet diatas kita bisa lihat bahwa kurang lebih jika argument yang diberikan hanya satu maka akan mencetak usage ..., nah intruksi tersebut mungkin tidak terlalu familiar dibaca sehingga saya sarankan untuk setidaknya membaca sedikit implementasi NekoVM.

https://github.com/HaxeFoundation/neko/blob/d8a3b463861779656a78c1a5e31c013580642185/vm/jit_x86.c

After all the docs reading session

kembali ke intruksi-intruksi module kita dapat perilakunya sebagai berikut

  • input didapat dari file
000C04 2043 var sys 000C06 2044 AccField io 000C08 2045 AccField File 000C0A 2046 Push 000C0B 2047 AccField getContent 000C0D 2048 ObjCall 1 000C0F 2049 Push
  • jika file yang diberikan tidak ada, maka print ...nyan sad..
000BD3 2012 var sys 000BD5 2013 AccField FileSystem 000BD7 2014 Push 000BD8 2015 AccField exists 000BDA 2016 ObjCall 1 000BDC 2017 Not 000BDD 2018 JumpIfNot 2041 000BDF 2019 string "No food today, nyann is sad ...!\n" 000BE1 2020 Push 000BE2 2021 var String 000BE4 2022 Push 000BE5 2023 AccField new 000BE7 2024 ObjCall 1 000BE9 2025 Push 000BEA 2026 AccNull 000BEB 2027 Push 000BEC 2028 var Sys 000BEE 2029 Push 000BEF 2030 AccField stderr 000BF1 2031 ObjCall 0 000BF3 2032 Push 000BF4 2033 AccField writeString 000BF6 2034 ObjCall 2
  • jika panjang input != 38, maka print It was inu..
001670 3745 MakeArray 37 001672 3746 Push 001673 3747 AccInt 38 001675 3748 Push 001676 3749 var Array 001678 3750 Push 001679 3751 AccField new1 00167B 3752 ObjCall 2 00167D 3753 Push 00167E 3754 AccStack1 00167F 3755 AccField length 001681 3756 Push 001682 3757 AccStack1 001683 3758 AccField length 001685 3759 Neq 001686 3760 JumpIfNot 3778 001688 3761 string "It was Inu food instead of Nyann's!\n" 00168A 3762 Push 00168B 3763 var String 00168D 3764 Push 00168E 3765 AccField new 001690 3766 ObjCall 1 001692 3767 Push 001693 3768 var Sys 001695 3769 Push 001696 3770 AccField println
  • jika input salah, maka print Wrong cat food..
0016CB 3806 AccStack 2 0016CD 3807 AccArray 0016CE 3808 Neq 0016CF 3809 JumpIfNot 3827 0016D1 3810 string "Wrong cat food, Nyann is dead." 0016D3 3811 Push 0016D4 3812 var String 0016D6 3813 Push 0016D7 3814 AccField new 0016D9 3815 ObjCall 1 0016DB 3816 Push 0016DC 3817 var Sys 0016DE 3818 Push 0016DF 3819 AccField println 0016E1 3820 ObjCall 1 0016E3 3821 AccInt 1 0016E5 3822 Push 0016E6 3823 var Sys 0016E8 3824 Push 0016E9 3825 AccField exit
  • jika benar, maka print Hackerika greets .. (flag)
0016F1 3829 Pop 2 0016F3 3830 string "\227\133\164/\225\144\160 - \203\149 -\227\131\158 Hackerika greets you and here's your cat food -> TCP1P{" 0016F5 3831 Push 0016F6 3832 var String 0016F8 3833 Push 0016F9 3834 AccField new 0016FB 3835 ObjCall 1 0016FD 3836 Push 0016FE 3837 AccStack 11 001700 3838 Add 001701 3839 Push 001702 3840 var Sys 001704 3841 Push 001705 3842 AccField print 001707 3843 ObjCall 1 001709 3844 string "}\n" 00170B 3845 Push 00170C 3846 var String 00170E 3847 Push 00170F 3848 AccField new 001711 3849 ObjCall 1

untuk algoritma checkernya sebenarnya cukup mudah kita dapat melihat ada 3 array yang di inisialisasi dengan panjang yang sama, lalu kita dapat melihat StringMap yang diikutin loop yang memanggil fungsi builtin hset

001552 3558 var haxe 001554 3559 AccField ds 001556 3560 AccField StringMap 001558 3561 Push 001559 3562 AccField new 00155B 3563 ObjCall 0 00155D 3564 Push 00155E 3565 AccInt 0 001560 3566 Push 001561 3567 AccStack 3 001563 3568 AccField length 001565 3569 Push 001566 3570 Loop 001567 3571 AccStack1 001568 3572 Push 001569 3573 AccStack1 00156A 3574 Lt 00156B 3575 JumpIfNot 3606 00156D 3576 Push 00156E 3577 AccStack 2 001570 3578 SetStack 0 001572 3579 Push 001573 3580 AccInt 1 001575 3581 Add 001576 3582 SetStack 2 001578 3583 AccStack 0 00157A 3584 Pop 1 00157C 3585 Push 00157D 3586 AccStack 3 00157F 3587 AccField h 001581 3588 Push 001582 3589 AccStack 7 001584 3590 Push 001585 3591 AccStack 2 001587 3592 AccArray 001588 3593 AccField __s 00158A 3594 Push 00158B 3595 AccStack 7 00158D 3596 Push 00158E 3597 AccStack 3 001590 3598 AccArray 001591 3599 Push 001592 3600 AccNull 001593 3601 Push 001594 3602 AccBuiltin hset 001596 3603 Call 4 001598 3604 Pop 1 00159A 3605 Jump 3570

fungsi-fungsi tersebut memiliki dokumentasi yang anda bisa akses disini

hal itu dilakukan sebanyak 2 kali sehingga kita bisa menebak bahwa dilakukan mapping karakter per karakter dari array pertama ke array kedua dan array kedua ke array ketiga, sehingga bisa langsung kita reverse untuk mendapat flag, script yang saya gunakan

nn = "neko_nico_neko_nii!" s0 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!?{}@_' s1 = '_FCI{B6mR9Sk@ETWJf2uHNK10gZhUQGYijL}DMn!qP3O8w4draxpsA7bVvl5ty?coXze' s2 = '8TSWLs}tdPm0ra6wDCZB49hU3Inz5fY{2eOQipK_V!@R?17jloNHbXqkExMyFGguvJAc' s3 = 'hZmhm32crLfcT5c9OwWcUW48crCmUcZ5cL6{6z' map1 = {} for i in range(68): map1[s2[i]] = s1[i] map2 = {} for i in range(68): map2[s1[i]] = s0[i] for i in s3: print(map2[map1[i]], end='')

sedikit tambahan ada string neko_nico_neko_nii! yang saya coba pahami dimana string tersebut digunakan yang ternyata tidak digunakan (mungkin :jack_o_lantern:), ternyata sekedar pengalihan isu, udah kayak

TCP1P{miamaow_c4t_1s_lyf3_n3k0_chan_is_4ever}

average flag checker

{38BB37FD-9B14-4DF3-8C79-2455CB3A8095}

melihat nama authornya bohong kalo dibilang average, JK.

Preliminary analysis

karena tidak ada yang menarik dari deskripsi soal dan attachment hanya diberikan satu buah file binary, maka kita langsung aja coba jalankan.

{33EE53EC-42D2-4867-A216-9A4419B3F0CA}

ternyata memang benar flag checker saat saya check size binarynya ternyata cukup besar untuk ukuran soal flag checker

{AF33B29D-67D8-4BBE-B180-5986A6721D26}

bahkan di ida tidak dapat di decompile

{ECDEAB7A-5910-4C65-A66C-52119E6840D3}

di awal operasi terdapat fungsi yang dipanggil terlebih dahulu yang mengembalikan sebuah alamat.

{67362404-173F-42B4-B48D-C4D0F0B5CDB5}

alamat tadi digunakan untuk menyimpan byte-byte.

image

dan karena banyaknya intruksi yang dalam binary tersebut kita dapat melakukan dynamic analysis menggunakan strace ataupun ltrace terlebih dahulu untuk mengetahui garis besar instruksi yang dilakukan binary.

ftt

dan kita dapat melihat suatu hal yang menarik yaitu pemanggilan ptrace karena disini saya menjalankan dengan strace maka hasil dari ptrace(PTRACE_TRACEME) adalah -1 yang menandakan bahwa proses tersebut sudah di tracing atau fungsi ptrace sudah dipanggil sebelumnya, kita tahu bahwa debugger biasanya menggunakan ptrace di balik layar tak terkecuali strace yang saya gunakan, sehingga bisa digunakan untuk menjadi mekanisme anti debug sederhana.

dapat dikonfirmasi pada intruksi-intruksi berikut

image

image

image

nilai dari var_14c dibandingkan dengan nilai 6, nilai 6 bisa didapat ketika ptrace pertama berhasil dan menginisialisasi nilai 2, lalu ptrace kedua gagal yang akan membuat nilai tadi * 3, sehingga dalam eksekusi normal tanpa debugger nilai akan selalu 6.

Interesting function

setelah semua inisialisasi nilai berhasil dilakukan kita dapat melihat sebuah pemanggilan fungsi setelahnya

{0AAAEB23-5ADA-435F-820A-4D496F4CD1D6}

__int64 __fastcall sub_94E0A(unsigned __int8 *a1) { __int64 result; // rax sub_94DD7(a1); result = *a1; if ( !(_BYTE)result ) return sub_94E0A(a1); return result; }

kita dapat melihat bahwa fungsi diatas adalah fungsi rekursif dan nilai dari a1 adalah var_148 atau alamat yang di inisialisasi diawal.

selanjutnya kita dapat melihat pada fungsi sub_94DD7, karena fungsi ini akan dipanggil berkali-kali

__int64 __fastcall sub_94DD7(__int64 a1) { unsigned int v2; // [rsp+1Ch] [rbp-4h] v2 = sub_9408F(a1); return sub_944B4(a1, v2); }

disini fungsi sub_9408F berfungsi seperti mengambil nilai dari alamat dan melakukan sebuah increment

__int64 __fastcall sub_9408F(__int64 a1) { int v1; // eax unsigned int v3; // [rsp+14h] [rbp-4h] v3 = *(_DWORD *)(a1 + 4 * ((int)sub_94042(a1, 33) + 40LL) + 4); v1 = sub_94042(a1, 33); sub_94065(a1, 33, v1 + 1); return v3; }

dari sini saya sadar bahwa binary adalah sebuah custom vm hal ini bisa dipastikan ketika melihat fungsi sub_944B4

{9451A106-BA11-42A2-A803-FCCE4DC3D57D}

karena saya sudah tahu bahwa ini vm maka banyak nilai yang tadinya di inisialisasi adalah bytecode dari vm tersebut, saya menggunakan gdb script untuk melakukan dump bytecode

import gdb gdb.execute("file chall") bp = [ 0x000055555555B16D, # 0x000055555555DA86, 0x00005555555E7EBC ] for i in bp: gdb.execute(f"b* {i}") gdb.execute(f"run <<< {'a'*36}") gdb.execute("set $rax=0") gdb.execute("c") gdb.execute("dump binary memory vm 0x7ffff7d5c0b4 0x7ffff7d6c477") gdb.execute("q")

disini saya melakukan bypass terhadap ptrace pertama agar tetap melakukan inisialisasi nilai 2.

dump bytecode saya ubah menjadi nilai integer agar memudahkan proses parsing bytecode nantinya

data = open('vm', 'rb').read() + b'\x00' vm = [] for i in range(0, len(data),4): vm.append(u32(data[i:i+4]))

dan karena ini adalah vm agar lebih mudah saya membuat ulang vm tersebut, nampaknya kita tidak perlu melakukan full recover dari logic vmnya, disini saya hanya melakukan recover terhadap instruksi yang digunakan pada bytecode yang sudah di dump

i = 0 data = [0] * 0x1000 mem = [0] * 0x1000 while True: if vm[i] == 16: arg1 = vm[i+1] arg2 = vm[i+2] data[arg2] = arg1 # if arg2 == 2 and arg1 == 1: # print(mem[256:292]) # break print(f'data[{arg2}] = {arg1}') i += 2 elif vm[i] == 17: arg1 = vm[i+1] arg2 = vm[i+2] data[arg2] = data[arg1] print(f"data[{arg2}] = data[{arg1}] ({data[arg1]})") i += 2 elif vm[i] == 18: arg1 = vm[i+1] arg2 = vm[i+2] mem[arg2] = data[arg1] print(f'mem[{arg2}] = data[{arg1}] ({data[arg1]})') i += 2 elif vm[i] == 19: arg1 = vm[i+1] arg2 = vm[i+2] data[arg2] = mem[arg1] print(f"data[{arg2}] = mem[{arg1}] ({mem[arg1]})") i += 2 elif vm[i] == 21: arg1 = vm[i+1] arg2 = vm[i+2] data[arg2] = mem[data[arg1]] print(f"data[{arg2}] = mem[data[{arg1}]] (mem[{data[arg1]}]) ({mem[data[arg1]]})") # break i += 2 elif vm[i] == 22: arg1 = vm[i+1] arg2 = vm[i+2] val = data[arg1] + data[arg2] data[32] = val print(f"data[32] = data[{arg1}] ({data[arg1]}) + data[{arg2}] ({data[arg2]}) ({val})") i += 2 elif vm[i] == 23: arg1 = vm[i+1] arg2 = vm[i+2] data[32] = data[arg1] ^ data[arg2] print(f"data[32] = data[{arg1}] ({data[arg1]}) ^ data[{arg2}] ({data[arg2]}) ({data[arg1] ^ data[arg2]})") # print(mem[:256]) # break i += 2 elif vm[i] == 24: arg1 = vm[i+1] arg2 = vm[i+2] data[32] = arg1 + data[arg2] print(f"data[32] = {arg1} + data[{arg2}] ({data[arg2]}) ({arg1 + data[arg2]})") i += 2 elif vm[i] == 26: arg1 = vm[i+1] arg2 = vm[i+2] data[32] = data[arg1] - data[arg2] print(f"data[32] = data[{arg1}] ({data[arg1]}) - data[{arg2}] ({data[arg2]}) ({data[arg1] - data[arg2]})") i += 2 elif vm[i] == 31: arg1 = vm[i+1] arg2 = vm[i+2] data[32] = data[arg1] & arg2 print(f"data[32] = data[{arg1}] ({data[arg1]}) & {arg2} ({data[arg1] & arg2})") i += 2 elif vm[i] == 32: arg1 = vm[i+1] arg2 = vm[i+2] val = data[arg1] print(f"mem[{val}], mem[{arg2}] = mem[{arg2}], mem[{val}] ({mem[arg2]}, {mem[val]})") mem[val], mem[arg2] = mem[arg2], mem[val] i += 2 elif vm[i] == 36: arg1 = vm[i+1] arg2 = vm[i+2] val = data[32] print(f"data[32] ({data[32]}) == arg1 ({arg1}) ({data[32] == {arg1}})") i += 2 elif vm[i] == 43: print("result = 1") else: print(hex(i), (vm[i])) print(data[:33]) print(vm[i:]) break i += 1

ketika dijalankan akan menghasilkan output sebagai berikut

{D2FC81C5-2DAB-4570-8CB3-E46FC362A608}

output tersebut saya pindahkan ke file tersendiri dan melakukan analisis lebih lanjut

How the checker works

logic output dapat dibagi menjadi beberapa bagian

untuk bagian pertama akan dilakukan inisialisasi data
{3D6E7C83-4258-40FD-B780-7E7E6B330951}

data yang sudah di inisialisasi tadi dilakukan beberapa operasi aritmatika lalu dilakukan swap

{787DF48A-0024-4BD9-B28F-692C0774E48B}

dari sini saya menyadari bahwa operasi ini mirip dengan operasi key scheduling pada algorimta rc4

hal ini bisa dibandingkan dengan bagaimana implementasi key scheduling pada algoritma rc4 sebenarnya

https://github.com/manojpandey/rc4/blob/master/rc4-3.py

def KSA(key): ''' Key Scheduling Algorithm (from wikipedia): for i from 0 to 255 S[i] := i endfor j := 0 for i from 0 to 255 j := (j + S[i] + key[i mod keylength]) mod 256 swap values of S[i] and S[j] endfor ''' key_length = len(key) # create the array "S" S = list(range(MOD)) # [0,1,2, ... , 255] j = 0 for i in range(MOD): j = (j + S[i] + key[i % key_length]) % MOD S[i], S[j] = S[j], S[i] # swap values return S

jika kita bedah sedikit maka sebenarnya itu sama saja, sebagai contoh disini

image

hal itu merepresentasikan line

j = (j + S[i] + key[i % key_length]) % MOD S[i], S[j] = S[j], S[i]

mari lanjutkan analisa terhadap output file tadi, setelah sbox berhasil diinisialisasi pada algoritma rc4 normal maka akan dilakukan xor terhadap plaintext, tetapi nampaknya berbeda pada kasus kali ini

image

kita dapat melihat input kita a atau 97 dilakukan beberapa operasi dan di chain ke nilai berikutnya, perhatikan bagaimana hasil xor dari input digunakan untuk xor nilai berikutnya (warna biru).

kita juga dapat melihat bahwa sbox nilai sbox yang sudah diinisialisasi tadi juga di chain untuk nilai sbox berikutnya (warna hijau).

tidak hanya disitu nampaknya ada operasi berikutnya sebelum dilakukan check.

{08ECCC1C-C08D-4318-A622-45C5A47F95BB}

setelah enkripsi sebelumnya ada proses pengurangan dengan suatu nilai x, pengurangan dilakukan satu persatu atau dengan proses decrement, dan nilai x berbeda untuk setiap karakter.

perhatikan lagi bahwa mem[261] bukanlah nilai pertama dari input kita setelah enkripsi tadi karena input pertama disimpan dalam mem[256], well ada banyak cara mappingnya mengingat nilai index tersebut sebenarnya sudah menunjukkan di index mana karakter tersebut, tetapi disini nampaknya saya mengambil rute yang lebih sulit, more on that later.

sekarang untuk part terakhir kita perlu mengambil nilai enkripsi dari flag sebenarnya.

{F98F2315-FB29-44A7-8684-4175F8925EBB}

Dump the value and rev for flag

summary dari operasi checker adalah sebagai berikut

  • sbox di inisialisasi
  • operasi xor chain input dan sbox
  • mapping karakter dan operasi pengurangan
  • check flag

awalnya saya dump array awal dan key yang digunakan tetapi saya merasa bahwa saya bisa langsung melakukan dump terhadap hasil akhir sbox yang akan akan digunakan untuk enkripsi dengan melakukan break sebelum operasi xor karena operasi tidak terjadi sebelum enkripsi dengan input.

elif vm[i] == 23: arg1 = vm[i+1] arg2 = vm[i+2] data[32] = data[arg1] ^ data[arg2] print(f"data[32] = data[{arg1}] ({data[arg1]}) ^ data[{arg2}] ({data[arg2]}) ({data[arg1] ^ data[arg2]})") print(mem[:256]) break i += 2

berikutnya kita perlu melakukan mapping terhadap nilai yang di check, seperti yang saya tadi ceritakan bahwa bisa melakukan remapping dengan melihat index daripada melakukan itu apa yang saya lakukan adalah ambil nilai akhir dari enkripsi kita dengan input 'a'*36 tadi, lalu ambil nilai yang digunakan saat proses pengurangan dengan x, lalu cari index nilai yang digunakan untuk pengurangan di nilai setelah enkripsi rc4, hal ini lebih sulit karena ada kemungkinan nilai sama sehingga yah cukup bodoh sebenarnya, kurang lebih seperti ini

i = True j = 0 for x,y in zip(begin, end): val = x - y idx = after_rc4.index(x) if x == 6: idx = (278 - 256) if i else (287 - 256) i = False offst[idx] = val + enc[j] j += 1

anda bisa melihat saya sebenarnya melakukan pengurangan index juga pada akhirnya, kelebihannya dapat sekaligus mencari nilai x dari setiap karakter pada saat pengurangan.

kita bisa lihat sekali lagi pada proses enkripsi tadi

image

panah kuning ini saya tandai dengan artian kita mengetahui nilai-nilai tersebut karena 74 diambil dari sbox[1] lalu kita juga tahu nilai dari sbox[74] sehingga kita bisa tahu nilai apa yang dixor melakukan xor lalu menyimpan nilai sebelum di xor untuk proses dekripsi karakter berikutnya.

Sebenarnya algoritmanya cukup simpel tapi ya skill issue bang, untuk full script sebagai berikut

enc = [51,253,167,166,163,202,217,143,131,46,91,71,111,187,17,135,48,189,232,79,217,26,95,67,4,135,139,242,241,32,223,84,48,137,143,4] sbox = [178,216,120,105,147,252,193,42,95,221,66,171,23,11,197,37,59,145,199,33,170,150,242,14,72,13,1,196,16,129,87,80,173,112,156,161,186,201,91,60,18,163,237,15,228,62,180,175,12,140,5,122,146,115,40,35,127,4,167,254,195,188,34,209,212,134,245,143,25,103,131,81,151,107,218,139,76,240,249,169,174,132,124,84,130,230,126,50,215,133,185,52,191,56,204,219,6,74,32,244,177,70,224,116,24,26,119,231,153,207,158,248,68,233,7,194,251,102,121,3,137,39,89,79,71,29,164,184,217,118,49,182,8,253,53,45,160,111,10,192,198,54,73,152,187,141,235,149,183,92,168,144,57,114,36,172,106,113,246,43,238,241,58,104,30,46,225,65,78,239,20,123,97,48,55,255,47,223,206,110,214,213,9,93,208,21,226,101,96,135,128,2,109,19,136,0,98,200,189,41,100,63,142,232,203,44,190,154,181,64,179,61,148,31,69,247,88,38,75,138,243,86,202,27,83,250,236,165,220,125,176,205,82,67,229,162,210,222,99,211,155,159,28,157,85,227,77,117,51,166,17,108,90,22,234,94] key = [235,39,240,220,203,126,149,22,38,174,44,67,195,20,149,199,57,42,48,92,85,96,6,170,212,154,221,164,167,172,91,110,5,142,37,121,62,183,127,130,67,235,47,75,120,104,209,228,108,3,9,6,114,13,193,82,132,148,167,227,150,40,56,70,13,74,209,52,209,146,50,223,206,34,177,92,199,101,164,192,153,211,61,70,198,225,83,190,85,189,112,195,159,57,239,34,138,165,124,249,59,196,121,127,27,227,225,110,71,134,40,198,93,65,20,12,22,120,107,98,248,165,173,64,174,32,250,158,231,2,218,6,171,70,113,156,58,209,96,59,40,222,17,145,245,18,120,38,178,237,16,200,150,160,0,184,90,42,104,31,61,27,27,165,93,129,247,69,87,230,163,209,244,126,110,199,30,123,162,201,85,225,0,254,109,254,95,192,108,184,142,35,198,42,33,216,77,49,65,219,246,0,50,151,22,0,96,42,177,159,205,57,195,107,162,48,142,246,129,129,132,178,177,187,93,25,63,70,218,193,64,169,4,195,105,26,177,147,229,108,134,7,247,31,113,234,64,207,115,188,118,178,174,155,167,123] after_sbox_key = [167, 74, 147, 107, 134, 255, 203, 55, 5, 205, 230, 21, 71, 195, 168, 111, 95, 33, 163, 228, 240, 1, 177, 252, 176, 54, 82, 141, 80, 236, 183, 242, 67, 136, 159, 190, 253, 232, 35, 50, 218, 209, 244, 249, 149, 171, 38, 196, 83, 248, 29, 22, 106, 28, 119, 142, 207, 206, 6, 250, 37, 96, 57, 69, 208, 184, 15, 174, 243, 215, 53, 60, 116, 173, 199, 152, 86, 117, 65, 0, 181, 227, 17, 197, 68, 59, 102, 225, 179, 73, 24, 212, 8, 40, 39, 123, 4, 158, 48, 85, 78, 222, 63, 194, 76, 49, 216, 105, 200, 172, 229, 110, 112, 233, 114, 185, 192, 104, 115, 130, 235, 10, 182, 99, 30, 191, 92, 23, 146, 150, 16, 122, 47, 238, 153, 26, 154, 45, 128, 137, 239, 7, 186, 140, 84, 234, 36, 246, 118, 162, 89, 34, 72, 79, 145, 113, 170, 27, 20, 210, 223, 64, 52, 139, 155, 213, 14, 126, 93, 133, 201, 245, 214, 224, 247, 2, 157, 164, 121, 9, 77, 88, 187, 18, 51, 143, 58, 251, 32, 165, 56, 41, 25, 189, 220, 166, 221, 66, 108, 202, 132, 90, 100, 180, 144, 138, 178, 61, 43, 217, 97, 198, 75, 169, 124, 254, 211, 31, 81, 193, 91, 226, 13, 87, 42, 3, 160, 129, 19, 156, 109, 131, 127, 70, 241, 101, 237, 98, 94, 175, 135, 219, 161, 103, 46, 62, 188, 120, 11, 204, 12, 148, 231, 125, 151, 44] after_rc4 = [64, 73, 127, 209, 157, 24, 185, 109, 207, 9, 65, 164, 28, 59, 170, 201, 62, 160, 124, 60, 20, 242, 6, 54, 154, 188, 244, 158, 216, 35, 191, 6, 89, 63, 150, 8] begin = [24,154,216,191,158,157,170,209,185,64,9,6,109,207,63,150,59,244,65,20,164,60,124,54,73,188,160,89,62,28,8,35,127,242,201,6] end = [14,152,201,174,147,127,164,191,163,37,6,-27,105,193,31,126,40,232,39,13,164,40,116,31,60,183,125,73,33,7,-26,4,126,217,173,-3] offst = [0]*36 i = True j = 0 for x,y in zip(begin, end): val = x - y idx = after_rc4.index(x) if x == 6: idx = (278 - 256) if i else (287 - 256) i = False offst[idx] = val + enc[j] j += 1 v2 = 0 v6 = 0 l = len(offst) bef = 0 for i in range(l): v5 = offst[i] v3 = after_sbox_key[i + 1] v2 = (v2 + v3) & 0xff after_sbox_key[v2], after_sbox_key[i + 1] = after_sbox_key[i+1], after_sbox_key[v2] v3 = after_sbox_key[i + 1] v4 = after_sbox_key[v2] v3 = after_sbox_key[(v3 + v4) & 0xff] v6 = v3 ^ v5 print(chr((v6 ^ bef) & 0xff), end='') bef = v5

TCP1P{h0w_d1d_y0u_50lv3_th1s_ch4ll?_9b14fa}

optimum

{F9B3D364-DC1F-48E2-8388-F8C51CCDB48A}

diberikan link mega yang berisi binary soal sebesar 87 MB

{FB984229-0A0F-429C-A913-CE7F055D7384}

dan sebuah perintah untuk menjalankan binary server tersebut, contoh ketika dijalankan

./server -H 4444 -f 1

kita bisa mengakses http://localhost:4444 dan akan melihat sebuah flag checker sebagai berikut

{4ED6C441-E3C4-48CC-92A1-3FFD7E770ADF}

Preliminary analysis

ketika dibuka dengan ida kita perlu menunggu sedikit lebih lama karena ukuran binarynya yang cukup besar, setelah berhasil dimuat kita bisa melihat banyak sekali fungsi

{A07A3F14-176F-4326-AB77-A57AD98ED9F3}

untuk melakukan reverse terhadap binary yang cukup besar seperti ini diperlukan waktu yang cukup lama oleh karena itu alangkah baiknya untuk mencari tahu binary apa sebenarnya ini.

karena simbol pada binary ini tidak di strip kita bisa melakukan searching pada nama-nama fungsi tersebut seperti contoh kita akan banyak menemui fungsi yang memiliki substring kphp pada namanya, sedikit melakukan searching kita dapat menemukan source code kphp pada github

https://github.com/VKCOM/kphp

atau kita bisa melihat pada output yang diberikan oleh server dan melihat php-master.cpp atau php-engine.cpp

{BA78637D-C103-40B8-A015-9E40428848F2}

dari situ kita bisa melakukan pencarian seperti php cpp compiler dan lain sebagainya yang salah satunya akan mengarah pada github kphp itu sendiri.

bahkan kita bisa menemukan dokumentasi dari kphp yang berbahasa inggris.

https://vkcom.github.io/kphp/

Where's the main logic

ketika binary dijalankan nantinya ia akan menjalankan fungsi run_main yang didalam fungsi tersebut akan dijalankan fungsi start_server dan di dalam start_server akan diinisialisasi worker dan master, sejujurnya saya kurang familiar tetapi kita bisa menganggapnya sebagai parent dan child, dalam cpp worker sendiri seperti thread CMIIW.

untuk mencari fungsi yang akan dieksekusi oleh worker kita dapat memperhatikan pada fungsi start_server kita akan melihat inisialisasi worker dan event_loop

{07302DEA-27EA-4C5B-A5C3-50C630A9605E}

kita dapat melihat bahwa parameter yang diberikan kepada fungsi init worker dipakai sebagai parameter juga di event loop, nah dari sini kita dapat melihat sekiranya fungsi apa yang akan dijalankan ketika ada koneksi masuk, kita bisa melakukan debugging dengan gdb ataupun ida itu sendiri, tetapi disini saya memilih menggunakan gdb.

image

dapat dilihat pada nilai yang saya tunjuk dengan panah warna merah, nilai tersebut adalah hasil dari fungsi worker_global_init fungsi tersebut mengembalikan fungsi yang nantinya dipakai oleh event loop.

ada cara lain sebenarnya seperti mencari semua fungsi dengan prefix f$ fungsi dengan prefix tersebut kemungkinan besar adalah fungsi yang berkaitan dengan php, perhatikan contoh dari kphp pada dokumentasinya di link berikut

https://vkcom.github.io/kphp/kphp-basics/compile-sample-script.html

{3991BF3B-5921-4552-995D-A9E96DD129BC}

pada dokumentasi tersebut fungsi yang didapat dari source code php yang dicompile akan memiliki prefix tersebut, meskipun dalam binary ini saya melihat banyak sekali fungsi dengan prefix itu kemungkinan adalah fungsi library php.

perhatikan juga bahwa stringnya akan memiliki prefix v$ tetapi pada kasus ini tidak ada v$ melainkan dengan prefix lain.

kembali pada fungsi tadi kita akan melihat hasil dekompilasi berikut

void __cdecl f$src_testa2eaf1d201790ddf$run() { f$src_testa2eaf1d201790ddf(); if ( !CurException.o.ptr ) finish(0LL, 0); }

dan pada fungsi f$src_testa2eaf1d201790ddf(); kita dapat melihat pemanggilan fungsi yang melakukan check flag yaitu f$flaggermeister

if ( !ptr || ptr->refcnt <= 0x7FFFFFEF && (++ptr->refcnt, (ptr = v23.o.ptr) == 0LL) ) { class_instance<C$MetaPHP>::warn_on_access_null(&v23); ptr = v23.o.ptr; } mixed::mixed((mixed *const)&s[0].storage_, &ptr->$flag); f$flaggermeister(s); print((const string *)s); string::~string((string *const)s); mixed::~mixed((mixed *const)&s[0].storage_); v20 = v23.o.ptr;

sebelum fungsi flaggermeister dipanggil kita dapat melihat sesuatu seperti

p = c_str_a32c8095873e006f.p; v15 = *((_DWORD *)c_str_a32c8095873e006f.p - 1); if ( !v18 ) { LABEL_16: if ( v15 <= 2147483631 ) *((_DWORD *)p - 1) = v15 + 1; s[0].storage_ = (uint64_t)p; print((const string *)&s[0].storage_); string::~string((string *const)&s[0].storage_); v16 = c_str_85d6655220b29dbb.p; v17 = *((_DWORD *)c_str_85d6655220b29dbb.p - 1); if ( v17 <= 2147483631 ) *((_DWORD *)c_str_85d6655220b29dbb.p - 1) = v17 + 1; s[0].storage_ = (uint64_t)v16; print((const string *)&s[0].storage_); string::~string((string *const)&s[0].storage_); goto LABEL_21; }

perhatikan c_str$ saya menyadari bahwa prefix c_str$ ini adalah untuk string yang digunakan pada source php asli seperti v$ diatas, string tersebut akan di resolve saat runtime sehingga kita perlu melakukan debugging.

{55C84067-FCFC-4F7E-9D51-1E96803F66DF}

Debugging webserver

untuk melakukan debugging dengan gdb ada sedikit trick yang bisa kita lakukan, karena kita perlu melakukan debug pada proses child / worker kita perlu mengubah mode follow fork ke child, tetapi ketika fork secara default proses child akan detach dari proses parentnya hal ini akan membuat server tetap berjalan apapun yang terjadi dengan child, sehingga ketika kita selesai dengan proses child kita tidak bisa kembali ke proses parentnya dalam sesi gdb sekarang.

solusinya kita bisa melakukan attach kembali dengan prosesnya dengan melakukan gdb a <pid> tetapi beberapa perilaku ketika debugging webserver yang saya temui tidak mengizinkan untuk mengubah mode follow fork ke child ketika proses sudah berjalan, harus sebelum webserver berjalan.

solusi lainnya adalah kita bisa kill proses webserver tersebut dengan command berikut

{DB58824B-AF77-4AFA-9B9C-D16A86E2975B}

sebenarnya kita bisa memulai webserver di port lain, tetapi itu akan membebankan komputer saya jika dilakukan terus menerus.

untuk menghindari hal-hal tersebut kita bisa set mode fork agar tidak detach, kurang lebih sebelum webserver berjalan kita bisa melakukan

set follow-fork-mode child
set detach-on-fork off

dan semisal ketika proses child mati kita bisa berganti dengan command inferior 1 (check dulu sebelumnya dengan info inferior)

Reverse flag function

setelah berhasil setup debugger kita bisa melihat string c_str$ sudah terinisialisasi ketika webserver sudah berjalan

{C3A198CA-7856-4F17-BAC9-AD479FAC0D31}

pada fungsi flaggermeister kita akan melihat potongan kode berikut

mixed::to_string(a2); v5 = *(_DWORD *)(*(_QWORD *)&v201.type_ - 12LL); string::~string((string *const)&v201); if ( v5 != 12 ) { p = c_str_52c4aef4a7bea04a.p; v7 = *((_DWORD *)c_str_52c4aef4a7bea04a.p - 1); if ( v7 <= 2147483631 ) *((_DWORD *)c_str_52c4aef4a7bea04a.p - 1) = v7 + 1; *(_QWORD *)&a1->type_ = p; mem = &array<Unknown>::array_inner::empty_array(void)::empty_array; goto LABEL_5; }

string diatas berisi False!

pwndbg> x/gx &c_str$52c4aef4a7bea04a 0xe521e0 <c_str$52c4aef4a7bea04a>: 0x0000000000b05fec pwndbg> x/s 0x0000000000b05fec 0xb05fec <_ZL3raw+44>: "False!"

sehingga dari situ kita tahu bahwa panjang flag adalah 12 karakter, lanjut pada potongan kode berikutnya kita akan melihat sebuah while loop yang menarik

while ( 1 ) { mixed::to_string(_R13); v25 = *(unsigned int *)(*(_QWORD *)&v201.type_ - 12LL); string::~string((string *const)&v201); if ( v23 >= v25 ) break; mixed::get_value(&v200, _R13); mixed::to_string(&v200); sizeb = f$ord(&s); mixed::get_value(&v201, _R13); mixed::to_string(&v201); v24 = v23++; v = sizeb ^ (2 * f$ord(&v197)); array<long>::set_value(&v191, v24, &v); string::~string(&v197); mixed::~mixed(&v201); string::~string(&s); mixed::~mixed(&v200); } v26 = c_st

variable v201 yang tadinya digunakan untuk menghitung panjang input digunakan lagi pada kode diatas, lalu dilakukan xor, kita bisa lihat langsung bagaimana hal tersebut terjadi pada gdb kita akan set breakpoint pada operasi xor, input yang kita gunakan adalah 'a'*12

{46C1010A-DEF3-4D70-A936-9973756B64C4}

kita bisa melihat disini input kita 'a' di xor dengan sesuatu nah jika kita debug dengan benar kita tahu bahwa operasinya sesungguhnya adalah sebagai berikut

ord('a') ^ (2 * ord('a'))

pada kode block berikutnya kita akan menemukan operasi matematika sebagai berikut

mixed::to_string(&v199); f$bcpow((const string *)&v201, &c_str_48cb7221c54d7cc0, (__int64)&v200); v41 = v194.p; if ( v194.p ) { v42 = *((_DWORD *)v194.p - 1); if ( v42 <= 2147483631 ) { v43 = v42 - 1; *((_DWORD *)v194.p - 1) = v43; if ( v43 < 0 ) { v70 = RuntimeAllocator::current(); RuntimeAllocator::free_script_memory(v70, v41 - 12, (unsigned int)(*((_DWORD *)v41 - 2) + 13)); } } } v194.p = *(char **)&v201.type_; *(_QWORD *)&v201.type_ = src; string::~string((string *const)&v201); string::~string((string *const)&v200); f$bcmul((const string *)&v201, &v195, (__int64)&v194); v44 = v192.p; if ( v192.p ) { v45 = *((_DWORD *)v192.p - 1); if ( v45 <= 2147483631 ) { v46 = v45 - 1; *((_DWORD *)v192.p - 1) = v46; if ( v46 < 0 ) { v71 = RuntimeAllocator::current(); RuntimeAllocator::free_script_memory(v71, v44 - 12, (unsigned int)(*((_DWORD *)v44 - 2) + 13)); } } } v192.p = *(char **)&v201.type_; *(_QWORD *)&v201.type_ = src; string::~string((string *const)&v201); f$bcadd((const string *)&v200, &v193, (__int64)&v192);

saya yakin disalah satu operasi input kita akan digunakan, tetapi untuk memastikannya mari pasang breakpoint pada ketiga operasi tersebut.

{36E90342-474A-462E-9BB8-C137CACC251C}

pada operasi xor kita melihat bahwa 256 akan dipangkat dengan 0 yang mana akan menghasilkan 1, perlu diingat meskipun yang kita lihat adalah string 256 sebenarnya string tersebut adalah sebuah integer dalam php karena dilakukan operasi matematika

{2723B45F-50E0-4202-95E9-10A990FA7F4D}

lalu berikutnya pada operasi perkalian kita melihat hasil dari operasi pangkat sebelumnya di kali dengan 163, nilai tersebut sebenarnya adalah hasil dari input kita yang di xor tadi

>>> ord('a') ^ (2 * ord('a')) 163

{77A3F995-C49B-4E0C-8AF0-500ABE8727AF}

lalu pada operasi penambahan hasil perkalian tadi ditambah dengan 0, jika kita lihat iterasi berikutnya 0 ini sebenarnya adalah nilai penambahan pada iterasi sebelumnya, karena disini iterasi pertama sehingga nilainya masih 0.

{95704D84-0D37-47B1-B0FE-657B2FA0C353}

lalu kita bisa melihat sebuah proses komparasi

v66 = *((unsigned int *)v193.p - 3); if ( (_DWORD)v66 == *((_DWORD *)c_str_9fb179c3b5897d26.p - 3) && !memcmp(v193.p, c_str_9fb179c3b5897d26.p, v66) )

sekali lagi kita bisa setup breakpoint pada operasi tersebut

{EE019DA7-47DB-48E8-A109-175588FCF4A9}

dari sini kita tahu bahwa nilai flag sebenarnya adalah 30882963414412829128273944991

kita bisa mengkonstruksi ulang operasi matematika tadi sebagai berikut

target = 30882963414412829128273944991 our_inp = [0x30]*12 val_add = 0 for i in range(12): val_pow = pow(256, i) val_mul = ((our_inp[i] * 2) ^ our_inp[i]) * val_pow val_add = val_add + val_mul assert val_add == target, "Oh no wrong input"

dan untuk mendapatkan input yang benar kita hanya perlu menggunakan z3, berikut script z3 yang saya gunakan

from z3 import * target = 30882963414412829128273944991 solver = Solver() our_inp = [BitVec(f'inp_{i}', 128) for i in range(12)] # 128 bit cus big # ascii constraint for inp in our_inp: solver.add(inp >= 0, inp <= 255) val_add = 0 for i in range(12): val_pow = 256 ** i val_mul = ((our_inp[i] * 2) ^ our_inp[i]) * val_pow val_add += val_mul solver.add(val_add == target) if solver.check() == sat: model = solver.model() result = [model[inp].as_long() for inp in our_inp] print(''.join(chr(b) for b in result)) else: print("cupu")

perlu diperhatikan karena nilai dari operasinya cukup besar sehingga disini saya menggunakan BitVec sebesar 128 bit.

TCP1P{u_9ot_m3_GG!}

Conclusion

Secara garis besar soal-soalnya sangat seru, meskipun sepertinya untuk beberapa soal idenya sudah pernah muncul di ctf lokal, overall good job tcp1p.