Overview
闲来无事,随便下载了个免费小说软件,对其中的登录时的参数进行了分析
Java层分析
抓包定位函数
在登录发送验证码时使用Charles抓包
sendSms_packet
根据关键词在Java层搜索
sendSms_search
查找用例,定位到函数
sendSms_h
可以看到将一些参数存入ArrayMap
中传入j.addSign()
函数中,这里的addSign
是我自己手动修改的名字
传入的Key
值对照数据包中的参数也都能够对的上
sendSms_content
跟进分析
继续跟进addSign
函数中
addSign
又添加了一个时间戳键值对后,经过getSortedParamStr
后传给hash
得到sign
值
getSortedParamStr
将键值对按键进行排序后,转成字符串返回。不同键值对之间用&
连接,键与值之间用=
连接
比如,对如下键值对进行转换
arraymap = {
"flag": "1",
"channelId": "1240202",
"imei": "____7548",
"device": "Nexus 5X",
}
channelId=1240202&device=Nxus 5X&flag=1&imei=___7548
Security
的hash
函数调用了JNISecurity
的hash2
函数,参数有Signature
的SHA1WithRSA
、KeyFactory
的RSA
,字符串转成的字节
Security_hash
JNISecurity
里的hash2
是一个native
函数,可以看到类里加载了UiControl
库,大概率就在libUiControl.so
里
JNISecurity_hash2
So层分析
定位到动态注册函数
解压后在lib
文件夹里找到libUiControl.so
先在Function
搜索hash2
,不出所料没有结果,所以这是动态注册的函数
search1
那么应该分析JNI_OnLoad
函数
由GetEnv
得知参数v26
类型为JNIEnv*
,另外看下面有调用固定常数偏移函数大概率都是JNIEnv*
参数,重设类型后就能分析出JNI
函数了
往下分析,在sub_7A5A8
函数中找到了调用了RegisterNatives
函数,可能是我们要找的hash
函数
sub_7A5A8
由RegisterNatives
的定义可知,
第一个参数clazz
是注册的函数所在的类
第二个参数methods
是JNINativeMethod
类型,里面包含了函数名、函数签名以及函数指针
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
不过这里的各个参数当前都是空的数据,需要先经过sub_78F54
解密才能得到原本的数据。第二个参数是我们的结果
sub_78F54
函数的逻辑也比较简单,主要运算的部分也就只有中间这一句而已
2F5008
v9
是第一个参数,结合具体值分析可以得到是取三位一组然后转成十进制数,和v12
异或
v12
是字符串"8080"
后面的v8 - (v10 & 0xFFFFFFFC)
其实就相当于v8&3
,只取了最后两位
可以得到一个简单的idapython解密脚本
def decrypt(start,size=0):
if size == 0:
size = get_item_end(start)-start
data = get_bytes(start,size)
key = [56,48]
out = []
for i in range(0,len(data),3):
tmp = data[i:i+3].decode()
if tmp.startswith('\x00'):
return out
t = int(tmp)
tmp = t^key[i//3%2]
out.append(chr(tmp))
s = ''.join(out)
return s
addrs = [0x2F5008,0x2F5084,0x2F5130,0x2F513D,0x2F51EC]
for i in range(len(addrs)):
if i !=len(addrs)-1:
size = addrs[i+1]-addrs[i]
else:
size = 0x10
s = decrypt(addrs[i])
print(hex(addrs[i]),''.join(s))
输出结果发现果然是我们想要找到的hash2
函数
重命名参数后,可以知道sub_877EC
是hash
函数的函数指针
sub_87324
是hash2
函数的函数指针
另外有个坑点,一开始分析的是arm64
的so文件,可以看到反编译的结果不是很好分析,头脑有点迷糊没有对上哪个函数指针对应哪个函数
后面换了32位的so文件才发现反编译效果好的太多了,不仅methods
数组各个参数排列的很整齐,甚至hash2
函数名的符号表都还在:cry:
sub_49B8C
让我想到了之前打的一个比赛中,两个不同架构的so文件,arm
的反编译有问题,反而x86
反编译的结果非常清晰
分析hash2函数
点进来同样发现调用了指针加偏移的函数,估计也是JNI
函数,不过还是跟进分析一下sub_78EEC
函数
sub_78EEC
也是这样的调用函数,惯性的改类型成JNIEnv*
发现得到的是FindClass
函数,显然不太对
sub_78EEC
FindClass
显然得不到JNIEnv*
类型的变量
sub_78EEC_2
交叉引用一下,发现在JNI_OnLoad
里调用了这个变量,才发现原来是JavaVM*
类型
qword_3E2860
改成JavaVM*
就得到了GetEnv
函数
将sub_87324
里的各个env变量修正后,JNI
函数就都能正常显示了,
将其中的几个字符串写在了旁边的注释中
函数逻辑也比较清晰了
sub_87324_2
还原算法
比较常见的JNI层调用Java算法的过程
- 先
FindClass
获取类对象
- 用
NewObjectV
获取类的实例
- 然后
GetMethodID
获取所要调用的method
的ID
- 调用
CallObjectMethodV
或CallVoidMethodV
等函数来调用对应的方法
翻译成Java代码,一个比较常规的签名算法
byte[] key = new byte[]{};
byte[] bArr = str.getBytes(StandardCharsets.UTF_8);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(key);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signature = Signature.getInstance("SHA1WithRSA");
signature.initSign(privateKey);
signature.update(bArr);
byte[] result = signature.sign();
retrun result;
type
是hash2
的第一个参数,传入的是固定的2
,因此这里使用的私钥是off_3BFC50+1=unk_2F5477
off_3BFC50
长度为0x279
hash2
签名后再Base64
一下即为sign
参数值
代码验证
使用python
还原算法后验证,同时实现了发包请求
class DeJian:
def __init__(self, phone):
self.phone = phone
self.logger = self.init_log()
def init_log(self):
# 创建logger实例
logger = logging.getLogger('DeJian')
# 设置日志级别
logger.setLevel(logging.DEBUG)
# 流处理器
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# 日志打印格式
formatter = logging.Formatter('%(message)s')
# 添加格式配置
ch.setFormatter(formatter)
# 添加日志配置
logger.addHandler(ch)
return logger
def req(self, method, url, **kwargs):
if kwargs.get("headers"):
# 如果传递过来的请求有头信息,那么我们就在头信息中做追加
kwargs["headers"].update = {
"content-type": "application/x-www-form-urlencoded",
"Host": "dj.palmestore.com",
"user-agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Nexus 5X Build/OPM1.171019.011)"
}
else:
# 如果传递过来的请求没有header
kwargs["headers"] = {
"content-type": "application/x-www-form-urlencoded",
"Host": "dj.palmestore.com",
"user-agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Nexus 5X Build/OPM1.171019.011)"
}
for k, v in kwargs["data"].items():
kwargs["data"][k] = self.url_encode(v)
kwargs["data"] = self.get_sorted_param_str(kwargs["data"])
self.logger.debug(f"请求的参数为{method},url为{url}, 其他参数为{kwargs}")
r = requests.request(method=method, url=url, **kwargs)
self.logger.info(f"响应内容为{r.json()}")
return r.json()
def url_encode(self, s):
r = ['+', '/', '=']
res = s
for i in r:
res = res.replace(i, '%' + hex(ord(i))[2:].upper())
return res
def sign(self, content):
private_key = b""
pri_key = RSA.importKey(private_key)
signer = PKCS1_v1_5.new(pri_key)
hash_obj = SHA1.new(content.encode())
sig1 = signer.sign(hash_obj)
signature = base64.b64encode(sig1).decode()
res = self.url_encode(signature)
return res
def get_sorted_param_str(self, dic):
content = ''.join([f'{k}={dic[k]}&' for k in sorted(dic)])[:-1]
return content
def sendSms(self):
url = ''
dic = {
"versionId": "20005056",
"device": "Nexus 5X",
"flag": "1",
"imei": "",
"phone": self.phone,
"times": "1",
"sendType": "0",
"channelId": "1240202",
"timestamp": "1669439016670",
}
dic["sign"] = self.sign(self.get_sorted_param_str(dic))
r = self.req('post', url, data=dic)
可以看到计算得到的sign
值和抓包得到的sign
值是一致的
sign_send
同时返回包也是成功
sns_reponses
手机上也是成功收到了短信
同样地该APP在登陆时的sign
参数也是类似的逻辑
login_packet
login_content
将之前的代码添加了登录的功能,实现了从获取验证码到登录的过程
output