新|PSPでの脆弱性探し、バイナリローダー、そしてHalf-Byte Loader移植のチュートリアル

フォーラムルール
フォーラムをご利用するにあたってのルールは以下に記載してあります。
初めてご利用になる方へ:最初にお読みください

新|PSPでの脆弱性探し、バイナリローダー、そしてHalf-Byte Loader移植のチュートリアル

未読記事by 173210 » 2015年2月27日(金) 16:37

初めに
公式ファームウェアのPSPで自作ソフトを実行するために必要なことは、
1. PSPでユーザーモードの脆弱性を見つける
2. ユーザーモードの脆弱性でバイナリーローダーを作成する
3. Exploitを利用してHBL(Half Byte Loader)などを移植する
の3つです。

この分野ではこのフォーラムの管理人であるまもすけ氏が詳しく、彼は何度か翻訳して日本語で紹介してきました。
その中で、彼はチュートリアルを作成していますが、様々な変化があり、残念ながら時代遅れのものになってしまっています。
そこで、valentine-hblを私がどう勝手にいじってしまったかに関する知識を用いて、新しくチュートリアルを書きます。
え?PSPはオワコンだろって?だから書くんだよ。忘れないように。

以前からの変化
・valentine-hblの設定ファイルの変更
以前は多くの設定ファイルがあり、特にsdk_loader.Sとsdk_hbl.Sは冗長でした。
現在は勝手に意味の分からない単一の設定ファイルにまとめられました。
さらに、設定も大幅に削減したので、移植がより簡単になりました。

・PSPLink Mod by 173210の公開
HBLの移植に必要となることがあります。宣伝ではない。たぶん。

目次
第1章: PSPのユーザーモードの脆弱性を探す
第2章: Exploitする文字列の作成
第3章: バイナリーローダーの作成
第4章: 新たなExploitにHBLを移植する

準備
最新のPSPSDKとRuby、SVNがインストールされていて、PATHが通してあることが前提です。
さらに、valentine-hblをチェックアウトしておいてください。
コード: 全て選択
svn checkout https://valentine-hbl.googlecode.com/svn/trunk/


注意
次の能力が必要です。
・ソフトウェアをPCにインストールできる
・コマンドラインを使える
・PATHを通すということの意味が分かる
・「ディレクトリ」「クラッシュ」「脆弱性」の意味が分かる
・cdコマンドが分かる
・"0x"の意味が分かる
ここでわからなくてググったあんたは最高。進むべし。
C言語とMIPSが読めるなら、つっ立てないでvalentine-hblの開発に参加しろ。

謝辞
このチュートリアルはまもすけ氏のものをパクって参考にしています。
ありがとうございました。
最後に編集したユーザー 173210 [ 2015年5月04日(月) 22:09 ], 累計 11 回
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

新|PSPでの脆弱性探し、バイナリローダー、そしてHalf-Byte Loader移植のチュートリアル

スポンサードリンク

スポンサードリンク
 

第1章: PSPのセーブデータexploitを探す

未読記事by 173210 » 2015年2月27日(金) 16:38

この章は、Exploitに成り得る脆弱性なのか単なるクラッシュなのかを切り分けるためのチュートリアルです。PSPでのゲームの脆弱性探しのチュートリアルでもあります。

セーブデータを読み込んだPSPをクラッシュさせるのはPSPのExploitへの第一歩であることは間違いありませんが、クラッシュ=Exploitではありません。クラッシュだけですべてを公開することはソニーに対策をお願いし、より強固なファームウェアへの成長を促していることになります。また、最近はExploitとして使っているゲーム名を公開しないことが一般的になっています。何らかの結果(HENやHBLなど)へ繋がる前に公開することも強固なファームウェアへの成長を促しているに過ぎないため現在ではしてはいけないこととして扱われています。かつてはHello World表示に成功した時点で公開していた時期もありますが、Hello Worldはバイナリーローダーで”Hello World”という文字を表示する自作コード起動に成功しただけですので実用性には欠けると言わざるを得ません。例えばH.BINの形でコンパイルした自作ソフトであれば起動しますが、EBOOT.PBPとして利用されているPSPの自作ソフトの起動はできません。

最近公開されているPSPのセーブデータExploitはほとんどがMaTiAz氏がGripshiftで実践した、「This is spartaaaaaa方式」を利用しています。これはセーブデータの名前の部分に続けて'a'のような文字を続けて入れたセーブデータを作成しクラッシュを引き起こすことで脆弱性を探す方式です。プレーヤーが最初のゲーム起動時に入力する名前の部分を脆弱性を探す際に利用するのはPSPが確実に読み込むデータでありセーブデータ内のどこに格納されているのかが分かりやすいという理由からです。理論的には名前の部分でなくとも確実に読み込むと分かっているデータエリアであれば構わないのですが、チュートリアルとしてはやはり簡単な名前の部分を使う前提でお話しします。

私自身様々なゲームでExploitを公開しましたが、私はハッカーと呼ばれる人種ではありませんし、ましてやプログラマーでもありません。そのため表現として不適切な部分があるとは思いますが御了承下さい。

この記事を読むにあたり、基本的なプログラミングについての概念、特に変数やアドレス、配列といった知識は知っておいた方が良いと思います。コマンドラインについても扱い方を知っておいた方が良いでしょう。アセンブラの知識はあるに越したことがありませんが絶対に必要なものでもありませんが、アセンブラの知識はあった方がより理解が深まるでしょう。

