【Writeups】Flare-on 9

01 - Flaredle

1
2
3
4
5
6
const CORRECT_GUESS = 57;
let rightGuessString = WORDS[CORRECT_GUESS];
if (guessString === rightGuessString) {
let flag = rightGuessString + '@flare-on.com';
...
}

flag 就是 WORDS[57]

02 - Pixel Poker

1
2
3
4
if (x == 0x52414c46 % 741 && y == 0x6e4f2d45 % 641) {
# print flag
...
}

點選 (95, 313) 這個格子就會噴出 flag 如下

03 - Magic 8 Ball

LLURULDUL 方向鍵
然後在下面輸入 gimme flag pls? 就會噴出 flag 如下

04 - darn_mice

1
2
3
4
5
for (int i = 0; i < 10; i++) {
void *ptr = malloc(0x1000);
*ptr = payload[i] + input[i];
(void(*)())(ptr)();
}

每個 byte 都會被當成 1-byte shellcode 呼叫,只有全部都是 0xc3 也就是 ret 才不會 crash

flag 是 i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com

05 - T8

這題給了一隻 PE 執行檔和一包 pcap

程式一開始會有個 anti-debug,判斷時間是滿月的時候才能執行
他有一個函式是在計算月亮的週期 OwO

pcap 裡面有兩個 requests 兩個 response
總之動態追一下可以發現他計算 request 內容的邏輯
基本上他就是用 RC4 加密,他產的 key 是 FO9 加上 0 到 65535 之間隨機一個數字,比如 FO91234,做 md5 的結果
所以爆搜一下就可以解出封包的內容,爆搜程式碼如下,要注意 wide string 的轉換 O_O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import hashlib
from base64 import *
from Crypto.Cipher import ARC4

def to_wide(x):
return b''.join([bytes([i, 0]) for i in x])

def check_wide(x):
for i, j in enumerate(x):
if i % 2 == 1 and j != 0:
return False
return True

def gen_key(x):
return to_wide(hashlib.md5(to_wide(x)).hexdigest().encode())

def rc4(key, m):
return ARC4.new(key).encrypt(m)

enc_req = b64decode("ydN8BXq16RE=")
enc_res = b64decode("TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg==")

for i in range(256 * 256):
key = gen_key(b'FO9' + str(i).encode())
dec_req = rc4(key, enc_req)
if check_wide(dec_req):
dec_res = rc4(key, enc_res)
break

print(dec_req)
print(dec_res)

然後解出第一個封包的內容之後,裡面的內容是用 , 分開的如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
\xe5\x07\t\x00\x03\x00\x0f\x00\r\x00%\x00\x03\x00b\x02
\x00\xdc\x07\n\x00\x06\x00\r\x00\r\x00%\x00\t\x00*\x03
\x00\xe1\x07\x0c\x00\x04\x00\x07\x00\r\x00%\x00$\x00\xe5\x00
\x00\xe0\x07\x05\x00\x05\x00\x06\x00\r\x00%\x00\x0b\x00&\x00
\x00\xe2\x07\n\x00\x01\x00\x08\x00\r\x00%\x00\x1f\x00E\x03
\x00\xe6\x07\x03\x00\x02\x00\x01\x00\r\x00%\x002\x00\xda\x00
\x00\xde\x07\x07\x00\x02\x00\x16\x00\r\x00%\x006\x00\xd1\x02
\x00\xde\x07\x05\x00\x03\x00\x0e\x00\r\x00%\x00\x01\x00\xe8\x00
\x00\xda\x07\x04\x00\x01\x00\x05\x00\r\x00%\x00:\x00\x0b\x00
\x00\xdd\x07\n\x00\x04\x00\x03\x00\r\x00%\x00\x16\x00\x16\x03
\x00\xde\x07\x01\x00\x02\x00\x0e\x00\r\x00%\x00\x10\x00\xc9\x00
\x00\xdc\x07\x0c\x00\x01\x00\n\x00\r\x00%\x000\x00\x0c\x02
\x00\xe6\x07\x02\x00\x01\x00\x1c\x00\r\x00%\x00"\x00K\x01
\x00\xe6\x07\t\x00\x05\x00\t\x00\r\x00%\x00!\x00m\x01

