GHIDRA、Playstation 1実行可能ファイル、FLIRT署名、およびPsyQ

みなさんこんにちは













私はあなたについては知りませんが、在庫の逆コンパイラも持ちながら、古いコンソールゲームを常に元に戻したかったのです。 そして今、私の人生のこの喜びの瞬間が訪れました- ギドラがきました。 私はそれが何であるかについては書きませんが、簡単にグーグルで検索できます。 そして、レビューは非常に異なっているので(特に逆行性)、新人がこの奇跡を立ち上げることを決定することさえ難しいでしょう...ここにあなたのための例があります: いつか実行して、ビジネスで確認します。」







簡単に言うと、Hydraを実行するのは怖くありません。 そして、ローンチ後に得られるものは、ユビキタスNSAからのブックマークとバックドアに対するすべての恐怖をブロックします。







だから、私は何を話しているのですか...そのような接頭辞があります: Sony Playstation 1PS1PSXカーリングアイロン )。 多くのクールなゲームが作成され、多くのフランチャイズが登場しましたが、今でも人気があります。 そして、それらがどのように機能するかを知りたいと思ったら:データ圧縮形式が使用されているかどうか、リソース圧縮が使用されているかどうか、何かをロシア語に翻訳してみてください。







まず、 Delphi



友人(PlaystationのBMP



のようなもの)と一緒にTIM



形式で作業するためのクールなユーティリティであるTim2Viewを作成しました 。 かつては成功を楽しんでいました(そして今は楽しんでいるかもしれません)。 それから、圧縮をもっと深くしたかった。







画像







そして、問題が始まりました。 当時、私はMIPS



精通していませんでした。 勉強しました。 また、私はIDA Pro



精通していませんでした( Playstation



よりも遅く、 Sega Mega Drive



ゲームをリバースするようになりました) しかし、インターネットのおかげで、 IDA Pro



PS1



ダウンロードと分析をサポートしていることがわかりました: PS-X EXE実行可能ファイル 。 IdaのSLUS_123.45



などの奇妙な名前と拡張子を持つゲームファイル( レミングと思われる)をアップロードしようとしましたが、 SLUS_123.45



コードの行を取得しました( SLUS_123.45



、Windows exeドライバーのおかげで、それが何であるか既にSLUS_123.45



ました) x86)、そして理解し始めました。













最初に理解しにくい場所は組立ラインでした。 たとえば、ある関数の呼び出しがあり、それがレジスタにロードされた直後に、この関数で使用されるパラメーターが表示されます。 要するに、ジャンプと関数呼び出しの前に、ジャンプ/呼び出しに続く命令が最初に実行され、それから呼び出しまたはジャンプ自体が実行されます。







私が経験したすべての困難の後、私は何とかゲームリソースのいくつかのパッカー/アンパッカーを書くことができました。 しかし、私はコードを実際に勉強したことがありません。 なんで? まあ、すべては当たり前です:多くのコード、BIOSへのアクセス、理解するのが事実上不可能な機能がありました(それらはライブラリであり、当時はカールするためのSDKがありませんでした)。







そして、何年も後に、 GHIDRA



が出GHIDRA



ます。 デコンパイラがサポートするプラットフォームにはMIPS



ます。 ああ! すぐに何かを逆コンパイルしてみましょう! しかし...私は残念を待っていました。 PS-X EXE



、HydraではサポートされPS-X EXE



いません。 問題ありません、自分で書いてください!







実際にコード



叙情的な余談はもう十分です。コードを書きましょう。 Ghidra



用に独自のダウンローダーを作成する方法については、 以前書いたことを既に知っていました 。 したがって、最初のカーリングアイロンのメモリマップ 、レジスタのアドレスを見つけるだけで、バイナリを収集して読み込むことができます。 すぐに言ってやった。







