灰产-如何破解请求验签(下)

灰产-如何破解请求验签(下)

image

写在前面

再来个前情提要:灰产-如何破解请求验签(上)

以下都是情节模拟,并非具体情况,请细加甄别

阿哈!Showmak……啊不,是新的 Sign 替换后不久就,两周左右就又被破解了呢——

“我们新的加签,可是从多个随机数池取值生成的 sign1,然后把sign1和多个参数混合在一起按时间戳不同取模排列生成的sign2,怎么可能……”

啊,虽然很想吐槽这反派一般被反击了感叹自己部署被打乱了的感觉,但是来不及为已经逝去的 sign2 感到悲伤了,接下来赶到战场的是……

是我这个苦力!

我打灰产?真的假的?要上吗?

image

总而言之

总而言之,在解释了旧 Sign 是怎么泄露的后,调查新 Sign 是怎么泄露的这个任务又又又落到我这里了= =,已经不想吐槽了

实现漏洞

“怎么实现的……额,实际上我们等于是接了一个生成库,你可以理解为类似SDK,里面有各种 Sign1 和 Sign2 的实现方式,什么移位啊取模啊都多久前的了,我都快忘了,你想看的话文档给你你可以看一下”

“有没有可能是这个实现的模块泄露了或者可以被调用啥的?”

“我觉得没有吧,感觉也有可能是 Hook 了,但就不知道是怎么弄的”

然后直到最后我除了在研发显示屏上看到了一眼实现文档,我再也没看到那实现方式一眼了

image

摸索开始了——

首先通过请求日志我发现,灰产用到的 sign1 有一批量重复的(说好的随机池随机取呢),但是生成的 sign2 是不重复的(sign1 只作为一个参数参与生成 sign2 的计算),我们并没有对 sign1 进行校验,所以导致这个 sign1 可以在请求中被替换,重复利用。

顺带一提的是,在其他应用里很早就出现这个情况了,不是我的话都还没发现……

image

定向Hook

sign1 的问题因为没有校验可以重复利用解决了,那 sign2 呢?

前端没有找到泄露 sign2 计算点,因为前端都可能没用新的 Sign;

应用端源码没看到 sign2 的计算方式,因为做了较好的混淆;

……

那选择就只有一个了!(折断拐杖)

对它使用 Hook 吧!

image

早在 MobSF移动安全测试框架|自动化静动态app分析框架 的使用中,我就接触了 Frida,why?因为一开始装不上,并且因为种种原因(MobSF 自带证书冲突等)执行动态分析的效果很差,虽然后来还是能试出来。

扯远了,回到现在,利用 Frida 对应用进行 Hook,我是在 Genymotion 上做的,当然,由于应用配置的关系,你可能需要先:

这里就要说了,为什么涉及到 Brida?

因为一开始是想用 Brida 直接 Hook 调用函数,传到 BurpSuite 直接修改请求,但是后来发现你根本不知道是哪个函数计算 sign2 ,别说改了,Hook 都不知道 Hook 谁,怎么办?

image

java.security.MessageDigest

还记得旧 Sign 最后的加密算法吗?SHA1-Hash

再看看新 Sign,尤其是 sign2 的样式,分明也是 SHA1!

嘛,灵光一现:不如利用 js 跟踪“java.security.MessageDigest”的调用,并输出加密入参与之后的值,js 参考:

image

入参值反解出来是一堆字节码,需要用 java 重新转一下回去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class exchange {
public static void main(String[] args) {
byte[] b = { 108, ……, 102,
105, ……, 105,
111, ……, 107,
45, ……, 61,
50, ……, 52,
32, ……, 117,
115, ……, 118,
105, ……, 110,
118, ……, 73,
70, ……, 78,
74, ……, 85,
85, ……, 108,
107, ……, 107,
45, ……, 110,
61, ……, 48,
67, ……, 45,
116, ……, 112,
61, ……, 104,
73, ……, 57 };
String s = new String(b);
System.out.println(s);
}
}



Output:
……data={"userId":"……"}……sign1=GORNJPBOGXJXZQRRKYKYZNGZXZSYOUUSCMCM……timestamp=1714030269543……

sign1&timestamp