每行都是代表某年某月某日,然後算一下是陰曆幾號,再把那個數字對到 a-z0-9 就可以組出 flag 了,大概吧 O3O?
反正後來我覺得好麻煩,就直接把解出來的封包內容倒回去 debugger,flag 就掉出來了 O_O

flag 是 i_s33_you_m00n@flare-on.com

看 code 點我

06 - à la mode

這題乍看之下是 .NET 程式
但用 dnspy 翻了一下發現沒什麼東西
在逛 PEBear 的時候就看到了 .NET Header 裡面的 Flags 寫了 Native EntryPoint

咦?難道這是隻普通的 dll
直接抄起我的 IDA Pro 和 x64dbg
咻咻咻,一陣亂跳,看到了一些 pipe 的東西,再多翻一下就翻到某個地方長得很像是在 decode flag 的地方,還有出現 MyV0ic3 字串
難道是要 create pipe 然後傳這個字串,好像很麻煩,我直接手起刀落把 cmp 的另一個變數一樣改成 MyV0ic3,再按了幾下 step over,登登,flag 就出現在 memory 了

flag 是 M1x3d_M0dE_4_l1f3@flare-on.com

p.s. 解這題的時候剛好要出門吃飯,原本想說只是先看一下題目,沒想到 flag 就自己掉出來了xD

07 - anode

這隻程式是用 node.js 寫的,是用 nexe 這個東西打包的
用文字編輯器打開看一下就會發現檔案最後面有 js 原始碼

js 原始碼裡面有一大堆的 switch case
主要的邏輯就是把你的輸入 -> 做一連串的 add, sub, xor 的操作 -> 檢查跟某個值是不是一樣的,是的話你的輸入就是 flag

我們可以直接去改最後的 js 原始碼,只要讓整個檔案的大小不變就不會噴 error,他執行的時候應該是直接抓一個固定的 offset?
加了一行 require("a.js"),這樣我就可以直接跑我自己的 js
簡單測試了一下,發現 node.js 被改過了,random 的輸出不 random,其他一些運算也被改過
正常來說是要比對一下原版的 node.js 和這個改過的 node.js 差在哪裡
但是我覺得好麻煩,我直接用 visual studio code 的超強 replace 功能把那一大串的 add 換成 sub,sub 換成 add,然後當做字串推到一個陣列
接著把陣列倒過來,拿去 eval 就可以 flag 了 OwO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...

/* 原本
case 1071664271:
if (Math.random() < 0.5) {
b[17] += b[0] + b[35] + b[12] + b[42] + b[14] + b[3] + 8;
b[17] &= 0xFF;
} else {
b[18] ^= (b[20] + b[23] + b[6] + b[12] + b[4] + b[25] + Math.floor(Math.random() * 256)) & 0xFF;
}
state = 175099911;
continue;
*/

// 處理過
case 1071664271:
if (Math.random() < 0.5) {
commands.push(`b[17] -= b[0] + b[35] + b[12] + b[42] + b[14] + b[3] + 8`);
commands.push(`b[17] &= 0xFF`);
} else {
commands.push(`b[18] ^= (b[20] + b[23] + b[6] + b[12] + b[4] + b[25] + ${Math.floor(Math.random() * 256))} & 0xFF`);
}
state = 175099911;
continue;

...

var b = [106, 196, 106, 178, 174, 102, 31, 91, 66, 255, 86, 196, 74, 139, 219, 166, 106, 4, 211, 68, 227, 72, 156, 38, 239, 153, 223, 225, 73, 171, 51, 4, 234, 50, 207, 82, 18, 111, 180, 212, 81, 189, 73, 76];
commands.reverse().forEach(command => eval(command));
console.log(b)

flag 是 n0t_ju5t_A_j4vaSCriP7_ch4l1eng3@flare-on.com

p.s. 雖然 flag 說 not just a javascript challenge,但是我完全把他當作 javascript 來解了,抱歉了xD

08 - backdoor

這題被大家說是全部裡面最難的,我覺得是蠻麻煩的,找不到地方偷吃步,或是其實有但我不知道xD (可以偷偷告訴我 O_O)

