禁止别人调试自己前端代码的 5 种方法

禁止别人调试自己前端代码的 5 种方法

本文转自 Toypipi 并作补充

无限 debugger

前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行。由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的。 以下是使用无限 debugger 方式阻止代码调试的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 基础禁止调试代码
*/
(() => {
function ban() {
setInterval(() => {
debugger;
}, 50);
}
try {
ban();
} catch (err) {}
})();

不过,如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大。可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger。这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint。

禁止断点的对策

如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpoint 为 false 也无用。当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的。

1
2
3
4
5
6
7
8
9
10
(() => {
function ban() {
setInterval(() => {
debugger;
}, 50);
}
try {
ban();
} catch (err) {}
})();

忽略执行的代码

可以通过添加 add script ignore list 需要忽略执行代码行或文件,也可以达到禁止无限 debugger。 不过,我们可以通过将 debugger 改写成 Function (“debugger”)(); 的形式来应对,Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件。当然使用的时候,为了更加的安全,最好使用加密后的脚本。

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
// 加密前
(() => {
function ban() {
setInterval(() => {
Function("debugger")();
}, 50);
}
try {
ban();
} catch (err) {}
})();

// 加密后
eval(
(function (c, g, a, b, d, e) {
d = String;
if (!"".replace(/^/, String)) {
for (; a--; ) e[a] = b[a] || a;
b = [
function (f) {
return e[f];
},
];
d = function () {
return "w+";
};
a = 1;
}
for (; a--; )
b[a] && (c = c.replace(new RegExp("\b" + d(a) + "\b", "g"), b[a]));
return c;
})(
'(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',
9,
9,
"block function setInterval Function debugger 50 try catch err".split(" "),
0,
{}
)
);

终极增强防调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(() => {
function block() {
if (
window.outerHeight - window.innerHeight > 200 ||
window.outerWidth - window.innerWidth > 200
) {
document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
}
setInterval(() => {
(function () {
return false;
})
["constructor"]("debugger")
["call"]();
}, 50);
}
try {
block();
} catch (err) {}
})();

其他防调试技术

除此之外,我们还可以通过【禁止右键菜单】、【禁止 F12 快捷键】、【检测开发者工具】等手段来禁止调试。

禁止右键菜单

禁止右键菜单是目前比较常见的一种禁止调试的方法,因为浏览器默认右键菜单是可以调出开发者工具的。可以通过以下 JS 代码实现:

1
2
3
document.oncontextmenu = function () {
return false;
};

这样一来,当用户在富文本编辑器的区域内右键时,就不会弹出菜单,从而避免了用户调出开发者工具的情况。

禁止 F12 快捷键

F12 快捷键是打开开发者工具的常用快捷键,禁止这个快捷键也是一种禁止调试的方法。可以通过以下 JS 代码实现:

1
2
3
4
5
document.onkeydown = function (e) {
if (e.keyCode === 123) {
return false;
}
};

这样一来,当用户按下 F12 键时,就会被拦截,从而避免了用户打开开发者工具的情况。

检测开发者工具

还有一种方法是通过检测开发者工具是否打开来实现禁止调试的目的,可以通过以下 JS 代码实现:

1
2
3
4
5
setInterval(function () {
if (typeof console.clear !== "undefined") {
location.reload();
}
}, 1000);

这段代码会每隔 1 秒检测一次当前页面是否打开了开发者工具,如果发现开发者工具已经打开,则会刷新当前页面,从而避免用户使用开发者工具对页面进行调试。

反编译小程序、数据加密解密、sign值绕过

反编译小程序、数据加密解密、sign值绕过

本文转自 sys0ne 并作补充

在一次渗透测试中对客户的小程序进行测试,一抓包就发现重重加密数据,在手机号一键登录数据包中发现泄露了session_key值,但是进行加密处理过,那就反编译小程序进行解密尝试。

反编译小程序

在点击该小程序时,C:\Users\xxx\Documents\WeChat Files\Applet在该目录下下会生成一个新的目录,最好时只留下面两个目录。

image

image

在生成的目录下找到__ APP __.wxapkg文件,使用UnpackMiniApp.exe进行解密,重新生成一个wxapkg文件