ok,入参也出来了,现在只剩一个问题:sign1 在入参中的位置。

其实到这里直接去遍历都可以,但是我还是采集了几份 sign1 在 sign2 生成位置与 timestamp 的关系进行比对,然后发现比对了个寂寞——差一位数的时间戳里 sign1 的插入位置可以相同的 = =:

image

所以不整了,收工!

image

写在后面

下篇也写完了,感觉好像也就这样,后续把加固啥的弄上议程吧,不然好像还有些硬编码问题也是头疼,但还要验证一下效果。

沉默,然后……事已至此,先吃(pao)饭(lu)吧

image

参考引用

灰产-如何破解请求验签(上)

MobSF移动安全测试框架|自动化静动态app分析框架

Genymotion_A11_libhoudini

How to install Xposed/EdXposed/LSPosed + Magisk with Genymotion Desktop?

brida从0配置

安卓逆向 - Frida Hook(抓包实践)

Frida java层自吐加密算法

灰产-如何破解请求验签(上)

灰产-如何破解请求验签(上)

image

前情提要

先来个前情提要:灰产-如何对APK修改后重签

在写那篇文章的我,是否会想到我竟然还有有关“灰产”的下文来写?而且就标题来说,还有分上下两篇!所以这4个月不到的时间我又经历了什么呢?

image

写在前面

以下都是情节模拟,并非具体情况,请细加甄别

让我们把时间拨回到年后。

年后回来一阵子,接到业务风控同学的反馈,我们有用户发现自己的账户被盗了,自己购买了不是自己的商品(不产生金额)并对这个商品统一匿名评价……

我第一反应卧槽哥们我们应用不是做了端校验,不同设备必须验证码登录,怎么连手机一起丢了才有后面能说漏洞导致账密泄漏的事?

image

后面通过网关日志、应用日志、接口日志等排查,发现这个用户的登录凭证(token)有从境外服务器请求服务的情况,可以基本确认是 token 的泄漏;

再和用户沟通以及进一步的深入排查,我们发现 token 的泄漏源于用户被钓鱼,这里细节不表;被社工得到的 token 会先从 ip 1 被收集并转发给 ip 2 进行购买请求与评论……

这里涉及到一个问题,我们的请求在各端做了验签,Sign 参数会根据不同端走不同的计算方式,想要更改 token 并发起购买请求,不重新计算 Sign是不可能通过校验的,只有两种可能:

  • 我们的 Sign 计算方式被破解
  • 我们的 Sign 计算方式可被任意调用

总而言之

总而言之,这个任务又落到我这里了= =

会议讨论对齐的时候,研发一致认为是在 H5 端的 Sign 计算方式太简单了,如何计算已经被破解,应该换上现在端上更安全的 Sign 计算就可以了!

直接破解一个计算方法理论上是不太容易的,我更在意的是是否是哪个地方泄漏了 Sign 的计算方法,或者在什么地方的计算代码可以被调用,直接入参入值然后得到结果。

但这里就要提到另一个事情了,研发不告诉你!

他们只表述这个好像比较简单就能破,但问他们怎么破的,不知道;问他们在哪里可能泄漏了,不知道:

“大概是从前端 JS 泄漏的?你看我们这里都做了混淆了,你看是不是就是这个位置,sign 关键词都命中了……”“可是为啥我调试/下断点没反应啊?”

image

后来嘛,在把钓鱼的口子堵了一下后,换上“更安全”的 Sign 计算方式后,喜闻乐见的钓鱼的口子也被绕过了,“更安全”的 Sign 计算也被破解了……

然后呢?

遭殃的还是我好吧,本来他们都认为可行的解决办法没用的时候,我就不得不花时间和精力来看看不在我 OKR 上的问题了

image

泄漏发现

算研发很配合安全工作,虽然告诉了我一个错误的泄漏位置,但起码提醒了我可能是 JS 就已经泄露了,没必要一来就去逆向找源码。

开始的思路还是去利用前端给到的 JS 复用:Burp Suite作为代理,复用目标站点的js代码处理指定的内容

后来发现 JSFlash 必须要能够指定调用代码函数才行,前端 JS 混淆了,指向不出来,直接分析不了