這隻是 .NET 的程式,dnspy 打開會看到一堆的 try except
一開始跑下去都會進到 except 裡面
因為他是在 except 裡面去建出一個 DynamicMethod,再把正確的 IL code 塞進去跑
但如果你直接把動態跑的時候看到的那段 IL code 抓下來塞回去檔案裡面,會發現還是錯的
因為他有一段是在做 dynamicILInfo.GetTokenFor,是在把外面世界的 metadata token 換成 dynamicILInfo 世界中的 metadata token,所以不能直接把那個 IL 複製出來,要複製再早一點還沒有換掉的,然後把正確的 metadata token 放上去
這邊我因為沒有找到好方法 (對 .NET 不熟 QQ),所以是用 dyspy 動態跑,手動複製出 IL code 和 metadata token,然後寫個 python script 去改原本的執行檔,最後可以正常的 decompile 所有函式

接著看到了一些 powershell command $(ping -n 1 10.10.21.201 | findstr /i ttl) -eq $null; 拿去 google 了一下就找到了這篇 APT34 targets Jordan Government using new Saitama backdoor
長得一模一樣,這題就是從這隻惡意程式改的

其中在處理不同的 task 的部分有發現一些在做比較字串的程式碼
感覺跟 flag 有關,就仔細看了一下,發現他是要依照某個順序把每一個 task 都跑過一次,就會去印 flag
他印 flag 的地方是從 PE 的 5aeb2b97 這個 section 抓資料出來,然後拿去解,中間還會跟 stacktrace 的字串攪在一起,十分的噁心
後來受不了,沒有可以偷吃步的地方,還是乖乖地寫了一個 dns server 跟程式互動,按照步驟去發 task id,執行完之後就噴出 flag 了

flag 是 W3_4re_Kn0wn_f0r_b31ng_Dyn4m1c@flare-on.com

看 code 點我

09 - encryptor

這隻是一個 Ransomware,標準的用 Symmetric Encryption 做加密,再用 Asymmetric Encryption 加密 Symmetric Encryption 的 key
很快就可以看出他有做 chacha20
但是一直找不到他用什麼 Asymmetric Encryption,一直在想是什麼複雜的加密演算法,一開始懷疑是 ECC,後來又再猜 NTRU
我看那個迴圈感覺很像是在做快速冪,在想是哪個演算法會做快速冪,不會是 RSA 吧他寫的那麼複雜,到底是哪個,等等難道真的是 RSA
後來測試一下,發現真的是 RSA,因為他在做大數的運算所以看起來很複雜,那些函式就只是在做加法乘法模運算而已 Orz

反正他就是把檔案做 chacha20 加密,加密用的 key 再用 RSA 做加密
RSA 加密之前先用 e=0x10001 去算出對應的 d,然後加密是直接 m^d % n
等於我們直接用 c^e % n 去解密就好

flag 是 R$A_$16n1n6_15_0pp0$17e_0f_3ncryp710n@flare-on.com

10 - Nur geträumt

這題是 m68k 的程式,非常古老的東西,要古董 Mac 電腦才跑得起來
不過我們只要照著提示去用 Mini vMac 就可以跑起來了
稍微互動一下就可以發現它其實就是在做 xor encryption,就是會把你輸入的 key 重複貼上直到跟被加密的內容等長,然後做 xor

所以重點就是,要輸入什麼 key?
已知最後面一定是 @flare-on.com 所以可以先解出 key 的最後 13 bytes 是 du etwas Zei
然後搭配他在程式裡面塞的一堆提示,就可以找到這首歌 NENA | 99 Luftballons [1983] [Offizielles HD Musikvideo]
google 一下他的歌詞就會發現第一句歌詞就是 key …
有幾個不是英文字母的在裡面,不過 flag 都是英文,猜一下也還好

flag 是 Dann_singe_ich_ein_Lied_fur_dich@flare-on.com

p.s. 這題最麻煩的步驟是把 encrypted flag 複製出來…

11 - The challenge that shall not be named.

這題是用 python 寫的,用 pyinstaller 打包成 exe
所以起手先用 pyinstxtractor 解出原始的 pyc (版本是 3.7.0)
然後再用 python-decompile3 解回原本的 python source code
接著就會發現他有用 pyarmor 做混淆

