每日優(yōu)鮮mfsig unidbg逆向分析
so層
base64
前面已經(jīng)分析了Java層的調(diào)用劳景,以及unidbg實(shí)現(xiàn),接下來(lái)結(jié)合unidbg和ida對(duì)so層進(jìn)行逆向克胳。
ida打開(kāi)libsign.so
类缤,函數(shù)窗口搜索Java
,可以看到靜態(tài)注冊(cè)的Java_cn_missfresh_wsg_SecurityLib_nativeSign
,進(jìn)入函數(shù)晤斩,修改a1
為JNIEnv *a1
emmm焕檬,參數(shù)個(gè)數(shù)好像對(duì)不上,先不管這個(gè)“谋茫現(xiàn)在從結(jié)果往前倒推实愚,v15
是輸出,它由v14
賦值烹俗,v14
由v18
或v19
賦值爆侣,但是從代碼看,這兩個(gè)好像都沒(méi)有被作為左值幢妄。打開(kāi)sub_36FC0
兔仰,返回后F5
一下,函數(shù)更新
所以最終它是由sub_36FC0
的第一個(gè)參數(shù)v17
賦值的蕉鸳,進(jìn)去看看
而它絕大部分的參數(shù)都被傳進(jìn)了一個(gè)函數(shù)sub_332F0
乎赴,值得點(diǎn)進(jìn)去看看
然后就看到了幾百行的代碼,里面由很多類似log的東西潮尝,從中看出似乎用了hmac算法榕吼,暫時(shí)不清楚摘要算法是什么,也看到了msfn
這個(gè)熟悉的字符串勉失。
但是幾百行代碼羹蚣,調(diào)用的函數(shù)少說(shuō)也有上10個(gè)了,層層調(diào)用乱凿,要從哪里開(kāi)始分析呢顽素。。
這時(shí)候就要用unidbg了徒蟆,先在sub_332F0
下個(gè)斷點(diǎn)
public MissFresh() {
//...
emulator.attach().addBreakPoint(module.base + 0x332f0+1);
}
結(jié)合ida來(lái)看胁出,r0
就是存結(jié)果的地方,不過(guò)現(xiàn)在剛進(jìn)入函數(shù)段审,結(jié)果還沒(méi)存進(jìn)去全蝶。打印其他寄存器看看
輸入blr
,在函數(shù)返回的地方下個(gè)斷點(diǎn)寺枉,然后輸入c
繼續(xù)執(zhí)行抑淫,然代碼運(yùn)行到函數(shù)返回處,這時(shí)候打印一下剛剛r0
的地址的數(shù)據(jù)
打印圖中地址的數(shù)據(jù)
這個(gè)就是我們的結(jié)果姥闪,那么現(xiàn)在我們要對(duì)0x402e7000
這個(gè)地址進(jìn)行跟蹤丈冬,看看是誰(shuí)對(duì)它進(jìn)行了寫操作
public MissFresh() {
//...
emulator.traceWrite(0x402e4000L, 0x402e4000L+16L);
}
可以看到有2輪寫的操作,不過(guò)我比較關(guān)心第一輪甘畅,因?yàn)檫@時(shí)候結(jié)果已經(jīng)生成了埂蕊,第二輪只是簡(jiǎn)單的移一下位置往弓,在字符串頭部添加mfsn
。
那么ida跳轉(zhuǎn)到0x37f76
蓄氧,它在sub_37F3C
這個(gè)函數(shù)
多熟悉的代碼啊函似,這一看就是base64
,點(diǎn)擊看看aAbcdefghijklmn
自定義的碼表喉童,接著下斷點(diǎn)看看輸入
emulator.attach().addBreakPoint(module.base + 0x37F3C+1);
在CyberChef驗(yàn)證一下
完全沒(méi)問(wèn)題撇寞,那么接下來(lái)就是找base64的輸入是怎么來(lái)的,從樣式來(lái)看堂氯,前9位是時(shí)間戳的前9位蔑担,后4位是時(shí)間戳的后4位,所以接下來(lái)就是找中間的長(zhǎng)度為64的輸入咽白。
輪換
對(duì)sub_37F3C
查看引用啤握,只有一個(gè)函數(shù)sub_37E5C
,進(jìn)去看看
我們已經(jīng)知道v7
就是base64
的輸入晶框,而它是由a2
賦值的排抬。繼續(xù)對(duì)sub_37E5C
查找引用,只有一個(gè)函數(shù)sub_332F0
這時(shí)候我們已經(jīng)從數(shù)百行代碼里找到了最后生成結(jié)果的地方授段,接下來(lái)就是繼續(xù)往前回溯蹲蒲。
當(dāng)然,我們也可以根據(jù)mfsn
這個(gè)字符串推測(cè)侵贵,v179
是最后存結(jié)果的地址届搁,而它又在sub_37E5C
被使用了,進(jìn)而找到突破口窍育。
總而言之咖祭,我們現(xiàn)在要找v116
是怎么生成的。
啥也別說(shuō)了蔫骂,看看sub_2F8F6
。
下個(gè)斷點(diǎn)看看
emulator.attach().addBreakPoint(module.base + 0x2F8F6+1);
運(yùn)行之后牺汤,發(fā)現(xiàn)它調(diào)用了很多次辽旋,哪次才是我想要查看的呢。首先檐迟,初始化完成之前的我們肯定不需要查看补胚,初始化之后它被調(diào)用了3次,打印輸入輸出之后發(fā)現(xiàn)是第2次追迟。
接下來(lái)對(duì)0x402a10f0
進(jìn)行跟蹤溶其,看看誰(shuí)對(duì)它進(jìn)行了寫操作。
emulator.traceWrite(0x402a10f0L, 0x402a10f0L+32L);
ida跳轉(zhuǎn)到0x36489
敦间,發(fā)現(xiàn)是sub_363DC
函數(shù)瓶逃。
下斷點(diǎn)看看輸入
emulator.attach().addBreakPoint(module.base + 0x363DC+1);
結(jié)合代碼可以分析出束铭,v14
就是"9566"
,也就是時(shí)間戳的后4位厢绝,v17
是"ABCDEFGH"
契沫,從sub_332F0
看出,它是一個(gè)定值昔汉。
結(jié)合代碼分析得出懈万,它是對(duì)輸入做一個(gè)輪換,然后得出結(jié)果靶病。代碼實(shí)現(xiàn)來(lái)驗(yàn)證一下会通。
_CONST = b'ABCDEFGH'
def sub_363DC(data, t2):
msg = bytes((data[i] + t2[i%4] + _CONST[i%8]) & 0xff for i in range(len(data)))
return msg
if __name__ == '__main__':
data = bytes.fromhex('088280800810011a40314538444143354432354541304643333341333233313246373130453043343734414645303839354332393634453337353237363444464535313738383838423001')
print(sub_363DC(data, b'9566').hex())
完全對(duì)上了!
protobuf
接下來(lái)就是看輪換函數(shù)的輸入是怎么來(lái)的娄周,通過(guò)更換時(shí)間戳和請(qǐng)求參數(shù)涕侈,發(fā)現(xiàn)輸入的前面一段088280800810011a40
和后面一小段3001
是不會(huì)變的。但是它到底是什么呢昆咽,是完全固定的無(wú)意義的值驾凶,還是其他什么東西。先繼續(xù)往前追溯掷酗。
前面提到调违,加密疑似用了hmac算法,當(dāng)時(shí)我們不太清楚用了什么摘要算法泻轰,不過(guò)現(xiàn)在我們從輸入中間那段長(zhǎng)度位64的16進(jìn)制字符串技肩,猜測(cè)它用了SHA256
摘要算法。接下來(lái)就是驗(yàn)證它是不是用了SHA256
浮声,是不是標(biāo)準(zhǔn)的SHA256
虚婿。
從前面已經(jīng)分析出sub_363DC
的r2
,也就是第3個(gè)參數(shù)泳挥,存著輸入然痊。
所以我們往前追溯v163
的調(diào)用。
進(jìn)去看看
這個(gè)函數(shù)干了什么屉符,我們可以通過(guò)后續(xù)的log推測(cè)一下
似乎是個(gè)protobuf序列化剧浸,那我們嘗試把之前得到的結(jié)果進(jìn)行個(gè)反序列化看看。
def decode(data):
process = subprocess.Popen(
["protoc", "--decode_raw"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
output = error = None
try:
output, error = process.communicate(data)
output = output.decode()
except OSError:
pass
finally:
if process.poll() != 0:
process.wait()
return output
if __name__ == '__main__':
data = bytes.fromhex('088280800810011a40314538444143354432354541304643333341333233313246373130453043343734414645303839354332393634453337353237363444464535313738383838423001')
print(decode(data))
反序列化成功矗钟,說(shuō)明使用了protobuf唆香。而16777218
等于0x1000002
,這個(gè)就是so調(diào)用初始化函數(shù)的時(shí)候傳入的值吨艇。
接下來(lái)就是編寫proto文件
syntax = "proto3";
message Data {
int32 initNumber = 1;
int32 a2 = 2;
string sign = 3;
int32 a6 = 6;
}
編譯生成python文件
protoc meiriyouxian.proto --python_out .
調(diào)用驗(yàn)證
import meiriyouxian_pb2
data = meiriyouxian_pb2.Data()
data.initNumber = 0x1000002
data.a2 = 1
data.sign = '1E8DAC5D25EA0FC33A32312F710E0C474AFE0895C2964E3752764DFE5178888B'
data.a6 = 1
data2 = data.SerializeToString()
print(data2)
當(dāng)然偷懶一點(diǎn)的辦法也有躬它,就是直接把前面和后面的字符寫死,反正最后變動(dòng)的只有中間的64個(gè)字符东涡。
hmac
接下來(lái)就是查找疑似HMAC-SHA256
的中間值冯吓,繼續(xù)看看sub_348B4
倘待,下斷點(diǎn)看看輸入
接下來(lái)從sub_348B4
的第5個(gè)參數(shù)繼續(xù)往前回溯,
sub_2E5A4
的輸出應(yīng)該是v145
桑谍,輸入是v179
延柠;sub_37D8C
的輸出應(yīng)該是v179
,輸入應(yīng)該是v202
由于sub_2E5A4
有被多次調(diào)用锣披,選擇先在sub_37D8C
下個(gè)斷點(diǎn)看看
emulator.attach().addBreakPoint(module.base + 0x37D8C+1);
輸入blr
在函數(shù)返回處下斷點(diǎn)贞间,輸入c
繼續(xù)執(zhí)行到函數(shù)返回處。查看原r0
的值
所以sub_37D8C
的返回值已經(jīng)有我們需要的值了,現(xiàn)在要看看它是怎么得出這個(gè)結(jié)果的。
我們已經(jīng)知道a2
是它的輸入障贸,所以先看看sub_2FB14
看看sub_367F6
進(jìn)入sub_36558
看看
這些都是SHA256的標(biāo)志,再看看dword_9E030
妥妥的SHA256
的K值峻仇。
現(xiàn)在我們已經(jīng)有很大把握確定它是HMAC-SHA256
,接下來(lái)就是找它的輸入邑商,以及它的key
摄咆,方便我們驗(yàn)證它是否是標(biāo)準(zhǔn)的實(shí)現(xiàn)。
回到主體函數(shù)sub_332F0
人断,繼續(xù)往前回溯
這個(gè)似乎是HMAC
的update
部分吭从,下個(gè)斷點(diǎn)看看
正好是Java層的請(qǐng)求參數(shù),沒(méi)有再拼接salt或者其他東西恶迈。
繼續(xù)往前回溯
應(yīng)該是HMAC
的init
函數(shù)涩金,進(jìn)入看看
看看sub_2FA30
兩個(gè)熟悉的數(shù)字0x36
和0x5C
,它們正是HMAC
的magic number
暇仲。
下個(gè)斷點(diǎn)看看
emulator.attach().addBreakPoint(module.base + 0x2FA30+1);
這個(gè)極有可能就是HMAC
的key
步做,有了key
和輸入,在CyberChef上驗(yàn)證一下奈附,不行我們?cè)倮^續(xù)分析全度。
完全正確!說(shuō)明是標(biāo)準(zhǔn)實(shí)現(xiàn)斥滤,接下來(lái)就是用代碼實(shí)現(xiàn)一下整個(gè)流程将鸵。
總結(jié)和代碼實(shí)現(xiàn)
sign的生成流程如下:
- HMAC-SHA256
- protobuf序列化
- 輪換函數(shù)
- 自定義base64
import binascii
import hashlib
import hmac
# 請(qǐng)自行生成
import meiriyouxian_pb2
_TABLE_RAW = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
_TABLE_MISS = b'abcdefghijklmnopqrstuvwxyz+ZYXWVUTSRQPONMLKJIHGFEDCBA/1234567890'
_TRANS = bytes.maketrans(_TABLE_RAW, _TABLE_MISS)
_TRANS_INV = bytes.maketrans(_TABLE_MISS, _TABLE_RAW)
_HMAC_KEY = b'PwwGKgCqZAc2PPb31TLnnqPNVFAAdq/X'
_CONST = b'ABCDEFGH'
def b64decode(data):
left = len(data) % 4
if left:
data += '=' * (4 - left)
data = data.encode().translate(_TRANS_INV)
msg = binascii.a2b_base64(data)
return msg
def b64encode(data):
"""
sub_37F3C
"""
msg = binascii.b2a_base64(data, newline=False)
msg = msg.translate(_TRANS)
msg = msg.decode().rstrip('=')
return msg
def sub_363DC(data, t2):
msg = bytes((data[i] + t2[i%4] + _CONST[i%8]) & 0xff for i in range(len(data)))
return msg
def calc_sign(params, ts, body='', init=0x1000002):
if isinstance(body, str):
body = body.encode()
if isinstance(ts, str):
ts = ts.encode()
if isinstance(params, (list, tuple, dict)):
if hasattr(params, 'items'):
params = params.items()
params = ''.join(f'{k}{v}' for k, v in sorted(params, reverse=True)).encode()
data = params + body
data2 = hmac.new(_HMAC_KEY, data, hashlib.sha256).hexdigest().upper()
pbuf = meiriyouxian_pb2.Data()
pbuf.initNumber = init
pbuf.a2 = 1
pbuf.sign = data2
pbuf.a6 = 1
data3 = pbuf.SerializeToString()
ts1 = ts[:9]
ts2 = ts[9:]
data4 = sub_363DC(data3, ts2)
data5 = ts1 + data4 + ts2
sign = 'mfsn' + b64encode(data5)
return sign
def test():
ts = b'1640187039566'
query = b'version9.7.0tdkeyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4xLjkiLCJwYWNrYWdlcyI6ImNuLm1pc3NmcmVzaC5hcHBsaWNhdGlvbiomOS43LjAiLCJwcm9maWxlX3RpbWUiOjI4MywiaW50ZXJ2YWxfdGltZSI6MTQ5NjksInRva2VuX2lkIjoiajViSUs1SmV1bUxzZUVWMVptb3ZxNHNzT0J4OXBCUlJsNk9kbzRlQ01iemZWNWNlUmswSjZYK2lLWE4rVkdJQ3N5S1V0MFByS1lHSE5tMm5iSlZIOHc9PSJ9source_device_id359906070748939sessionandroid0.95648171296963921640187024465screen_width1440screen_height2560realVersionplatformandroidisShow0imeifbc64376480ee60e43e933dae0258d3fdevtka3JZZ1NRVzNZWW9ZMERIVDFvQmJ6MytoVkxsQWJuV1RnLzV2MnpXYVZGUTFqN09zUFIzeFd6WWo3dkNsb0J4MEY2Q1FGeTZhZXpQaA0KdFlZWXAzQkxicmxiTm5rejE0SEt5UE84UVpWOXdWRUxJem0rd0ZiV2QzVks4cFphMmphQWJFYmJrK3dFQXRCL1N6eEtXNmp3eHc9PQ==device_id359906070748939currentLng113.97177currentLat22.540642android_id581e0c22a2843d73android_channel_valuept-lingdu002access_tokenSM_Device_ID2021120821500831e6fbaf8244fa2c94916c1cfe02a8a701cd5c98e2bbb3dc'
sign = calc_sign(query, ts)
print(sign)
assert sign == 'mfsnmtyAmde3nBaBUFN49M+lVLS5Kl5CEJBaI65LJJ90K7pbJ+K5JZcGJJdaJKKKE5FaIJgJGIddK6w2J6KJI6sFEJgDJkGDHk0bDl9IKJg1I6w1FkX5otu1nU'
if __name__ == '__main__':
test()
代碼僅供把玩。