PSPLinkでゲームを起動する
PSPLinkはデバッグに必要不可欠なツールです。まずはPSPLinkをインストールしてあるCFWのPSPが必要です。
私が改変したPSPLinkの使用を強く勧めます。場合によってはvalentine-hblの移植で必要になります。
Releases · 173210/psplinkusb · GitHub
PSPLinkのインストール方法などはネットで検索すれば出てきますので割愛します。PSPLinkは数種類のprxファイルで構成されています。PSPではPSPLinkをプラグインの形で利用します。sepluginsフォルダにusbhostfs.prxとpsplink.prxを入れて、そのプラグインをゲーム起動中有効にするためにgame.txtでpsplink.prxを記述しておいてリカバリーメニューで有効にしておいてください。psplink.prxを有効にしておけばusbhostfs.prxはPSPLinkが自動で有効状態にしてくれますのでusbhostfs.prxをわざわざ有効にする必要はありません。基本的にはPSPを遠隔操作するRemoteJoyがキー入力を送信するところでコマンドを送信するのがPSPLinkだと思ってください。チュートリアルはどこにでもあるであろうRemoteJoyを使えるようセットアップできていればPSPLinkは使えるはずです。
すべてインストールが完了し準備ができたら、PCでusbhostfs_pc.exeを起動してゲームを起動したPSPとPCをUSBケーブルで繋ぎpspsh.exeを起動してください。うまく動作すれば、usbhostfs_pcの画面には”Connected to device”と表示され、pspshには"host0:/>”と出てきます (画像はまもすけ氏による)。
5029182583_2747f09e62.jpg


セーブデータExploitの基礎
セーブデータExploitのほとんどは「スタックバッファオーバーフロー」という脆弱性を利用しています。詳しい仕組みを知りたい方はググってください。

さて、セーブデータでスタックバッファオーバーフローを引き起こすための最も一般的な方法は非常に長い文字列をセーブデータ内のどこかに仕込む方法です。文字列は文字の集まりなので仕込むのは簡単です。更に文字列は一般的には復号化されたセーブデータファイルであれば、そのファイル内のどこにあるのかを探すのは非常に簡単です。ゲーム中に入手したゴールドメダルの数が格納されている場所を探すのは簡単なことではありませんが、文字列であればPSPLinkにある検索機能を使えば非常に簡単です。ただし、名前の場合文字コードを考えなければならない日本語(2バイト文字)にするよりはアルファベットを用いた1バイト文字のほうが検索が簡単になります(「文字列」よりも"STRING"の方が簡単です)。スタックバッファオーバーフローを引き起こす目的でプレイヤー名をゲームで入力しセーブデータを作成する場合は、ひらがなしか受け付けないなどの特殊な場合を除いてプレーヤー名をアルファベットで入力しておくことをお勧めします。また、PSPでプレイヤー名を入力する際何文字目かでそれ以上入力できないようになっている場合がほとんどですが、できる限り限界まで文字を入れておいた方が検索の精度が上がります。

名前を入力したら、一旦セーブしてゲームを終了し、もう一度起動させてセーブデータを再び読み込ませてください。では、ここでスレッドがスタックに文字列を読み込むか試してみましょう。
次のコマンドを入力してユーザーモードのスレッドを見つけてください。
コード: 全て選択
uidlist Thread

スレッドの大きなリストが出力されます。attrが0xFFのスレッドを探してください。それがユーザーモードのスレッドです。
コード: 全て選択
[Thread] UID 0xCCCCCCCC (attr 0x0 entry 0xEEEEEEEE)
(Name): user_main, (UID): 0xDDDDDDDD, (entry): 0x0FFFFFFF (attr): 0xFF # <- This thread is usermode!
(Name): NAME, (UID): 0xBBBBBBBB, (entry): 0x88000000 (attr): 0x0 # <- This Thread is not usermode. :/
# 以下略

次に、このコマンドを入力してスタックのアドレスとサイズを調べます。(0xDDDDDDDDはスレッドのUIDで置き換えてください。)
コード: 全て選択
thinfo 0xDDDDDDDD

こんなふうにスレッドの情報が表示されます。
コード: 全て選択
UID: 0xDDDDDDDD - Name: user_main
Attr: 0xBBBBBBBB - Status: 0/STATUS- Entry: 0xEEEEEEEE
Stack: 0x0AAAAAAA - StackSize 0x00CCCCCC - GP: 0x0FFFFFFF
InitPri: 32 - CurrPri: 32 - WaitType 0
WaitId: 0x00000000 - WakeupCount: 0 - ExitStatus: 0x00000000
RunClocks: 0 - IntrPrempt: 0 - ThreadPrempt: 0
ReleaseCount: 0, StackFree: 0

この例ではアドレスは0x0AAAAAAAでサイズは0x00CCCCCCと表示されています。