直接用 python 去跑解出來的 .py 檔案,會發現它噴了一個錯誤,而且有 stacktrace,位置是在本地的 crypt 函式庫
裡面有用到 linux 平台才有的東西,有點古怪
我就直接去改那個 crypt.py,把 import _crypt 註解掉,然後就會發現他去呼叫 crypt 的時候找不到 _crypt,因為我們沒有 import 他
可以直接讓他 return None,接著就又看到他去呼叫 ARC4,一樣直接改他

1
2
3
4
5
6
7
def ARC4(x):
print('arc4', x)
class A:
def encrypt(self, y):
print('arc4.encrypt', y)
return b''
return A()

然後 flag 就被我們印出來了xD

flag 是 Pyth0n_Prot3ction_tuRn3d_Up_t0_11@flare-on.com

Read more

【CTF Writeups】VolgaCTF Quals 2020 - F-Hash

這題給了一個 x86-64 ELF Executable,直接跑下去跑不出來,一直卡在那裡,逆向一下會發現 13B0 這個函式是一個遞迴函式,他的虛擬碼大概長下面這樣,會一直遞迴呼叫前兩層的答案,很明顯的有很多重複的子問題,這時候就是要用 Dynamic Programming 的思路來把算過的答案記下來就不會跑那麼久了,所以這題就是要優化這個函式,把程式跑完就會印出 flag。

1
2
3
4
5
def _13B0(depth, a, b):
...
r1 = _13B0(depth - 1, a, b)
r2 = _13B0(depth - 2, a, b)
...

以下提供三種解法,讀者可以跟著練習一下。

Rewrite Function with Python

最直覺的方法就是把 IDA decompile 出來的 code 搬到 python 上重寫一下,沒什麼技術,這是我在賽中用的方法,但就是要注意一下型態的問題,比如兩個 unsigned int 相乘可能 overflow 在 python 裡面要 mod (1 << 32),更多細節請看下面的程式碼。

solve-rewrite.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python3

table = [0]
for i in range(26):
table += [i * 10 + 1 + (0xf6 << 120)] * 10

def bitcountsum(a, b):
a %= (1 << 64)
b %= (1 << 64)
return bin(a).count('1') + bin(b).count('1')

def calc(a, b, depth = 256):
ans = [0]
ans.append((bitcountsum(a, b), 0, 0))
ans.append((bitcountsum(a ^ 1, b), 0, 1))

