【问题标题】:How to systematically populate a whitelist for a sandboxing program?如何系统地填充沙盒程序的白名单?
【发布时间】:2019-12-11 00:07:01
【问题描述】:

Lua 编程(第 4 版)的第 260-263 页,作者讨论了如何在 Lua 中实现“沙盒”(即运行不受信任的代码)。

在限制不受信任的代码可以运行的功能时,他建议采用“白名单方法”:

我们永远不应该考虑要删除哪些功能,而是要添加哪些功能。


这个问题是关于将这个建议付诸实践的工具和技术。(我想在这一点上会有一些混淆,我想先强调一下。)


作者给出以下代码作为基于允许功能白名单的沙盒程序的说明。 (我添加或移动了一些 cmets,并删除了一些空白行,但我已经从书中逐字复制了可执行内容。

-- From p. 263 of *Programming in Lua* (4th ed.)
-- Listing 25.6. Using hooks to bar calls to unauthorized functions

local debug = require "debug"
local steplimit = 1000    -- maximum "steps" that can be performed
local count = 0           -- counter for steps

local validfunc = {       -- set of authorized functions
  [string.upper] = true,
  [string.lower] = true,
  ... -- other authorized functions
}

local function hook (event)
  if event == "call" then
    local info = debug.getinfo(2, "fn")
    if not validfunc[info.func] then
      error("calling bad function: " .. (info.name or "?"))
    end
  end
  count = count + 1
  if count > steplimit then
    error("script uses too much CPU")
  end
end

local f = assert(loadfile(arg[1], "t", {}))  -- load chunk
debug.sethook(hook, "", 100)                 -- set hook
f()                                          -- run chunk

马上我就对这段代码感到困惑,因为钩子测试事件类型(if event == "call" then...),然而,当设置钩子时,只请求计数事件(debug.sethook(hook, "", 100))。所以,与validfunc的整个歌舞都是徒劳的。

也许这是一个错字。所以我尝试使用这段代码进行试验,但我发现将白名单技术付诸实践非常困难。下面的示例是我遇到的问题类型的非常简化的说明

首先,这里是作者的代码稍作修改的版本。

#!/usr/bin/env lua5.3
-- Filename: sandbox
-- ----------------------------------------------------------------------------
local debug = require "debug"

local steplimit = 1000    -- maximum "steps" that can be performed
local count = 0           -- counter for steps

local validfunc = {       -- set of authorized functions
  [string.upper] = true,
  [string.lower] = true,
  [io.stdout.write] = true,
  -- ...    -- other authorized functions
}

local function hook (event)
  if event == "call" then
    local info = debug.getinfo(2, "fnS")
    if not validfunc[info.func] then
      error(string.format("calling bad function (%s:%d): %s",
                          info.short_src, info.linedefined, (info.name or "?")))
    end
  end
  count = count + 1
  if count > steplimit then
    error("script uses too much CPU")
  end
end

local f = assert(loadfile(arg[1], "t", {}))     -- load chunk
validfunc[f] = true
debug.sethook(hook, "c", 100)                   -- set hook
f()                                             -- run chunk

第二个sn-p相对于第一个sn-p最显着的区别是:

  1. debug.sethook 的调用将"c" 作为掩码;
  2. 已加载块的f 函数被添加到validfunc 白名单中;
  3. io.stdout.write被添加到validfunc白名单中;

当我使用这个sandbox 程序运行如下所示的一行脚本时:

# Filename: helloworld.lua

io.stdout:write("Hello, World!\n")

...我收到以下错误:

% ./sandbox helloworld.lua
lua5.3: ./sandbox:20: calling bad function ([C]:-1): __index
stack traceback:
    [C]: in function 'error'
    ./sandbox:20: in function <./sandbox:16>
    [C]: in metamethod '__index'
    helloworld.lua:3: in local 'f'
    ./sandbox:34: in main chunk
    [C]: in ?

我尝试通过将以下内容添加到 validfunc 来解决此问题:

  [getmetatable(io.stdout).__index] = true,

...但我仍然遇到几乎相同的错误。我可以继续猜测并尝试添加更多内容,但这是我想避免的。


我有两个相关的问题:

  1. 我可以添加什么到validfunc 以便sandbox 将运行helloworld按原样)完成?
  2. 更重要的是,什么是系统化的方法来确定要添加到白名单表中的内容?