然后我就在前端登录页 Source - js 找到了 Sign 计算点,并且下断点步进调试看到了所有 Sign 的计算入参:

image

image

它端验证

在前端 JS 调试的过程中能看到有一个 secret 参数是不会暴露在 request 里的,其他端的如何获得?

Jadx 逆向反编译一下应用端,以 SIGN 为关键词,在源代码中发现了硬编码在其他端使用的 secret,以及使用的 Sign 最后加签算法:

image

image

写在后面

上篇就到这里,写的好像很简单,解过程也没遇到什么麻烦,但是还得说和大伙配合的还是不好,我还是认为不管是锻炼或者检验个人能力也好,工作任务忙不想接手深入这个事情也好,如果是以把事情完成的结论来说,相互配合是很重要的。

以旧 Sign 为例,大家说的好像每个人都知道这个是怎么生成的,但就是没人和你说,告诉的还是错误的位置,即使真的可能知道是怎么生成的不知道是在哪里泄露的,那信息汇通还是更重要的。研发完全可以告诉你“我猜是在前端 JS 泄漏的,位置是在这里,他的调用是这样的……”给你演示出来,如果真的在前端直接 Debug 下断点走一遍,就会知道一开始的位置告诉错了,并且一下就把泄漏点找出来了。

不会浪费双方的时间的。

image

参考引用

灰产-如何对APK修改后重签

Burp Suite作为代理,复用目标站点的js代码处理指定的内容

JS逆向-数据包解签名实战案例

JS逆向-数据包解签名实战案例

本文转自ichi9o 并作补充

0x00 前言

通常情况下,数据包中的签名字段会包含sign字符串,如signappsign等等,然后根据字段在JS代码中寻找签名的过程,并进行分析。
以下内容为一个真实案例,撒,哈气灭路!

0x01 获取被签名数据

通过数据包可知该网站的签名字段是 sign

image

在浏览器中的源代码搜索字段sign,找到签名代码
首当其冲的就是app.*.js样式的文件,在app.72b81572.js文件中发现了可疑点

image

分析 function k(e) 可知,是要对 r 参数进行签名的,r 又跟e、t、a、n 这几个参数相关,因此要搞清楚这几个参数的值是怎么来的,所有就在函数开始的第一行就下断点来跟进。

1
2
3
4
5
6
7
8
9
10
11
12
function k(e) {
const t = g()
, n = m();
let a;
a = e ? b(e) : {};
const r = e ? `${c.a.stringify(a.hasParams)}&${t}&time=${n}` : `&${t}&time=${n}`;
return e = Object.assign({}, a.params, {
sign: h()(decodeURIComponent(r)),
time: n
}),
e
}

首先是 t,跟进 g 函数,执行到 return 就可以发现 t = 年 + “5616” + 月 + 日

image

然后就是 n,这个 e 的值呢,根据跟进可知是 cookie 里面的,不过这个 e 在本案例的作用不大,最后通过 return 就可以知道返回的值就是:当前时间戳- e

image

接下来就是 a,根据返回的结果可知 a 的值和 e 相同,通过分析数据包可知,e的内容即请求参数(GET)或请求体(POST)

image

然后 r 的值就出来了,也就是签名的明文

image

综上所述
r = 请求内容 + & + t + & + n(这里的请求内容需要注意的是,POST Data是Json格式的要转换为:key1=value1&key1=value1…)
其中
t = 年 + “5616” + 月 + 日
n = 当前时间戳(本案例可不减e也可成功)

0x02 分析加密算法

将断点打在签名代码行,跟进代码的执行,发现加密的类名是 Md5,所有可以计算 r 的 md5 值,与代码执行的 sign 结果进行比较。

image

可以看到,该网站使用的是 md5 计算的 sign

image

0x03 编写 mitmproxy 脚本

mitmproxy 脚本是实时加载的,因此 mitmproxy 只要带着脚本运行,就可以边调试了。
mitmproxy 使用命令

1
mitmdump -p 777 -s .\mitmscript.py --flow-detail 0

image

