Halo semuanya

Terlepas dari pengalaman hebat saya dalam membalikkan game untuk Sega Mega Drive
, saya tidak pernah memutuskan untuk melakukannya, dan mereka tidak menemukan saya di Internet. Tapi, tempo hari ada crackie lucu yang ingin menyelesaikan. Saya berbagi dengan Anda keputusan ...
Deskripsi
Deskripsi tugas dan rum itu sendiri dapat diunduh di sini .
Terlepas dari kenyataan bahwa daftar sumber daya mengatakan Hydra, standar de facto di antara alat untuk debugging dan membalikkan game di Sega adalah Smd Ida Tools . Ia memiliki semua yang Anda butuhkan untuk menyelesaikan creme ini:
- Pemuat rum untuk Ida
- Debugger
- Lihat dan ubah memori RAM / VDP
- Menampilkan informasi yang hampir lengkap tentang VDP
Kami melepaskan rilis terbaru ke dalam plugin untuk Ide dan mulai melihat apa yang kami miliki.
Solusi
Peluncuran game Shogi apa pun dimulai dengan eksekusi vektor Reset
. Pointer ke sana dapat ditemukan di DWORD kedua dari awal rum.


Kami melihat beberapa fungsi tidak dikenal mulai dari alamat 0x27A
. Mari kita lihat apa yang ada di sana.
sub_2EA ()

Dari pengalaman saya sendiri, saya akan mengatakan bahwa ini biasanya terlihat seperti fungsi menunggu interupsi VBLANK
selesai. Mari kita lihat di mana lagi ada panggilan ke variabel byte_FF0026
:

Kita melihat bahwa bit nol hanya diatur dalam interupsi VBLANK
. Jadi kita memanggil variabel vblank_ready
, dan fungsi tempat itu diperiksa adalah wait_for_vblank
.
sub_60E ()
Selanjutnya, fungsi sub_60E
dipanggil oleh kode. Mari kita lihat apa yang ada di sana:

Apa yang ditulis perintah pertama ke VDP_CTRL
adalah perintah kontrol VDP
. Untuk mengetahui apa yang dia lakukan, kita berdiri di perintah ini dan tekan tombol J
:

Kita melihat bahwa entri dalam CRAM
(tempat penyimpanan palet) diinisialisasi. Ini berarti bahwa semua kode fungsi selanjutnya cukup menetapkan semacam palet awal. Dengan demikian, fungsi ini bisa disebut init_cram
.
sub_71A ()

Kami melihat bahwa beberapa perintah lagi ditransfer ke VDP_CTRL
, lalu tekan lagi J
dan temukan bahwa perintah ini menginisialisasi rekaman dalam memori video:

Selanjutnya, untuk memahami apa yang ditransfer ke memori video, tidak masuk akal. Karena itu, kita cukup memanggil fungsi load_vdp_data
.
sub_C60 ()
Hal yang hampir sama terjadi di sini seperti pada fungsi sebelumnya, oleh karena itu, tanpa masuk ke detail, kita cukup memanggil fungsi load_vdp_data2
.
sub_8DA ()
Sudah ada lebih banyak kode. Dan selain itu, fungsi lain disebut dalam fungsi ini. Mari kita lihat di sana - di sub_D08
.
sub_D08 ()

Kita melihat bahwa dalam register D0
perintah untuk VDP_CTRL
, di D1
- nilai yang akan diisi VRAM
, dan di D2
dan D3
- lebar dan tinggi isi (karena ternyata dua siklus: internal dan eksternal). Panggil fungsi fill_vram_by_addr.
sub_8DA ()
Kami kembali ke fungsi sebelumnya. Setelah nilai dalam register D0
dikirimkan sebagai perintah untuk VDP_CTRL
, tekan tombol J
pada nilai. Kami mendapatkan:

