Halo semuanya

Saya tidak tahu tentang Anda, tetapi saya selalu ingin membalikkan game konsol lama, juga memiliki dekompiler. Dan sekarang, saat yang menggembirakan dalam hidup saya telah tiba - GHIDRA telah keluar. Saya tidak akan menulis tentang apa itu, Anda dapat dengan mudah google itu. Dan, ulasannya sangat berbeda (terutama dari tingkat retrograde) sehingga akan sulit bagi pendatang baru untuk bahkan memutuskan untuk meluncurkan keajaiban ini ... Ini contoh untuk Anda: " Saya bekerja untuk ide selama 20 tahun, dan saya melihat Hydra Anda dengan penuh rasa tidak percaya, karena NSA. Tetapi ketika- Saya akan menjalankannya dan memeriksanya dalam praktik . "
Singkatnya - menjalankan Hydra tidak menakutkan. Dan apa yang kami dapatkan setelah peluncuran akan memblokir semua rasa takut Anda akan bookmark dan backdoors dari NSA di mana-mana.
Jadi, apa yang saya bicarakan ... Ada awalan seperti itu: Sony Playstation 1 ( PS1 , PSX , Curling iron ). Banyak game keren diciptakan untuk itu, sekelompok waralaba muncul yang masih populer. Dan suatu hari saya ingin mengetahui cara kerjanya: format data apa yang ada di sana, apakah kompresi sumber daya digunakan, mencoba menerjemahkan sesuatu ke dalam bahasa Rusia (saya akan mengatakan segera bahwa saya belum menerjemahkan permainan apa pun).
Saya mulai dengan menulis utilitas keren untuk bekerja dengan format TIM
dengan seorang teman di Delphi
(ini seperti BMP
dari dunia Playstation): Tim2View . Pada suatu waktu, menikmati kesuksesan (dan mungkin sekarang menikmati). Lalu saya ingin masuk lebih dalam ke kompresi.

Dan kemudian masalah dimulai. Saya tidak terbiasa dengan MIPS
saat itu. Butuh belajar. Saya juga tidak terbiasa dengan IDA Pro
(saya datang untuk membalik game di Sega Mega Drive
lambat dari Playstation
). Tetapi, berkat Internet, saya mengetahui bahwa IDA Pro
mendukung pengunduhan dan analisis file yang dapat dieksekusi PS1
: PS-X EXE . Saya mencoba mengunggah file game (sepertinya Lemmings ) dengan nama dan ekstensi yang aneh, seperti SLUS_123.45
di Ida, saya mendapat banyak baris kode assembler (untungnya, saya sudah memiliki ide tentang apa itu, berkat driver Windows exe di bawah x86), dan mulai mengerti.

Tempat sulit pertama yang harus dipahami adalah jalur perakitan. Misalnya, Anda melihat panggilan ke beberapa fungsi, dan segera setelah itu memuat ke register parameter yang harus digunakan dalam fungsi ini. Singkatnya, sebelum ada lompatan dan fungsi panggilan, instruksi mengikuti lompatan / panggilan pertama kali dieksekusi, dan hanya kemudian panggilan atau melompat itu sendiri.
Setelah semua kesulitan yang saya lalui, saya berhasil menulis beberapa paket / pembongkar sumber daya game. Tapi saya belum pernah benar-benar mempelajari kode. Mengapa Ya, semuanya biasa-biasa saja: ada banyak kode, akses ke BIOS dan fungsi yang hampir mustahil untuk dipahami (mereka adalah perpustakaan, dan saya tidak punya SDK untuk pengeritingan), instruksi yang bekerja dengan tiga register pada saat yang sama, kurangnya dekompiler.
Jadi, setelah bertahun-tahun, GHIDRA
. Di antara platform yang didukung oleh decompiler adalah MIPS
. Oh sukacita! Mari kita coba mendekompilasi sesuatu segera! Tapi ... saya sedang menunggu mengecewakan. PS-X EXE
tidak didukung oleh Hydra. Itu bukan masalah, kami akan menulis sendiri!
Sebenarnya kode
Cukup penyimpangan liris, mari kita menulis kode. Bagaimana cara membuat pengunduh saya sendiri untuk Ghidra
, saya sudah punya ide tentang apa yang saya tulis sebelumnya . Oleh karena itu, tetap hanya menemukan Peta Memori dari curling iron pertama, alamat register dan, Anda dapat mengumpulkan dan memuat binari. Tidak lebih cepat dikatakan daripada dilakukan.
Kode sudah siap, register dan wilayah ditambahkan dan dikenali, tetapi masih ada tempat kosong besar di tempat-tempat di mana fungsi perpustakaan dan fungsi BIOS dipanggil. Dan, sayangnya, Hydra tidak memiliki dukungan FLIRT
. Jika tidak, mari kita tambahkan.
Format tanda tangan FLIRT
diketahui dan dijelaskan dalam file pat.txt
, yang dapat ditemukan di Ida SDK. Ida juga memiliki utilitas untuk membuat tanda tangan ini secara khusus dari file perpustakaan Playstation
, dan disebut: ppsx
. Saya mengunduh SDK untuk PsyQ Playstation Development Kit
bernama PsyQ Playstation Development Kit
, menemukan file lib
sana dan mencoba membuat setidaknya beberapa tanda tangan dari mereka - berhasil. Ternyata teks kecil di mana setiap baris memiliki format tertentu. Tetap menulis kode yang akan menguraikan baris-baris ini, dan menerapkannya pada kode.