第 (2) 部分是这篇文章的核心。我正在寻找可以消除填充白名单表问题的猜测的工具/技术。

(我知道如果我将io.stdout:write 替换为print,我可以让helloworld 工作,在sandboxvalidfunc 中注册print,并将{print = print} 作为最后一个参数传递给loadfile,但这样做并不能回答一般问题,即如何系统地确定需要添加到白名单中以允许某些特定代码在沙箱中工作.)


编辑: 询问@DarkWiiPlayer 指出,calling bad function 错误是由调用未注册函数 (__index?) 触发的,这是对之前的响应的一部分attempt to index a nil value 错误。所以,这篇文章的问题都是关于系统地确定要添加到 validfunc 的内容以允许 Lua 正常发出 attempt to index a nil value 错误。

我应该补充一点,哪个函数的调用触发了导致calling bad function 错误消息的钩子执行的问题目前完全不清楚。此错误消息将错误归咎于 __index,但我怀疑这可能是一个红鲱鱼,可能是由于 Lua 中的错误。

为什么要怀疑 Lua 中的错误?如果我将sandbox 中的error 调用稍微更改为

      error(string.format("calling bad function (%s:%d): %s (%s)",
                          info.short_src, info.linedefined, (info.name or "?"),
                          info.func))

...那么错误信息如下所示:

lua5.3: ./sandbox:20: calling bad function ([C]:-1): __index (function: 0x55b391b79ef0)
stack traceback:
    [C]: in function 'error'
    ./sandbox:20: in function <./sandbox:16>
    [C]: in metamethod '__index'
    helloworld.lua:3: in local 'f'
    ./sandbox:34: in main chunk
    [C]: in ?

这并不奇怪,但如果现在我将helloworld.lua 更改为

# Filename: helloworld.lua

nonexistent()
io.stdout:write("Hello, World!\n")

...在sandbox下运行,错误信息变为

lua5.3: ./sandbox:20: calling bad function ([C]:-1): nonexistent (function: 0x556a161cdef0)
stack traceback:
    [C]: in function 'error'
    ./sandbox:20: in function <./sandbox:16>
    [C]: in global 'nonexistent'
    helloworld.lua:3: in local 'f'
    ./sandbox:34: in main chunk
    [C]: in ?

从这个错误信息中,我们可以断定nonexistent 是一个真正的函数;毕竟,它就在0x556a161cdef0!但我们知道nonexistent 名副其实:它不存在!

臭虫的味道肯定在空气中。触发钩子的函数可能真的应该从那些触发"c"-masked 钩子的函数中排除?尽管如此,在这种特殊情况下,对 debug.info 的调用似乎返回了不一致的信息(因为函数的名称 [例如 nonexistent] 显然与实际的函数对象 [例如function: 0x556a161cdef0] 应该触发了钩子)。

【问题讨论】:

  • 我刚刚回来更新我的答案,现在已经过了一段时间,并注意到我无法在这台 PC 上重现该问题。不知道为什么,但它实际上打印了一条对我有意义的错误消息,尽管我之前确实设法得到了你的奇怪错误。无论哪种方式,这只是 Lua 很奇怪,真正的问题出在其他地方;有关详细信息,请参阅我的答案。

标签: reflection lua hook sandbox introspection


【解决方案1】:

(底部有最终答案,请随意跳到&lt;hr&gt;这一行)

我会一步一步解释我的调试过程。