关于脚本的编写这里给出以下 mitmproxy 的一些常用的属性和方法ctx:

  • ctx.log.info(): 用于在 mitmproxy 的日志中输出信息。
  • ctx.options: 用于访问 mitmproxy 的配置选项,您可以在配置文件中定义这些选项。
  • ctx.master: mitmproxy 的 Master 对象,提供了一些控制代理行为的方法。
  • ctx.proxy: mitmproxy 的 ProxyConfig 对象,提供了有关代理配置的信息。
  • ctx.client: mitmproxy 的 ClientConnection 对象,表示客户端连接的相关信息。
  • ctx.server: mitmproxy 的 ServerConnection 对象,表示服务器连接的相关信息。
  • ctx.protocol: mitmproxy 的 ProtocolHandler 对象,表示处理请求和响应的协议处理器。

在 mitmproxy 脚本中,可以使用以下一些回调函数来处理不同阶段的 flow 对象:

  • def request(flow: mitmproxy.http.HTTPFlow) -> None: 当 mitmproxy 拦截到请求时调用此回调函数,可以获取和处理请求的各种信息。
  • def response(flow: mitmproxy.http.HTTPFlow) -> None: 当 mitmproxy 拦截到响应时调用此回调函数,可以获取和处理响应的各种信息。
  • def error(flow: mitmproxy.http.HTTPFlow) -> None: 当请求或响应出现错误时调用此回调函数,可以获取和处理错误信息。
  • def clientconnect(flow: mitmproxy.tcp.TCPFlow) -> None: 当客户端连接到 mitmproxy 时调用此回调函数,可以获取和处理客户端连接的相关信息。
  • def serverconnect(flow: mitmproxy.tcp.TCPFlow) -> None: 当 mitmproxy 连接到服务器时调用此回调函数,可以获取和处理服务器连接的相关信息。
  • flow.request.method: 获取请求的方法(GET、POST等)。
  • flow.request.scheme: 获取请求的协议(http 或 https)。
  • flow.request.host: 获取请求的主机名。
  • flow.request.port: 获取请求的端口号。
  • flow.request.path: 获取请求的路径部分。
  • flow.request.url: 获取完整的请求URL。
  • flow.request.headers: 获取请求的头部信息,是一个字典对象,可以通过键来访问特定的头部字段。
  • flow.request.cookies: 获取请求中的Cookie信息,是一个字典对象,可以通过键来访问特定的Cookie。
  • flow.request.query: 获取请求的查询参数,是一个字典对象,可以通过键来访问特定的查询参数。
  • flow.request.content: 获取请求的内容,如果请求是POST请求且带有内容,则可以通过该属性来访问请求的内容。
  • flow.request.text: 获取请求的内容,并以文本形式返回。
  • flow.request.urlencoded_form: 获取请求的URL编码表单数据,是一个字典对象,可以通过键来访问特定的表单字段。
  • flow.request.multipart_form: 获取请求的多部分表单数据,是一个列表对象,列表中的每个元素都是一个字典,表示一个表单字段。
  • flow.request.content: 获取请求的原始内容,以字节形式返回。

附上本案例的代码供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from datetime import datetime
from mitmproxy import ctx
import hashlib
import random
import json
import time


class Modify:
def __init__(self):
self.plaintext = ''
self.new_sign = ''
current_datetime = datetime.now()
self.today = f"{current_datetime.year}5616{current_datetime.month:02d}{current_datetime.day:02d}"

def md5(self):
md5_hash = hashlib.md5()
md5_hash.update(self.plaintext.encode('utf-8'))
self.new_sign = md5_hash.hexdigest()

def request(self, flow):
if flow.request.method == "POST":
ctx.log.info(f'\n原POST请求体:{flow.request.text}')
data = json.loads(flow.request.get_text())
if data != '{}' and 'sign' in data:
del data['sign']
del data['time']
for k in data:
self.plaintext += f"{k}={str(data[k])}&"
timestamp = int(time.time() * 1000)
self.plaintext += f"{self.today}&time={timestamp}"
self.md5()

