从流程外部注入Node.js / V8动态代码

2021-01-30 15:44:59

远程调试很有趣。本文介绍了一种方法,该方法可通过启用远程检查器界面,然后使用Chrome调试协议来动态更改正在运行的Node.js进程的行为。

在Linux和MacOS上,可以将SIGUSR1信号发送到正在运行的Node.js进程。该过程将打开仅在本地接口上侦听的websocket服务器。通过连接到Websocket服务器,可以在Node.js进程上启动调试会话,从而将代码注入其中。最终,人们可以在从套接字断开连接之前先关闭websocket接口。

检测代码可能是我被要求执行的最酷的任务之一。 Sqreen for Node.js要求用户将代理(名为sqreen的npm软件包)导入其应用程序,以便它可以直接在该过程中提供安全功能。

最近,我决定挑战自己,看看我是否可以在检测已经运行的应用程序的约束下做同样的事情。接下来是一个有趣的Node.js远程调试示例。

注意:此博客文章中公开的方法不应在生产或任何实际产品中使用。本文仅描述了我用来通过Node.js中的远程调试获得有趣结果的黑客。但是,我在这里使用的一些工具可用于收集有关生产应用程序的特定数据。例如,看一下我关于内存泄漏调试的较早的文章。

如上一篇文章中所述,是否可以在以下情况下在正在运行的Node.js进程上启用调试器:

如果所有这些假设都成立,那么您可以将SIGUSR1信号发送到应用程序。这可以通过外壳完成:

至此,我们已经成功更改了Node.js进程的状态并启用了调试器。可以通过使用Google Chrome或Chromium并检查chrome:// inspect的内容来确认

这就是乐趣的开始。现在,该进程正在调试模式下运行,我们希望连接到该进程并开始使用Chrome DevTools协议查找http.Server的实例,然后将所需的内容注入其中。

为此,我们将使用chrome-remote-interface软件包:它将帮助我们将DevTools协议与友好的编程接口一起使用。对于我们来说,要使用Node.js的Chrome DevTools来完成我们想做的事情太先进了。

这里的目标是获取在进程中运行的http.Server实例上的指针,以稍后更改其状态。

我们之后使用的方法都是该域的一部分,因此我们不需要启用任何其他域。如果我们需要另一个,我们也需要首先启用它。

Runtime.evaluate命令在远程进程中运行任意表达式。换句话说,我们可以在连接到的Node.js进程中运行任何代码。在这里,我们执行以下操作:

我们使用includeCommandLineAPI标志,如果没有该标志,则不会向脚本环境提供require方法,并且执行将返回错误

该方法的返回值包含一个指向Node.js进程堆中http.Server原型的指针。我们将其传递给以下指令

这两个调用根据先前调用的结果为我们提供了指向http.Server的每个实例的指针。 ServerPrototypeResult.result.objectId是一个字符串,引用了require(' http')。Server.prototype的值。在此字符串上,我们调用Runtime.queryObjects,它返回一个指向数组的指针。此数组包含以http.Server.prototype作为原型的对象列表。

我们在此数组上调用Runtime.getProperties以获取该数组的属性列表。将有一个名为0的属性,该属性将指向我们要标识的HTTP服务器的实例(如果在使用同一原型的过程中有多个对象,则将有更多的编号属性)。

serverInstance包含一个字符串值,该字符串值是我们指向目标进程中运行的http.Server实例的指针!

假设我们可以使用HTTP服务器作为参数来运行一个函数,如何使它记录每个传入的请求?我提出以下功能:

在HTTP服务器的“请求”事件中获取侦听器列表

使用将为所有传入HTTP请求记录HTTP方法和URL的函数包装侦听器

现在,我们如何将其注入到http.Server实例上?好吧,让我们使用在本文前面的部分中找到的指针进行操作。为此,我们将以下代码添加到我们的远程调试器脚本中。

对Runtime.evaluate的首次调用将加载patchListeners函数并将其附加到进程。这是因为我们无法在Runtime.callFunctionOn上使用includeCommandLineAPI参数,因此在使用它时将永远不会定义require。

Runtime.callFunctionOn将使用由objectId参数定义的值来调用给定函数。

因此,我们将调用函数function(){process.patchListeners(this)},并将其作为serverInstance中指针所引用的值。 serverInstance是指向HTTP服务器的指针!

完成此操作后,我们调用一个脚本来删除添加到过程对象上的丑陋污染。

最后,我们可以使用以下命令禁用调试模式并与实例断开连接

还有注入器(我们假设patchListeners函数位于名为toInject.js的模块中):

当我们启动服务器并对服务器运行一些HTTP请求时,它什么也不记录。现在,当我们对其执行注入程序脚本(将其置于调试模式之后)时,它将生成以下日志:

请注意,即使没有日志显示,调试器也已被禁用,如果我们想重新连接到它,我们将需要再次发送USR1信号。

在本文中,我们使用了一个正在运行的Node.js HTTP服务器,并且从另一个本地Node.js进程中,我们能够向其中注入脚本以使其记录所有传入的HTTP请求。

这凸显了Chrome DevTools协议的强大功能:可以在运行过程中以编程方式更改任何内容。

我不确定该方法是否具有现实生活/生产用途,但是可以构建许多凉爽的工具来帮助调试和了解使用本文方法的Node.js进程如何工作。在Node.js中进行远程调试并使用如此出色的工具进行破解,总体而言非常有趣。