for i in range(3, depth + 1):
v15, v16 = ans[i - 1], ans[i - 2]
v13 = ((v15[0] + v15[1] * (1 << 64)) + (v16[0] + v16[1] * (1 << 64)) + bitcountsum((v15[2] + v16[2]) ^ a, b)) % (1 << 128)
v14 = table[i]
while True:
if (v14 >> 64) > (v13 >> 64):
break
if (v14 >> 64) == (v13 >> 64):
if v14 % (1 << 64) >= v13 % (1 << 64):
break
k = max(1, (v13 >> 64) // (v14 >> 64))
v13 = (v13 - k * v14) % (1 << 128)
ans.append((v13 % (1 << 64), (v13 >> 64), (v15[2] + v16[2]) % (1 << 64)))
return ans

al = [0x6369757120656854, 0x706d756a20786f66, 0x20797a616c206568, 0]
bl = [0x206e776f7262206b, 0x74207265766f2073, 0x80676f64, 0x2b]

for a, b in zip(al, bl):
print(list(map(hex, calc(a, b)[256])))

GDB

另一個方法是我在賽後看 別人 用的,在 gdb 寫 python 去 hook 13B0 的開頭和結尾,在開頭判斷這組參數有沒有出現過了,跑過就把參數的 depth 設成 1 也就是 base case 讓他不要再往下遞迴了,而因為同一組函式的 Start, End Hook 沒辦法共享資訊,所以需要維護一個 state 來放目前的參數,在結尾的時候一樣是看這組參數有沒有出現過,有就把答案寫上去,沒有就把答案存起來下次就不會再跑一次了。

solve-gdb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import gdb

def register(name):
return int(gdb.parse_and_eval(name))

def read(address, size):
inf = gdb.inferiors()[0]
return inf.read_memory(address, size).tobytes()

def write(address, buf):
inf = gdb.inferiors()[0]
inf.write_memory(address, buf)

memory = {}
state = []

class Start(gdb.Breakpoint):
def __init__(self, location):
super(Start, self).__init__(spec = location, type = gdb.BP_BREAKPOINT, internal = False, temporary = False)
def stop(self):
state.append((register('$rdi'), register('$rsi'), register('$rdx'), register('$rcx')))
if memory.get(state[-1][1:]) is not None:
gdb.execute('set $rsi = 1')

class End(gdb.Breakpoint):
def __init__(self, location):
super(End, self).__init__(spec = location, type = gdb.BP_BREAKPOINT, internal = False, temporary = False)
def stop(self):
global state
buf, h = state[-1][0], state[-1][1:]
if memory.get(h) is None:
memory[h] = (read(buf, 8), read(buf + 8, 8), read(buf + 16, 8))
else:
write(buf, memory[h][0])
write(buf + 8, memory[h][1])
write(buf + 16, memory[h][2])
state = state[:-1]

Start(f'*{0x0000555555554000 + 0x13b0}')
End(f'*{0x0000555555554000 + 0x1424}')

gdb f-hash 之後,在 gdb 裡面執行 source solve-gdb.py 就可以跑上面的程式碼了
或是也可以在執行 gdb 的時候就載入 gdb -x solve-gdb.py f-hash

Frida

這個方法也是我賽後看 別人 用的,frida 真的是好東西,之前剛好有研究一點 frida,第一次用在比賽中,基本上跟前一個解法一樣去 hook 函式的開頭和結尾,不過 frida 又更方便了,請看下面程式碼。

solve-frida.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var base = ptr(Process.enumerateModulesSync()[0].base)
var recursive_func_ptr = base.add(0x13b0)

var mem = {}
Interceptor.attach(recursive_func_ptr, {
onEnter: function (args) {
this.buf = args[0]
this.hash = args[1] + '-' + args[2] + '-' + args[3]
if (mem[this.hash] !== undefined) {
args[1] = ptr(1)
}
},
onLeave: function (retval) {
if (mem[this.hash] === undefined) {
mem[this.hash] = [this.buf.readU64(), this.buf.add(8).readU64(), this.buf.add(16).readU64()]
} else {
this.buf.writeU64(mem[this.hash][0])
this.buf.add(8).writeU64(mem[this.hash][1])
this.buf.add(16).writeU64(mem[this.hash][2])
}
}
})

最後執行 frida --no-pause --runtime=v8 -l solve-frida.js ./f-hash 就可以了

Flag

1
VolgaCTF{16011432ba16efc8dcf779477985b3b9}

  1. https://github.com/OAlienO/CTF/tree/master/2020/VolgaCTF/F-Hash
  2. https://pastebin.com/Dj6wteXk
  3. https://sectt.github.io/writeups/Volga20/f-hash/README
Read more

【安卓逆向】透過 Burp Suite Proxy 夜神模擬器

前言

在上一篇 adb 的環境佈置中,我們是用 Android Emulator,但我想要 reverse 的 app 只有支援 arm ( 蠻多 app 都沒有支援 x86-64 的 ),而在我 x86-64 機器上的 Android Emulator 上開 arm 的虛擬機很慢,所以我就換用了 Nox Player,他同時支援 arm 跟 x86-64 的架構,速度也挺快的。

恩等等,我剛剛才發現原來 Nox Player 是在 VirtualBox 上面開一台虛擬機跑,傻眼。

怎麼知道這個 app 支援什麼

用 apktool 解開 apk 檔後,看 /lib 資料夾下面有哪些資料夾,可能會有 arm64-v8aarmeabi-v7ax86x86_64 等,這些就是這個 app 用到的函式庫,沒有對應架構的函式庫當然就是不支援了,或者有些 app 會將每個架構分開發佈,只要去下載對應架構的 app 就可以了。

adb 怎麼連上夜神模擬器

夜神模擬器預設會把 adb server 開在 port 62001, 62025, 62026, … ( 我不知道為什麽 62001 直接跳到 62025 )
所以 adb connect localhost:62001 就可以啦

Drony

主要是參考這篇 Android Hacking | Setup Global Proxy for All Apps in Android (without root) with Burp Suite
的教學,在使用 Drony 前,我還有用過另一款叫 ProxyDroid,不過沒成功,不知道出了什麼事。

基本流程是這樣的,因為 Drony 本身也是一個 proxy server,所以要先在 Android 的設定中將 proxy 導向到 Drony,然後在 Drony 的設定中將 proxy 導向主機的 Burp Suite。

第一步

打開 Android 的設定,照著下面這樣點
設定 > 無限與網路 > Wi-Fi > 你的 Wi-FI 的名字 ( 長按他 ) > 修改網路 > 顯示進階選項 > Proxy ( 手動 )
主機名稱填 127.0.0.1,通訊埠填 8020 ( Drony 預設的通訊埠 )

第二步

打開 Drony 的設置,照著下面這樣點
設置 > 網路 > 無線網路 > 你的 Wi-FI 的名字
代理類型選手冊 ( 也就是 Manual,真爛的翻譯 ),主機名稱填主機的 ip,通訊埠填 8080 ( Burp Suite 預設的通訊埠 )

怎麼找主機的 ip

主機基本上會是 Nox Player 的 default gateway ( 其實就是在 VirtualBox 的 NAT Mode ),所以下 adb shell ip route show 找到 default gateway 就是主機的 ip 了

完成

這樣就設定好啦,在日誌頁面把開關打開就可以了。

Burp Suite 憑證安裝

順便安裝一下 Burp Suite 的憑證,這樣就不會一直跳憑證問題了
先到 http://burp 下載 Burp Suite 的憑證,載下來是 der 副檔名的話,先把他改名成 cer 副檔名結尾
打開 Android 的設定,照著下面這樣點
設定 > 個人 > 安全性 > 憑證儲存空間 > 從 SD 卡安裝 ( 選 cacert.cer )
安裝的時候他會叫你設定一下 PIN 碼

Read more

【安卓逆向】Frida Hook 動態調試

今天我們要來練習用 frida 在 Android 上做動態調試

逆之呼吸壹之型 - 一般函式

先來寫個簡單的範例 APP
有一個按扭和一個輸入欄,輸入名字之後,按下按鈕,就會顯示 Hello 加上你輸入的名字
不會寫 APP 的小朋友可以先去 youtube 上找教學,有一大堆

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
resultTextView.setText(say(something));
}
});
}