image

将生成的wxapkg文件拖到wxapkgconvertor.exe进行反编译,就会在wxapkg文件目录下生成一个新的文件夹,使用微信开发者工具打开就可以看到小程序的源码了

image

image

解密AES函数

发现存在数据加密,编译小程序,分析源码

image

对小程序进行调试,直接全局搜AES字段,发现 AES 的加密密钥硬编码在源代码中。

decryptByAes 是一个用于解密的函数,它使用 AES算法解密输入的密文,返回明文。解密过程需要密文、密钥和初始化向量(IV),并使用 CBC 模式和 PKCS7 填充。

image

根据默认密钥编辑 python 脚本(通用脚本,直接替换key或iv即可)

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii

def decrypt_by_aes(encrypted_hex, key_hex=None, iv_hex=None):

# 默认 Key 和 IV
DEFAULT_KEY_HEX = "31323334353??/414243444566"
DEFAULT_IV_HEX = "3031323334???43444546"

# 如果没有传入 Key 或 IV,使用默认值
key_hex = key_hex if key_hex is not None else DEFAULT_KEY_HEX
iv_hex = iv_hex if iv_hex is not None else DEFAULT_IV_HEX

# 将 Hex 字符串转换为 bytes
key = binascii.unhexlify(key_hex)
iv = binascii.unhexlify(iv_hex)
encrypted_data = binascii.unhexlify(encrypted_hex)

# 初始化 AES-CBC 解密器
cipher = AES.new(key, AES.MODE_CBC, iv)

# 解密并去除 PKCS7 填充
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)

# 返回 UTF-8 解码后的明文
return decrypted_data.decode('utf-8')


# 示例用法
if __name__ == "__main__":
encrypted_hex = "30313233343536373839414243444546"

try:
decrypted_text = decrypt_by_aes(encrypted_hex)
print("解密结果:", decrypted_text)
except Exception as e:
print("解密失败:", str(e))
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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii

def encrypt_by_aes(plaintext, key_hex=None, iv_hex=None):
DEFAULT_KEY_HEX = "31323334353??/414243444566"
DEFAULT_IV_HEX = "3031323334???43444546"

# 如果未提供 key_hex 或 iv_hex,使用默认值
key_hex = key_hex if key_hex is not None else DEFAULT_KEY_HEX
iv_hex = iv_hex if iv_hex is not None else DEFAULT_IV_HEX

# 将十六进制字符串转换为字节
key = binascii.unhexlify(key_hex) # 32 字节密钥
iv = binascii.unhexlify(iv_hex) # 16 字节 IV

# 将明文转换为字节(UTF-8 编码)
plaintext_bytes = plaintext.encode('utf-8')

# 初始化 AES-CBC 加密器
cipher = AES.new(key, AES.MODE_CBC, iv)

# 添加 PKCS7 填充并加密
padded_data = pad(plaintext_bytes, AES.block_size) # PKCS7 填充
ciphertext = cipher.encrypt(padded_data)

# 将密文转换为十六进制字符串
ciphertext_hex = binascii.hexlify(ciphertext).decode('utf-8')

return ciphertext_hex

# 示例用法
if __name__ == "__main__":
# 测试数据
plaintext = "Hello, World!"
try:
# 使用默认密钥和 IV 加密
encrypted_default = encrypt_by_aes(plaintext)
print("加密结果:", encrypted_default)

except Exception as e:
print("加密失败:", str(e))

image

1
encryptedData=1b39aaa7b79f513b2958b671d8b8ad700ce3788963bc8ae3f91b61588cb963f05397695b3405dd75b2b91bc8e922d6918818e0d7eea3a835847b34b998bf5adf892fa1b41a193365a52970d063de10cb2d920aa066695d3187377a63c8efc0db1133cbcbb0f3d4c5def03e4c045a52f1c4401dbf23023e890a37c86773821b698a3f4f82d89840c33c14c0701cc8854b495e9c187106f7c265cd2fae34f320413adcff074bde1f189007c91a451054eee26ad058f3cdc74c67eba9f61ea1e010667b714c4d114559d14e8c9e2afb5c478c44d26c11de9bb88b1ec8e27f48269c&iv=b9d1b5783b906c84d20ae0b1ac1373c9d65e85bafb3922801b1f4681aadb8267&sessionKey=5a556c5a8288afd815bd6ef2a96ddf2e2e7b795abbc4304ac333a2352296d2e9&openId=c9731afaa0966e3778383bfb7bfa58f25ae50bd3edf21082414af311ea9fdb92&thirdType=56e32732df18962b0dab4454799fdd64&encryptFlag=1&timeHashPf=1744596343852&sign=01EC6E15CCE27E17FC5D5E58C1430E96

