Frida Playground
から実戦経験が積めます。
インストール用のIPAも配布されているのでSideloadlyでちゃちゃっとインストールしてしまいましょう。
Challenges
早速簡単な方から解いていきます。
適当にインストールしたせいでBundle IDがわからないので調べます。
$ frida-ps -Ua
PID Name Identifier
---- ---------------------- ------------------------
2069 AppStore com.apple.AppStore
2060 Nintendo Switch Online com.nintendo.znca
3130 Playground eu.nviso.fridaplayground
2033 Search com.apple.Spotlight
1993 Settings com.apple.Preferences
2039 Sileo org.coolstar.SileoStore
2068 palera1nLoader com.samiiau.loader
というわけでeu.nviso.fridaplaygroundという値であることがわかりました。
1.01 Print parameter(int)
ボタンを押すとトリガーとなって秘密の値が整数値で保存されるので、その値を覗き見しましょうという問題。
Hopper Disassemblerで静的解析をするとメソッド名が-[VulnerableVault setSecretInt:]とわかりました。
わかったのでこのメソッドをHookするコードをfrida-traceで生成します。
frida-trace -Uf eu.nviso.fridaplayground -m "-[VulnerableVault setSecretInt:]"
生成されたコードを編集して、
onEnter(log, args, state) {
log(`-[VulnerableVault setSecretInt:${args[2]}]`);
}
とすると、
2735 ms -[VulnerableVault setSecretInt:0x2a]
5580 ms -[VulnerableVault setSecretInt:0x2a]
5862 ms -[VulnerableVault setSecretInt:0x2a]
実行されるたびに0x2aが指定されているのがわかります。よって答えは42となります。
1.02 Print parameter(NSNumber)
frida-trace -Uf eu.nviso.fridaplayground -m "-[VulnerableVault setSecretNumber:]"
を実行してみます。
そして単純に先程と同じようにargs[2]の中身を覗いてみると、
4128 ms -[VulnerableVault setSecretNumber:0xb4982075a0073cf5]
6770 ms -[VulnerableVault setSecretNumber:0xb4982075a0073cf5]
6953 ms -[VulnerableVault setSecretNumber:0xb4982075a0073cf5]
実行ごとに同じ値が出力されるのは先程と同じですが、やけに値が大きいです。
更に、再度起動して実行してみると、
2818 ms -[VulnerableVault setSecretNumber:0x9014183a86167f29]
3614 ms -[VulnerableVault setSecretNumber:0x9014183a86167f29]
3833 ms -[VulnerableVault setSecretNumber:0x9014183a86167f29]
というように値が変わってしまいました。
これは、値ではなくポインタと考えるべきでしょう。今回はNSNumberが入っているとわかっているので、ドキュメントを見てみます。
NSNumberの資料を見るとNSNumberはAn object wrapper for primitive scalar numeric valuesとあるのでオブジェクトであることがわかります。
先頭にNSとついているのは全てObjective-Cなので、
onEnter(log, args, state) {
log(`-[VulnerableVault setSecretNumber:${new ObjC.Object(args[2])}]`);
}
としてObjective-Cのオブジェクトにキャストしてみます。
3031 ms -[VulnerableVault setSecretNumber:42]
3697 ms -[VulnerableVault setSecretNumber:42]
3979 ms -[VulnerableVault setSecretNumber:42]
すると答えが42であることがわかりました。
1.03 Print parameter(NSString)
frida-trace -Uf eu.nviso.fridaplayground -m "-[VulnerableVault setSecretString:]"
として今度は文字列を表示させてみます。
onEnter(log, args, state) {
log(`-[VulnerableVault setSecretString:${args[2]}]`);
}
すると今度は以前の二つと違って押すたびに値が変わってしまいました。
5332 ms -[VulnerableVault setSecretString:0x28327bfc0]
6037 ms -[VulnerableVault setSecretString:0x28320d740]
6250 ms -[VulnerableVault setSecretString:0x283202010]
なぜポインタが変わったのかは謎なのですが、とりあえずNSStringもObjective-Cのオブジェクトなので、
onEnter(log, args, state) {
log(`-[VulnerableVault setSecretString:${new ObjC.Object(args[2])}]`);
}
としてみます。
2835 ms -[VulnerableVault setSecretString:The Answer to Life, The Universe, And Everything]
5466 ms -[VulnerableVault setSecretString:The Answer to Life, The Universe, And Everything]
5651 ms -[VulnerableVault setSecretString:The Answer to Life, The Universe, And Everything]
すると答えはThe Answer to Life, The Universe, And Everythingとわかりました。
1.04 Replace parameter
次はボタンを押すとwinIfTrueというメソッドに常にFalseが返されているものをTrueを返すようにするという問題です。
で、ここまで使ってきたfrida-traceではメソッドの流れを追えるだけで関数の返り値を返すような機能はついていません。
そこで次は別のアプローチを取ります。というか、これまでの三問もその解き方をすることが可能でした。
const VulnerableVault = ObjC.classes.VulnerableVault;
function solve104() {
const method = VulnerableVault['- winIfTrue:'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
console.log(args[2])
},
})
}
試しにこのようなコードを書いて、scripts/vulnerable.jsとします。
これの実行方法は、
$ frida -l scripts/vulnerable.js -Uf eu.nviso.fridaplayground
____
/ _ | Frida 16.1.7 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to iPhone (id=480a9329aa853b4346fd87728802db31d653b0aa)
Spawned `eu.nviso.fridaplayground`. Resuming main thread!
[iPhone::eu.nviso.fridaplayground ]-> solve104()
で、起動したら実行したい関数を実行します。
その後、ボタンを押してみると、
0x0
0x0
0x0
と表示されました。つまり、メソッドの引数として常にFalseが渡されています。
ここに0x1を入れればいいのですが、どうすれば良いでしょうか?
0x1を代入する
function solve104() {
const method = VulnerableVault['- winIfTrue:'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
args[2] = 1;
},
})
}
これは上手くいきません。Error: expected a pointerというエラーが表示されます。
trueを代入する
function solve104() {
const method = VulnerableVault['- winIfTrue:'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
args[2] = true;
},
})
}
これも同様のエラーがでます。args[2]はポインタなのでポインタに値を入れることはできません。
そこでNativePointerを返すptrを利用します。
function ptr(value: string | number): NativePointer
Short-hand for new NativePointer(value).
文字列または数値が入れられるとのことなので代入してみましょう。
ptr(0x1)
function solve104() {
const method = VulnerableVault['- winIfTrue:'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
args[2] = ptr(0x1);
},
})
}
何故かこれだとランタイムエラーはでないのですが、クリアになりませんでした。
理由は検討中
ptr(‘0x1’)
function solve104() {
const method = VulnerableVault['- winIfTrue:'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
args[2] = ptr('0x1');
},
})
}
文字列を入れるとクリアできました。なお、0x0以外の値は何を入れてもTrue扱いなので0x2とかでも通ります。
1.05 Print return value(string)
今度は返り値の文字列を見ろという問題です。
いろいろ解法はあるのですが、一番簡単なのは静的解析です。
-[VulnerableVault getSecretString]:
0000000100005e30 adr x0, #0x100010d08
0000000100005e34 nop
0000000100005e38 ret
メソッドを見ると単に0x100010d08の値を返しているだけであることがわかります。
この場合、答えの文字列はバイナリに書き込まれていて単にそのアドレスを返しているだけであると考えるべきでしょう。
となれば、0x100010d08に何が書き込まれているかを見るだけです。
cfstring_VulnerableVault_g3n3r_t3dStr1ng:
0000000100010d08 dq 0x0000000100024508, 0x00000000000007c8, 0x000000010000d019, 0x000000000000001f ; "VulnerableVault_g3n3r@t3dStr1ng", DATA XREF=-[VulnerableVault getSecretString]
とあるので、答えはVulnerableVault_g3n3r@t3dStr1ngであることがわかりました。
ただ、これではfridaの練習にならないのでそちらでも対応します。
この関数は引数を取らないのでメソッドの最後の:が不要になります。
というか
:って引数があるかどうかを意味していたんですね(今更
frida-trace -Uf eu.nviso.fridaplayground -m "-[VulnerableVault getSecretString]"
として、今回は引数がないのでonEnterをみても意味がないのでonLeaveを変更します。
onLeave(log, retval, state) {
log(`-[VulnerableVault getSecretString] => ${new ObjC.Object(retval)}`);
}
静的解析から返り値がCFStringであるとわかっているので、やはりnew ObjC.Object()でキャストします。
返り値に入ってるx0の値をインスタンスにすれば良いので上のコードになるわけですね。
4281 ms -[VulnerableVault getSecretString] => VulnerableVault_g3n3r@t3dStr1ng
こうして、同じ結果が得られました。
1.06 Replace return value
メソッドが常にFalseを返すのでTrueを返すようにしなさいという問題です。
-[VulnerableVault hasWon]:
0000000100005d4c mov w0, #0x0
0000000100005d50 ret
このメソッドは常に0を返すだけです。
引数がないのでやはりonEnterを弄る意味はなく、返り値を変更するのでonLeaveを調整します。
ptr(‘0x1’)
function solve106() {
const method = VulnerableVault['- hasWon'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
retval = ptr('0x1');
}
})
}
さっきと同じように書き換えればよいかと思うのですが、これは正しく動きません。
ptr(0x1)
function solve106() {
const method = VulnerableVault['- hasWon'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
retval = ptr(0x1);
}
})
}
こちらも同様です。
replace(ptr(0x1))
function solve106() {
const method = VulnerableVault['- hasWon'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
retval.replace(ptr(0x1));
}
})
}
失敗します。
replace(ptr(‘0x1’))
function solve106() {
const method = VulnerableVault['- hasWon'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
retval.replace(ptr('0x1'));
}
})
}
これで正常に返り値を変更できます。
別解について
静的解析から単に0x1を返せばよいのはすぐに分かるので、
00005d4c 20008052
とおきかえるようなコードが書けたらいいと思うのですが、そういうのはできないんでしたっけ?
1.07 Print return value(bytearray)
次はBytearrayを表示せよとの問題です。
BytearrayとはSwiftでいうところの[UInt8]で、暗号化のときなどによく出てきます。アプリ解析において暗号化は避けて通れない道なのでしっかりと勉強します。
まずは静的解析でコードを読んでみます。
-[VulnerableVault getSecretKey]:
0000000100005ee8 sub sp, sp, #0x30defined at 0x10000c8f4 (instance method), DATA XREF=0x10000c8f4
0000000100005eec stp fp, lr, [sp, #0x20]
0000000100005ef0 add fp, sp, #0x20
0000000100005ef4 nop
0000000100005ef8 ldr x8, =___stack_chk_guard
0000000100005efc ldr x8, [x8]
0000000100005f00 stur x8, [fp, var_8]
0000000100005f04 adr x8, #0x10000ca78 ; "$3cr3T8yt34rr4yGsjeb"
0000000100005f08 nop
0000000100005f0c ldr x9, [x8]
0000000100005f10 str x9, [sp, #0x20 + var_18]
0000000100005f14 ldur x8, [x8, #0x7]
0000000100005f18 stur x8, [sp, #0x20 + var_11]
0000000100005f1c nop
0000000100005f20 ldr x0, =_OBJC_CLASS_$_NSData
0000000100005f24 nop
0000000100005f28 ldr x1, =aDatawithbytesl
0000000100005f2c add x2, sp, #0x8
0000000100005f30 mov w3, #0xf
0000000100005f34 bl imp___stubs__objc_msgSend
0000000100005f38 mov fp, fp
0000000100005f3c bl imp___stubs__objc_retainAutoreleasedReturnValue
0000000100005f40 ldur x8, [fp, var_8]
0000000100005f44 nop
0000000100005f48 ldr x9, =___stack_chk_guard
0000000100005f4c ldr x9, [x9]
0000000100005f50 cmp x9, x8
0000000100005f54 b.ne loc_100005f64
0000000100005f58 ldp fp, lr, [sp, #0x20]
0000000100005f5c add sp, sp, #0x30
0000000100005f60 b imp___stubs__objc_autoreleaseReturnValue
よくわからない感じですが、答えは$3cr3T8yt34rr4yGsjebとわかります。
どこにもリターンがなくて変な感じがするのですが、このメソッド自体は指定されたポインタにバッファのポインタか何かを書き込むだけで何も返さないのだともいます。
だからよくわからないimp___stubs__objc_autoreleaseReturnValueもvoidのreturnみたいなものなんじゃないかと思っておきます。
こちらも引数がないのでonEnterをhookする方法は使えません。
onLeave(log, retval, state) {
const object = new ObjC.Object(retval);
log(`-[VulnerableVault getSecretKey] => ${object}`);
log(`-[VulnerableVault getSecretKey] => ${object.bytes()}`);
log(`-[VulnerableVault getSecretKey] => ${object.bytes().readUtf8String(object.length())}`);
}
のように書いてみます。
9572 ms -[VulnerableVault getSecretKey]
9572 ms -[VulnerableVault getSecretKey] => {length = 15, bytes = 0x243363723354387974333472723479}
9572 ms -[VulnerableVault getSecretKey] => 0x281310530
9572 ms -[VulnerableVault getSecretKey] => $3cr3T8yt34rr4y
すると、静的解析から得られた結果と同じ$3cr3T8yt34rr4yが得られました。
1.08 Call function on object
getSelfというメソッドを呼ぶと自分自身が返るのでVulnerableのwin()を呼べば良いということになります。
function solve108() {
const method = VulnerableVault['- getself'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
const object = new ObjC.Object(retval);
object.win();
}
})
}
というわけで特に面白くもないコードになりました。
1.09 Call function with arguments on object
1.08と似た感じなのですが、winIfFrida:and27042を呼べとあります。
これは引数が”Frida”, 27042のときに成功するメソッドなのでgetselfで自身を取得したときにこのメソッドを呼びます。
function solve109() {
const method = VulnerableVault['- getself'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
const object = new ObjC.Object(retval);
object.winIfFrida_and27042_("Frida", 27042);
}
})
}
:は_に置き換えられるらしい。なぜそうなるのかはよくわからない。
1.10 Find HiddenVault instance
function solve110() {
const method = VulnerableVault['- doNothing'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
const HiddenVault = ObjC.classes.HiddenVault;
const object = ObjC.chooseSync(HiddenVault)[0];
object.win();
},
})
}
メソッドの中で無関係のインスタンスを呼ぶこともできます。
VulnerableVaultインスタンスからdoNothingが呼ばれたときにHiddenVaultのインスタンスを取得する感じです。
ただ、なんでこんなコードになるのかは読んでいてもよく分からなかったです。最初のインスタンスを取ってくるのがこの感じなのかなという感じ。
ちなみにobject["- win"]();としても正しい結果が得られます。
1.11 Call secret function of HiddenVault
1.10と似た感じですがHiddenVaultの隠されたメソッドを呼べとあります。
静的解析をすると-[HiddenVault super_secret_function]というのがあるのでこれのことでしょう。
1.09と同じようにインスタンスがあるんだから直接呼んでしまえばいいやと思えば反応しませんでした。
super_secret_function
function solve111() {
const method = VulnerableVault['- doNothing'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
const HiddenVault = ObjC.classes.HiddenVault;
const object = ObjC.chooseSync(HiddenVault)[0];
object.super_secret_function();
},
})
}
[”- HiddenVault super_secret_function”]
こちらだと動きます。何故なんでしょうか。
function solve111() {
const method = VulnerableVault['- doNothing'];
Interceptor.attach(method.implementation, {
onEnter: function (args) {
const HiddenVault = ObjC.classes.HiddenVault;
const object = ObjC.chooseSync(HiddenVault)[0];
object["- super_secret_function"]();
},
})
}
関数名にアンダーバーがついていると
:を置換したときの_と区別がつかないからなのではないかと思い始めてきました。
こちらの方法で呼ぶ方が確実そうな気がします。
1.12 Modify ByteArray
ByteArrayで返ってくる値のうち42より大きい値を42にして返せとあります。
function solve112() {
const method = VulnerableVault['- generateNumbers'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
const object = new ObjC.Object(retval);
}
})
}
これでオブジェクト自体は取ってこれるのですが、中身の値を入れ替えようとするとobjectの中身をすべて見ておきかえる必要があります。
配列の長さの取得
// TypeError: not a function
object.length();
とでて中身が取得できません。というのもこのオブジェクトはただの配列だからです。Objective-Cにはlengthというメソッドがないので取ってこれません。
1.07でlength()で取ってこれたのはByteArrayだったからです。今回のはNSMutableArrayなのでそのメソッドがないというわけです。
NSMutableArray
NSMutableArrayはArrayの継承クラスなのでcountなどが使えます。
よってobject.count()を使いましょう。
配列の中身
object[i]のような感じでインデックスを使ってアクセスしたくなりますがこのコード自体はJavascriptのものなのでデータが取れません。やってもundefinedが返ってくるだけです。
配列の中身を参照したければobject[- objectAtIndex:](i)とすればよいです。
function solve112() {
const method = VulnerableVault['- generateNumbers'];
Interceptor.attach(method.implementation, {
onLeave: function (retval) {
const object = new ObjC.Object(retval);
for(let i = 0; i < object.count(); i++) {
if (object["- objectAtIndex:"](i) >= 42) {
object["- setObject:atIndex:"](42, i);
}
}
}
})
}
多分だけれどforEachみたいな高級なメソッドは使えないです。
まとめ
今回はチュートリアルのBasicの12問について簡単に解説しました。
自分も知らないことがあって勉強になりました。
記事は以上。