每日优鲜mfsig unidbg逆向分析
so层
base64
前面已经分析了Java层的调用,以及unidbg实现,接下来结合unidbg和ida对so层进行逆向。
ida打开libsign.so
,函数窗口搜索Java
,可以看到静态注册的Java_cn_missfresh_wsg_SecurityLib_nativeSign
,进入函数,修改a1
为JNIEnv *a1
emmm,参数个数好像对不上,先不管这个。现在从结果往前倒推,v15
是输出,它由v14
赋值,v14
由v18
或v19
赋值,但是从代码看,这两个好像都没有被作为左值。打开sub_36FC0
,返回后F5
一下,函数更新
所以最终它是由sub_36FC0
的第一个参数v17
赋值的,进去看看
而它绝大部分的参数都被传进了一个函数sub_332F0
,值得点进去看看
然后就看到了几百行的代码,里面由很多类似log的东西,从中看出似乎用了hmac算法,暂时不清楚摘要算法是什么,也看到了msfn
这个熟悉的字符串。
但是几百行代码,调用的函数少说也有上10个了,层层调用,要从哪里开始分析呢。。
这时候就要用unidbg了,先在sub_332F0
下个断点
public MissFresh() {
//...
emulator.attach().addBreakPoint(module.base + 0x332f0+1);
}
结合ida来看,r0
就是存结果的地方,不过现在刚进入函数,结果还没存进去。打印其他寄存器看看
输入blr
,在函数返回的地方下个断点,然后输入c
继续执行,然代码运行到函数返回处,这时候打印一下刚刚r0
的地址的数据
打印图中地址的数据
这个就是我们的结果,那么现在我们要对0x402e7000
这个地址进行跟踪,看看是谁对它进行了写操作
public MissFresh() {
//...
emulator.traceWrite(0x402e4000L, 0x402e4000L+16L);
}
可以看到有2轮写的操作,不过我比较关心第一轮,因为这时候结果已经生成了,第二轮只是简单的移一下位置,在字符串头部添加mfsn
。
那么ida跳转到0x37f76
,它在sub_37F3C
这个函数
多熟悉的代码啊,这一看就是base64
,点击看看aAbcdefghijklmn
自定义的码表,接着下断点看看输入
emulator.attach().addBreakPoint(module.base + 0x37F3C+1);
在CyberChef验证一下
完全没问题,那么接下来就是找base64的输入是怎么来的,从样式来看,前9位是时间戳的前9位,后4位是时间戳的后4位,所以接下来就是找中间的长度为64的输入。
轮换
对sub_37F3C
查看引用,只有一个函数sub_37E5C
,进去看看
我们已经知道v7
就是base64
的输入,而它是由a2
赋值的。继续对sub_37E5C
查找引用,只有一个函数sub_332F0
这时候我们已经从数百行代码里找到了最后生成结果的地方,接下来就是继续往前回溯。
当然,我们也可以根据mfsn
这个字符串推测,v179
是最后存结果的地址,而它又在sub_37E5C
被使用了,进而找到突破口。
总而言之,我们现在要找v116
是怎么生成的。
啥也别说了,看看sub_2F8F6
。
下个断点看看
emulator.attach().addBreakPoint(module.base + 0x2F8F6+1);
运行之后,发现它调用了很多次,哪次才是我想要查看的呢。首先,初始化完成之前的我们肯定不需要查看,初始化之后它被调用了3次,打印输入输出之后发现是第2次。
接下来对0x402a10f0
进行跟踪,看看谁对它进行了写操作。
emulator.traceWrite(0x402a10f0L, 0x402a10f0L+32L);
ida跳转到0x36489
,发现是sub_363DC
函数。
下断点看看输入
emulator.attach().addBreakPoint(module.base + 0x363DC+1);
结合代码可以分析出,v14
就是"9566"
,也就是时间戳的后4位,v17
是"ABCDEFGH"
,从sub_332F0
看出,它是一个定值。
结合代码分析得出,它是对输入做一个轮换,然后得出结果。代码实现来验证一下。
_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())
完全对上了!
protobuf
接下来就是看轮换函数的输入是怎么来的,通过更换时间戳和请求参数,发现输入的前面一段088280800810011a40
和后面一小段3001
是不会变的。但是它到底是什么呢,是完全固定的无意义的值,还是其他什么东西。先继续往前追溯。
前面提到,加密疑似用了hmac算法,当时我们不太清楚用了什么摘要算法,不过现在我们从输入中间那段长度位64的16进制字符串,猜测它用了SHA256
摘要算法。接下来就是验证它是不是用了SHA256
,是不是标准的SHA256
。
从前面已经分析出sub_363DC
的r2
,也就是第3个参数,存着输入。
所以我们往前追溯v163
的调用。
进去看看
这个函数干了什么,我们可以通过后续的log推测一下
似乎是个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 output
if __name__ == '__main__':
data = bytes.fromhex('088280800810011a40314538444143354432354541304643333341333233313246373130453043343734414645303839354332393634453337353237363444464535313738383838423001')
print(decode(data))
反序列化成功,说明使用了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_pb2
data = meiriyouxian_pb2.Data()
data.initNumber = 0x1000002
data.a2 = 1
data.sign = '1E8DAC5D25EA0FC33A32312F710E0C474AFE0895C2964E3752764DFE5178888B'
data.a6 = 1
data2 = data.SerializeToString()
print(data2)
当然偷懒一点的办法也有,就是直接把前面和后面的字符写死,反正最后变动的只有中间的64个字符。
hmac
接下来就是查找疑似HMAC-SHA256
的中间值,继续看看sub_348B4
,下断点看看输入
接下来从sub_348B4
的第5个参数继续往前回溯,
sub_2E5A4
的输出应该是v145
,输入是v179
;sub_37D8C
的输出应该是v179
,输入应该是v202
由于sub_2E5A4
有被多次调用,选择先在sub_37D8C
下个断点看看
emulator.attach().addBreakPoint(module.base + 0x37D8C+1);
输入blr
在函数返回处下断点,输入c
继续执行到函数返回处。查看原r0
的值
所以sub_37D8C
的返回值已经有我们需要的值了,现在要看看它是怎么得出这个结果的。
我们已经知道a2
是它的输入,所以先看看sub_2FB14
看看sub_367F6
进入sub_36558
看看
这些都是SHA256的标志,再看看dword_9E030
妥妥的SHA256
的K值。
现在我们已经有很大把握确定它是HMAC-SHA256
,接下来就是找它的输入,以及它的key
,方便我们验证它是否是标准的实现。
回到主体函数sub_332F0
,继续往前回溯
这个似乎是HMAC
的update
部分,下个断点看看
正好是Java层的请求参数,没有再拼接salt或者其他东西。
继续往前回溯
应该是HMAC
的init
函数,进入看看
看看sub_2FA30
两个熟悉的数字0x36
和0x5C
,它们正是HMAC
的magic number
。
下个断点看看
emulator.attach().addBreakPoint(module.base + 0x2FA30+1);
这个极有可能就是HMAC
的key
,有了key
和输入,在CyberChef上验证一下,不行我们再继续分析。
完全正确!说明是标准实现,接下来就是用代码实现一下整个流程。
总结和代码实现
sign的生成流程如下:
- HMAC-SHA256
- protobuf序列化
- 轮换函数
- 自定义base64
import binascii
import hashlib
import 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 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()
代码仅供把玩。