通过脚本解密出encryptedData、sessionKey、iv 值。

image

获得原始数据。

image

绕过sign值校验

修改手机号,改为任意用户的手机号,在进行加密处理,但是存在 sign 值校验,可以看到sign值时md5加密,不可逆,但是我们可以了解sign值的加密流程,修改数据重新加密sign值,然后替换sign值,进行绕过。

全局搜索sign,分析源码中的 setsigntrue 函数

image

1
2
3
4
5
6
7
setSignature: function(t) {
var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : "", a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "", i = "";
for (var o in n) t[o] = n[o];
var s = this.objKeySort(t);
for (var u in s) i = "".concat(i).concat(u, "=").concat(t[u]);
return i += "".concat(r).concat(a), e.MD5(i).toString().toUpperCase();
},

这个函数接受四个参数:t,n,r,a。根据代码,t是主要的参数,n是可选参数,默认是空对象,r和a也有默认值。函数内部首先将n的属性合并到t中,也就是说,n中的键值对会被添加到t对象里。然后,它调用this.objKeySort(s),这里的s是处理后的t对象。objKeySort的作用是对对象的键进行排序,返回一个排序后的新对象。接下来,函数遍历排序后的对象s,将每个键值对以“key=value”的形式拼接成字符串i。然后,i后面拼接上r和a,最后对这个拼接后的字符串进行MD5加密,并转换为大写字符串返回。

  1. 将传入的n参数合并到t对象中,也就是把额外的参数添加到t里。
  2. 对t对象的所有键进行排序,得到排序后的对象s。
  3. 遍历排序后的s,将每个键值对拼接成“key=value”的字符串,并将这些字符串按顺序连接起来,形成字符串i。
  4. 在i的末尾添加r和a的值,得到最终的拼接字符串。
  5. 对这个最终字符串进行MD5哈希,并将结果转为大写,作为sign值返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import hashlib

params = {
"encryptFlag": "1",
"encryptedData": "1b39aaa7b79f513b2958b671d8b8ad700ce3788963bc8ae3f91b61588cb963f05397695b3405dd75b2b91bc8e922d6918818e0d7eea3a835847b34b998bf5adf892fa1b41a193365a52970d063de10cb2d920aa066695d3187377a63c8efc0db1133cbcbb0f3d4c5def03e4c045a52f1c4401dbf23023e890a37c86773821b698a3f4f82d89840c33c14c0701cc8854b495e9c187106f7c265cd2fae34f320413adcff074bde1f189007c91a451054eee26ad058f3cdc74c67eba9f61ea1e010667b714c4d114559d14e8c9e2afb5c478c44d26c11de9bb88b1ec8e27f48269c",
"iv": "b9d1b5783b906c84d20ae0b1ac1373c9d65e85bafb3922801b1f4681aadb8267",
"openId": "c9731afaa0966e3778383bfb7bfa58f25ae50bd3edf21082414af311ea9fdb92",
"sessionKey": "5a556c5a8288afd815bd6ef2a96ddf2e2e7b795abbc4304ac333a2352296d2e9",
"thirdType": "56e32732df18962b0dab4454799fdd64",
"timeHashPf": "1744596343852"
}

# 按字母顺序排序键
sorted_keys = sorted(params.keys())
# 拼接键值对
raw_str = "".join([f"{key}={params[key]}" for key in sorted_keys])
# 追加固定字符串
raw_str += "zoe-health3f7e609f0a22108e"
# MD5 计算并大写
sign = hashlib.md5(raw_str.encode()).hexdigest().upper()
print(sign)

