前端加密对抗-通过CDP远程调用Debug断点函数

前端加密对抗-通过CDP远程调用Debug断点函数

通过CDP远程调用Debug断点函数的作用

大多时候,我们在进行断点调试网页算法的时候,会跟踪到很多莫名其妙的自己写的算法,或者是做了很多混淆的代码,这些我们自己手动转换成其他语言的算法是比较麻烦的,淘神费力。

这个时候可以通过打一个Debug断点在我们可以调用加密/解密函数的地方,然后通过CDP协议进行调用。

分析CDP协议

  1. 在devtools设置中打开Protocol Monitor用于监测cdp协议的调用记录

    image-20230209100003519

  2. 只看几个关键的cdp协议请求/响应内容。

  3. Debugger.paused 官方文档说明 此方法会返回当前Debug触发时,暂停点的callFrame信息

image-20230209101318874

  1. 当我们debug在这个点,并且在console输入命令时,我们是可以调用当前暂停断点位置的所有函数作用域内的函数,此时调用时候的CDP内容为Debugger.evaluateOnCallFrame方法,请求内容包含一个expression为执行的表达式内容

image-20230209101354964

工具实现

基于以上协议的分析,我们可以自己实现一个基于当前断点位置的函数调用,从而实现一个直接调用加密/解密函数的小工具。

伪代码

1
2
3
4
5
6
7
8
9
onEvents(`Debugger.paused`, function(event) {
for(var callFrame in event.callFrames) {
// 遍历所有的callfram信息
}

// 提取某个callframe调用
var resp = CDPSend(`Debugger.evaluateOnCallFrame`, { callFrameId: event.callFrames[0], expression: 'someEncryptFunction(xx)' })
println(resp)
})

实战

1. 当前数据包的请求和响应包都是加密的状态

image-20230209101549621

2. 查找下断点的地方

通过Networks查看请求触发的函数调用栈

image-20230209101957850

过去下断点并且触发

image-20230209102156209

跟踪几次发现了加密算法的函数调用点,此处虽然用的是SM4加密,不过还有其他函数在处理,手动还原算法的太过于麻烦。

image-20230209102304174

通过旁边的Call Stack调用栈回溯到上一个地方可以看到当前函数的调用方法(图中e.b这个地方是上图所示的地方,点击上一个也就是下图的(anonymous)地方)

image-20230209102518230

可以看到这个函数作用域下的函数加密方法为 Object(ht.b), 第一个参数为待加密的字符串,第二个参数为一个常量。我们在此处打一个debug断点,然后触发到这里

image-20230209102643747

测试加密

image-20230209102818521

3. 工具演示

工具可以自己通过以上介绍的流程来研究并且编写辅助脚本,本质还是通过CDP协议控制。 此工具之后更加完善之后再进行开源

  1. 工具参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  cmd ./remotejs_darwin_amd64 -h
NAME:
remotejs_darwin_amd64 - A new cli application

USAGE:
remotejs_darwin_amd64 [global options] command [command options] [arguments...]

COMMANDS:
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--chromepath value, --cp value browser bin path
--devtools enable devtools (default: false)
--remote-debug-port value, --rp value browser remote debug port, like ws://127.0.0.1:9999
--headless enable headless (default: false)
--web-port value web api port (default: "8002")
Request API 0.0.0.0:8002/remote, [POST] eval=
  1. 打开浏览器(有内置的chrome也可以通过连接远程remote debug port来进行操作)
  2. 在浏览器中找到之前找到的断点并且触发
  3. 远程调用, 此处通过eval参数可以传递我们的数据到debug处的console进行调用,这里和我们手动执行console是一样的结果。其中的变量都是我们可以自己改变的。

image-20230209103840583

remotejs工具的日志
image-20230209103956462

4. 结合mitmdump完成整个加密解密流程

  1. 加密函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def request_remote_js(self, data: str):
    data = data.replace(" ", "")
    r = requests.post(
    "http://127.0.0.1:8002/remote",
    data={
    "eval": "Object(ht.b)('{}', _dyn$.t(622))".format(data)
    }
    )
    message = r.json().get("message", None)

    if message:
    return message
    return ""
  2. 解密函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def request_remote_js_decrypt(self, data: str):
    data = data.replace(" ", "")
    r = requests.post(
    "http://127.0.0.1:8002/remote",
    data={
    "eval": "Object(ht.a)('{}')".format(data)
    }
    )
    message = r.json().get("message", None)

    if message:
    return message
    return ""
  3. 调用流程

1
2
Burp -> Mitmproxy -- Request ---> 加密(Request Body)
<--- Response -- 解密(Response Body)
  1. 完整代码
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

class RemoteJsDemo(AutoDecoderClass):

def request_remote_js(self, data: str):
data = data.replace(" ", "")
r = requests.post(
"http://127.0.0.1:8002/remote",
data={
"eval": "Object(ht.b)('{}', _dyn$.t(622))".format(data)
}
)
message = r.json().get("message", None)

if message:
return message
return ""

def request_remote_js_decrypt(self, data: str):
data = data.replace(" ", "")
r = requests.post(
"http://127.0.0.1:8002/remote",
data={
"eval": "Object(ht.a)('{}')".format(data)
}
)
message = r.json().get("message", None)

if message:
return message
return ""

def request(self, flow: http.HTTPFlow):
resp = self.request_remote_js(flow.request.content.decode("utf-8"))
flow.request.content = json.dumps({"encryptedData": resp}).encode("utf-8")

def response(self, flow: http.HTTPFlow):
resp = json.loads(flow.response.content.decode("utf-8")).get("data", None)

if resp:
flow.response.content = str(self.request_remote_js_decrypt(resp)).encode("utf-8")

  1. 最后的效果, 请求和响应都是解密操作的状态下进行

image-20230209104841775

References