コードの準備が整い、レジスタとリージョンが追加されて認識されましたが、ライブラリ関数とBIOS関数が呼び出される場所にはまだ大きな空白がありました。 また、残念ながら、HydraにはFLIRT



サポートがありませんでした。 そうでない場合は、追加しましょう。







FLIRT



署名の形式は既知であり、 pat.txt



ファイルに記述されてpat.txt



ます。このファイルはIda SDKにあります。 Idaには、特にPlaystation



ライブラリファイルからこれらの署名を作成するためのユーティリティもあり、 ppsx



と呼ばれます。 PsyQ Playstation Development Kit



というPsyQ Playstation Development Kit



用のSDKをダウンロードし、そこにlib



ファイルを見つけて、それらから少なくともいくつかの署名を作成しようとしました-成功しました。 各行に特定の形式の小さなテキストが表示されます。 これらの行を解析し、それらをコードに適用するコードを記述することは残っています。













パトパーサー



各行には特定の形式があるため、正規表現を記述することは論理的です。 次のようになりました。







 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})+)?$");
      
      





それでは、モジュールのリストでオフセット、タイプ、関数名を個別に強調表示するために、別個の正規表現を記述します。







 private static final Pattern modulePat = Pattern.compile("([:\\^][0-9A-F]{4}@?) ([\\.\\w]+) ");
      
      





次に、各署名のコンポーネントを個別に見ていきましょう。







  1. 最初に16進数のバイトシーケンス( 0-9A-F )がありますが、その一部は任意(ドット文字「。」)にできます。 したがって、このようなシーケンスを格納するクラスを作成します。 MaskedBytes



    と呼んでい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); } }
      
      





  1. CRC16



    計算元のブロックの長さ。
  2. CRC16



    、独自の多項式( 0x8408



    )を使用します。


カウントコード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; }
      
      





  1. 「モジュール」の全長(バイト単位)。
  2. グローバル名のリスト(必要なもの)。
  3. 他の名前へのリンクのリスト(これも必要です)。
  4. テールバイト。


モジュール内の各名前には、特定のタイプと先頭からのオフセットがあります。 タイプは、タイプに応じて、文字::、^、@のいずれかで示すことができます。









一方では、すべてが単純ですが、リンクは簡単に関数への参照ではなく(したがって、ジャンプは相対になります)、グローバル変数への参照になります。 問題は何ですか? PSXでは、1つの命令でDWORD



全体をレジスタにプッシュすることはできません。 これを行うには、半分の形でダウンロードします。 実際、 MIPS



命令サイズは4バイトに制限されています。 そして、最初に1つの命令から半分を取得し、次に次の命令を逆アセンブルして、後半を取得するだけでよいようです。 しかし、それほど単純ではありません。 前半は命令5をロードして戻すことができ、モジュール内のリンクは後半をロードした後にのみ提供されます。 洗練されたパーサーを作成する必要がありました(おそらく変更可能です)。







その結果、3種類の名前のenum



型を作成します。







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"; } } }
      
      





テキストの16進シーケンスとドットを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; }
      
      





個々の関数に関する情報(関数の名前、モジュール内のオフセット、およびタイプ)を格納するクラスについては、すでに考えることができます。







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; } }
      
      





そして最後に: pat



ファイルの各行に示されているすべてのものを保存するクラス、つまり、バイト、crc、オフセット付きの名前のリスト:







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; } }
      
      





主なことは、これらすべてのクラスを作成するコードを作成することです。







patファイルの1行を解析
 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; }
      
      





すべてのpatファイル行の解析
 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); } }
      
      





署名の1つが認識された関数を作成するコード:







関数作成
 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); } }
      
      





先に述べたように、最も難しい場所は、別の名前/変数へのリンクを数えることです(おそらくコードを改善する必要がある):







リンク数
 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); } }
      
      





そして、最後の仕上げ-署名を適用します。







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); } } }
      
      





ここで、1つの興味深い関数findBytes()