data["sign"] = self.new_sign
data["time"] = timestamp
flow.request.set_text(json.dumps(data).replace(' ', ''))
ctx.log.info(f'\n新POST请求体:{flow.request.text}')
self.plaintext = ''
else:
ctx.log.info('无参数,无需改签')
elif flow.request.method == "GET":
ctx.log.info(f'\n原GET请求体:{flow.request.query}')
query = flow.request.query
if query != '{}' and 'sign' in query:
del query['sign']
del query['time']
if query:
for k in query:
self.plaintext += f"{k}={str(query[k])}&"
else:
self.plaintext = "&"
timestamp = int(time.time()) * 1000
self.plaintext += f"{self.today}&time={timestamp}"
self.md5()

query["sign"] = self.new_sign
query["time"] = timestamp
ctx.log.info(f'\n新GET请求体:{flow.request.query}')
self.plaintext = ''
else:
ctx.log.info('无参数,无需改签')

@staticmethod
def response(flow):
if '签名校验失败!' in flow.response.text:
ctx.log.error(f'\n签名异常:\nurl => {flow.request.path}\n异常信息 => {flow.response.text}')


addons = [
Modify()
]

灰产-如何对APK修改后重签

灰产-如何对APK修改后重签

image

写在前面

前段时间没有事做的时候,组织上仿佛看穿了我很咸,突然让我去研究下灰产包,让我分析用了什么技术&做出了什么改动。

image

总而言之

虽然入职就听说了公司应用之前有被灰产“薅羊毛”的问题,但真让我弄我也没专门弄过啊,而且这部分之前是交给风控组去做的,这么久了整不来靠我嘛?而且我也想吐槽这灰产包你们又是怎么弄来的,还有两份不同技术实现的……

image

签名篡改

如果想对apk内部内容进行修改,从而达到实现某些功能或规避某些审查的目的,就一定要对签名进行重签,这一点上只需要和正版签名比对一下便能发现。如何检测使用了这些篡改应用的用户等不在这次的任务范围内,篡改功能点内容也不展开讨论,此处仅讨论绕过审查重签的技术。

image

MT APP签名检查及绕过

对灰产包简单的逆向后,我在第一个包很快地就发现了一个KillApplication.java

image

很显然,做篡改的兄弟还不够细,或者根本没懂相应的原理性知识,留下了如此明显的痕迹,并且在URL参数直接暴露了篡改方式:

ApkSignatureKillerEx

image

算是让我直接回忆起之前用过的MT管理器,好像酷安就能下到,不过我已经很久没用过了,不知道MT现在是自带这种签名方式还是作为外部插件使用的。

而MT APP签名检查及绕过在Android逆向-获取APP签名 一文中有详细表述,在此不做赘述(懒)

NP APP签名检查及绕过

那我们再来看下一个,啊,还不用我找,MobSF都给我翻出来了:

image

你是?

image

顺着FuckSign.java2863678687@qq.com 这两个信息(痕迹也是太明显了),我找到了另一种篡改方式:

NP-Manager

image

直接获取下来也很容易就复现了篡改与绕过:

image

写在后面

首先啊,风控安全真是和什么都斗争不断,除开apk包篡改,也还有模拟GPS定位、虚拟用户接码手机号等等,还好不是我直接负责:

image

然后,之前文章提到过外网可能是MobSF官方开的一个在线检测的平台,直接暴露了非常多的检测应用报告,无独有偶,我在做这次灰产分析的时候,在搜索引擎寻找FuckSign.java,发现了国内一个仿MobSF的在线检测平台,从搜索引擎记录来看,市场上很多其他应用也不堪灰产侵扰:

image

我在发现这个检测平台的时候,它给检测项部分“需要开通VIP”解锁,该说是刻意而为导致规避了部分问题还是啥呢……总之,我在几周后重新访问的时候,他已经是崩溃状态,嘛,不好评价:

image

最后,要做灰产黑产,就要从原理性把痕迹去除或转化,不能留太明显的技术痕迹,虽然可以说这些最后找到的都是开源项目,是卖刀者不是作恶人,但淹死的多是会水的,这里分享一篇文章:灰产,赚点快钱?

image

参考引用

ApkSignatureKillerEx

Android逆向-获取APP签名

NP-Manager

Firebase Installations API 密钥硬编码

灰产,赚点快钱?

Android逆向-获取APP签名

Android逆向-获取APP签名

本文转自Taardisaa 并作补充