这是一个非常奇怪的现象。经过一些测试,我设法缩小了一点:

  • 由于您将{} 传递给加载,因此该函数在空环境下运行,因此io 实际上为零(而io.stdout 无论如何都会出错)
  • 尝试索引io(这是一个零值)时直接发生错误
  • 函数__index是一个C函数(见错误信息)

我的第一个直觉是 __index 在内部某处被调用。因此,为了找出它的作用,我决定看看它的当地人,希望能猜出它的作用。

我拼凑的一个快速辅助函数:

local function locals(f)
   return function(f, n)
      local name, value = debug.getlocal(f+1, n)
      if name then
         return n+1, name, value
      end
   end, f, 1
end

在引发错误的行之前插入:

      for idx, name, value in locals(2) do
         print(name, value)
      end
      error(string.format("calling bad function (%s:%d): %s", info.short_src, info.linedefined, (info.name or "?")))

这导致了一个有趣的结果:

(*temporary)    stdin:43: attempt to index a nil value (global 'io')
(*temporary)    table: 0x563cef2fd170
lua: stdin:29: calling bad function ([C]:-1): __index
stack traceback:
    [C]: in function 'error'
    stdin:29: in function <stdin:21>
    [C]: in metamethod '__index'
    stdin:43: in function 'f'
    stdin:49: in main chunk
    [C]: in ?

shell returned 1

为什么会有一个带有完全不同错误信息的临时字符串值?

顺便说一句,这个错误完全有道理; io 不存在,因为环境为空,因此对其进行索引显然会引发该错误。

老实说,这是一个非常有趣的错误,但我将把它留到这里,因为您正在学习该语言,这个提示可能足以让您自己弄清楚。这也是一个在更实际的环境中实际使用(并了解)debug 模块的好机会。


实际解决方案

过了一段时间后,我回来为这个问题添加一个适当的解决方案,但我真的已经这样做了。奇怪的错误报告只是 Lua 很奇怪。真正的错误是加载块时设置的空环境,正如我在上面的几段中提到的那样。

来自手册:

load (chunk [, chunkname [, mode [, env]]]) 加载一个块。

[...]

如果结果函数有上值,则第一个上值设置为 env 的值(如果给定了该参数),或者设置为全局环境的值。其他上值用 nil 初始化。 (当您加载一个主块时,生成的函数将始终只有一个上值,即 _ENV 变量(参见 §2.2)。但是,当您加载从函数创建的二进制块(参见 string.dump)时,生成的函数可以有任意数量的上值。)所有上值都是新鲜的,也就是说,它们不与任何其他函数共享。

[...]

现在,在“主块”中,即从文本 Lua 文件加载的块中,第一个(也是唯一的)upvalue 始终是该块的环境,所以它将在哪里寻找“全局”(this在 Lua 5.1 中略有不同)。由于传入的是空表,因此该块无法访问任何全局变量,例如 stringio

因此,当函数f() 尝试索引io 时,Lua 会抛出错误“尝试索引一个零值”,因为io 是零。无论出于何种原因,Lua 进行了一些内部函数调用,最终触发了黑名单,从而导致了一个新的错误,影响了前一个错误;如果不使用debug 库来获取有关调用堆栈的其他信息,这使得调试此错误非常不便并且几乎不可能。

当我在查看发出阻塞调用的函数的本地变量时注意到原始错误消息后,我最终才意识到这一点。

我希望这能解决问题:)

【讨论】:

  • 感谢您的回答。在阅读了您的原始答案后,我想出了您在附录中写的一些内容。即,钩子是由响应“尝试索引零值”错误而调用的函数触发的。我仍然想知道如何确定原始错误所抱怨的函数__index(以便我可以将其添加到validfunc)。但是,我怀疑使用普通的 Lua 是不可能的。可能需要在 gdb(或类似的)下运行 Lua 解释器。
  • 我开始怀疑 Lua 中存在错误。我已在我的帖子中添加了这方面的信息。
猜你喜欢
  • 2011-06-20
  • 1970-01-01
  • 2021-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-06
相关资源
最近更新 更多