Sekali lagi, dari pengalaman membalikkan game ke Sega, saya dapat mengatakan bahwa perintah ini menginisialisasi rekaman ubin pemetaan. Alamat yang dimulai dari $Fxxx
, $Exxx
, $Dxxx
, $Cxxx
dalam 90% kasus akan menjadi alamat daerah dengan pemetaan yang sama. Apa itu pemetaan:
ini adalah nilai yang dapat Anda tentukan tempat menampilkan ubin ini atau itu di layar (ubin adalah kuadrat 8x8
piksel).
Jadi fungsinya bisa disebut sebagai init_tile_mappings
.
sub_CDC ()

Perintah pertama menginisialisasi catatan di alamat $F000
. Satu catatan: di antara alamat " pemetaan ", masih ada wilayah di mana tabel sprite disimpan (ini adalah posisi mereka, ubin yang mereka tuju, dll.) Cari tahu wilayah mana yang bertanggung jawab atas apa yang dapat di-debug. Tetapi untuk saat ini, kami tidak membutuhkan ini, jadi mari kita panggil saja fungsi init_other_mappings
.
Juga, kita melihat bahwa dalam fungsi ini dua variabel diinisialisasi: word_FF000A
dan word_FF000C
. Dari pengalaman saya sendiri (ya, ia memutuskan) Saya akan mengatakan bahwa jika ada dua variabel yang berdekatan di ruang alamat dan terkait dengan pemetaan, maka dalam banyak kasus mereka akan menjadi koordinat dari beberapa objek (misalnya, sprite). Karenanya, saya sarankan memanggil mereka sprite_pos_x
dan sprite_pos_y
. Kesalahan dalam x
dan y
diizinkan sejak itu selanjutnya di bawah debugging akan mudah diperbaiki.
VBLANK
Karena loop berjalan lebih jauh dalam kode, kita dapat mengasumsikan bahwa kita telah menyelesaikan inisialisasi dasar. Sekarang Anda dapat melihat interupsi VBLANK
.

Kami melihat bahwa dua variabel bertambah (yang aneh, dalam daftar tautan ke masing-masing variabel itu benar-benar kosong). Tapi, karena mereka diperbarui sekali per frame, Anda dapat memanggil mereka timer1
dan timer2
.
Selanjutnya, fungsi sub_2FE
. Mari kita lihat apa yang ada di sana:
sub_2FE ()

Dan di sana - bekerja dengan port IO_CT1_DATA
(bertanggung jawab atas joystick pertama). Alamat port dimuat ke register A0
, dan diteruskan ke fungsi sub_310
. Kami pergi ke sana:
sub_310 ()

Pengalaman saya membantu saya lagi. Jika Anda melihat kode yang berfungsi dengan joystick, dan dua variabel dalam memori, maka satu menyimpan pressed keys
, dan yang kedua menahan held keys
, yaitu. cukup tekan dan tahan tombol. Jadi mari kita sebut variabel-variabel ini: pressed_keys
dan held_keys
. Dan kemudian fungsinya bisa disebut sebagai update_joypad_state
.
sub_2FE ()
Sebut fungsinya sebagai read_joypad
.
Lingkaran handler
Sekarang semuanya terlihat jauh lebih jelas:

Jadi siklus ini merespons tombol yang ditekan, dan melakukan tindakan yang sesuai. Mari kita pergi melalui masing-masing fungsi yang disebut dalam loop.
sub_4D4 ()

Ada banyak kode. Mari kita mulai dengan fungsi pertama bernama: sub_60C
.
sub_60C ()
Dia tidak melakukan apa-apa - mungkin kelihatannya begitu awalnya. Baru saja kembali dari fungsi saat ini adalah rts
. Tetapi, karena hanya melompat ( bsr
) yang terjadi di atasnya, yang berarti rts
akan mengembalikan kita ke loop handler. Saya akan memanggil fungsi ini sebagai retn_to_loop
.
sub_4D4 ()
Selanjutnya, kita melihat panggilan ke variabel word_FF000E
. Ini tidak digunakan di mana pun kecuali untuk fungsi saat ini dan, pada awalnya, tujuannya tidak jelas bagi saya. Tetapi, jika Anda melihat lebih dekat, kita dapat mengasumsikan bahwa variabel ini hanya diperlukan untuk penundaan kecil antara pemrosesan penekanan tombol. ( Ini sudah diimplementasikan dengan buruk dalam rum ini, tapi, saya pikir, tanpa variabel ini akan jauh lebih buruk ).

