Halo
Nama saya Marat Gayanov, saya ingin berbagi dengan Anda solusi saya untuk masalah dari
kontes Best Reverser , untuk menunjukkan cara membuat keygen untuk kasus ini.
Deskripsi
Dalam kompetisi ini, peserta diberikan permainan ROM untuk Sega Mega Drive (
best_reverser_phd9_rom_v4.bin ).
Tugas: untuk mengambil kunci seperti itu yang bersama-sama dengan alamat email peserta akan diakui valid.
Jadi solusinya ...
Alat-alatnya
Memeriksa Panjang Kunci
Program tidak menerima setiap kunci: Anda harus mengisi seluruh bidang, ini adalah 16 karakter. Jika kuncinya lebih pendek, Anda akan melihat pesan: โPanjangnya salah! Coba lagi ... ".
Mari kita coba temukan baris ini di program, yang mana kita akan menggunakan pencarian biner (Alt-B). Apa yang akan kita temukan?
Kami tidak hanya akan menemukan ini, tetapi juga jalur layanan lain di sekitarnya: โKunci salah! Coba lagi ... "dan" KAMU ADALAH REVERSER TERBAIK! ".
Saya menetapkan
WRONG_LENGTH_MSG
,
WRONG_LENGTH_MSG
, dan
WRONG_KEY_MSG
untuk kenyamanan.
Istirahat membaca alamat
0x0000FDFA
- cari tahu siapa yang bekerja dengan pesan "Panjangnya salah! Coba lagi ... ". Dan jalankan debugger (itu akan berhenti beberapa kali sebelum kunci dapat dimasukkan, cukup tekan F9 di setiap perhentian). Masukkan email Anda, kunci
ABCD
.
Debugger mengarah ke
0x00006FF0 tst.b (a1)+
:
Tidak ada yang menarik di blok itu sendiri. Jauh lebih menarik siapa yang mentransfer kendali di sini. Kami melihat tumpukan panggilan:
Klik dan dapatkan di sini - ke instruksi
0x00001D2A jsr (sub_6FC0).l
:
Kami melihat bahwa semua pesan yang mungkin ditemukan di satu tempat. Tetapi mari kita cari tahu di mana kontrol ditransfer ke dalam blok
WRONG_KEY_LEN_CASE_1D1C
. Kami tidak akan mengatur jeda, cukup gerakkan kursor di atas panah ke blok. Pemanggil terletak di
0x000017DE loc_17DE
(yang akan saya ganti namanya menjadi
CHECK_KEY_LEN
):
0x000017EC cmpi.b 0x20 (a0, d0.l)
alamat
0x000017EC cmpi.b 0x20 (a0, d0.l)
(instruksi dalam konteks ini terlihat untuk melihat apakah ada karakter kosong di akhir array karakter kunci), restart, masukkan kembali surat dan kunci
ABCD
. Debugger berhenti dan menunjukkan bahwa kunci yang dimasukkan terletak di alamat
0x00FF01C7
(disimpan pada saat itu dalam register
a0
):
Ini adalah temuan yang bagus, melalui itu kami akan mengambil semuanya. Tapi pertama-tama, tandai byte kunci untuk kenyamanan:
Menggulir ke atas dari tempat ini, kita melihat bahwa surat disimpan di sebelah tombol:
Kami menyelam lebih dalam dan lebih dalam, dan inilah saatnya untuk menemukan kriteria untuk kebenaran kunci. Sebaliknya, bagian pertama dari kunci.
Kriteria untuk kebenaran bagian pertama dari kunci
Perhitungan awal
Adalah logis untuk mengasumsikan bahwa segera setelah memeriksa panjang, operasi lain dengan kunci akan mengikuti. Pertimbangkan blok segera setelah cek:
Blok ini sedang menjalani pekerjaan pendahuluan. Fungsi
get_hash_2b
(dalam aslinya adalah
sub_1526
) dipanggil dua kali. Pertama, alamat byte pertama dari kunci ditransmisikan ke sana (register
a0
berisi alamat
KEY_BYTE_0
), yang kedua - kelima (
KEY_BYTE_4
).
Saya menamai fungsi seperti ini karena menganggapnya seperti hash 2-byte. Ini adalah nama yang paling bisa dimengerti yang saya ambil.
Saya tidak akan mempertimbangkan fungsi itu sendiri, tetapi saya akan segera menulisnya dengan python. Dia melakukan hal-hal sederhana, tetapi deskripsinya dengan tangkapan layar akan memakan banyak ruang.
Hal paling penting untuk dikatakan tentang itu: alamat input diberikan ke input, dan pekerjaan sedang dilakukan pada 4 byte dari alamat ini. Artinya, mereka mengirim byte pertama kunci ke input, dan fungsinya akan bekerja dengan 1,2,3,4. Diposting kelima, fungsi ini bekerja dengan 5,6,7,8. Dengan kata lain, di blok ini ada perhitungan selama paruh pertama kunci. Hasilnya ditulis ke register
d0
.
Jadi fungsi
get_hash_2b
:
Segera tulis fungsi decoding hash:
Saya tidak membuat fungsi decoding yang lebih baik, dan itu tidak sepenuhnya benar. Karena itu, saya akan memeriksanya seperti ini (tidak sekarang, tetapi nanti):
key_4s == decode_hash_4s(get_hash_2b(key_4s))
Periksa pengoperasian
get_hash_2b
. Kami tertarik pada status register
d0
setelah fungsi dieksekusi. Kami
0x000017FE
break pada
0x000017FE
,
0x00001808
, kunci yang kami masukkan
ABCDEFGHIJKLMNOP
.
Nilai
0xABCD
,
0xEF01
dimasukkan dalam register
d0
. Dan apa yang akan diberikan
get_hash_2b
?
>>> first_hash = get_hash_2b("ABCD") >>> hex(first_hash) 0xabcd >>> second_hash = get_hash_2b("EFGH") >>> hex(second_hash) 0xef01
Verifikasi berlalu.
Kemudian
xor eor.w d0, d5
diproduksi, hasilnya dimasukkan dalam
d5
:
>>> hex(0xabcd ^ 0xef01) 0x44cc
Memperoleh hash
0x44CC
adalah
0x44CC
dan terdiri dari perhitungan awal. Selanjutnya, semuanya menjadi lebih rumit.
Kemana perginya hash
Kami tidak bisa melangkah lebih jauh jika kami tidak tahu bagaimana program bekerja dengan hash. Tentunya itu bergerak dari
d5
ke memori, karena Register berguna di tempat lain. Kita dapat menemukan peristiwa semacam itu melalui jejak (menonton
d5
), tetapi tidak manual, tetapi otomatis. Script berikut akan membantu:
#include <idc.idc> static main() { auto d5_val; auto i; for(;;) { StepOver(); GetDebuggerEvent(WFNE_SUSP, -1); d5_val = GetRegValue("d5"); // d5 if (d5_val != 0xFFFF44CC){ break; } } }
Biarkan saya mengingatkan Anda bahwa kita sekarang pada istirahat terakhir
0x00001808 eor.w d0, d5
. Rekatkan skrip (
Shift-F2
), klik
Run
Script akan berhenti pada instruksi
0x00001C94 move.b (a0, a1.l), d5
, tetapi pada saat ini
d5
telah dihapus. Namun, kita melihat bahwa nilai dari
d5
digerakkan oleh instruksi
0x00001C56 move.w d5,a6
: ditulis ke dalam memori di alamat
0x00FF0D46
(2 byte).
Ingat: hash disimpan di 0x00FF0D46
.Kami menangkap instruksi yang dibaca dari
0x00FF0D46-0x00FF0D47
(kami menetapkan jeda untuk membaca). Tertangkap 4 blok:




