市面上很多小说阅读软件,这些小说软件美其名曰免费,实际上翻两页就是一大段广告,十分影响阅读体验,本篇文章通过数字猫小说软件入手,对其进行去广告以及相关算法分析。
软件没加壳,直接用JEB载入,通过搜索“AD”之类定位广告:
相关函数符号还没混淆:
isVideoRewardExpire判断看广告视频的阅读奖励是否过期,这里直接返回false。

还有一处也是类似的逻辑:

用apklab直接找到相关函数修改之

修改结果如下:

为了方便抓包,移除SSL证书绑定:

经过测试,APK还有签名校验,在so里面,直接把2该为1即可过掉该校验:

重打包,可以看到,广告已经去掉了:

光是去广告就完了吗?对于20年的书虫来说还不够,有时我想在kindle上阅读该小说,所以我决定做一个简陋的下载器。
有两个包十分关键,第一个包获取章节信息,包括章节名称和id等。可以看到请求都有签名sign字段。

第二个包是获取小说下载地址

下载过来是一个压缩包,打开发现里面都是以章节id命名的txt文件,很明显还加密过了:

思路很清晰了,先搞定sign算法,再搞定小说书籍解密算法。
先通过JEB定位sign算法的地方,发现这个函数最终还是调用了so里面的函数:

先用frida hook该函数,打印下日志,看看传参是啥,脚本如下:
很明显,传入的是HTTP的get或者post参数,参数key按升序排列。

为了分析sign算法,定位到了so里面的处理逻辑,把key append到字符串结尾,再求md5

这里我直接在memcpy打个断点,用IDA连上去查看key,当然也可以继续用frida hook。

好的,现在已经成功获取到sign的key,直接写出相关sign算法如下:
搞定了sign算法之后,开始分析小说数据的解密算法。
解密算法也在so里面,但是回调了java层里面的AES解密算法

java算法如下,把数据base64解密后,取前16个字节作为IV,传入的key直接作为AES-CBC的解密密钥进行解密:

这里继续用frida hook:
通过日志直接获取aes解密密钥,经过测试,不同的小说解密密钥都是一样的:

解密算法如下:
下载器源码如下,关键地方已经和谐处理:
效果如下:

通过以上分析,该软件基本的加固都没有,所以能很容易定位关键点,分析逻辑,而且在本人分析时候发现该软件阅读时间不够还能直接领取金币奖励,当然这是后话了。
好了就到这里了,我去看歪嘴龙王了