运行 python 文件,输出的 md5 值和原始数据中的 sign 值一致

image

image

现在流程全部走完了

复现流程

抓取登录数据包,将密文都进行解密处理后,输入任意手机号,加密处理,将加密后的内容放入加密sign脚本文件中生成一个新的sign值,替换sign。就可以实现任意手机号登录。

image

image

image

image

【爬虫】网站反debugger、内存爆破以及网站限制开发者工具

【爬虫】网站反debugger、内存爆破以及网站限制开发者工具

本文转自 NPE~ 并作补充

常见情形

目前网站反爬手段有很多,比如:网站反debugger、内存爆破、限制开发者工具等

情况一:debugger(右键不在此停止)

问题: 有部分网站做了反debugger处理,当我们打开开发者工具的时候会直接断点停在某个位置。

image

  • 解决办法:右键点击,选择永不在此停止。然后放过断点,即可继续调试。

前提:该debugger构造函数需要没有内容。如果网站开发者自定义的debugger构造函数后面有内容,是在写入数据的话(不断写入空列表、其他字符等),我们跳过断点会导致持续写入,导致内存溢出,页面卡死。

image

情况二:内存爆破

绝大多数网站反debugger会更加高深,会使用到内存爆破以及混淆技术。导致我们通过简单的右键不在此停止无法破解。

我们使用右键不在此停止,并且跳过断点后,会导致浏览器卡死。

情况三:右键被管理员禁用

部分网站会直接限制我们使用开发者工具,我们右键或者F12打开均无效,都被禁用。

我们想右键,打开开发者工具时,发现被禁用。

image

情况四:监测函数执行时间

image

我们通过快捷键强制打开开发者工具,发现页面提示非法调试。

image

image

最终发现页面是通过判断当前window大小以及函数运行时间来确认是否存在非法调试。

解决办法

方法一:控制台-右键不再此停止

如果我们打开开发者工具后,发现自动进入断点。

  1. 首先尝试,鼠标右击永不在此停止
  2. 放过该断点,即可继续调试

方法二:本地注入JS,覆盖之前老的JS

如果方法一无效:放过断点后会导致浏览器页面卡死。即可使用本地注入JS方法,将debugger构造器置空,覆盖之前的JS。

  • 分析浏览器卡死原因:网站开发者自定义的debugger构造器不为空,在持续写入数据。
  • 本地注入JS不能刷新网站,否则会导致我们本地注入的JS失效。

分析原因:浏览器卡死原因

我们单步执行断点,发现进入了VM虚拟环境,并且该JS是进行了混淆的。

image

下面这种有特殊字符+数字+字母的,基本就是进行过JS混淆的:

image

对于这种混淆过的JS代码,我们需要对其进行还原:

还原方法:复制JS代码,然后在控制台执行即可

1
2
//我们需要进行分段还原,否则一次性全部执行,会报not a function
[s(0, -259, -213, 0, "I#ue") + f(1271, 974, 1135, "e!3a") + "r"](r[f(1221, 1495, 1373, "v&9u")](r[g(1467, 0, 0, "&UOm")], r[n(0, "ujyL", -6)]))[s(0, 13, 12, 0, "7Iko")](r[g(1540, 0, 0, "nX(R")]);

image

还原后JS代码:‘constructor’ apply stateObject => 可以推测出,该自定义构造函数在初始化时,还做了加载某个对象的操作。及时我们右键点击了不再此停止,放过断点,但是依然会有源源不断的对象进行加载,最后导致浏览器卡死。

解决办法:本地注入JS

我们已经定位到了浏览器卡死是因为自定义的debugger构造器不断注入对象导致,那么我们就可以本地执行代码,替换该JS片段,让debugger构造器返回空,无法实现注入对象。

  1. 新建代码片段

image

1
2
3
4
5
6
7
8
9
10
// 将debugger构造函数替换置空,防止内存爆破
// 1. after_debugger_handle 接收我们的构造器
// 2. 判断构造器如果构造了一个debugger的东西,我们就将其置空
after_debugger_handle = Function.prototype.constructor;
Function.prototype.constructor = function(a){
if (a == "debugger") {
return function(){};
}
return after_debugger_handle(a);
};
  1. 点击下方的执行按钮,执行代码片段