準備はこれで終わりです。脆弱性探しを始めましょう!
まず、どうにかして文字列をスタックに読み込ませなければいけません。必要となった時にコピーするはずなので、その文字列を表示する画面を開いたら読み込まれるかもしれません。
文字列がコピーされたと思ったら、pspshにコマンドを入力してスタックの中の文字列を探しましょう。(0x0AAAAAAAをアドレス、0x00CCCCCCをサイズ、STRINGを入力した文字列にそれぞれ置き換えてください。
コード: 全て選択
findstr 0x0AAAAAAA 0x00CCCCCC STRING

すると、一致した文字列のアドレスが次のように出力されます。(0x0EEEEEEEがアドレス)
コード: 全て選択
Found match at address 0x0EEEEEEE

やりました!99%の確率でそのゲームはExploitできます。

TIP: 文字列を見つけられない時は…
スタックに文字列を見つけられないかもしれません。文字列が他の関数で上書きされているかもしれません。でも心配御無用、解決策があります。
文字列をコピーする際にスレッドをクラッシュさせて、上書きされる前に止めればいいのです。
次の指示に従ってください。

まず、セーブデータを復号化しなければなりません。MagicSaveはセーブデータを復号化する非常に優れたツールです。ググって使い方を調べましょう。すぐに復号化できます。
次に、バイナリエディタでセーブデータを開いてください。バイナリエディタの使い方もググればいっぱい見つかります。
セーブデータを開いたら、バイナリエディタの検索機能でセーブデータ内に保存されている入力した文字列を探し、こんな感じに長い文字列で上書きしてください (画像はwololo氏による)。
sparta.jpg

編集がおわったら、MagicSaveで暗号化するか、復号化したデータを読ませてください。
何かしらの動作を行うとクラッシュするかもしれません。全くクラッシュしなければ、文字列をより長くしてください。もしセーブデータが破損していると言われたら諦めてください。
クラッシュしたら、findstrコマンドをもう一度試してみてください。
最後に編集したユーザー 173210 [ 2015年5月04日(月) 20:53 ], 累計 20 回
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

第2章: Exploitする文字列の作成

未読記事by 173210 » 2015年2月27日(金) 16:38

コマンドを入力するのにつかれましたか?でも、この章はより努力と知識を要します。
MIPSアセンブリを読まなければなりません。ただ、MIPSを読んだことがなくてもまずは試してみてください。

解説
まずは、呼び出し規約を理解する必要があります。簡単にどうすべきなのか説明しましょう。

関数は一時的にスタックにデータを保存します。
コード: 全て選択
+------------------------+ 0x0AAAAAAA - 低い
|                        |
|                        |
|                        |  - 空き
|                        |
|                        |
+------------------------+ - スタックポインタが示すアドレス
|         データA         | - 関数Aに使われているデータ
~~~~~~~~~~~~~~~~~~~~~~~~~~
|         データX         |
+------------------------+ (0x0AAAAAAA + 0x00CCCCCC) - 高い

スタックポインタはスタックの現在の一番低いアドレスを指します。
関数A関数Bを呼ぶと、関数Bはスタックポインタを減らし、必要があればリターンアドレス(完了後に戻るアドレス)をスタックに保存します。
コード: 全て選択
+------------------------+ 0x0AAAAAAA - 低い
|                        | - 空き
+------------------------+ - スタックポインタが示すアドレス
|         データB         | - 関数Bで使われるデータ
|                        |
|         データb         | - 関数Bで使われるデータ
|     リターンアドレス      | - 戻るアドレス
|         データA         | - 関数Aで使われるデータ
~~~~~~~~~~~~~~~~~~~~~~~~~~
|         データX         |
+-------------------+ (0x0AAAAAAA + 0x00CCCCCC) - 高い

ここで、関数Bがセーブデータに挿入された長い文字列を読み込みます。
コード: 全て選択
+------------------------+ 0x0AAAAAAA - 低い
|                        | - 空き
+------------------------+ - スタックポインタが示すアドレス
|         データB         | - 関数Bに使われるアドレス
|The spartaaaaaaaaaaaaaaa| 0x0EEEEEEE - 入力した文字列
|aaaaaaaaaaaaaaaaaaaaaaaa| - 関数Bに使われるデータ
|aaaaaaaaaaaaaaaaaaaaaaaa| - 戻るアドレス
|         データA         | - 関数Aで使われるデータ
~~~~~~~~~~~~~~~~~~~~~~~~~~
|         データX         |
+------------------------+ (0x0AAAAAAA + 0x00CCCCCC) - Highest Address

あらあら、リンクさん、データbとリターンアドレスが上書きされてしまったようですねえ。
それなら、あなたのコードのアドレスをリターンアドレスとして保存してはどうでしょう?最後の部分をそのアドレスと置き換えます。関数Bが終わったらあなたのコードにジャンプするはずです。
コード: 全て選択
+------------------------+ 0x0AAAAAAA - 低い
|                        | - 空き
+------------------------+ - スタックポインタが示すアドレス
|         データB         | - 関数Bに使われるアドレス
|The spartaaaaaaaaaaaaaaa| 0x0EEEEEEE - 入力した文字列
|aaaaaaaaaaaaaaaaaaaaaaaa| - 関数Bに使われるデータ
|  自分のコードへのアドレス  | - 戻るアドレス
|         データA         | - 関数Aで使われるデータ
~~~~~~~~~~~~~~~~~~~~~~~~~~
|         データX         |
+------------------------+ (0x0AAAAAAA + 0x00CCCCCC) - 高い

いい感じです。でも破損したデータb関数Bが終了する前にクラッシュさせかねません。"aaaa..."をクラッシュを起こさず、文字列としても認識されるデータb改で置き換えましょう。
コード: 全て選択
+------------------------+ 0x0AAAAAAA - 低い
|                        | - 空き
+------------------------+ - スタックポインタが示すアドレス
|         データB         | - 関数Bに使われるアドレス
|The spartaaaaaaaaaaaaaaa| 0x0EEEEEEE - 入力した文字列
|        データB改        | - 関数Bに使われるデータ
|  自分のコードへのアドレス  | - 戻るアドレス
|         データA         | - 関数Aで使われるデータ
~~~~~~~~~~~~~~~~~~~~~~~~~~
|         データX         |
+------------------------+ (0x0AAAAAAA + 0x00CCCCCC) - 高い

そしてついにあなたのコードが実行されます。実際にやってみましょう。

1. スレッドをクラッシュさせる
第一章では文字列がUIDが0xDDDDDDDDのスレッドのスタック内のアドレス0x0EEEEEEEにコピーされることがわかりました。
どうにかしてスレッドをスタックポインタがそのアドレスより低い時にクラッシュさせる必要があります。2つ方法があります。

1つはbpthコマンドです。とても簡単にスレッドをクラッシュさせます。
コード: 全て選択
bpth 0xDDDDDDDD

0xDDDDDDDDをUIDで置き換えてください。
クラッシュすると、例外(Exception)が表示されます。
コード: 全て選択
Exception - Breakpoint
Thread ID - 0xDDDDDDDD
Th Name - user_main
Module ID - 0x%08X
Mod Name - %s
EPC - 0x0CCCCCCC
Cause - 0x%08X
BadVAddr - 0x%08X
Status - 0x%08X
zr:0x00000000 at:0xDEADBEEF v0:0xDEADBEEF v1:0xDEADBEEF
a0:0xDEADBEEF a1:0xDEADBEEF a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0xDEADBEEF s1:0xDEADBEEF s2:0xDEADBEEF s3:0xDEADBEEF
s4:0xDEADBEEF s5:0xDEADBEEF s6:0xDEADBEEF s7:0xDEADBEEF
t8:0xDEADBEEF t9:0xDEADBEEF k0:0xDEADBEEF k1:0x00000000
gp:0xDEADBEEF sp:0x0FFFFFFF fp:0xDEADBEEF ra:0xDEADBEEF
0x0CCCCCCC: 0x0000000D '....' - break 0

sp (スタックポインタ) の値を見てください。0x0FFFFFFFは0x0EEEEEEEより大きいです。失敗のようです。
この方法で失敗したら、もうひとつの方法で試す必要があります。これは第1章でTipとして説明されています。そのTipを見てください。

成功すると、こんな例外が出ます。
コード: 全て選択
Exception - Breakpoint
Thread ID - 0xDDDDDDDD
Th Name - user_main
Module ID - 0x%08X
Mod Name - %s
EPC - 0x08888888
Cause - 0x%08X
BadVAddr - 0x%08X
Status - 0x%08X
zr:0x00000000 at:0xDEADBEEF v0:0xDEADBEEF v1:0xDEADBEEF
a0:0xDEADBEEF a1:0xDEADBEEF a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0xDEADBEEF s1:0xDEADBEEF s2:0xDEADBEEF s3:0xDEADBEEF
s4:0xDEADBEEF s5:0xDEADBEEF s6:0xDEADBEEF s7:0xDEADBEEF
t8:0xDEADBEEF t9:0xDEADBEEF k0:0xDEADBEEF k1:0x00000000
gp:0xDEADBEEF sp:0x0EEEEEE0 fp:0xDEADBEEF ra:0xDEADBEEF
0x08888888: 0x0000000D '....' - break 0

0x0EEEEEE0は0x0EEEEEEEより若干小さいです。スタックをダンプして文字列がコピーされていることを確認しましょう。
コード: 全て選択
memdump 0x0EEEEEEE

コード: 全て選択
         - 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f - 0123456789abcdef
-----------------------------------------------------------------------------
0EEEEEEE - 54 68 65 20 73 70 61 72 74 61 00 00 00 00 00 00 - The sparta......
0EEEEEFE - FF 00 00 00 00 00 98 99 99 09 00 00 00 00 00 00 - ................
0EEEEF0E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF1E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF2E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF3E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF4E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF5E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF6E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF7E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF8E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEF9E - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEFAE - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEFBE - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEFCE - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................
0EEEEFDE - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - ................

うん、コピーされてますね。この時点のEPC (例では0x08888888) をメモしておいてください。

2. コードを逆アセンブルする
さて、ここでは0x08888888に書かれたコードがどのように動くか理解する必要があります。これはMIPSアセンブリの知識が必要になります。
その部分をこのコマンドで逆アセンブルします。 (SIZEを見たい命令数で置き換えてください。)
コード: 全て選択
disasm $epc SIZE

逆アセンブルされたコードを手に入れたら、まずはjr命令か[i]jalr命令を見つけてください。例えば、この命令は$raに格納されたアドレスにジャンプすることを意味します。
コード: 全て選択
jr $ra

次に、$raにアドレスを格納するコードを見つけてください。スタックを参照しているかもしれません。
コード: 全て選択
lw $ra, 32($sp)

この例ではそのアドレスは$sp+32に格納されているようです。次のコマンドで確認してください。
コード: 全て選択
peekw $sp+32

コード: 全て選択
0x0EEEEF00: 0x09999998

コード: 全て選択
disasm 0x09999998-8

コード: 全て選択
0x09999990: 0x0C222220 - ' "".' jal 0x08888880

どうやらそのアドレスなようです。つまり長さが($sp+32)-0x0EEEEEEE+4の文字列でExploitできるということになります。
コード: 全て選択
calc $sp+32-0x0EEEEEEE+4

コード: 全て選択
22


3. スタックをダンプする
十分な情報がてにはいりました。文字列を作成しましょう。
まず、ベースとなるデータ (最初の例ではデータb) が必要かもしれません。クラッシュを起こさないセーブデータを読み込んで文字列をスタックに読み込ませる動作をしてください。
文字列がコピーされる直前に、ブレークポイントを2で見つけたアドレス (例では0x08888888) に設置します。
コード: 全て選択
bpset 0x08888888

するとクラッシュします。
コード: 全て選択
0x08888888: 0x0000000D - "...." break 0

スタックをダンプしましょう。
コード: 全て選択
savemem 0x0EEEEEEE 22 memdump.bin


4. 文字列として認識されるようにデータ書き換えて、アドレスを書き込む
memdump.binをバイナリエディタで開いて0x00を0x20で置き換えてください。0x00は文字列の終端として認識されます。そして文字列の最後を67 45 23 01で置き換えてください。MIPSはリトルエンディアンであることに注意してください。
最後に、文字列を終端させるために0x00を末尾に付け加えてください。こんな感じになるはずです。
コード: 全て選択
         - 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f - 0123456789abcdef
-----------------------------------------------------------------------------
00000000 - 54 68 65 20 73 70 61 72 74 61 20 20 20 20 20 20 - The sparta     
00000010 - FF 20 20 20 20 20 67 45 23 01 00                - ÿ     gE#..


Exploitする文字列を手に入れました。セーブデータに文字列を挿入しましょう。やり方は第一章のTIPで簡単に説明されています。
では、あなたのセーブデータでExploitしてみましょう。
失敗すると、こんな例外が表示されます。
コード: 全て選択
Exception - Bus error (data)
Thread ID - 0xDDDDDDDD
Th Name - user_main
Module ID - 0x%08X
Mod Name - %s
EPC - 0x0888888C
Cause - 0x%08X
BadVAddr - 0x20202020
Status - 0x%08X
zr:0x00000000 at:0xDEADBEEF v0:0xDEADBEEF v1:0xDEADBEEF
a0:0x20202020 a1:0xDEADBEEF a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0xDEADBEEF s1:0xDEADBEEF s2:0xDEADBEEF s3:0xDEADBEEF
s4:0xDEADBEEF s5:0xDEADBEEF s6:0xDEADBEEF s7:0xDEADBEEF
t8:0xDEADBEEF t9:0xDEADBEEF k0:0xDEADBEEF k1:0x00000000
gp:0xDEADBEEF sp:0x0EEEEEE0 fp:0xDEADBEEF ra:0xDEADBEEF
0x0888888C: 0x8C840000 '....„Œ' - lw $a0, 0($a0)

Exploitする前にクラッシュしました。例外を理解して修正してください。

成功すると、こんな例外が表示されます。
コード: 全て選択
Exception - Bus error (instr) # <- Important
Thread ID - 0xDDDDDDDD
Th Name - user_main
Module ID - 0x%08X
Mod Name - %s
EPC - 0x01234567 # <- Important
Cause - 0x%08X
BadVAddr - 0x01234567
Status - 0x%08X
zr:0x00000000 at:0xDEADBEEF v0:0xDEADBEEF v1:0xDEADBEEF
a0:0xDEADBEEF a1:0xDEADBEEF a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0xDEADBEEF s1:0xDEADBEEF s2:0xDEADBEEF s3:0xDEADBEEF
s4:0xDEADBEEF s5:0xDEADBEEF s6:0xDEADBEEF s7:0xDEADBEEF
t8:0xDEADBEEF t9:0xDEADBEEF k0:0xDEADBEEF k1:0x00000000
gp:0xDEADBEEF sp:0x0EEEEEE0 fp:0xDEADBEEF ra:0x01234567
最後に編集したユーザー 173210 [ 2015年5月04日(月) 22:03 ], 累計 7 回
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

第3章: バイナリーローダーの作成

未読記事by 173210 » 2015年2月27日(金) 16:38

この章について
PSPで自作ソフトを実行するための最初のステップはユーザーモードexploitを発見することです。これはゲームでのバッファーオーバーフローや画像を操作するといったテクニックを利用してPSPのRAMをコントロールすることになります。
こうした脆弱性を見つけて、値が可変するレジスタ$raをフルコントロールします。次のステップはその実証となります。バイナリーローダーを作るにあたり基本的な考え方は、記述したコードからメモリースティックに置いたファイルにある別のコードを読み込み実行させるということになります。
バイナリーローダーの作成は方法さえわかってしまえばそんなに複雑ではありませんが、どこかに情報が集まっている訳ではないためできるかどうかは知識の有無に依存してしまいます。
ということでこちらです。

警告: これから書く方法は最良の方法ではありませんが、誰でも簡単にPSPで”Hello World”を表示(自作コードの実行が可能なことを証明)することが可能になります。今回サンプルに使ったsparta_sdkはもちろんこのチュートリアルに沿って作ったわけではありませんし、本来であればアセンブラなどの基礎も覚えた上で取り組むべきことであるのは事実です。しかし原則を主張していても実利は生まれません。試してみたらできた、という楽しみ方の提案だと思って読んでください。

このガイドでは皆さんが既に$raの値を自由に変えることならばできるという前提で書きます。ゲームのセーブデータを例にあげてみましょう。
PSPLinkの使い方も知っているものとします。16進数エディタの使い方やCやMIPSを扱うに当たっての言語の記述の仕方、基本的なMakefileの大まかな知識、そして自作を作りたいという気持ちも既にお持ちだとの前提です。

目次
ゲームの脆弱性を使ってバイナリーローダーを作成するためのステップは以下の通りです。

1.ジャンプする場所を探す
2.インポートする関数を探す
3.ゲームに適用するSDK(開発キット)を作成する
4.バイナリーローダーをコンパイルする
5.バイナリーローダーをセーブデータに書き込む
6.動作を実証するためのバイナリーファイルを作成する
7.トラブルシューティング

ジャンプする場所を探す

文字列の前とか後とかにしましょう。
たとえば、文字列のアドレスが0xAAAAAAAA、文字列の長さがsizeであれば、0xAAAAAAAA-256とか、0xAAAAAAAA+size+256などです。
アドレスを決めたら、そこに適当にデータを入れて安全か確認しましょう。
たとえば、アドレスをloadaddrに決めたとすると、次のようにpspshに入力してデータを入れます。
コード: 全て選択
fillb loadaddr 256 0x11

そのあとセーブして、再度ゲームを起動してからクラッシュした時点までゲームを進めます。
その時、次のコマンドをpspshに入力して、データが破壊されていないか確かめます。
コード: 全て選択
memdump loadaddr

破壊されていなければ、そのアドレスを使いましょう。今後はジャンプする場所をloadaddrとします。

インポートする関数を探す
PSPLinkを起動させ、次のコマンドを入力します。
コード: 全て選択
modaddr 0x08900000

モジュールの情報が次のように表示されるはずです。
コード: 全て選択
UID: 0xUUUUUUUU Attr: TTTT - Name: %s
Entry: 0x08900000 - GP: 0xGGGGGGGG - TextAddr: 0xEEEEEEEE
TextSize: 0xSSSSSSSS - DataSize: 0xDDDDDDDD BssSize: 0xBBBBBBBB

さらに次のコマンドを入力します。(0xUUUUUUUUは先の出力を参照)
コード: 全て選択
modimp 0xUUUUUUUU > modimp.txt

これでmodimp.txtにモジュールがインポートしている関数のアドレスが記録されました。

SDKの作成
gen_sdk_modimp.rbを使用してください。使い方は簡単です。valentine-hblのパス/toolsにmodimp.txtをコピーして、次のコマンドを実行するだけです。
コード: 全て選択
cd valentine-hblをチェックアウトしたパス/tools
ruby gen_sdk_modimp.rb

しばし待つと、sdk.Sが出力されます。これは必ず保存しておいてください!

バイナリーローダーをコンパイルする
私が軽量なSDKを作成したので、こちらを使用してください。
Light Binary Loader for PSP
保存して、テキストエディタで開いてください。
基本的な作業内容は関数アドレスを置き換えることです。Matiaz氏はGripshiftで使えるようなアドレスを入れていますので自分が見つけたゲームexploitとはそこが異なります。記述してある3つの関数(sceIoOpen、sceIORead、sceIOClose)を、先ほど作成したsdk.Sに沿って自分のゲームでの値に置き換えてください。sceIoCloseとなっている0x08A88590を0x08C88590に置き換える、などという感じです。
ではこのファイルをコンパイルしましょう。
アセンブラのコンパイルはPSPSDKで提供されている各種ツールのなかでも特に難しいということはありません。では、ここでコンパイルのためのコマンドを紹介します。

コード: 全て選択
psp-as loader.s
psp-objcopy -O binary a.out a.bin


最初がコードをコンパイルするコマンドで、次がそれをバイナリー版にするコマンドです。

バイナリーローダーをセーブデータに書き込む
バイナリーローダーのコンパイルができればあとは簡単で、セーブデータに書き込むだけです。文字列からの位置でセーブデータ内のどこに保存すべきかは解ると思います。
あとはセーブするだけです。

動作を実証するためのバイナリーファイルを作成する
バイナリローダーができれば、これは本当に簡単です。
まず、以下のコードをbreak.sとして保存します。
コード: 全て選択
break 0;

次に、次のコマンドでコンパイルします。
コード: 全て選択
psp-as break.s
psp-objcopy -O binary a.out H.BIN

これでできたH.BINをMemory Stickにコピーして、バイナリローダーを発動させてみてください。
成功すると、PSPLinkに"Exception - Break"と表示され、EPCに0x09000000が代入されているはずです。

トラブルシューティング
Exploitの動作がうまく行かない場合、いくつかの原因が考えられます。コンピューターのプログラムは書かれた流れ通りに実行されるだけなので、動作環境が不十分であってもバカ正直に間違った動作をしてしまいます。そんな時はPSPLinkの利用が成功の鍵となります。クラッシュしてしまったらRAMの状態を調べてみてください。正しいアドレスにきちんとジャンプしていますか?バイナリーローダーはそこにちゃんとありますか? すべて正しい場所にあって、おかしな理由で途切れたりしていませんか?実際に動作しているかを確認するためにバイナリーローダーにブレークポイントを追加してみてください。
最後に編集したユーザー 173210 [ 2015年5月04日(月) 22:07 ], 累計 2 回
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

第4章: 新たなexploitにHBLを移植する

未読記事by 173210 » 2015年2月28日(土) 10:46

セーブデータを利用したユーザーモードexploitを見つけて、次にバイナリーローダーを作成。で、次はどうしたらよいのでしょうか。ご存じだとは思いますが、PSPでHello Worldだけというのはもう誰も喜んでくれません。もちろんHello Worldが実現できることはは素晴らしいことですが、それだけでは何の成果も得られません。単にソニーに対してexploitの存在をアピールするだけで、せっかくの脆弱性が何らかの結果に結び付く前に早々とパッチされてしまうことは自明の理です。

さて、Exploitを発見後の次のステップといえば、理想的にはHENやカスタムファームウェアとなります。もちろんそれを実現するには新たにカーネルExploitが必要ですが、そのために脆弱性を探すのはかなり面倒です。ですのでユーザーの利益に成りうるより現実的な選択はHBLの移植となります。HBLはノーマルPSPで合法的なコンテンツを楽しむための扉を開くアプリケーションです。我々はそのためにHBLを開発したのですからセーブデータを使ったゲームExploitをHBLに移植することは簡単にできるようになっています。

このチュートリアルはこの記事を書いている現時点ではどのファームウェアにも対応しています。理論上HBLはこの先出るであろうファームウェアでも動作するはずですが、もちろん今後新たなセキュリティシステムが採用されてしまう可能性もありますのでその場合にはチュートリアルに沿って移植しても動作しないこともあり得ます。

0. 実はカンタン
HBLはPatapon2以外のセーブデータExploitにも簡単に移植できるようになっています。特定の一ファイルを除き、ゲームごとに必要なファイルは後程詳しく書きますがほとんどがサブフォルダごとに分けて入れておくことになります。チュートリアルを実行するに当たっては、シェルを扱う基礎知識とPSPSDKでの作業に必要なスキル、脆弱性を探す知識、バイナリーローダーまたはHello Worldのコーディングに必要な知識、Ruby言語の知識が必要です(一般的にはRuby以外のスクリプト言語を知っていれば簡単に把握できます。違いはそんなに多くありません)。

1. 新たに発見したexploit用のファイルを作成する
第2章で"Exception - Break"と表示させましたが、その状態で次のコマンドをpspshに入力します。
コード: 全て選択
savemem 0x08800000 0x01800000 memdump.bin
uidlist > uidlist.txt
lwmtxlist > lwmtxlist.txt

ここで作成したファイル群をvalentine-hblのパス/toolsにコピーします。
そのあと、次のコマンドを入力して設定ファイルを作成します(コードネームは適宜読み替え)
コード: 全て選択
cd valentine-hblのパス/tools
ruby gen_exploit_config.rb valentine-hblのパス/include/exploits/コードネーム.h

設定ファイルが作成されたら、テキストエディタで開き、TODOとなっている部分を書き換えます。
LOADER_ADDRは0x09000000、HBL_ROOTはHBLのルートディレクトリです。

2. コンパイル
次のコマンドを入力してコンパイルします。
コード: 全て選択
cd valentine-hblのパス
make EXPLOIT=コードネーム


すべて完了したらH.BINとHBL.PRXをHBL_ROOTで指定したディレクトリにそれぞれ入れます。これであなたが発見したexploitで起動するHBLの中身は完成です。

3. 最後に、大切なこと
HBLはGPL(General Public License)のライセンスに基づき頒布されています。二次的著作物をコンパイルして頒布する場合ソースコードも頒布しなければなりません。我々にソースコードを要求されるようなことが無いよう注意して下さい。
このチュートリアルは皆さんに是非、という想いで作りましたが、完璧でしっかりしたものではありません。HBLへの移植は簡単ですが、うまくいったあかつきには自分で調査できる程度のスキルなら身についていることと思います。とはいえ分からないことがあった時には遠慮なく聞いて下さい。
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

ポインタのオーバーフロー

未読記事by 173210 » 2015年8月27日(木) 11:09

ポインタは大抵既に書き込まれた値を読み込むために使われます。すなわち、ポインタのバッファオーバーフローの脆弱性を用いてExploitするには、ポインタを操作するだけではなく、そのポインタが示す先にあるデータにも細工を施さなければなりません。
次に、例を示します。

コード: 全て選択
Exception - Address load/inst fetch
Thread ID - 0x04739369
Th Name   - user_main
Module ID - 0x04741149
Mod Name  - Someone very famous
EPC       - 0x08AF5F88
Cause     - 0x10000010
BadVAddr  - 0x8A5F9C30
Status    - 0x60088613
zr:0x00000000 at:0x08AB8A08 v0:0x00000000 v1:0x00000000
a0:0x8A5F9C30 a1:0x09DF1BB0 a2:0x20202020 a3:0x20200000
t0:0x00002000 t1:0x00000020 t2:0x09FFED54 t3:0x00000000
t4:0x099EE360 t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0x09FFEDC0 s1:0x08D22580 s2:0x00000001 s3:0xFFFFFFFF
s4:0x08D225B8 s5:0x08D225B4 s6:0x08D225B0 s7:0x08D225AC
t8:0xDEADBEEF t9:0xDEADBEEF k0:0x09FFFB00 k1:0x00000000
gp:0x00000000 sp:0x09FFED50 fp:0x08D225A8 ra:0x08AF5F2C
0x08AF5F88: 0x8C850000 '....' - lw         $a1, 0($a0)

$a0が破損しています。確認してみましょう。

コード: 全て選択
> disasm $epc-16 5
0x08AF5F78: 0x8FA40004 '....' - lw         $a0, 4($sp)
0x08AF5F7C: 0x8CA50004 '....' - lw         $a1, 4($a1)
0x08AF5F80: 0x00042080 '. ..' - sll        $a0, $a0, 2
0x08AF5F84: 0x00A42021 '! ..' - addu       $a0, $a1, $a0
0x08AF5F88: 0x8C850000 '....' - lw         $a1, 0($a0)
> peekw $sp+4
0x09FFED54: 0x20202020

つまり、0x09DF1BB0 + (セーブデータ上の1ワード << 2) から値を読み込んでいるということです。これでポインタが操作できることが確認できました。続けて、このポインタが最終的にどんな影響をもたらすのか調べていきます。

コード: 全て選択
> setreg $a0 0x08800000 # とりあえず、正規のアドレスを代入して例外を取り除く
> bpset $epc+4 # 次の命令で停止させる
> exresume
0x08AF5F8C: 0x0000000D '....' - break      0x0
> setreg $a0 0xDEADBEE0 # 再び不正な値を代入する
> setreg $a1 0xDEADBEE4 # 読み込まれた値も既知の値で上書きする
> bpdel $epc
> exresume
Exception - Address load/inst fetch
Thread ID - 0x04739369
Th Name   - user_main
Module ID - 0x04741149
Mod Name  - Someone very famous
EPC       - 0x08AF59DC
Cause     - 0x10000010
BadVAddr  - 0xDEADBF64
Status    - 0x60088613
zr:0x00000000 at:0xDEADBEEF v0:0x00000000 v1:0x8809C680
a0:0xDEADBEE4 a1:0x00000000 a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0x08D22580 s1:0x08D22580 s2:0x00000000 s3:0x00000000
s4:0x08C10000 s5:0x80000000 s6:0x7FFFFFFF s7:0x00000000
t8:0xDEADBEEF t9:0xDEADBEEF k0:0x09FFFB00 k1:0x00000000
gp:0x00000000 sp:0x09FFED90 fp:0x00000002 ra:0x08AF46E4
0x08AF59DC: 0x8C850080 '....' - lw         $a1, 128($a0)
> disasm $epc 25 # Exploitできないか調べる
0x08AF59DC: 0x8C850080 '....' - lw         $a1, 128($a0)
0x08AF59E0: 0x38A50009 '...8' - xori       $a1, $a1, 0x9
0x08AF59E4: 0x2CA50001 '...,' - sltiu      $a1, $a1, 1
0x08AF59E8: 0x30A500FF '...0' - andi       $a1, $a1, 0xFF
0x08AF59EC: 0x14A00011 '....' - bnez       $a1, 0x08AF5A34
0x08AF59F0: 0x00000000 '....' - nop       
0x08AF59F4: 0x0E29B7D5 '..).' - jal        0x08A6DF54
0x08AF59F8: 0x44806000 '.`.D' - mtc1       $zr, $fcr12
0x08AF59FC: 0x3C053E99 '.>.<' - lui        $a1, 0x3E99
0x08AF5A00: 0x34A5999A '...4' - ori        $a1, $a1, 0x999A
0x08AF5A04: 0x44856800 '.h.D' - mtc1       $a1, $fcr13
0x08AF5A08: 0x8E040040 '@...' - lw         $a0, 64($s0)
0x08AF5A0C: 0x3C053F80 '.?.<' - lui        $a1, 0x3F80
0x08AF5A10: 0x0E29B7B8 '..).' - jal        0x08A6DEE0
0x08AF5A14: 0x44856000 '.`.D' - mtc1       $a1, $fcr12
0x08AF5A18: 0x8E040040 '@...' - lw         $a0, 64($s0)
0x08AF5A1C: 0x8C85000C '....' - lw         $a1, 12($a0)
0x08AF5A20: 0x24A500E0 '...$' - addiu      $a1, $a1, 224
0x08AF5A24: 0x84A60000 '....' - lh         $a2, 0($a1)
0x08AF5A28: 0x8CA50004 '....' - lw         $a1, 4($a1)
0x08AF5A2C: 0x00A0F809 '....' - jalr       $a1
0x08AF5A30: 0x00862021 '! ..' - addu       $a0, $a0, $a2
0x08AF5A34: 0x8FB00000 '....' - lw         $s0, 0($sp)
0x08AF5A38: 0x8FBF0004 '....' - lw         $ra, 4($sp)
0x08AF5A3C: 0x03E00008 '....' - jr         $ra
> setreg $a0 0x08800000 # 無理っぽいので先に進む
> bpset $epc+4
> exresume
0x08AF59E0: 0x0000000D '....' - break      0x0
> setreg $a0 0xDEADBEE4
> setreg $a1 0xDEADBEE8
> bpdel $epc
> exresume
Exception - Address load/inst fetch
Thread ID - 0x04739369
Th Name   - user_main
Module ID - 0x04741149
Mod Name  - Someone very famous
EPC       - 0x08A6DF60
Cause     - 0x10000010
BadVAddr  - 0xDEADBEF4
Status    - 0x60088613
zr:0x00000000 at:0xDEADBEEF v0:0x00000000 v1:0x8809C680
a0:0xDEADBEE4 a1:0x00000000 a2:0xDEADBEEF a3:0xDEADBEEF
t0:0xDEADBEEF t1:0xDEADBEEF t2:0xDEADBEEF t3:0xDEADBEEF
t4:0xDEADBEEF t5:0xDEADBEEF t6:0xDEADBEEF t7:0xDEADBEEF
s0:0xDEADBEE4 s1:0x08D22580 s2:0x00000000 s3:0x00000000
s4:0x08C10000 s5:0x80000000 s6:0x7FFFFFFF s7:0x00000000
t8:0xDEADBEEF t9:0xDEADBEEF k0:0x09FFFB00 k1:0x00000000
gp:0x00000000 sp:0x09FFED80 fp:0x00000002 ra:0x08AF59FC
0x08A6DF60: 0x8E04000C '....' - lw         $a0, 12($s0)
> disasm $epc 7
0x08A6DF60: 0x8E04000C '....' - lw         $a0, 12($s0)
0x08A6DF64: 0xE7AC0000 '....' - swc1       $fpr12, 0($sp)
0x08A6DF68: 0x248400D0 '...$' - addiu      $a0, $a0, 208
0x08A6DF6C: 0x84850000 '....' - lh         $a1, 0($a0)
0x08A6DF70: 0x8C860004 '....' - lw         $a2, 4($a0)
0x08A6DF74: 0xAFBF0008 '....' - sw         $ra, 8($sp)
0x08A6DF78: 0x00C0F809 '....' - jalr       $a2

お分かりですか? $s0は0x08AF5F8Cで代入した$a1の値です。当然$s0は制御できるので、$a0, $a2も制御でき、結果としてjalr $a2で、Exploitできるということになります。最終的に$a2に代入される値をC言語で直してみるとこんな感じです。
コード: 全て選択
$a2 = *(uint32_t *)((*(uint32_t *)(*(uint32_t *)(0x09DF1BB0 + (セーブデータ上の1ワード << 2)) + 12) + 208) + 4)

随分わかりやすくなりましたね(ね?)。では、実際にExploitしてみます。

まずは、0x09DF1BB0 + (セーブデータ上の1ワード << 2)でセーブデータ内のアドレス、0x08C60CE0を指してみます。方程式をとく簡単なお仕事です。wをセーブデータ上の1ワードとします。
コード: 全て選択
0x09DF1BB0 + (w << 2) = 0x08C60CE0
w << 2 = 0x08C60CE0 - 0x09DF1BB0
w << 2 = 0xFEE6F130
w = 0xFEE6F130 >> 2
w = 0x3FB9BC4C

これをセーブデータ上の1ワードに代入します。

$a2は次のようになります。
コード: 全て選択
$a2 = *(uint32_t *)((*(uint32_t *)(*(uint32_t *)(0x09DF1BB0 + (セーブデータ上の1ワード << 2)) + 12) + 208) + 4)
$a2 = *(uint32_t *)((*(uint32_t *)(0x08C60CE0 + 12) + 208) + 4)
$a2 = *(uint32_t *)(*(uint32_t *)(0x08C60CE0 + 12) + 212)


当然、*(uint32_t *)0x08C60CE0 + 12もセーブデータ内を指す必要があります。使い回しをしたいので、ここは*(uint32_t *)0x08C60CE0 + 12 = 0x08C60CE0となるようにします。
コード: 全て選択
*(uint32_t *)0x08C60CE0 + 12 = 0x08C60CE0
*(uint32_t *)0x08C60CE0 = 0x08C60CE0 - 12
*(uint32_t *)0x08C60CE0 = 0x08C60CD4

この値を代入します。

再度$a2を求めてみます。
コード: 全て選択
$a2 = *(uint32_t *)(*(uint32_t *)(0x08C60CE0 + 12) + 212)
$a2 = *(uint32_t *)(*(uint32_t *)0x08C60CE0 + 212)
$a2 = *(uint32_t *)(0x08C60CD4 + 212)
$a2 = *(uint32_t *)0x08C60DA8

0x08C60DA8もセーブデータ内のアドレスです。0x08C60DA8に当たる部分にバイナリローダーへのアドレスを代入します。
173210
 
記事: 23
登録日時: 2011年7月12日(火) 20:09

Re: 新|PSPでの脆弱性探し、バイナリローダー、そしてHalf-Byte Loader移植のチュートリアル

未読記事by Friontromes1967 » 2024年3月19日(火) 09:06

かなり長い間、PSP でプレイするのが好きでした。それは素晴らしいことでしたが、最近、ゲーム用コンピューターを購入する機会がありました。 そして、あなたはどう思いますか? 今はCounter-Strikeで遊んでます!

これは私のゲーム人生においてまったく新しいレベルであり、このゲームを十分に楽しむことができません。 この戦略、戦術、競争、すべてが素晴らしいです。

また、私のお気に入りのことの 1 つは、csgo ケース 入手 プラットフォームをチェックして、ゲーム内で最高の銃スキンを入手することです。 クールなスキンがゲームをさらにエキサイティングで視覚的に魅力的なものにすることは認めざるを得ません。

ゲームに関するヒントがある場合、またはスキンを探すのが好きな場合は、私に知らせてください。 私たちの経験について話し合い、共有しましょう。
Friontromes1967
 
記事: 2
登録日時: 2024年2月14日(水) 07:02


Return to PSP Hack

オンラインデータ

このフォーラムを閲覧中のユーザー: なし & ゲスト[20人]

cron