Patparser
Karena setiap baris memiliki format tertentu, akan logis untuk menulis ekspresi reguler. Ternyata seperti ini:
private static final Pattern linePat = Pattern.compile("^((?:[0-9A-F\\.]{2})+) ([0-9A-F]{2}) ([0-9A-F]{4}) ([0-9A-F]{4}) ((?:[:\\^][0-9A-F]{4}@? [\\.\\w]+ )+)((?:[0-9A-F\\.]{2})+)?$");
Nah, untuk menyorot kemudian dalam daftar modul secara terpisah offset, jenis, dan nama fungsi, kami menulis regexp terpisah:
private static final Pattern modulePat = Pattern.compile("([:\\^][0-9A-F]{4}@?) ([\\.\\w]+) ");
Sekarang mari kita lihat komponen masing-masing tanda tangan secara terpisah:
- Pertama adalah urutan hex byte ( 0-9A-F ), di mana beberapa dari mereka bisa berupa apa saja (karakter titik "."). Oleh karena itu, kami membuat kelas yang akan menyimpan urutan seperti itu. Saya menyebutnya
MaskedBytes
:
MaskedBytes.java package pat; public class MaskedBytes { private final byte[] bytes, masks; public final byte[] getBytes() { return bytes; } public final byte[] getMasks() { return masks; } public final int getLength() { return bytes.length; } public MaskedBytes(byte[] bytes, byte[] masks) { this.bytes = bytes; this.masks = masks; } public static MaskedBytes extend(MaskedBytes src, MaskedBytes add) { return extend(src, add.getBytes(), add.getMasks()); } public static MaskedBytes extend(MaskedBytes src, byte[] addBytes, byte[] addMasks) { int length = src.getBytes().length; byte[] tmpBytes = new byte[length + addBytes.length]; byte[] tmpMasks = new byte[length + addMasks.length]; System.arraycopy(src.getBytes(), 0, tmpBytes, 0, length); System.arraycopy(addBytes, 0, tmpBytes, length, addBytes.length); System.arraycopy(src.getMasks(), 0, tmpMasks, 0, length); System.arraycopy(addMasks, 0, tmpMasks, length, addMasks.length); return new MaskedBytes(tmpBytes, tmpMasks); } }
- Panjang blok tempat
CRC16
dihitung. CRC16
, yang menggunakan polinomialnya sendiri ( 0x8408
):
Hitung Kode CRC16 public static boolean checkCrc16(byte[] bytes, short resCrc) { if ( bytes.length == 0 ) return true; int crc = 0xFFFF; for (int i = 0; i < bytes.length; ++i) { int a = bytes[i]; for (int x = 0; x < 8; ++x) { if (((crc ^ a) & 1) != 0) { crc = (crc >> 1) ^ 0x8408; } else { crc >>= 1; } a >>= 1; } } crc = ~crc; int x = crc; crc = (crc << 8) | ((x >> 8) & 0xFF); crc &= 0xFFFF; return (short)crc == resCrc; }
- Total panjang "modul" dalam byte.
- Daftar nama global (apa yang kita butuhkan).
- Daftar tautan ke nama lain (juga diperlukan).
- Byte ekor.
Setiap nama dalam modul memiliki tipe tertentu dan offset relatif terhadap awal. Jenis dapat ditunjukkan oleh salah satu karakter ::, ^, @, tergantung pada jenis:
- " : NAME ": nama global. Demi nama-nama seperti itulah saya memulai semuanya;
- " : NAME @ ": nama / label lokal. Mungkin tidak diindikasikan, tetapi biarkan;
- " ^ NAME ": tautan ke nama.
Di satu sisi, semuanya sederhana, tetapi tautan dapat dengan mudah bukan merupakan referensi ke suatu fungsi (dan, karenanya, lompatan akan relatif), tetapi ke variabel global. Apa, katamu, masalahnya? Dan di PSX Anda tidak dapat mendorong seluruh DWORD
ke dalam register dengan satu instruksi. Untuk melakukan ini, unduh dalam bentuk setengah. Faktanya adalah, dalam MIPS
ukuran instruksi dibatasi hingga empat byte. Dan, tampaknya, Anda hanya perlu mendapatkan setengah dari satu instruksi, dan kemudian membongkar yang berikutnya - dan mendapatkan setengah kedua. Tapi tidak sesederhana itu. Babak pertama dapat memuat kembali instruksi 5, dan tautan dalam modul hanya akan diberikan setelah memuat bagian kedua. Saya harus menulis parser yang canggih (mungkin bisa dimodifikasi).
Sebagai hasilnya, kami membuat enum
untuk tiga jenis nama:
ModuleType.java package pat; public enum ModuleType { GLOBAL_NAME, LOCAL_NAME, REF_NAME; public boolean isGlobal() { return this == GLOBAL_NAME; } public boolean isLocal() { return this == LOCAL_NAME; } public boolean isReference() { return this == REF_NAME; } @Override public String toString() { if (isGlobal()) { return "Global"; } else if (isLocal()) { return "Local"; } else { return "Reference"; } } }
Mari kita menulis kode yang mengubah urutan dan titik heksadesimal teks untuk mengetik MaskedBytes
:
hexStringToMaskedBytesArray () private MaskedBytes hexStringToMaskedBytesArray(String s) { MaskedBytes res = null; if (s != null) { int len = s.length(); byte[] bytes = new byte[len / 2]; byte[] masks = new byte[len / 2]; for (int i = 0; i < len; i += 2) { char c1 = s.charAt(i); char c2 = s.charAt(i + 1); masks[i / 2] = (byte) ( (((c1 == '.') ? 0x0 : 0xF) << 4) | (((c2 == '.') ? 0x0 : 0xF) << 0) ); bytes[i / 2] = (byte) ( (((c1 == '.') ? 0x0 : Character.digit(c1, 16)) << 4) | (((c2 == '.') ? 0x0 : Character.digit(c2, 16)) << 0) ); } res = new MaskedBytes(bytes, masks); } return res; }
Anda sudah dapat memikirkan kelas yang akan menyimpan informasi tentang masing-masing fungsi individu: nama fungsi, offset dalam modul, dan ketik:
ModuleData.java package pat; public class ModuleData { private final long offset; private final String name; private final ModuleType type; public ModuleData(long offset, String name, ModuleType type) { this.offset = offset; this.name = name; this.type = type; } public final long getOffset() { return offset; } public final String getName() { return name; } public final ModuleType getType() { return type; } }
Dan, terakhir: kelas yang akan menyimpan segala sesuatu yang ditunjukkan pada setiap baris file pat
, yaitu: byte, crc, daftar nama dengan offset:
SignatureData.java package pat; import java.util.Arrays; import java.util.List; public class SignatureData { private final MaskedBytes templateBytes, tailBytes; private MaskedBytes fullBytes; private final int crc16Length; private final short crc16; private final int moduleLength; private final List<ModuleData> modules; public SignatureData(MaskedBytes templateBytes, int crc16Length, short crc16, int moduleLength, List<ModuleData> modules, MaskedBytes tailBytes) { this.templateBytes = this.fullBytes = templateBytes; this.crc16Length = crc16Length; this.crc16 = crc16; this.moduleLength = moduleLength; this.modules = modules; this.tailBytes = tailBytes; if (this.tailBytes != null) { int addLength = moduleLength - templateBytes.getLength() - tailBytes.getLength(); byte[] addBytes = new byte[addLength]; byte[] addMasks = new byte[addLength]; Arrays.fill(addBytes, (byte)0x00); Arrays.fill(addMasks, (byte)0x00); this.fullBytes = MaskedBytes.extend(this.templateBytes, addBytes, addMasks); this.fullBytes = MaskedBytes.extend(this.fullBytes, tailBytes); } } public MaskedBytes getTemplateBytes() { return templateBytes; } public MaskedBytes getTailBytes() { return tailBytes; } public MaskedBytes getFullBytes() { return fullBytes; } public int getCrc16Length() { return crc16Length; } public short getCrc16() { return crc16; } public int getModuleLength() { return moduleLength; } public List<ModuleData> getModules() { return modules; } }
Sekarang yang utama: kita menulis kode untuk membuat semua kelas ini:
Mem-parsing satu baris file tepuk yang diambil private List<ModuleData> parseModuleData(String s) { List<ModuleData> res = new ArrayList<ModuleData>(); if (s != null) { Matcher m = modulePat.matcher(s); while (m.find()) { String __offset = m.group(1); ModuleType type = __offset.startsWith(":") ? ModuleType.GLOBAL_NAME : ModuleType.REF_NAME; type = (type == ModuleType.GLOBAL_NAME && __offset.endsWith("@")) ? ModuleType.LOCAL_NAME : type; String _offset = __offset.replaceAll("[:^@]", ""); long offset = Integer.parseInt(_offset, 16); String name = m.group(2); res.add(new ModuleData(offset, name, type)); } } return res; }
Parsing semua baris file tepuk private void parse(List<String> lines) { modulesCount = 0L; signatures = new ArrayList<SignatureData>(); int linesCount = lines.size(); monitor.initialize(linesCount); monitor.setMessage("Reading signatures..."); for (int i = 0; i < linesCount; ++i) { String line = lines.get(i); Matcher m = linePat.matcher(line); if (m.matches()) { MaskedBytes pp = hexStringToMaskedBytesArray(m.group(1)); int ll = Integer.parseInt(m.group(2), 16); short ssss = (short)Integer.parseInt(m.group(3), 16); int llll = Integer.parseInt(m.group(4), 16); List<ModuleData> modules = parseModuleData(m.group(5)); MaskedBytes tail = null; if (m.group(6) != null) { tail = hexStringToMaskedBytesArray(m.group(6)); } signatures.add(new SignatureData(pp, ll, ssss, llll, modules, tail)); modulesCount += modules.size(); } monitor.incrementProgress(1); } }
Kode untuk membuat fungsi tempat salah satu tanda tangan dikenali:
Penciptaan Fungsi private static void disasmInstruction(Program program, Address address) { DisassembleCommand cmd = new DisassembleCommand(address, null, true); cmd.applyTo(program, TaskMonitor.DUMMY); } public static void setFunction(Program program, FlatProgramAPI fpa, Address address, String name, boolean isFunction, boolean isEntryPoint, MessageLog log) { try { if (fpa.getInstructionAt(address) == null) disasmInstruction(program, address); if (isFunction) { fpa.createFunction(address, name); } if (isEntryPoint) { fpa.addEntryPoint(address); } if (isFunction && program.getSymbolTable().hasSymbol(address)) { return; } program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED); } catch (InvalidInputException e) { log.appendException(e); } }
Tempat yang paling sulit, seperti yang disebutkan sebelumnya, adalah menghitung tautan ke nama / variabel lain (mungkin kode perlu ditingkatkan):
Jumlah Tautan public static void setInstrRefName(Program program, FlatProgramAPI fpa, PseudoDisassembler ps, Address address, String name, MessageLog log) { ReferenceManager refsMgr = program.getReferenceManager(); Reference[] refs = refsMgr.getReferencesFrom(address); if (refs.length == 0) { disasmInstruction(program, address); refs = refsMgr.getReferencesFrom(address); if (refs.length == 0) { refs = refsMgr.getReferencesFrom(address.add(4)); if (refs.length == 0) { refs = refsMgr.getFlowReferencesFrom(address.add(4)); Instruction instr = program.getListing().getInstructionAt(address.add(4)); if (instr == null) { disasmInstruction(program, address.add(4)); instr = program.getListing().getInstructionAt(address.add(4)); if (instr == null) { return; } } FlowType flowType = instr.getFlowType(); if (refs.length == 0 && !(flowType.isJump() || flowType.isCall() || flowType.isTerminal())) { return; } refs = refsMgr.getReferencesFrom(address.add(8)); if (refs.length == 0) { return; } } } } try { program.getSymbolTable().createLabel(refs[0].getToAddress(), name, SourceType.IMPORTED); } catch (InvalidInputException e) { log.appendException(e); } }
Dan, sentuhan terakhir - terapkan tanda tangan:
applySignatures () public void applySignatures(ByteProvider provider, Program program, Address imageBase, Address startAddr, Address endAddr, MessageLog log) throws IOException { BinaryReader reader = new BinaryReader(provider, false); PseudoDisassembler ps = new PseudoDisassembler(program); FlatProgramAPI fpa = new FlatProgramAPI(program); monitor.initialize(getAllModulesCount()); monitor.setMessage("Applying signatures..."); for (SignatureData sig : signatures) { MaskedBytes fullBytes = sig.getFullBytes(); MaskedBytes tmpl = sig.getTemplateBytes(); Address addr = program.getMemory().findBytes(startAddr, endAddr, fullBytes.getBytes(), fullBytes.getMasks(), true, TaskMonitor.DUMMY); if (addr == null) { monitor.incrementProgress(sig.getModules().size()); continue; } addr = addr.subtract(imageBase.getOffset()); byte[] nextBytes = reader.readByteArray(addr.getOffset() + tmpl.getLength(), sig.getCrc16Length()); if (!PatParser.checkCrc16(nextBytes, sig.getCrc16())) { monitor.incrementProgress(sig.getModules().size()); continue; } addr = addr.add(imageBase.getOffset()); List<ModuleData> modules = sig.getModules(); for (ModuleData data : modules) { Address _addr = addr.add(data.getOffset()); if (data.getType().isGlobal()) { setFunction(program, fpa, _addr, data.getName(), data.getType().isGlobal(), false, log); } monitor.setMessage(String.format("%s function %s at 0x%08X", data.getType(), data.getName(), _addr.getOffset())); monitor.incrementProgress(1); } for (ModuleData data : modules) { Address _addr = addr.add(data.getOffset()); if (data.getType().isReference()) { setInstrRefName(program, fpa, ps, _addr, data.getName(), log); } monitor.setMessage(String.format("%s function %s at 0x%08X", data.getType(), data.getName(), _addr.getOffset())); monitor.incrementProgress(1); } } }
Di sini Anda dapat berbicara tentang satu fungsi menarik: findBytes()
. Dengannya, Anda dapat mencari urutan byte tertentu, dengan masker bit yang ditentukan untuk setiap byte. Metode ini disebut seperti ini:
Address addr = program.getMemory().findBytes(startAddr, endAddr, bytes, masks, forward, TaskMonitor.DUMMY);
Akibatnya, alamat dari mana byte dimulai dikembalikan, atau null
.
Menulis sebuah analisa
Mari kita lakukan dengan indah, dan kita tidak akan menggunakan tanda tangan jika kita tidak mau, tetapi biarkan pengguna memilih langkah ini. Untuk melakukan ini, Anda harus menulis penganalisa kode Anda sendiri (Anda bisa melihat yang ada di daftar ini - itu saja, ya):