image

控制台未报错,表明注入成功,此时我们放过断点,发现浏览器不再卡死,即可继续调试。

注意:

  1. 注入后不能刷新页面,否则页面重新加载会导致之前的注入失效。

image

  1. 注入需要等网页数据全部加载完成后再进行,否则可能会报注入失败,JS函数找不到。

拓展:JS混淆(对网页的JS代码进行处理加密)

概念

网站开发者为了防止我们对网站进行调试或者逆向分析,会对JS代码进行处理、保护。

  • 实际就是对JS代码进行编码、加密处理等。

JS压缩混淆:删除无用空白、缩短变量名

JavaScript 压缩混淆主要通过删除无用的空白字符、注释、缩短变量名等方式减小代码体积。压缩混淆不仅可以减少文件大小,还能在一定程度上增加代码阅读难度。

OB混淆:插入不透明谓词(函数逻辑)

OB 混淆(Opaque Predicate Obfuscation)是一种复杂的混淆技术,通过插入不透明谓词来掩盖代码的真实逻辑,使逆向工程师难以理解代码的实际功能。

原始代码:

1
2
3
if (x > 10) {
console.log("x is greater than 10");
}

OB混淆后:

1
2
3
4
5
6
function isTrue() {
return Math.random() > 0.5; // 不透明谓词
}
if (isTrue() || x > 10) {
console.log("x is greater than 10");
}

在这个示例中,isTrue 函数的返回值是随机的,不透明谓词使得控制流变得更加难以预测。逆向工程师必须理解 isTrue 函数的实现才能准确分析代码逻辑。

变量混淆:重命名函数方法,改为a、b、c等

变量混淆通过重命名变量和函数名来增加代码的阅读难度,使得代码更难以理解。混淆后的变量名通常是无意义的短字符,如 a、b、c 等。

字符串混淆:对字符串进行编码和转换

字符串混淆通过对字符串进行编码和转换,使得字符串的真实内容难以直接查看。常用的字符串混淆技术包括 Base64 编码和字符替换。

属性加密:对部分字段,如:用户名、年龄等进行加密

属性加密通过对对象属性进行加密,保护对象的内部数据。常用的加密方法包括简单的 XOR 操作和更复杂的加密算法。

控制流平坦化:引入虚拟机+状态机,加大代码逻辑复杂度

控制流平坦化通过重构代码的控制流,使得代码逻辑更加复杂和难以理解。平坦化技术常用于保护程序的执行流程。控制流平坦化通过引入虚拟机和状态机来重构代码的控制流。

案例:原始的条件语句被转换为一个状态机,控制流被重构成一系列状态和转换。这种方法使得代码的控制流更加复杂,增加了理解和分析的难度。

原始代码:

1
2
3
4
5
if (x > 10) {
console.log("x is greater than 10");
} else {
console.log("x is 10 or less");
}

控制流平坦化处理后代码:

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
const states = [0, 1];
let state = 0;

function execute() {
switch (state) {
case 0:
if (x > 10) {
state = 2;
return;
} else {
state = 3;
return;
}
case 1:
console.log("x is greater than 10");
state = 4;
return;
case 2:
console.log("x is 10 or less");
state = 4;
return;
case 4:
// end state
return;
}
}

while (state !== 4) {
execute();
}

案例

可以看到下方对有一串字母以及数字,这种情况一般就是JS混淆,让我们无法知晓他JS源代码是什么样子。

image

解决办法:

  • 如果该网站使用的是开源的JS混淆代码,我们可以通过工具进行还原。
  • 如果网站使用的是自定义的混淆逻辑,我们只有手动通过控制台进行处理还原(直接复制混淆代码,然后放入控制台,回车即可进行还原。)

image

参考文章

https://blog.csdn.net/weixin_52392194/article/details/141159872

iOS移动命令渗透测试备忘录

iOS移动命令渗透测试备忘录

本文转自 周边 并作补充

注意:iOS应用程序与Android应用程序具有不同的环境,此处的某些命令仅适用于MacOS。

**iOS**指南

