问题

最近因为一些原因,想要在python中通过Chrome的远程调试功能来实现Javascript的自动调试分析,但是文档中并没有说明详细各种配置的效果,导致再想实现某些功能的时候无法获得相关的信息。
本文将以在调试过程中查看调用栈内变量的信息为例说明一个可以复现所有Devtools中操作的方法
想要获得调用栈内的变量信息,首先就要获得栈的信息,通过观察文档,想法是通过调用如下接口来获得栈的信息

想要查看栈就需要传入栈ID,Debugger.paused事件的返回值中包含了StackTraceId

但是在实际的返回包中却不存在这个ID或者栈,比如下面代码的返回结果缺没有调用栈的信息,相关文档中也没说明该如何获得调用栈的信息

import pychrome

# 连接远程调试
remote_chrome = "http://127.0.0.1:9222"
browser = pychrome.Browser(url=remote_chrome)

# 新建标签页
tab = browser.new_tab()
tab.start()
tab.Page.enable()
tab.Debugger.enable()

def DebugPaused(**kw):
    with open("result.json","w", encoding="utf-8") as f:
        import json
        f.write(json.dumps(kw, indent=4))
    tab.Debugger.resume()
# 绑定调试函数
tab.Debugger.paused = DebugPaused
# 设置断点
tab.DOMDebugger.setXHRBreakpoint(url="")
# 打开网页
tab.Page.navigate(url="https://www.baidu.com/")

while True:
    input()

执行后Debugger.paused的返回

在查看文档和其他教程中也没有找到相关的方法,因此就只能另辟蹊径,通过抓包分析Devtools来复现相关的操作

方法

  1. 首先通过命令启动chrome浏览器并开启远程调试,启动浏览器后打开任意网页

chrome.exe --remote-debugging-port=9222

  1. 然后打开wireshark,这里直接抓回环网卡的数据包,并过滤协器中输入websocket来过滤数据包

    这里要先运行wireshark是因为要让wireshark能抓到整个远程调试的过程
    因为chrome的远程调试是通过websocket协议来实现,wireshark要能抓到websocket连接建立时的数据包才能获得连接使用的密钥,之后才能解析相关的ws数据包

  2. 然后在浏览器新建标签页,通过chrome://inspect进入远程调试界面

    在这里可以设置远程的信息,这里我们直接用localhost:9229,这样在后面的抓包步骤中我们可以通过Wireshark直接监听回环网卡来得到相关的数据包

  3. 在远程调试界面中通过inspec来连接远程调试

    远程调试界面如下
  4. 然后开始想要的调试操作,这里我们用下面的例子来进行
    1. 添加XHR/fetch 断点,当网页发送任意数据包时进行调试
    2. 然后刷新网页,然后查看相关的调用栈信息,这里我们从上往下随便点几个,好让我们之后分析数据包的规律
  5. 之后就可以停止抓包,然后分析数据包内容
    在分析阶段主要关注如下两个地方
    端口主要是我们分辨消息是由哪一方发送(9229为远程调试服务发送),而其他端口就是远程调试客户端
    最后面的text data就是通信内容,是我们主要关注的地方

    数据部分分为三个字段:id、method和params,id表示本次请求的id,通过id来定位对应的响应;method是调用的方法;params就是传入的参数了
    我们找id=2的响应包,正常的响应包包含两个字段id和result,result表示方法的执行结果
    1. 首先我们看到上面通信内容的method中的Netowrk.enable,含义就是启用Network的事件报告,我们需要使用其他事件类型时也要先将事件报告启用。说直白一点就是要调用哪种类型的事件就要先将类型启用。
    2. 接下来重点找调试相关的数据包。
      首先是将调试类型的报告启用
      atus=done&style=none&taskId=u87523f04-ec8c-4bc3-9eee-519c8f57559&title=&width=1053)
      然后就是一些列调试的配置操作,相关的方法含义可以通过文档查看
      1. 定义在何时暂停
      2. 启用异步调用堆栈跟踪,并设置栈的深度
      3. 设置跳过步进和暂停的配置
      4. 我们在第5步a中添加的断点
      5. 然后我们找Debugger.paused相关的数据包,这里因为数据包比较多,直接用wireshark的搜索功能来查找


        查看数据内容,发现虽然包含了asyncStackTrace的内容,但是并没有包含每一层栈的变量信息,因此继续往后找数据包
      6. 往后找,发现如下请求

        和如下响应

        将数据提取出来查看

        可以看到通过Runtime.getProperties方法得到了的信息好像就是变量
      7. 回到Devtools,查看第一层调用栈,发现这些信息对于的就是调用栈第一层的局部变量信息
      8. 现在回过头去分析请求的包的参数

        可以看到最主要的参数就是objectId,现在往回找这个objectId,发现是在Debugger.paused方法中的callFrames结构中获取

        callFrames由callFrame字段含义如下

        其中scopeChain各字段含义如下
      9. 通过分析不难知道,我们想要获取调用栈中的变量信息其实并不要那么麻烦,可以直接通过Debugger.paused返回结果callFrames中的每一个callFrame来获取,并且callFrames中callFrame也是由栈顶往栈底排序(多观察几个websocket包即可发现)
  6. 那么我们想要在python中通过远程调试获得每层栈的信息并不用通过Debugger.getStackTrace这么复杂,在得到Debugger.paused事件的返回结果后,可以直接通过Runtime.getProperties来获得每一层调用栈中不同变量的信息