Jadi, untuk masuk ke dalam daftar ini, Anda harus mewarisi dari kelas AbstractAnalyzer
dan mengganti beberapa metode:
- Konstruktor. Itu harus memanggil konstruktor dari kelas dasar dengan nama, deskripsi penganalisa, dan jenisnya (lebih lanjut tentang itu nanti). Ini terlihat seperti ini bagi saya:
public PsxAnalyzer() { super("PSYQ Signatures", "PSX signatures applier", AnalyzerType.INSTRUCTION_ANALYZER); }
getDefaultEnablement()
. Menentukan apakah penganalisis kami selalu tersedia, atau hanya jika kondisi tertentu terpenuhi (misalnya, jika loader kami digunakan).canAnalyze()
. Apakah mungkin menggunakan penganalisis ini sama sekali pada file biner yang dapat diunduh?
Paragraf 2 dan 3 pada prinsipnya dapat diverifikasi oleh satu fungsi tunggal:
public static boolean isPsxLoader(Program program) { return program.getExecutableFormat().equalsIgnoreCase(PsxLoader.PSX_LOADER); }
Di mana PsxLoader.PSX_LOADER
menyimpan nama bootloader, dan didefinisikan sebelumnya di dalamnya.
Total, kami memiliki:
@Override public boolean getDefaultEnablement(Program program) { return isPsxLoader(program); } @Override public boolean canAnalyze(Program program) { return isPsxLoader(program); }
registerOptions()
. Sama sekali tidak perlu mendefinisikan ulang metode ini, tetapi jika kita perlu menanyakan sesuatu kepada pengguna, misalnya path ke file pat sebelum analisis, maka yang terbaik adalah melakukan ini dalam metode ini. Kami mendapatkan:
private static final String OPTION_NAME = "PSYQ PAT-File Path"; private File file = null; @Override public void registerOptions(Options options, Program program) { try { file = Application.getModuleDataFile("psyq4_7.pat").getFile(false); } catch (FileNotFoundException e) { } options.registerOption(OPTION_NAME, OptionType.FILE_TYPE, file, null, "PAT-File (FLAIR) created from PSYQ library files"); }
Di sini perlu diklarifikasi. Metode statis getModuleDataFile()
dari kelas Application
mengembalikan path lengkap ke file di direktori data
, yang ada di pohon modul kami, dan dapat menyimpan file yang diperlukan yang ingin kita lihat nanti.
Nah, metode registerOption()
opsi dengan nama yang ditentukan dalam OPTION_NAME
, jenis File
(yaitu, pengguna akan dapat memilih file melalui kotak dialog biasa), nilai dan deskripsi default.
Selanjutnya Karena kami tidak akan memiliki kesempatan normal untuk merujuk ke opsi yang terdaftar nanti, kami akan perlu mendefinisikan kembali metode optionsChanged()
:
@Override public void optionsChanged(Options options, Program program) { super.optionsChanged(options, program); file = options.getFile(OPTION_NAME, file); }
Di sini kita cukup memperbarui variabel global sesuai dengan nilai baru.
Metode yang added()
. Sekarang yang utama: metode yang akan dipanggil ketika penganalisis dimulai. Di dalamnya kita akan menerima daftar alamat yang tersedia untuk analisis, tetapi kita hanya perlu yang berisi kode. Karena itu, Anda perlu memfilter. Kode terakhir:
Menambahkan metode () @Override public boolean added(Program program, AddressSetView set, TaskMonitor monitor, MessageLog log) throws CancelledException { if (file == null) { return true; } Memory memory = program.getMemory(); AddressRangeIterator it = memory.getLoadedAndInitializedAddressSet().getAddressRanges(); while (!monitor.isCancelled() && it.hasNext()) { AddressRange range = it.next(); try { MemoryBlock block = program.getMemory().getBlock(range.getMinAddress()); if (block.isInitialized() && block.isExecute() && block.isLoaded()) { PatParser pat = new PatParser(file, monitor); RandomAccessByteProvider provider = new RandomAccessByteProvider(new File(program.getExecutablePath())); pat.applySignatures(provider, program, block.getStart(), block.getStart(), block.getEnd(), log); } } catch (IOException e) { log.appendException(e); return false; } } return true; }
Di sini kita melihat daftar alamat yang dapat dieksekusi, dan mencoba menerapkan tanda tangan di sana.

Kesimpulan dan penutupnya
Suka semuanya. Sebenarnya, tidak ada yang super rumit di sini. Ada beberapa contoh, komunitasnya ramai, Anda dapat dengan aman bertanya tentang apa yang tidak jelas saat menulis kode. Intinya: bootloader dan penganalisis yang berfungsi untuk file yang dapat dieksekusi Playstation 1
.

Semua kode sumber tersedia di sini: ghidra_psx_ldr
Rilis di sini: Rilis