Install Brew, 打开 terminal (Finder -> Application -> Utilities -> Terminal) 键入命令 :
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Install XCode, 打开 terminal 并运行命令 :
$ xcode-select --install
或者您可以手动下载 Apple Website

将您的Apple ID注册为Apple Developer Account。您无需为此部分的Apple Developer Program付费,只需将您的帐户注册到Developer中,因为我们只需要获得用于在XCode或其他工具上签名ipa文件的证书即可。

通过USB的(iPROXY)

http://iphonedevwiki.net/index.php/SSH_Over_USB

从应用商店或iPhone/iPad设备下载.ipa文件

需要越狱

Clutch

Clutch

Frida Script

Frida-ios-dump command: $ iproxy 2222 22 $ ./dump.py BundleID

iOS二进制分析

ios-Analysis

Download : ios-analysis

安装 :

1
2
3
$ git clone https://github.com/IAIK/ios-analysis
$ cd ios-analysis
$ git submodule update --init --recursive

如果您遇到这样的错误 : error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60

运行这个命令: git config http.postBuffer 524288000

使用Windows登录并安装.ipa

  • 下载altsigner
  • 安装最新的iTunes(注意:使用二进制文件安装,请勿从Microsoft Store安装)
  • 打开Itunes并选择设备以复制设备的UDID
  • 打开altsigner.exe,填写您的[电子邮件,密码,UDID,.ipa文件路径]

使用我们的配置文件签名IPA文件

该工具非常有用:) iOS App Signer基于GUI。要生成我们的配置文件,您可以在安装应用程序时在XCODE上进行配置。

疑难问题

如果您收到这样的错误消息 If you have previously trusted your certificate using Keychain, please set the Trust setting back to the system default

不要惊慌,这样做:

  1. 进入Xcode并从“首选项”中删除您的帐户
  2. 去 ~/Library/MobileDevice/Provisioning 在finder中配置文件并删除其中的文件
  3. 进入钥匙串并删除所有提及Mac Developer,iOS Developer等的个人证书等
  4. 将您的帐户重新添加到Xcode中,然后选择吊销现有证书(如果无法吊销,请保留该证书)
  5. 转到xcode,然后尝试将虚拟应用程序安装到设备中。此步骤将触发苹果生成新的我们的证书。
  6. 然后,打开iOS App Signer

记一次重签名错误

记一次重签名错误

本文转自 听月 并作补充

什么问题?

最近打开Cydia就闪退,我就重新去下载了uncOver,打算重新越狱一下,但是在重签名的时候一直失败,提示”No codesigning certificates found”的错误。

image

什么原因导致?

由于我之前使用iOS App Signer这个MAC软件一直没有问题,今天遇到这个问题,我就想去github把源码克隆到本地,然后调试一下,看看具体是什么原因导致。
打开源码直接搜索No codesigning发现,这个错误是一个Alert弹框,和我们遇到的弹框信息一致。

image

最终调试发现,在程序调用/usr/bin/security find-identity -v -p codesigning命令时由于返回值为空,才触发错误弹框。

image

我把命令拷贝到终端执行,发现返回有效的identity的数量是0:

1
2
$ /usr/bin/security find-identity -v -p codesigning
0 valid identities found

此处的命令的意思是:

1
2
3
4
5
6
7
8
9
10
11
12
/usr/bin/security:
A simple command line interface which lets you administer keychains,
manipulate keys and certificates, and do just about anything the
Security framework is capable of from the command line.
(是一个管理钥匙串、keys和证书的命令行接口)

find-identify:
查找证书和私钥key的命令

-v: 只显示有效的证书,默认显示所有的证书

-p: 指定策略

具体的命令行的文档查看一下:

1
2
3
4
5
6
7
8
9
10
11
$ /usr/bin/security find-identity --help
find-identity: illegal option -- -
Usage: find-identity [-p policy] [-s string] [-v] [keychain...]
-p Specify policy to evaluate (multiple -p options are allowed)
Supported policies: basic, ssl-client, ssl-server, smime, eap,
ipsec, ichat, codesigning, sys-default, sys-kerberos-kdc, macappstore, appleID
-s Specify optional policy-specific string (e.g. DNS hostname for SSL,
or RFC822 email address for S/MIME)
-v Show valid identities only (default is to show all identities)
If no keychains are specified to search, the default search list is used.
Find an identity (certificate + private key).