Selanjutnya, kami memiliki sejumlah besar kode yang entah bagaimana memproses sprite_pos_y
dan sprite_pos_y
, yang hanya dapat mengatakan satu hal - ini diperlukan untuk menampilkan sprite pilihan di sekitar karakter yang dipilih dalam alfabet.
Jadi sekarang Anda dapat dengan aman memberi nama fungsi sebagai update_selection
. Mari kita lanjutkan.

Kode memeriksa apakah bit dari beberapa tombol yang ditekan diatur, dan memanggil fungsi-fungsi tertentu. Mari lihat mereka.
sub_D28 ()

Semacam sihir perdukunan. Pertama, WORD
diambil dari variabel word_FF0018
, lalu satu instruksi menarik dijalankan:
bsr.w *+4
Perintah ini hanya melompat ke instruksi yang mengikutinya.
Berikutnya adalah keajaiban lain:
move.l d0,(sp) rts
Nilai dalam register D0
ditempatkan di bagian atas tumpukan. Perlu dicatat bahwa, untuk Shogi, dan juga untuk beberapa x86
, alamat pengirim dari fungsi ketika dipanggil diletakkan di atas tumpukan. Dengan demikian, instruksi pertama menempatkan beberapa alamat di atas, dan yang kedua mengangkatnya dari stack dan membuat transisi di sepanjang itu. Trik bagus .
Sekarang Anda perlu memahami apa nilai ini dalam variabel, yang kemudian melewati. Tapi pertama-tama, sebut saja variabel ini jmp_addr
.
Dan fungsinya akan disebut ini:
sub_D38
: goto_to_d0
sub_D28
: jump_to_var_addr
jmp_addr
Cari tahu di mana variabel ini diisi. Kami melihat daftar referensi:

Hanya ada satu tempat untuk menulis ke variabel ini. Mari kita lihat dia.
sub_3A4 ()

Di sini, tergantung pada koordinat sprite (ingat bahwa ini kemungkinan besar alamat karakter yang dipilih), nilai ini atau itu dimasukkan. Kita melihat bagian kode berikut:

Nilai yang ada digeser ke kanan oleh 4 bit, nilai baru ditempatkan di byte rendah, dan hasilnya dimasukkan ke dalam variabel lagi. Secara teori, variabel jmp_addr
kami menyimpan karakter yang bisa kita masukkan pada layar input kunci. Perhatikan juga bahwa ukuran variabel adalah WORD
.
Bahkan, fungsi sub_3A4
bisa disebut update_jmp_addr
.
sub_414 ()
Sekarang kita hanya memiliki satu fungsi tersisa di loop, yang tidak dikenali. Dan itu disebut sub_414
.

update_jmp_addr
menyerupai kode fungsi update_jmp_addr
, hanya pada akhirnya kita memiliki sub_45E
fungsi sub_45E
. Mari lihat di sana.
sub_45E ()

Kita melihat bahwa nomor #$4B1E2003
dimasukkan dalam register D0
, yang kemudian dikirim ke VDP_CTRL
, yang berarti bahwa kita sedang berurusan dengan perintah kontrol VDP
lain. Kami menekan J
, kami menerima perintah catatan di wilayah dengan pemetaan $Cxxx
.
Selanjutnya, kode berfungsi dengan variabel byte_FF0014
, yang tidak digunakan di mana pun kecuali fungsi saat ini. Jika Anda melihat dari dekat bagaimana menggunakannya, Anda akan melihat bahwa jumlah maksimum yang dapat diinstal di dalamnya adalah 4
. Saya memiliki asumsi bahwa ini adalah panjang saat ini dari kunci yang dimasukkan. Mari kita periksa.
Jalankan debugger
Saya akan menggunakan debugger dari Smd Ida Tools
, tetapi, pada kenyataannya, beberapa Gens KMod atau Gens ReRecording akan cukup. Yang paling utama adalah ada fitur dengan tampilan alamat di memori.