String say (String something) {
return "Hello " + something;
}
}

python 負責呼叫 frida api 做注入,javascript 是被注入進去做事的
我們的目標是 hook 函式 say,並在原本的輸出文字後面加上 !!!

hook.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import frida

def on_message(message, payload):
print(message)

device = frida.get_usb_device()
pid = device.spawn(["com.example.myapplication"])
session = device.attach(pid)

with open("script.js") as f:
script = session.create_script(f.read())
script.on("message", on_message)
script.load()

device.resume(pid)

input()
script.js
1
2
3
4
5
6
7
Java.perform(() => {
main = Java.use("com.example.myapplication.MainActivity")
main.say.implementation = function (something) {
var ret = this.say(something)
return ret + '!!!'
}
})

逆之呼吸貳之型 - 重載函式

改一下範例 APP,多加上一個接受數字做輸入的 say 函式
接收到數字後,就輸出 Hello 加上輸入的數字的平方

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
try {
Integer number = Integer.parseInt(something);
resultTextView.setText(say(number));
} catch (NumberFormatException e) {
resultTextView.setText(say(something));
}
}
});
}

String say (String something) {
return "Hello " + something;
}

String say (Integer number) {
number = number * number;
return "Hello " + number.toString();
}
}

兩個 say 都是一樣的名字,所以 hook 的時候要用 overload 去區分,overload 參數放的是目標函式輸入參數的型態
這次我們在新的 say 函式的輸出文字後面加上 ???
hook.py 跟上一個例子一樣就不再貼一次了