怎么解决?

由于使用/usr/bin/security find-identity -v -p codesigning命令无法查询本地有效的重签名证书,我首先想到的就是更换查询策略,根据命令行文档可以看出,策略有很多种,包括basic, ssl-client ssl-server, smime, eap,ipsec, ichat, codesigning, sys-default, sys-kerberos-kdc, macappstore, appleID。经过尝试,发现策略改成appleID的时候是可以查询到有效证书的。但是这两个命令是有区别的。具体的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
poet@poetdeMacBook-Pro ~ % /usr/bin/security find-identity -v -p codesigning
1) C766972EC48052D14E3A2D715F03960E10B4XXXX "Apple Development: Zhang San (MGRQJNV411)"
2) 8EA5E415105CEE96F75EAFA443B6AC992FC9CCCC "Apple Development: Li Si (SB6BFS5U22)"
3) AEF5450BA48F61EE1B5A718B7563D855E306DDDD "iPhone Developer: Zhang San (MGRQJNV433)"
3 valid identities found
poet@poetdeMacBook-Pro ~ % /usr/bin/security find-identity -v -p appleID
1.2.840.113635.78.1.30
1) F87912EA9F939D6F044F1B86A165C5D8AC8CAAAA "com.apple.idms.appleid.prd.5675685073322f516b6a565a55775471374c72424b671234" (CSSMERR_TP_CERT_EXPIRED)
2) 8751799A43C85FEF3914DE664E2A05251F40BBBB "Mac Developer: Lao Wang (H2C8DTYK00)" (CSSMERR_TP_CERT_EXPIRED)
3) C766972EC48052D14E3A2D715F03960E10B4CCCC "Apple Development: Zhang San (MGRQJNV411)" (Missing required extension)
4) 8EA5E415105CEE96F75EAFA443B6AC992FC9DDDD "Apple Development: Li Si (SB6BFS5U22)" (Missing required extension)
5) AEF5450BA48F61EE1B5A718B7563D855E306EEEE "iPhone Developer: Zhang San (MGRQJNV433)" (Missing required extension)
5 valid identities found

经过上面的对比,发现策略是codesigning的时候查询出来的是所有有效的重签名的证书,不包含其他的证书(比如:苹果的idms官方证书、MAC电脑的证书等),但是策略是appleID的时候查询出来的证书一定包含策略codesigning查询出来的证书。所以,当使用策略codesigning无法查询出证书情况的时候,可以考虑把策略换成appleID来查询。所以解决办法就是将codesigning策略换成appleID的策略。

提交Pull Request

解决这个问题之后,我去github仓库的issue里面看,发现很多人也有类似的问题,我就自己尝试提交了一个Pull Request到作者的仓库,虽然作者很久没有更新了,但是如果后续还有人碰到此问题可以通过类似的办法解决。如何提交Pull Request,我是参考github上的一个帖子,具体步骤如下:

如何在别人的开源项目中提交自己的Pull Request ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.先在本地创建一个空文件夹,里面准备放克隆过来的代码. --> 我在本地Downloads文件夹下创建了一个名为 gitMessagekit 的文件夹.
2.在"终端"中通过cd命令进入到gitMessagekit文件夹下(将"自己电脑的用户名"换成你自己的电脑的用户名). --> cd /Users/自己电脑的用户名/3.Downloads/gitMessagekit
3.在"终端"输入克隆命令 git clone 开源项目源代码的url. --> git clone https://github.com/MessageKit/MessageKit.git
4.进入到克隆所在的文件夹. --> cd /Users/自己电脑的用户名/Downloads/gitMessagekit/MessageKit
5.用查看命令查看一下开源项目都有多少个分支. --> git branch -a
6.找到自己要切换的分支,准备切换分支,在这里我要切换到3.0.0-beta分支. --> git checkout remotes/origin/3.0.0-beta
7.基于远程分支新建本地分支(3.0.0-beta),2条命令. --> git branch 3.0.0-beta git checkout 3.0.0-beta
8.打开/Users/自己电脑的用户名/Downloads/gitMessagekit/MessageKit该路径下的代码,对代码进行修改.
9.添加修改. --> git add 你修改的文件
10.提交修改. --> git commit -m "fix 某某问题"
11.去自己的git仓库,准备fork一下开源项目MessageKit到自己的仓库(repository)中.
12.即将关联自己fork过的项目. --> git remote add upstream git@github.com:xxjldh/MessageKit.git
13.推送本地的分支(3.0.0-beta)到自己fork过的仓库中,2条命令. --> git fetch origin git merge origin/3.0.0-beta
14.在即将提交时出现这样一个错误,git@github.com: Permission denied (publickey).解决办法(https://www.jianshu.com/p/f22d02c7d943)
15.最后push自己的分支到自己fork过的仓库中. --> git push upstream 3.0.0-beta
16.在开源项目https://github.com/MessageKit/MessageKit.git的pull request中添加自己刚修改过的文件, 点"comment pull request"即可.

下载链接

下载App重签名工具

参考链接

Mac Security工具使用总结
security命令
如何给开源项目贡献代码

Android加固之后Apk重签名

Android加固之后Apk重签名

本文转自 ganzhijie 并作补充

Java:jarsigner java自带的jar签名,也就是我们Android打包的v1签名,签名方案只能v1。
Android:apksigner Android特有的签名,也就是打包的v2签名,支持多种签名方案(v1~v4)。

需要注意的是,因为 apksigner 是Google在 Android 7.0 Nougat 推出的,所以我们的版本号的选择需要 >= 24.0.3 ,否则只能选择 jarsigner 方式打v1包。

如果您使用的是 apksigner,则必须在为 APK 文件签名之前使用 zipalign。如果您在使用 apksigner 为APK 签名之后对 APK 做出了进一步更改,签名便会失效。
如果您使用的是 jarsigner,则必须在为 APK 文件签名之后使用 zipalign。

下面是检查apk是否对齐的方法,打开终端输入:
zipalign -c -v 4 apk路径
例如:
zipalign -c -v 4 /android/tools/jiagu/apk/jiagu.apk

接下来就是利用终端,实现apk对齐操作,在打开的终端输入:
zipalign -v 4 「需要对齐操作的apk地址」 「对齐之后生成的地址」
例如:
zipalign -v 4 /android/tools/jiagu/apk/jiagu.apk /android/tools/jiagu/apk/zipaligned.apk
出现 “Verification succcessful” 为对齐成功。

下面是检查是否签名的终端语句,在打开的终端输入:

apksigner verify -v 检查的apk路径
例如:
apksigner verify -v /android/tools/jiagu/apk/zipaligned.apk

接下来继续在终端输入「apksigner」重签名语句

apksigner sign -verbose –ks 「jks文件路径」 –v1-signing-enabled (「true/false」v1打包开启/关闭) –v2-signing-enabled (「true/false」v2打包开启/关闭) -ks-key-alias (jks别名 key-alias) –ks-pass pass: (jks密码,key store password) –key-pass pass:(key 密码,key password) –out 「生成的apk路径,重签名后的」 「对齐之后的apk路径」
例如:
apksigner sign -verbose –ks /android/tools/jiagu/apk/ks/abc.keystore –v1-signing-enabled true –v2-signing-enabled true –ks-key-alias abc.keystore –ks-pass pass:123456 –key-pass pass:123456 –out /android/tools/jiagu/apk/signed.apk /android/tools/jiagu/apk/zipaligned.apk
终端运行结果如下:出现 Signed 则为重签名成功。

总结:apksigner签名流程。

加固并下载 ===> 检查是否对齐 ===> 对齐「zipalign」 ===> 检查是否对齐 ===> 检查apk是否签名 ===> 「apksigner」重签名 ===> 是否重签名成功

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

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

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

更新一下,现在完全有更简单的方式去拿到相关的数据了,示例可以看:拼图碎片:【实用教程】记Simplehook+Lspatch+Shizuku 免root使用教程 番茄为例

参考引用

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

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 密钥硬编码

灰产,赚点快钱?