Teori saya telah dikonfirmasi. Jadi byte_FF0014
variabel sekarang dapat key_length
.
Ada variabel lain: dword_FF0010
, yang juga hanya digunakan dalam fungsi saat ini, dan isinya, setelah menambahkan perintah awal di D0
(ingat, ini adalah nomor #$4B1E2003
), dikirim ke VDP_CTRL
. Tanpa berpikir add_to_vdp_cmd
, saya menamai variabel add_to_vdp_cmd
.
Jadi, apa fungsi ini lakukan? Saya memiliki asumsi bahwa dia menggambar karakter yang dimasukkan. Memeriksa ini sederhana - dengan meluncurkan debugger dan membandingkan status sebelum memanggil fungsi sub_45E
dan setelah:
Kepada:

Setelah:

Saya benar - fungsi ini menggambar karakter yang dimasukkan. Kami menyebutnya do_draw_input_char
, dan fungsi yang memanggilnya ( sub_414
) adalah draw_input_char
.
Apa sekarang?
Mari kita periksa sekarang bahwa variabel yang kita sebut jmp_addr
benar-benar menyimpan kunci yang dimasukkan. Kami akan menggunakan Memory Watch
sama:

Seperti yang Anda lihat, dugaan itu benar. Apa yang ini berikan pada kita? Kita bisa lompat ke alamat apa saja. Tapi yang mana? Dalam daftar fungsi, semua sudah beres:

Lalu saya baru saja mulai menggulir kode sampai saya menemukan ini:

Mata terlatih melihat urutan $4E, $75
pada akhir byte yang tidak terisi. Ini adalah opcode dari instruksi rts
, mis. kembali dari fungsi. Jadi byte yang tidak terisi ini dapat menjadi kode dari beberapa fungsi. Mari kita coba menunjuk mereka sebagai kode, tekan C
:

Jelas, ini adalah kode fungsi. Anda juga dapat menekan P
di atasnya untuk membuat kode berfungsi. Ingat nama ini: sub_D3C
.
Kemudian muncul pemikiran: bagaimana jika Anda menggunakan sub_D3C
? Kedengarannya bagus, meskipun satu lompatan di sini jelas tidak akan cukup, karena tidak ada lagi tautan ke variabel word_FF0020
.
Lalu sebuah pemikiran muncul di benak saya: bagaimana jika kita mencari kode yang tidak terisi lainnya? Buka dialog Binary search
(Alt + B), masukkan urutan 4E 75
di dalamnya, centang kotak Find all occurrences
:

Klik
untuk memulai pencarian, kami mendapatkan hasil berikut.

Setidaknya dua tempat lagi di rum mungkin berisi kode fungsi, Anda perlu memeriksanya. Kami klik pada opsi pertama, gulir sedikit ke atas, dan sekali lagi kami melihat urutan byte yang tidak ditentukan. Tunjukkan sebagai fungsi? Ya! Tekan P
mana byte dimulai:

Keren! Sekarang kita memiliki fungsi sub_34C
. Kami mencoba mengulangi hal yang sama dengan yang terakhir dari opsi yang ditemukan, dan ... kami mendapatkan mengecewakan. Ada begitu banyak byte sebelum 4E 75
sehingga tidak jelas di mana fungsi dimulai. Dan, jelas, tidak semua byte di atas adalah kode, karena banyak byte duplikat.
Tentukan awal fungsi
Akan lebih mudah bagi kita untuk menemukan awal fungsi jika kita menemukan di mana data berakhir. Bagaimana cara melakukannya? Sebenarnya sama sekali tidak rumit:
- Kami memutar sebelum data dimulai (akan ada tautan ke mereka dari kode)
- Kami mengikuti tautan dan mencari siklus di mana ukuran data ini akan muncul
- Tandai array
Jadi, kami melakukan paragraf pertama ...:

... dan kami segera melihat bahwa dalam siklus dari array kami, 4 byte data disalin sekaligus (karena move.l
) ke VDP_DATA
. Selanjutnya kita melihat nomor 2047
. Pada awalnya, mungkin terlihat bahwa ukuran akhir dari array adalah 2047 * 4
, tetapi loop berbasis dbf
mengeksekusi +1
iterasi lebih banyak, karena Nilai perbandingan terakhir bukan 0
, tetapi -1
.
Total: ukuran array adalah 2048 * 4 = 8192
. Nyatakan byte sebagai array. Untuk melakukan ini, klik *
dan tentukan ukurannya:

Kami memutar ke ujung array, dan kami melihat ada byte, yang persis byte dari kode:


Sekarang kita memiliki fungsi sub_2D86
, dan kita memiliki segalanya untuk menyelesaikan celah ini! Mari kita lihat apa fungsi yang baru dibuat.
sub_2D86 ()
Dan itu hanya menempatkan nilai #$4147
dalam register D1
dan memanggil fungsi sub_34C
. Lihatlah dia.
sub_34C ()

Kita melihat bahwa di sini nilai variabel word_FF0020
. Jika Anda melihat jmp_addr
, kita akan melihat tempat lain di mana catatan dalam variabel ini terjadi, dan ini akan menjadi tempat di mana saya ingin melompati variabel jmp_addr
. Ini mengkonfirmasi firasat bahwa sub_D3C
harus melompat ke sub_D3C
.
Tetapi apa yang terjadi selanjutnya terlalu malas untuk saya mengerti, jadi saya melemparkan rum ke GHIDRA , menemukan fungsi ini, dan melihat kode yang diuraikan:
void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Kita melihat bahwa variabel dengan nama aneh in_D1w
, dan juga variabel DAT_00ff0020
, yang dengan alamatnya menyerupai word_FF0020
disebutkan di word_FF0020
.
in_D1w
memberitahu kita bahwa nilai ini diambil dari register D1
, atau lebih tepatnya dari setengah WORD yang lebih muda, dan set register D1
fungsi yang melewatinya. Ingat #$4147
? Jadi, Anda perlu menunjuk register ini sebagai argumen input ke fungsi.
Untuk melakukan ini, di jendela dengan kode yang didekompilasi, klik kanan pada nama fungsi, dan pilih item menu Edit Function Signature
:

Untuk menunjukkan bahwa fungsi mengambil argumen melalui register tertentu, yaitu, bukan dengan metode standar untuk konvensi panggilan saat ini, Anda perlu memeriksa Use Custom Storage
dan klik pada ikon dengan tanda tambah hijau :

Posisi untuk argumen input baru muncul. Kami klik dua kali padanya, dan kami mendapatkan dialog yang menunjukkan jenis dan media argumen:

Dalam kode yang didekompilasi, kita melihat bahwa in_D1w
adalah tipe ushort
, yang berarti kita akan menentukannya di bidang tipe. Kemudian klik tombol Add
:

Sebuah posisi akan muncul untuk menunjukkan medium dari argumen, kita perlu menentukan register D1w
di Location
, dan klik OK
:

Kode yang didekompilasi akan berbentuk:
void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Kita param_1
bahwa nilai param_1
kita adalah konstan, diteruskan oleh fungsi panggilan dan sama dengan #$4147
. Lalu apa yang seharusnya menjadi nilai DAT_00ff0020
? Kami mempertimbangkan:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
Karena xor
- operasi dapat dibalik, semua angka konstan dapat saling bertengkar dan mendapatkan nilai variabel DAT_00ff0020
.
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
Ternyata nilai variabel harus 0x4553
. Sepertinya saya sudah melihat tempat di mana nilai tersebut ditetapkan ...

Kesimpulan dan keputusan
Kami sampai pada hasil berikut:
- Pertama, Anda perlu melompat ke alamat
0x0D3C
, untuk ini Anda harus memasukkan kode 0D3C
- Lompat ke fungsi di
0x2D86
, yang menetapkan nilai D1
untuk mendaftar #$4147
, untuk ini Anda harus memasukkan kode 2D86
Secara eksperimental, kami menemukan tombol yang perlu ditekan untuk memeriksa kunci yang dimasukkan: B
Kami mencoba:

Terima kasih