Java.perform(function () {
var Encryption
=
Java.use(
'com.qimao.qmsdk.tools.encryption.Encryption'
);
Encryption.sign.implementation
=
function () {
var s
=
arguments[
0
];
var r
=
this.sign(s)
send(
'sign: '
+
s
+
' result: '
+
r);
return
r;
}
});
Java.perform(function () {
var Encryption
=
Java.use(
'com.qimao.qmsdk.tools.encryption.Encryption'
);
Encryption.sign.implementation
=
function () {
var s
=
arguments[
0
];
var r
=
this.sign(s)
send(
'sign: '
+
s
+
' result: '
+
r);
return
r;
}
});
def
getsign(param_dict):
sign_key
=
'd3dGiJ**********'
before_sign
=
'';
for
key
in
sorted
(param_dict):
before_sign
+
=
f
'{key}={str(param_dict[key])}'
before_sign
+
=
sign_key
sign
=
MD5.new(before_sign.encode(
'utf8'
)).hexdigest()
return
sign
def
getsign(param_dict):
sign_key
=
'd3dGiJ**********'
before_sign
=
'';
for
key
in
sorted
(param_dict):
before_sign
+
=
f
'{key}={str(param_dict[key])}'
before_sign
+
=
sign_key
sign
=
MD5.new(before_sign.encode(
'utf8'
)).hexdigest()
return
sign
Java.perform(function () {
var AESManager
=
Java.use(
'com.km.encryption.aes.AESManager'
);
AESManager.decrypt.overload(
'java.lang.String'
,
'java.lang.String'
).implementation
=
function () {
var s
=
arguments[
0
];
var k
=
arguments[
1
];
var r
=
this.decrypt(s,k)
send(
'decrpt book: '
+
s
+
' key: '
+
k);
return
r;
}
});
Java.perform(function () {
var AESManager
=
Java.use(
'com.km.encryption.aes.AESManager'
);
AESManager.decrypt.overload(
'java.lang.String'
,
'java.lang.String'
).implementation
=
function () {
var s
=
arguments[
0
];
var k
=
arguments[
1
];
var r
=
this.decrypt(s,k)
send(
'decrpt book: '
+
s
+
' key: '
+
k);
return
r;
}
});
def
decrypt_chapter(s):
aes_key
=
b
'242ccb**********'
iv_enc_data
=
base64.b64decode(s)
aes_iv
=
iv_enc_data[
0
:
16
]
enc_data
=
iv_enc_data[
16
:]
unpad
=
lambda
s: s[:
-
ord
(s[
len
(s)
-
1
:])]
aes
=
AES.new(aes_key,AES.MODE_CBC,aes_iv)
return
unpad(aes.decrypt(enc_data))
def
decrypt_chapter(s):
aes_key
=
b
'242ccb**********'
iv_enc_data
=
base64.b64decode(s)
aes_iv
=
iv_enc_data[
0
:
16
]
enc_data
=
iv_enc_data[
16
:]
unpad
=
lambda
s: s[:
-
ord
(s[
len
(s)
-
1
:])]
aes
=
AES.new(aes_key,AES.MODE_CBC,aes_iv)
return
unpad(aes.decrypt(enc_data))
import
requests
import
base64
import
string
import
zipfile
import
time
import
json
import
io
from
Crypto.
Hash
import
MD5
from
Crypto.Cipher
import
AES
def
getsign(param_dict):
sign_key
=
'd3dGiJ**********'
before_sign
=
'';
for
key
in
sorted
(param_dict):
before_sign
+
=
f
'{key}={str(param_dict[key])}'
before_sign
+
=
sign_key
sign
=
MD5.new(before_sign.encode(
'utf8'
)).hexdigest()
return
sign
def
decrypt_chapter(s):
aes_key
=
b
'242ccb**********'
iv_enc_data
=
base64.b64decode(s)
aes_iv
=
iv_enc_data[
0
:
16
]
enc_data
=
iv_enc_data[
16
:]
unpad
=
lambda
s: s[:
-
ord
(s[
len
(s)
-
1
:])]
aes
=
AES.new(aes_key,AES.MODE_CBC,aes_iv)
return
unpad(aes.decrypt(enc_data))
def
qm_get(url,params,headers
=
None
):
if
not
headers:
headers
=
{
'app-version'
:
'50810'
,
'platform'
:
'android'
,
'reg'
:
'0'
,
'AUTHORIZATION'
:'',
'application-id'
:
'com.****.reader'
,
'net-env'
:
'1'
,
'channel'
:
'unknown'
,
'qm-params'
:'',
}
headers.update({
"sign"
: getsign(headers)})
params.update({
"sign"
: getsign(params)})
res
=
requests.get(url,params
=
params,headers
=
headers,verify
=
False
)
return
str
(res.text)
def
get_book_download_url(bookid):
download_url
=
'https://api-bc.****.com/api/v1/book/download'
params
=
{
'source'
:
'1'
,
'type'
:
'1'
,
'id'
:bookid,
}
book_dl_info
=
json.loads(qm_get(download_url,params))
return
book_dl_info[
'data'
][
'link'
]
def
get_chapter_list(bookid):
chapter_url
=
'https://api-ks.****.com/api/v1/chapter/chapter-list'
params
=
{
'is_all_update'
:
'0'
,
'id'
:bookid,
}
chapter_list
=
json.loads(qm_get(chapter_url,params))
return
chapter_list[
'data'
][
'chapter_lists'
]
def
get_book_data(link):
res
=
requests.get(link,verify
=
False
)
return
res.content
if
__name__
=
=
"__main__"
:
logo
=
print
(logo)
book_id
=
198241
with
open
(
str
(book_id)
+
'.txt'
,
'wb+'
) as fd:
download_link
=
get_book_download_url(book_id)
chapter_list
=
get_chapter_list(book_id)
chapter_list.sort(key
=
lambda
x:x[
'chapter_sort'
])
book_data
=
get_book_data(download_link)
book_file
=
io.BytesIO(book_data)
with zipfile.ZipFile(book_file) as zip_ref:
for
chapter_info
in
chapter_list:
chapter_id
=
chapter_info[
'id'
]
with zip_ref.
open
(chapter_id
+
'.txt'
,
"r"
) as fo:
data
=
fo.read()
chapter_data
=
decrypt_chapter(data)
print
(chapter_data)
fd.write(chapter_info[
'title'
].encode(
'utf8'
)
+
b
'\r\n'
+
chapter_data)
print
(
'------------------------------------------------------------------------------------------------'
)
print
(
'[+]解密完成!!!!!'
)
import
requests
import
base64
import
string
import
zipfile
import
time
import
json
import
io
from
Crypto.
Hash
import
MD5
from
Crypto.Cipher
import
AES
def
getsign(param_dict):
sign_key
=
'd3dGiJ**********'
before_sign
=
'';
for
key
in
sorted
(param_dict):
before_sign
+
=
f
'{key}={str(param_dict[key])}'
before_sign
+
=
sign_key
sign
=
MD5.new(before_sign.encode(
'utf8'
)).hexdigest()
return
sign
def
decrypt_chapter(s):
aes_key
=
b
'242ccb**********'
iv_enc_data
=
base64.b64decode(s)
aes_iv
=
iv_enc_data[
0
:
16
]
enc_data
=
iv_enc_data[
16
:]
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!