script.js
1
2
3
4
5
6
7
8
9
10
11
Java.perform(() => {
main = Java.use("com.example.myapplication.MainActivity")
main.say.overload("java.lang.String").implementation = function (something) {
var ret = this.say(something)
return ret + '!!!'
}
main.say.overload("java.lang.Integer").implementation = function (number) {
var ret = this.say(number)
return ret + '???'
}
})

逆之呼吸參之型 - 隱藏函式

再改一下範例 APP,多加上一個變數 secret 和一個函式 getSecret,在 onCreate 裡面會給 secret 一個隨機值,我們的目標就是找出這個值

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private int secret;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

secret = (int) (Math.random() * 100);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
try {
Integer number = Integer.parseInt(something);
resultTextView.setText(say(number));
} catch (NumberFormatException e) {
resultTextView.setText(say(something));
}
}
});
}

String say (String something) {
return "Hello " + something;
}


String say (Integer number) {
number = number * number;
return "Hello " + number.toString();
}

int getSecret () {
return secret;
}
}

因為我們的目標不是自己 new 一個物件出來抓 secret,這樣就只是一個我們自己就可以生成的隨機值而已
我們是要找出目前已經存在的那個 instance 的 secret,在實際例子中可能就會是一組隨機生成的密碼之類的
所以我們要用到 Java.choose 去抓 instance,抓到 instance 後,可以

  1. instance.getSecret() 呼叫函式搞定
  2. instance.secret.value 存取變數搞定

然後用 send 可以把資料傳到 python 端的 on_message 做處理

script.js
1
2
3
4
5
6
7
8
9
Java.perform(function () {
Java.choose("com.example.myapplication.MainActivity", {
onMatch: function (instance) {
send(instance.getSecret()) // call function
send(instance.secret.value) // access vairable
},
onComplete: function () {}
})
})

疑難雜症

Q : 有函式 a 跟變數 a 同名怎麼辦 ?
A : a 存取函式,_a 存取變數


  1. https://github.com/hookmaster/frida-all-in-one
  2. How to access class member variable if there’s a member function called the same name?
Read more

【安卓逆向】Android Studio Emulator + ADB 環境佈置

紀錄一下怎麼設定好一台 Android 的虛擬機

  1. 我是用 Android Studio 裡面的 Emulator,所以要先裝一下 Android Studio
  2. 打開 AVD Manager 後點 Create Virtual Device

  1. 選一個 device,比如 Pixel 3a
  2. 選一個 system image,比如 Pie
  3. 完成

ADB ( Android Debug Bridge )

路徑

在 Android SDK 的 platform-tools 裡面,用 macos 的話應該會在 /Users/xxx/Library/Android/sdk/platform-tools/
沒有的話可以去 官網

基本功能

指令 解釋
adb devices 列出所有裝置
adb root 用 root 權限重開 adb 服務
adb shell 互動式的 shell
adb shell "ls" 執行一行指令
adb push ./myfile /data/local/tmp/ 傳檔案進去
adb pull /data/local/tmp/myfile ./ 抓檔案出來
adb reboot 重開機,可以簡單粗暴的驗證有沒有設置成功

如果有多台裝置的話,要加 -s 指定哪一個裝置,比如 adb -s emulator-5554 shell

疑難雜症

Q : 遇到 adbd cannot run as root in production builds 怎麼辦 ?
A : 在選 image 的時候要選 target 是 Google APIs 的

Q : 怎麼卸載 system image ?
A : 打開 SDK Manager ( 在 AVD Manager 旁邊 ),勾選 Show Package Details,就可以看到下載過的 system image,取消勾選再按 OK 就卸載了

Q : 怎麼卸載 app ?
A : 除了麻瓜的方法外,也可以在 adb shell 拿到 shell 之後,用 pm list packages 看有哪些 app,再用 pm uninstall -k com.example.test_app 卸載 app

Q : 怎麼把 apk 抓出來?
A : 先用 pm path com.example.test_app 找出 apk 的路徑,再用 adb pull /data/app/com.example.test_app.apk ./ 抓出來

其他的 Android 虛擬機

如果你只是想玩遊戲的話,可以參考下面幾款模擬器

  1. BlueStacks
  2. NoxPlayer ( 夜神模擬器 )
  3. MemuPlay ( 逍遙模擬器 )
  4. 等等
Read more