根据上面的分析结果改写代码如下

import pychrome

# 连接远程调试
remote_chrome = "http://127.0.0.1:9222"
browser = pychrome.Browser(url=remote_chrome)

# 新建标签页
tab = browser.new_tab()
tab.start()
tab.Page.enable()
tab.Debugger.enable()
tab.Runtime.enable()


def GetObject(objectId):
    result = tab.Runtime.getProperties(
        objectId=objectId,
        ownProperties=False,
        accessorPropertiesOnly=False,
        generatePreview=True,
        nonIndexedPropertiesOnly=False,
    )
    return result["result"]


def DebugPaused(**kw):
    callFrames = kw.get("callFrames")
    result = dict()
    for callFrame in callFrames:
        result[callFrame["callFrameId"]] = dict()
        # 遍历每一个调用栈
        # 先获得范围内的变量集合
        scopeChain = callFrame["scopeChain"]
        for _vars in scopeChain:
            # 这里只获取局部变量
            if _vars["type"] == "local":
                _objectId = _vars["object"]["objectId"]
                _object = GetObject(_objectId)
                result[callFrame["callFrameId"]][_objectId] = _object
    with open("result.json", "w", encoding="utf-8") as f:
        import json
        f.write(json.dumps(kw, indent=4))
    with open("stack.json", "w", encoding="utf-8") as f:
        import json
        f.write(json.dumps(kw, indent=4))
    tab.Debugger.resume()


# 绑定调试函数
tab.Debugger.paused = DebugPaused
# 设置断点
tab.DOMDebugger.setXHRBreakpoint(url="")
# 打开网页
tab.Page.navigate(url="https://www.baidu.com/")

while True:
    input()

结果

但是查看Debugger.paused的数据部分并没有如同在Devtools中的asyncStackTrace数据

因此再根据分析Devtools流量,再添加如下一些设置

tab.Debugger.enable(maxScriptsCacheSize=10000000)
tab.Debugger.setAsyncCallStackDepth(maxDepth=50)

可以看到我们再python中也能获得asyncStackTrace信息

最后的完整代码如下

import pychrome

# 连接远程调试
remote_chrome = "http://127.0.0.1:9222"
browser = pychrome.Browser(url=remote_chrome)

# 新建标签页
tab = browser.new_tab()
tab.start()
tab.Page.enable()
tab.Debugger.enable()
tab.Runtime.enable()
tab.Debugger.enable(maxScriptsCacheSize=10000000)
tab.Debugger.setAsyncCallStackDepth(maxDepth=50)

def GetObject(objectId):
    result = tab.Runtime.getProperties(
        objectId=objectId,
        ownProperties=False,
        accessorPropertiesOnly=False,
        generatePreview=True,
        nonIndexedPropertiesOnly=False,
    )
    return result["result"]


def DebugPaused(**kw):
    callFrames = kw.get("callFrames")
    result = dict()
    for callFrame in callFrames:
        result[callFrame["callFrameId"]] = dict()
        # 遍历每一个调用栈
        # 先获得范围内的变量集合
        scopeChain = callFrame["scopeChain"]
        for _vars in scopeChain:
            # 这里只获取局部变量
            if _vars["type"] == "local":
                _objectId = _vars["object"]["objectId"]
                _object = GetObject(_objectId)
                result[callFrame["callFrameId"]][_objectId] = _object
    with open("result.json", "w", encoding="utf-8") as f:
        import json
        f.write(json.dumps(kw, indent=4))
    with open("stack.json", "w", encoding="utf-8") as f:
        import json
        f.write(json.dumps(kw, indent=4))
    tab.Debugger.resume()


# 绑定调试函数
tab.Debugger.paused = DebugPaused
# 设置断点
tab.DOMDebugger.setXHRBreakpoint(url="")
# 打开网页
tab.Page.navigate(url="https://www.baidu.com/")

while True:
    input()

总结

虽然并没有准确的文档告诉我们该如何通过远程调试的接口实现各种Devtools中的功能,但是我们可以通过抓包的方式分析流量来轻松查看Devtools实现部分功能的时候执行了那些配置和操作,这样我们就可以通过远程调试协议复现这些配置并来编写自动化脚本帮我们完成更多的工作。

参考文档

远程调试接口文档:https://chromedevtools.github.io/devtools-protocol/