Bagaimana cara memilih yang benar / benar?
Kembali ke awal:
Blok ini menentukan apakah program akan menuju ke
LOSER_CASE
atau ke
WINNER_CASE
:
Kita melihat bahwa dalam register
d1
harus nol untuk menang.
Di mana nol ditetapkan? Cukup gulir ke atas:
Jika
loc_1EEC
terpenuhi di blok
loc_1EEC
:
*(a6 + 0x24) == *(a6 + 0x22)
maka kita mendapatkan nol di
d5
.
Jika kita menghentikan instruksi
0x00001F16 beq.w loc_20EA
, kita akan melihat bahwa
a6 + 0x24 = 0x00FF0D6A
dan nilai
0x4840
disimpan di sana. Dan dalam
a6 + 0x22 = 0x00FF0D68
disimpan.
Jika kita memasukkan kunci berbeda, email, kita akan melihat bahwa
0xCB4C -
.
Paruh pertama kunci hanya akan diterima jika di 0x00FF0D6A
juga akan ada 0xCB4C
. Ini adalah kriteria untuk ketepatan bagian pertama dari kunci.Kami mencari tahu blok mana yang ditulis dalam
0x00FF0D6A
-
0x00FF0D6A
istirahat pada catatan, masukkan surat dan kunci lagi.
Dan kita akan menemukan blok
loc_EAC
ini (sebenarnya ada 3, tetapi dua yang pertama hanya nol
0x00FF0D6A
):
Blok ini milik fungsi
sub_E3E
.
Melalui tumpukan panggilan, kami mengetahui bahwa fungsi
sub_E3E
dipanggil di blok
loc_1F94
,
loc_203E
:


