Minggu ini saya bermain TCP1P CTF bersama tim L3ak, alhamdulillahnya kami berhasil melakukan full clear beberapa category salah satunya reverse engineering
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:.
dari link yang diberikan pada deskripsi soal kita tahu bahwa soal ini pasti berkaitan dengan NekoVM
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
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.
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.
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
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.
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.
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
kembali ke intruksi-intruksi module kita dapat perilakunya sebagai berikut
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
...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
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
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
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}
melihat nama authornya bohong kalo dibilang average
, JK.
karena tidak ada yang menarik dari deskripsi soal dan attachment hanya diberikan satu buah file binary, maka kita langsung aja coba jalankan.
ternyata memang benar flag checker saat saya check size binarynya ternyata cukup besar untuk ukuran soal flag checker
bahkan di ida tidak dapat di decompile
di awal operasi terdapat fungsi yang dipanggil terlebih dahulu yang mengembalikan sebuah alamat.
alamat tadi digunakan untuk menyimpan byte-byte.
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.
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
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.
setelah semua inisialisasi nilai berhasil dilakukan kita dapat melihat sebuah pemanggilan fungsi setelahnya
__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
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
output tersebut saya pindahkan ke file tersendiri dan melakukan analisis lebih lanjut
logic output dapat dibagi menjadi beberapa bagian
untuk bagian pertama akan dilakukan inisialisasi data
data yang sudah di inisialisasi tadi dilakukan beberapa operasi aritmatika lalu dilakukan swap
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
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
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.
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.
summary dari operasi checker adalah sebagai berikut
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
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}
diberikan link mega yang berisi binary soal sebesar 87 MB
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
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
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
atau kita bisa melihat pada output yang diberikan oleh server dan melihat php-master.cpp
atau php-engine.cpp
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.
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
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.
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
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.
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
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
)
setelah berhasil setup debugger kita bisa melihat string c_str$
sudah terinisialisasi ketika webserver sudah berjalan
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
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.
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
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
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.
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
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!}
Secara garis besar soal-soalnya sangat seru, meskipun sepertinya untuk beberapa soal idenya sudah pernah muncul di ctf lokal, overall good job tcp1p.