每日优鲜mfsig unidbg逆向分析
每日优鲜mfsig unidbg逆向分析so层
base64
前面已经分析了Java层的调用,以及unidbg实现,接下来结合unidbg和ida对so层进行逆向。
ida打开libsign.so,函数窗口搜索Java,可以看到静态注册的Java_cn_missfresh_wsg_SecurityLib_nativeSign,进入函数,修改a1为JNIEnv *a1
image-20211225094228289
emmm,参数个数好像对不上,先不管这个。现在从结果往前倒推,v15是输出,它由v14赋值,v14由v18或v19赋值,但是从代码看,这两个好像都没有被作为左值。打开sub_36FC0,返回后F5一下,函数更新
image-20211225094728040
所以最终它是由sub_36FC0的第一个参数v17赋值的,进去看看
image-20211225095448360
而它绝大部分的参数都被传进了一个函数sub_332F0,值得点进去看看
image-20211225095833407
image-20211225095857927
image-20211225095937949
image-20211225095952965
然后就看到了几百行的代码,里面由很多类似log的东西,从中看出似乎用了hmac算法,暂时不清楚摘要算法是什么,也看到了msfn这个熟悉的字符串。
但是几百行代码,调用的函数少说也有上10个了,层层调用,要从哪里开始分析呢。。
这时候就要用unidbg了,先在sub_332F0下个断点
public MissFresh() { //... emulator.attach().addBreakPoint(module.base + 0x332f0+1);}
image-20211225100657360
结合ida来看,r0就是存结果的地方,不过现在刚进入函数,结果还没存进去。打印其他寄存器看看
image-20211225100924463
输入blr,在函数返回的地方下个断点,然后输入c继续执行,然代码运行到函数返回处,这时候打印一下刚刚r0的地址的数据
image-20211225101422775
打印图中地址的数据
image-20211225101454583
这个就是我们的结果,那么现在我们要对0x402e7000这个地址进行跟踪,看看是谁对它进行了写操作
public MissFresh() { //... emulator.traceWrite(0x402e4000L, 0x402e4000L+16L);}
image-20211225101958758
可以看到有2轮写的操作,不过我比较关心第一轮,因为这时候结果已经生成了,第二轮只是简单的移一下位置,在字符串头部添加mfsn。
那么ida跳转到0x37f76,它在sub_37F3C这个函数
image-20211225102254858
多熟悉的代码啊,这一看就是base64,点击看看aAbcdefghijklmn
image-20211225102448422
自定义的码表,接着下断点看看输入
emulator.attach().addBreakPoint(module.base + 0x37F3C+1);
image-20211225102639079
在CyberChef验证一下
image-20211225102933609
完全没问题,那么接下来就是找base64的输入是怎么来的,从样式来看,前9位是时间戳的前9位,后4位是时间戳的后4位,所以接下来就是找中间的长度为64的输入。
轮换
对sub_37F3C查看引用,只有一个函数sub_37E5C,进去看看
image-20211225103334974
image-20211225103529007
我们已经知道v7就是base64的输入,而它是由a2赋值的。继续对sub_37E5C查找引用,只有一个函数sub_332F0
image-20211225103932863
image-20211225104006639
这时候我们已经从数百行代码里找到了最后生成结果的地方,接下来就是继续往前回溯。
当然,我们也可以根据mfsn这个字符串推测,v179是最后存结果的地址,而它又在sub_37E5C被使用了,进而找到突破口。
总而言之,我们现在要找v116是怎么生成的。
image-20211225104757920
啥也别说了,看看sub_2F8F6。
image-20211225104935825
下个断点看看
emulator.attach().addBreakPoint(module.base + 0x2F8F6+1);
运行之后,发现它调用了很多次,哪次才是我想要查看的呢。首先,初始化完成之前的我们肯定不需要查看,初始化之后它被调用了3次,打印输入输出之后发现是第2次。
image-20211225112922991
接下来对0x402a10f0进行跟踪,看看谁对它进行了写操作。
emulator.traceWrite(0x402a10f0L, 0x402a10f0L+32L);
image-20211225113115812
ida跳转到0x36489,发现是sub_363DC函数。
image-20211225113333321
下断点看看输入
emulator.attach().addBreakPoint(module.base + 0x363DC+1);
image-20211225113516594
image-20211225113537658
image-20211225113548433
结合代码可以分析出,v14就是"9566",也就是时间戳的后4位,v17是"ABCDEFGH",从sub_332F0看出,它是一个定值。
image-20211225113957105
结合代码分析得出,它是对输入做一个轮换,然后得出结果。代码实现来验证一下。
_CONST = b'ABCDEFGH'def sub_363DC(data, t2): msg = bytes((data + t2 + _CONST) & 0xff for i in range(len(data))) return msgif __name__ == '__main__': data = bytes.fromhex('088280800810011a40314538444143354432354541304643333341333233313246373130453043343734414645303839354332393634453337353237363444464535313738383838423001') print(sub_363DC(data, b'9566').hex())
image-20211225114607586
完全对上了!
protobuf
接下来就是看轮换函数的输入是怎么来的,通过更换时间戳和请求参数,发现输入的前面一段088280800810011a40和后面一小段3001是不会变的。但是它到底是什么呢,是完全固定的无意义的值,还是其他什么东西。先继续往前追溯。
前面提到,加密疑似用了hmac算法,当时我们不太清楚用了什么摘要算法,不过现在我们从输入中间那段长度位64的16进制字符串,猜测它用了SHA256摘要算法。接下来就是验证它是不是用了SHA256,是不是标准的SHA256。
从前面已经分析出sub_363DC的r2,也就是第3个参数,存着输入。
image-20211225120036326
所以我们往前追溯v163的调用。
image-20211225120149338
进去看看
image-20211225140100656
这个函数干了什么,我们可以通过后续的log推测一下
image-20211225141735844
似乎是个protobuf序列化,那我们尝试把之前得到的结果进行个反序列化看看。
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 outputif __name__ == '__main__': data = bytes.fromhex('088280800810011a40314538444143354432354541304643333341333233313246373130453043343734414645303839354332393634453337353237363444464535313738383838423001') print(decode(data))
image-20211225141936839
反序列化成功,说明使用了protobuf。而16777218等于0x1000002,这个就是so调用初始化函数的时候传入的值。
接下来就是编写proto文件
syntax = "proto3";message Data { int32 initNumber = 1; int32 a2 = 2; string sign = 3; int32 a6 = 6; }
编译生成python文件
protoc meiriyouxian.proto --python_out .
调用验证
import meiriyouxian_pb2data = meiriyouxian_pb2.Data()data.initNumber = 0x1000002data.a2 = 1data.sign = '1E8DAC5D25EA0FC33A32312F710E0C474AFE0895C2964E3752764DFE5178888B'data.a6 = 1data2 = data.SerializeToString()print(data2)
image-20211225143257895
当然偷懒一点的办法也有,就是直接把前面和后面的字符写死,反正最后变动的只有中间的64个字符。
hmac
接下来就是查找疑似HMAC-SHA256的中间值,继续看看sub_348B4,下断点看看输入
image-20211225143933889
image-20211225143945200
接下来从sub_348B4的第5个参数继续往前回溯,
image-20211225144315920
image-20211225144340705
sub_2E5A4的输出应该是v145,输入是v179;sub_37D8C的输出应该是v179,输入应该是v202
由于sub_2E5A4有被多次调用,选择先在sub_37D8C下个断点看看
emulator.attach().addBreakPoint(module.base + 0x37D8C+1);
image-20211225145659606
image-20211225145728122
输入blr在函数返回处下断点,输入c继续执行到函数返回处。查看原r0的值
image-20211225145851418
image-20211225145907681
所以sub_37D8C的返回值已经有我们需要的值了,现在要看看它是怎么得出这个结果的。
image-20211225151921347
我们已经知道a2是它的输入,所以先看看sub_2FB14
image-20211225152413418
看看sub_367F6
image-20211225153355859
进入sub_36558看看
image-20211225153448580
image-20211225153511163
这些都是SHA256的标志,再看看dword_9E030
image-20211225153602667
妥妥的SHA256的K值。
现在我们已经有很大把握确定它是HMAC-SHA256,接下来就是找它的输入,以及它的key,方便我们验证它是否是标准的实现。
回到主体函数sub_332F0,继续往前回溯
image-20211225154019122
这个似乎是HMAC的update部分,下个断点看看
image-20211225154159843
image-20211225154251091
image-20211225154308043
正好是Java层的请求参数,没有再拼接salt或者其他东西。
继续往前回溯
image-20211225154423116
应该是HMAC的init函数,进入看看
image-20211225154536612
看看sub_2FA30
image-20211225154623219
两个熟悉的数字0x36和0x5C,它们正是HMAC的magic number。
下个断点看看
emulator.attach().addBreakPoint(module.base + 0x2FA30+1);
image-20211225154939099
image-20211225155010828
这个极有可能就是HMAC的key,有了key和输入,在CyberChef上验证一下,不行我们再继续分析。
image-20211225155305021
完全正确!说明是标准实现,接下来就是用代码实现一下整个流程。
总结和代码实现
sign的生成流程如下:
HMAC-SHA256protobuf序列化轮换函数自定义base64
import binasciiimport hashlibimport hmac# 请自行生成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 msgdef b64encode(data): """ sub_37F3C """ msg = binascii.b2a_base64(data, newline=False) msg = msg.translate(_TRANS) msg = msg.decode().rstrip('=') return msgdef sub_363DC(data, t2): msg = bytes((data + t2 + _CONST) & 0xff for i in range(len(data))) return msgdef 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 data4 = sub_363DC(data3, ts2) data5 = ts1 + data4 + ts2 sign = 'mfsn' + b64encode(data5) return signdef 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()
image-20211225160432289
代码仅供把玩。
页:
[1]