Ingat kami menemukan 4 blok sebelumnya?
loc_1F94
kami lihat di sana - ini adalah awal dari algoritma pemrosesan kunci utama.
Loc_1 loop penting pertama
Fakta bahwa
loc_1F94
adalah sebuah siklus terlihat dari kode: ia dieksekusi
d4
kali (lihat instruksi
0x00001FBA d4,loc_1F94
):
Apa yang harus dicari:
- Ada fungsi
sub_5EC
. - Instruksi 0x00001FB4 jsr (a0) memanggil fungsi sub_E3E (ini dapat dilihat dengan jejak sederhana).
Apa yang terjadi di sini:
- Fungsi
sub_5EC
menulis hasil eksekusi ke register d0
(ini dibahas pada bagian terpisah di bawah). - Byte pada address
sp+0x33
( 0x00FFFF79
, debugger memberitahu kita) disimpan dalam register d1
, itu sama dengan byte kedua dari alamat hash utama ( 0x00FF0D47
). Ini mudah dibuktikan jika Anda memecahkan rekor pada 0x00FFFF79
: ini akan bekerja pada instruksi 0x00001F94 move.b 1(a2), 0x2F(sp)
. Register a2
pada saat ini menyimpan alamat 0x00FF0D46
- alamat hash, yaitu, 0x1(a2) = 0x00FF0D46 + 1
- alamat byte kedua dari hash. - Register
d0
ditulis d0^d1
.
- Hasil xor'a yang dihasilkan diberikan ke fungsi
sub_E3E
, perilaku yang tergantung pada perhitungan sebelumnya (ditampilkan di bawah). - Ulangi
Berapa kali siklus ini berjalan?
Temukan ini. Jalankan skrip berikut:
#include <idc.idc> static main() { auto pc_val, d4_val, counter=0; while(pc_val != 0x00001F16) { StepOver(); GetDebuggerEvent(WFNE_SUSP, -1); pc_val = GetRegValue("pc"); if (pc_val == 0x00001F92){ counter++; d4_val = GetRegValue("d4"); print(d4_val); } } print(counter); }
0x00001F92 subq.l 0x1,d4
- di sini ditentukan apa yang akan terjadi di
d4
segera sebelum loop:
Kami berurusan dengan fungsi sub_5EC.
sub_5EC
Sepotong kode yang signifikan:
di mana
0x2c(a2)
selalu
0x00FF1D74
.
Bagian ini dapat ditulis ulang seperti ini di pseudo-code:
d0 = a2 + 0x2C *(a2+0x2C) = *(a2+0x2C) + 1 #*(0x00FF1D74) = *(0x00FF1D74) + 1 result = *(d0) & 0xFF
Yaitu, 4 byte dari
0x00FF1D74
adalah alamatnya, karena mereka diperlakukan seperti pointer.
Bagaimana cara menulis ulang fungsi
sub_5EC
dengan python?
- Atau membuat memori dump dan bekerja dengannya.
- Atau cukup tuliskan semua nilai yang dikembalikan.
Metode kedua saya lebih suka, tetapi bagaimana jika, dengan data otorisasi yang berbeda, nilai yang dikembalikan berbeda? Lihat ini.
Script akan membantu dalam hal ini:
#include <idc.idc> static main() { auto pc_val=0, d0_val; while(pc_val != 0x00001F16){ pc_val = GetRegValue("pc"); if (pc_val == 0x00001F9C) StepInto(); else StepOver(); GetDebuggerEvent(WFNE_SUSP, -1); if (pc_val == 0x00000674){ d0_val = GetRegValue("d0") & 0xFF; print(d0_val); } } }
Saya baru saja membandingkan keluaran ke konsol dengan kunci berbeda, surat.
Menjalankan skrip beberapa kali dengan tombol yang berbeda, kita akan melihat bahwa fungsi
sub_5EC
selalu mengembalikan nilai berikutnya dari array:
def sub_5EC_gen(): dump = [0x92, 0x8A, 0xDC, 0xDC, 0x94, 0x3B, 0xE4, 0xE4, 0xFC, 0xB3, 0xDC, 0xEE, 0xF4, 0xB4, 0xDC, 0xDE, 0xFE, 0x68, 0x4A, 0xBD, 0x91, 0xD5, 0x0A, 0x27, 0xED, 0xFF, 0xC2, 0xA5, 0xD6, 0xBF, 0xDE, 0xFA, 0xA6, 0x72, 0xBF, 0x1A, 0xF6, 0xFA, 0xE4, 0xE7, 0xFA, 0xF7, 0xF6, 0xD6, 0x91, 0xB4, 0xB4, 0xB5, 0xB4, 0xF4, 0xA4, 0xF4, 0xF4, 0xB7, 0xF6, 0x09, 0x20, 0xB7, 0x86, 0xF6, 0xE6, 0xF4, 0xE4, 0xC6, 0xFE, 0xF6, 0x9D, 0x11, 0xD4, 0xFF, 0xB5, 0x68, 0x4A, 0xB8, 0xD4, 0xF7, 0xAE, 0xFF, 0x1C, 0xB7, 0x4C, 0xBF, 0xAD, 0x72, 0x4B, 0xBF, 0xAA, 0x3D, 0xB5, 0x7D, 0xB5, 0x3D, 0xB9, 0x7D, 0xD9, 0x7D, 0xB1, 0x13, 0xE1, 0xE1, 0x02, 0x15, 0xB3, 0xA3, 0xB3, 0x88, 0x9E, 0x2C, 0xB0, 0x8F] l = len(dump) offset = 0 while offset < l: yield dump[offset] offset += 1
Jadi
sub_5EC
siap.
Baris berikutnya adalah
sub_E3E
.
sub_E3E
Sepotong kode yang signifikan:
Dekripsi:
, d2, . a2 0xFF0D46, a2 + 0x34 = 0xFF0D7A d0 = *(a2 + 0x34) *(a2 + 0x34) = *(a2 + 0x34) + 1 , a0 a0 = d0 *(a0) = d2 offset, d2. a2 0xFF0D46, a2 + 0x24 = 0xFF0D6A - , (. ) 0x00000000, d0 = *(a2 + 0x24) d2 = d0 ^ d2 d2 = d2 & 0xFF d2 = d2 + d2 - 2 0x00011FC0 + d2, ROM, 0x00011FC0 + d2 a0 = 0x00011FC0 d2 = *(a0 + d2) 8 d0 = d0 >> 8 d2 = d0 ^ d2 *(a2 + 0x24) = d2
Fungsi
sub_E3E
turun ke langkah-langkah ini:
- Simpan argumen input ke array.
- Hitung offset offset.
- Tarik 2 byte di alamat
0x00011FC0 + offset
(ROM). - Hasil =
( >> 8) ^ (2 0x00011FC0 + offset)
.
Bayangkan fungsi
sub_E3E
dalam formulir ini:
def sub_E3E(prev_sub_E3E_result, d2, d2_storage): def calc_offset(): return 2 * ((prev_sub_E3E_result ^ d2) & 0xff) d2_storage.append(d2) offset = calc_offset() with open("dump_00011FC0", 'rb') as f: dump_00011FC0_4096b = f.read() some = dump_00011FC0_4096b[offset:offset + 2] some = int.from_bytes(some, byteorder="big") prev_sub_E3E_result = prev_sub_E3E_result >> 8 return prev_sub_E3E_result ^ some
dump_00011FC0
hanyalah sebuah file tempat saya menyimpan 4096 byte dari
[0x00011FC0:00011FC0+4096]
.
Aktivitas sekitar 1FC4
Kami belum melihat alamat
0x00001FC4
, tetapi mudah ditemukan, karena blok berjalan hampir segera setelah siklus pertama.
Blok ini mengubah konten di alamat
0x00FF0D46
(register
a2
), dan di situlah hash kunci disimpan, jadi kami sekarang mempelajari blok ini. Mari kita lihat apa yang terjadi di sini.
- Kondisi yang menentukan apakah cabang kiri atau kanan dipilih adalah:
( ) & 0b1 != 0
. Yaitu, bit pertama dari hash diperiksa. - Jika Anda melihat kedua cabang, Anda akan melihat:
- Dalam kedua kasus, pergeseran ke kanan dengan 1 bit terjadi.
- Di cabang kiri operasi hash dilakukan
0x8000
. - Dalam kedua kasus, nilai hash yang diproses ditulis ke alamat
0x00FF0D46
, yaitu, hash diganti dengan nilai baru. - Perhitungan lebih lanjut tidak kritis, karena, secara kasar, tidak ada operasi tulis di
(a2)
(tidak ada instruksi di mana operan kedua akan berada (a2)
).
Bayangkan sebuah blok seperti ini:
def transform(hash_2b): new = hash_2b >> 1 if hash_2b & 0b1 != 0: new = new | 0x8000 return new
Loop penting kedua adalah loc_203E
loc_203E
- loop, karena
0x0000206C bne.s loc_203E
.
Siklus ini menghitung hash dan di sini adalah fitur utamanya:
jsr (a0)
adalah panggilan ke fungsi
sub_E3E
yang telah kita periksa - ia bergantung pada hasil sebelumnya dari pekerjaannya sendiri dan pada beberapa masukan argumen (dilewatkan melalui register
d2
atas, dan di sini melalui
d0
)
Mari cari tahu apa yang diteruskan padanya melalui register
d0
.
Kami telah bertemu dengan konstruksi
0x34(a2)
- fungsi
sub_E3E
menyimpan argumen yang diteruskan di sana. Ini berarti bahwa argumen yang diteruskan sebelumnya digunakan dalam loop ini. Tapi tidak semua.
Dekripsi bagian kode:
2 a2+0x1C move.w 0x1C(a2), d0 neg.l d0 a0 sub_E3E movea.l 0x34(a2), a0 , d0 2 a0-d0( d0 ) move.b (a0, d0.l), d0
Intinya adalah tindakan sederhana: pada setiap iterasi, ambil
d0
argumen yang tersimpan dari akhir array. Artinya, jika 4 disimpan dalam
d0
, maka kita mengambil elemen keempat dari akhir.
Jika demikian, apa tepatnya yang
d0
? Di sini saya melakukannya tanpa skrip, tetapi cukup menuliskannya, membuat istirahat di awal blok ini. Inilah mereka:
0x04, 0x04, 0x04, 0x1C, 0x1A, 0x1A, 0x06, 0x42, 0x02
.
Sekarang kita memiliki segalanya untuk menulis fungsi perhitungan hash kunci lengkap.
Fungsi perhitungan hash penuh
def finish_hash(hash_2b):
Pemeriksaan Kesehatan
- Dalam debugger, kami menetapkan break ke alamat
0x0000180A move.l 0x1000,(sp)
(segera setelah perhitungan hash). - Istirahat ke alamat
0x00001F16 beq.w loc_20EA
(perbandingan hash akhir dengan 0xCB4C
konstan). - Di program, masukkan kunci
ABCDEFGHIJKLMNOP
, tekan Enter
. - Debugger berhenti di
0x0000180A
, dan kami melihat bahwa nilai 0xFFFF44CC
0x44CC
register d5
, 0x44CC
adalah hash pertama. - Kami memulai debugger lebih lanjut.
- Kami berhenti di
0x00001F16
dan melihat bahwa pada 0x00FF0D6A
terletak 0x4840
- hash terakhir
- Sekarang periksa fungsi finish_hash (hash_2b) kami:
>>> r = finish_hash(0x44CC) >>> print(hex(r)) 0x4840
Kami mencari kunci yang tepat 1
Kunci yang benar adalah kunci ini yang hash terakhirnya adalah
0xCB4C
(ditemukan di atas). Maka pertanyaannya: apa yang harus menjadi hash pertama untuk final menjadi
0xCB4C
?
Sekarang mudah untuk mengetahuinya:
def find_CB4C(): result = [] for hash_2b in range(0xFFFF+1): final_hash = finish_hash(hash_2b) if final_hash == 0xCB4C: result.append(hash_2b) return result >>> r = find_CB4C() >>> print(r)
Output dari program menunjukkan bahwa hanya ada satu opsi: hash pertama harus
0xFEDC
.
Karakter apa yang kita butuhkan sehingga hash pertama mereka adalah
0xFEDC
?
Karena
0xFEDC = __4_ ^ __4_
, Anda hanya perlu menemukan
__4_
, karena
__4_ = __4_ ^ 0xFEDC
. Dan kemudian memecahkan kode kedua hash.
Algoritma adalah sebagai berikut:
def get_first_half(): from collections import deque from random import randint def get_pairs(): pairs = [] for i in range(0xFFFF + 1): pair = (i, i ^ 0xFEDC) pairs.append(pair) pairs = deque(pairs) pairs.rotate(randint(0, 0xFFFF)) return list(pairs) pairs = get_pairs() for pair in pairs: key_4s_0 = decode_hash_4s(pair[0]) key_4s_1 = decode_hash_4s(pair[1]) hash_2b_0 = get_hash_2b(key_4s_0) hash_2b_1 = get_hash_2b(key_4s_1) if hash_2b_0 == pair[0] and hash_2b_1 == pair[1]: return key_4s_0, key_4s_1
Banyak pilihan, pilih apa saja.
Kami mencari kunci yang tepat 2
Bagian pertama dari kunci sudah siap, bagaimana dengan yang kedua?
Ini adalah bagian yang paling mudah.
Sepotong kode yang bertanggung jawab terletak di
0x00FF2012
, saya mendapatkannya dengan penelusuran manual, dimulai dengan alamat
0x00001F16 beg.w loc_20EA
(validasi pada paruh pertama kunci). Dalam register
a0
adalah alamat email,
loc_FF2012
adalah sebuah siklus, karena
bne.s loc_FF2012
. Ini dieksekusi selama ada
*(a0+d0)
(byte surat berikutnya).
Dan
jsr (a3)
memanggil fungsi
get_hash_2b
sudah dikenal, yang sekarang bekerja dengan bagian kedua kunci.
Mari kita buat kode lebih jelas:
while(d1 != 0x20){ d2++ d1 = d1 & 0xFF d3 = d3 + d1 d0 = 0 d0 = d2 d1 = *(a0+d0) } d0 = get_hash_2b(key_byte_8) d3 = d0^d3 d0 = get_hash_2b(key_byte_12) d2 = d2 - 1 d2 = d2 << 8 d2 = d0^d2 if (d2 == d3) success_branch
Dalam register
d2
-
( -1) << 8
. Di
d3
, jumlah byte karakter surat.
Kriteria kebenaran adalah sebagai berikut:
__ ^ d2 == ___2 ^ d3
.
Kami menulis fungsi pemilihan bagian kedua kunci:
def get_second_half(email): from collections import deque from random import randint def get_koeff(): k1 = sum([ord(c) for c in email]) k2 = (len(email) - 1) << 8 return k1, k2 def get_pairs(k1, k2): pairs = [] for a in range(0xFFFF + 1): pair = (a, (a ^ k1) ^ k2) pairs.append(pair) pairs = deque(pairs) pairs.rotate(randint(0, 0xFFFF)) return list(pairs) k1, k2 = get_koeff() pairs = get_pairs(k1, k2) for pair in pairs: key_4s_0 = decode_hash_4s(pair[0]) key_4s_1 = decode_hash_4s(pair[1]) hash_2b_0 = get_hash_2b(key_4s_0) hash_2b_1 = get_hash_2b(key_4s_1) if hash_2b_0 == pair[0] and hash_2b_1 == pair[1]: return key_4s_0, key_4s_1
Keygen
Surat harus dalam batas.
def keygen(email): first_half = get_first_half() second_half = get_second_half(email) return "".join(first_half) + "".join(second_half) >>> email = "M.GAYANOV@GMAIL.COM" >>> print(keygen(email)) 2A4FD493BA32AD75
Terima kasih atas perhatian anda! Semua kode tersedia di
sini .