很久以前开的blog,关于如何获取APP签名。不知道为啥要写这个了。

Android逆向-APP签名

生成JKS签名

Android studio 如何生成jks签名文件 - 简书 (jianshu.com)

打开AndroidStudio

1
Build-->Generate Signed APK-->APK

然后Key store path选择Create New

然后设置好存储路径,密码也设置一下(偷懒写个123456)

Key的别名就叫key,密码一样简单。

然后剩下的Certificate全填taardisCountry Code填11451。

反正创建成功后,就在选定路径下出现了jks密钥文件。

APK签名

将APK魔改,重新打包后,需要重新签名。

参考:Android之通过 apksigner 对 apk 进行 手动签名_恋恋西风的博客-CSDN博客

1
apksigner.bat sign --verbose --ks D:\Android\Keystore\taardis.jks --v1-signing-enabled false --v2-signing-enabled true --ks-key-alias key --ks-pass pass:123456 --key-pass pass:123456 --out D:\Android\Frida\gadget\bs.apk D:\Android\Frida\gadget\b.apk

成功后提示:

1
Signed

获取APK签名

首先APK解包:

1
apktool d <apk>

然后在 META-INF 文件夹拿到 CERT.RSA 文件。之后:

1
keytool -printcert -file CERT.RSA

不过Keytool似乎是Java的工具,不管了现在用不上。

JEB/JADX

这种反编译器也能直接看到APK的签名信息。

MT APP签名检查及绕过

L-JINBIN/ApkSignatureKillerEx: 新版MT去签及对抗 (github.com)

从“去除签名验证”说起 - 腾讯云开发者社区-腾讯云 (tencent.com)

过签名校验(2) – MT 的 IO 重定向实践 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

MT提供的签名绕过方式能够实现对API和APK方式的绕过。但是对于SVC的则无能为力。

1
2
3
4
String signatureExpected = "3bf8931788824c6a1f2c6f6ff80f6b21";
String signatureFromAPI = md5(signatureFromAPI());
String signatureFromAPK = md5(signatureFromAPK());
String signatureFromSVC = md5(signatureFromSVC());

API检测

PackageManager直接获得签名。