について話すことができます。 これを使用すると、各バイトに指定されたビットマスクを使用して、特定のバイトシーケンスを検索できます。 メソッドは次のように呼び出されます。







 Address addr = program.getMemory().findBytes(startAddr, endAddr, bytes, masks, forward, TaskMonitor.DUMMY);
      
      





その結果、バイトの開始アドレス、またはnull



が返されます。







アナライザーの作成



美しくしましょう。必要ない場合は署名を使用しませんが、ユーザーにこの手順を選択させます。 これを行うには、独自のコードアナライザーを作成する必要があります(このリストにあるものを見ることができます-それだけです)。













したがって、このリストに割り込むには、 AbstractAnalyzer



クラスから継承し、いくつかのメソッドをオーバーライドする必要があります。







  1. コンストラクター。 名前、アナライザーの説明、およびそのタイプ(後で詳しく説明します)を使用して、基本クラスのコンストラクターを呼び出す必要があります。 私には次のように見えます:


 public PsxAnalyzer() { super("PSYQ Signatures", "PSX signatures applier", AnalyzerType.INSTRUCTION_ANALYZER); }
      
      





  1. getDefaultEnablement()



    。 アナライザーが常に使用可能かどうか、または特定の条件が満たされた場合のみ(たとえば、ローダーが使用されている場合)を決定します。
  2. canAnalyze()



    。 ダウンロード可能なバイナリファイルでこのアナライザーを使用することはできますか?

    パラグラフ2および3は、原則として、1つの機能で検証できます。


 public static boolean isPsxLoader(Program program) { return program.getExecutableFormat().equalsIgnoreCase(PsxLoader.PSX_LOADER); }
      
      





PsxLoader.PSX_LOADER



にはブートローダーの名前が格納されており、その中で以前に定義されています。







合計、我々は持っています:







 @Override public boolean getDefaultEnablement(Program program) { return isPsxLoader(program); } @Override public boolean canAnalyze(Program program) { return isPsxLoader(program); }
      
      





  1. registerOptions()



    。 このメソッドを再定義する必要はまったくありませんが、たとえば分析前にpatファイルへのパスなどをユーザーに尋ねる必要がある場合は、このメソッドでこれを行うのが最適です。 取得するもの:


 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"); }
      
      





ここで明確にする必要があります。 Application



クラスの静的メソッドgetModuleDataFile()



は、モジュールのツリーにあるdata



ディレクトリ内のファイルへのフルパスを返し、後で参照する必要なファイルを保存できます。







さて、 registerOption()



メソッドは、 OPTION_NAME



で指定された名前、タイプFile



(つまり、ユーザーは通常のダイアログボックスでファイルを選択できる)、デフォルト値、および説明を使用してオプションをregisterOption()



します。







次。 なぜなら 登録されたオプションを後で参照する通常の機会はありません; optionsChanged()



メソッドを再定義する必要があります:







 @Override public void optionsChanged(Options options, Program program) { super.optionsChanged(options, program); file = options.getFile(OPTION_NAME, file); }
      
      





ここでは、新しい値に従ってグローバル変数を更新するだけです。







added()



メソッド。 ここで重要なのは、アナライザーの起動時に呼び出されるメソッドです。 その中で、分析に使用できるアドレスのリストを受け取りますが、必要なのはコードを含むアドレスのみです。 したがって、フィルタリングする必要があります。 最終コード:







()メソッドを追加
 @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; }
      
      





ここでは、実行可能なアドレスのリストを調べて、そこに署名を適用しようとします。













結論と結末



すべてが好きです。 実際、ここではそれほど複雑なことはありません。 いくつかの例がありますが、コミュニティは活発です。コードの作成中に不明な点について安全に質問できます。 ボトムライン: Playstation 1



実行可能ファイルの動作中のブートローダーおよびアナライザー。













すべてのソースコードは、 ghidra_psx_ldrから入手できます。

ここでのリリース: リリース








All Articles