1
2
3
4
5
6
7
8
9
private byte[] signatureFromAPI() {
try {
@SuppressLint("PackageManagerGetSignatures")
PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
return info.signatures[0].toByteArray();
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}

APK检测

找到APP私有文件夹下的base.apk,然后得到签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private byte[] signatureFromAPK() {
try (ZipFile zipFile = new ZipFile(getPackageResourcePath())) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)")) {
InputStream is = zipFile.getInputStream(entry);
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(is);
return x509Cert.getEncoded();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

SVC检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private byte[] signatureFromSVC() {
try (ParcelFileDescriptor fd = ParcelFileDescriptor.adoptFd(openAt(getPackageResourcePath()));
ZipInputStream zis = new ZipInputStream(new FileInputStream(fd.getFileDescriptor()))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)")) {
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(zis);
return x509Cert.getEncoded();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

绕过

Java层的东西不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static void killOpen(String packageName) {
try {
// Native层Hook
System.loadLibrary("SignatureKiller");
} catch (Throwable e) {
System.err.println("Load SignatureKiller library failed");
return;
}
// 读取/proc/self/maps读取APP路径
String apkPath = getApkPath(packageName);
if (apkPath == null) {
System.err.println("Get apk path failed");
return;
}
// 读取自身APK文件(私有目录下的base.apk)
File apkFile = new File(apkPath);
// 在APP私有目录下创建origin.apk文件
File repFile = new File(getDataFile(packageName), "origin.apk");
try (ZipFile zipFile = new ZipFile(apkFile)) {
// 将APK中的origin.apk给提取出来(origin.apk是MT去签是生成的,是初始没有被去签的APK)
String name = "assets/SignatureKiller/origin.apk";
ZipEntry entry = zipFile.getEntry(name);
if (entry == null) {
System.err.println("Entry not found: " + name);
return;
}
// 读取出来
if (!repFile.exists() || repFile.length() != entry.getSize()) {
try (InputStream is = zipFile.getInputStream(entry); OutputStream os = new FileOutputStream(repFile)) {
byte[] buf = new byte[102400];
int len;
while ((len = is.read(buf)) != -1) {
os.write(buf, 0, len);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 传入底层so Hook
hookApkPath(apkFile.getAbsolutePath(), repFile.getAbsolutePath());
}

然后看Native层,实际上是XHook,用于替换libc函数。

1
mt_jni.c

实际上就做了个字符串替换,有意将原本要打开的APK替换成origin.apk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const char *apkPath__;
const char *repPath__;

int (*old_open)(const char *, int, mode_t);
static int openImpl(const char *pathname, int flags, mode_t mode) {
//XH_LOG_ERROR("open: %s", pathname);
if (strcmp(pathname, apkPath__) == 0){
//XH_LOG_ERROR("replace -> %s", repPath__);
return old_open(repPath__, flags, mode);
}
return old_open(pathname, flags, mode);
}

JNIEXPORT void JNICALL
Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv *env, __attribute__((unused)) jclass clazz, jstring apkPath, jstring repPath) {
apkPath__ = (*env)->GetStringUTFChars(env, apkPath, 0);
repPath__ = (*env)->GetStringUTFChars(env, repPath, 0);

xhook_register(".*\\.so$", "openat64", openat64Impl, (void **) &old_openat64);
xhook_register(".*\\.so$", "openat", openatImpl, (void **) &old_openat);
xhook_register(".*\\.so$", "open64", open64Impl, (void **) &old_open64);
xhook_register(".*\\.so$", "open", openImpl, (void **) &old_open);

xhook_refresh(0);
}

通过Hook open函数,可以把基于APK读取的签名方式给绕过。

下面提供一个绕过基于PackageManager的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private static void killPM(String packageName, String signatureData) {
// 构造一个假的签名
Signature fakeSignature = new Signature(Base64.decode(signatureData, Base64.DEFAULT));
Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR;
Parcelable.Creator<PackageInfo> creator = new Parcelable.Creator<PackageInfo>() {
@Override
public PackageInfo createFromParcel(Parcel source) {
PackageInfo packageInfo = originalCreator.createFromParcel(source);
if (packageInfo.packageName.equals(packageName)) { //
if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {
packageInfo.signatures[0] = fakeSignature; // 将虚假的签名放入packageInfo,取代原来的
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (packageInfo.signingInfo != null) {
Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();
if (signaturesArray != null && signaturesArray.length > 0) {
signaturesArray[0] = fakeSignature;
}
}
}
}
return packageInfo;
}

@Override
public PackageInfo[] newArray(int size) {
return originalCreator.newArray(size);
}
};
try {
// 用假的creator替换原来的PackageInfo.CREATOR
findField(PackageInfo.class, "CREATOR").set(null, creator);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 解除某些Android系统API的使用限制?
HiddenApiBypass.addHiddenApiExemptions("Landroid/os/Parcel;", "Landroid/content/pm", "Landroid/app");
}
try {
// 清空签名缓存
Object cache = findField(PackageManager.class, "sPackageInfoCache").get(null);
// noinspection ConstantConditions
cache.getClass().getMethod("clear").invoke(cache);
} catch (Throwable ignored) {
}
try {
// 清空签名缓存
Map<?, ?> mCreators = (Map<?, ?>) findField(Parcel.class, "mCreators").get(null);
// noinspection ConstantConditions
mCreators.clear();
} catch (Throwable ignored) {
}
try {
// 清空签名缓存
Map<?, ?> sPairedCreators = (Map<?, ?>) findField(Parcel.class, "sPairedCreators").get(null);
// noinspection ConstantConditions
sPairedCreators.clear();
} catch (Throwable ignored) {
}
}

参考

Java Keytool 介绍 - 且行且码 - 博客园 (cnblogs.com)

获取Android应用签名的几种方式 - 简书 (jianshu.com)

Android studio 如何生成jks签名文件 - 简书 (jianshu.com)

apktool重新打包时报错_apktool 忽略错误信息__y4nnl2的博客-CSDN博客

Android之通过 apksigner 对 apk 进行 手动签名_恋恋西风的博客-CSDN博客

apksigner | Android 开发者 | Android Developers (google.cn)