首页
社区
课程
招聘
[旧帖] [原创]Lua 破解指南(一)(二) 0.00雪花
发表于: 2014-10-12 18:39 13110

[旧帖] [原创]Lua 破解指南(一)(二) 0.00雪花

2014-10-12 18:39
13110
目录  
  • 前言
  • 一 跟踪,脱壳,解包,解密
  • 二 标准化 luac
  • 三 反汇编,阅读,指令级修改
  • 四 反编译,函数级修改
  • 五 luac 函数替换


  • 前言

      当今的手游,十个有九个是 lua 做的,其中七个是 coco2d-x 的。很多其他程序嵌入的脚本也直接用了 lua。为了保密,资源文件中 lua 脚本基本都是已加密的字节码文件,还有许多是非标准字节码。此时破解便是难题,本文便为此提供一些破解思路。

      本文适用于 lua5.1,大部分适用于 5.0 5.2 (部分指令与格式细节除外),对 luajit 1.x 和 2.x 不太适用。
      目前最广泛使用的也应该是 lua5.1 版本。coco2d-x 用的也是 5.1的语法,可选官方版本和luajit版本虚拟机。

    程序编译时的处理流程如下:
     lua源文件--obfuscate-->lua源文件(混淆后)--compile-->luac文件(带调试用的变量名和行号)--strip-->luac文件(不带调试信息)
     lua/luac--加密/打包-->资源文件
      obfuscate/混淆:变量名函数名替换,不可还原,只能算小困难,而且目前用的人不多。
      compile/编译:lua源文件编译为字节码文件,此文以后称之为 luac文件。而且很多是非标准的字节码格式。即使是标准的字节码,也只能保证反汇编成功,反编译技术不够成熟。基本上所有用 lua 的手游都做了这一步。
      strip:去除调试信息,删除所有 local upval 名,作用范围,调试用的指令对应源文件行号。对反编译是巨大的障碍。目前用的人也不多。

    程序运行时流程很简单:
     资源文件--解包/解密-->lua/luac-->lua虚拟机

    用到的源码与工具:
     lua-5.1 源码
     ChunkSpy
     LuaAssemblyTools
     luadec51

    一 跟踪,解包,解密
     目的是得到 luac文件,不管是不是标准格式。
     这里我只能提供思路。我见过 简单的异或加密,密码加密zip,AES加密。
     追 luaL_loadbuffer , luaL_loadfile , lua_load 这三个函数,应该可以找到解密函数。
     hook 这3个函数,可以直接导出 luac 文件。如果没编译甚至是 lua源文件。
     hook luaV_execute 可以拿到 Proto* ,如果能用 luaU_dump 导出的话,就是 luac文件。
     hook 抓数据效率比较低,还很难所有脚本都被执行。最好是找到解密函数,然后自己做解包程序,甚至打包程序。
     例如:
      aes加密,gdb内存抓key Angry Birds Rio encrypts settings/highscores with AES
      xor加密 【求助】某热门游戏 android so部分解密算法
      hook luaV_execute【原创】用LuaDec逆回LUA脚本内容
    二 标准化 luac
     目的是将非标准 luac 转化为标准格式。
     有两种方案来处理得到的 luac  
    • 得到一个能够执行上述非标 luac 的 lua 版本
    •   找到 lua51.dll liblua51.so 之类,编程直接调用。只能用于执行几个关键lua函数。或者导出 luac 对比文件格式。
        改 lua 源文件,编译出一个。这样做什么都可以,还能编译出对应的 luadec 。
        这就必须要搞清楚非标 lua 是如何修改的。

    • 将非标 luac 转换成标准格式 luac
    •   就可以用标准 lua 工具了,反汇编,汇编,反编译,都能用。

     常用 lua 修改方法  
    • 改文件头
    •   改后的luac文件,用标准 lua 程序载入时显示“ bad header ”
        最简单的修改方法,打开二进制文件可以观察出,很容易改好。
        例如:【原创】Corona SDK的iphone游戏存档校验分析一例
      在 lua.h 中修改
      #define LUA_SIGNATURE   "\033Lua"
      


    • 改数字类型
    •   改后的luac文件,用标准 lua 程序载入时显示“ bad header ”
        lua 只有一种数字类型,可以在编译时更改。默认是 8位 double, 一般可以改成 4位 float, 4位 int 和 8位 long long
        chunkspy 程序对此方法基本免疫。
        例如:前面提到的Angry Birds Rio,数字类型就改成 float了
      在 luaconf.h 中修改
      #define LUA_NUMBER_DOUBLE
      #define LUA_NUMBER double
      
      其他的一些格式化和转化用函数最好也修改。例如,改 float 时
      LUA_NUMBER to float
      LUA_NUMBER_SCAN to "%f"
      LUA_NUMBER_FMT to "%.7g"
      l_mathop(x) to (x##f)
      lua_str2number to use strtof

    • OpCode 重定义
    •   用标准 lua 程序载入时显示“ bad code ”
        破解非常麻烦
        例如:
         简单交换4跳指令记一款宽带拨号器加密算法逆向实例
         某雷 B**T ui 基本每条指令都改了
         J*2+1 早期版本用了 cnumber 补丁,加了2条指令,结果导致指令非标准
        改 lopcodes.h 和 lopcodes.c 的这些地方,可以调换指令的顺序,也可以插入几个没用的占位指令,注意指令最多 64 个
      // 改有 "ORDER OP" 的地方
      /* grep "ORDER OP" if you change these enums */
      enum OpCode; // lopcodes.h
      const lu_byte luaP_opmodes[]; // lopcodes.c
      const char *const luaP_opnames[]; // lopcodes.c 
      


     那么,怎么知道非标 lua 是如何修改的呢?
      一是观察法,二进制编辑器打开一个luac文件,结合 lua 源代码人工分析。
      二是对比法,对比下面的 lua 源文件,allopcodes.lua,在标准lua编译和非标lua编译后的差别。
    local u1,u2,u3
    function f1(a1,a2,...)
    	local l0 = a1; -- move
    	local l1 = 1 -- loadk
    	local l2 = true -- loadbool
    	local l3 = nil -- loadnil
    	local l4 = u1[g1] -- gettupval, getglobal, gettable
    	g1 = l1 -- setglobal
    	u2 = l2 -- setupval
    	l3[l2] = l1 -- settable
    	local l5 = { -- newtable
    		l1, l2; -- move, setlist
    		x = l2 -- settable
    	}
    	local l6 = l5:x() -- self, call
    	local l7 = -((l0+l1-l2)*l3/l4%l5)^l6 -- add, sub, mul, div, mod, pow, unm
    	local l8 = #(not l7) -- not, len
    	local l9 = l7..l8 -- concat
    	if l1==l2 and l2<l3 or l3<=l4 then -- eq, lt, le, jmp
    		for i = 1, 10, 2 do -- forprep
    			l0 = l0 and l2 -- test
    		end -- forloop
    	else -- jmp
    		for k,v in ipairs(l5) do
    			l4 = l5 or l6 -- testset
    		end -- tforloop
    	end
    	do
    		local l21, l22 = ... -- vararg
    		local function f2() -- closure
    			return l21, l22
    		end
    		f2(k,v) -- call
    	end --close
    	return f1() -- return, tailcall
    end
    

     如何让被破解程序中的lua载入上面的程序,并导出呢?
      1. 如果你有办法让程序执行下面的 C 代码的话,参考 lua 源代码里面的 luac.c 写入文件,或者 lstrlib.c 的 str_dump 写入内存
    lua_State* L = luaL_newstate();
    luaL_loadbuffer or luaL_loadstring or luaL_loadfile
    Closure* c=(Closure*)lua_topointer(L, -1);
    Proto* f = c->l.p;//Closure如果改了偏移会不同
    luaU_dump(L, f, writer, D, 0);//非导出函数, string.dump 
    lua_close(L);

      2. 让程序执行 lua 代码 导出到文件或者string
       可以 hook luaL_loadbuffer 然后偷天换日
    -- luaopen_string 此库必须打开
    allopcodes() allopcodes.lua文件内容 end
    chunk = string.dump(allopcodes)
    if luaopen_io then --如果 io 可用,则可以直接写文件
        file:write(chunk)
    else
        output(chunk,to_any_where) --想办法把 chunk 搞出来,直接读内存也行
        return chunk
    end


    下面是一段很长的 luac 标准格式分析,列出修改 lua虚拟机后可能变化的地方。用 chunkspy 生成,加了中文注释
    Pos   Hex Data           Description or Code
    ------------------------------------------------------------------------
    0000                     ** source chunk: localetest.lua
                             ** global header start **
    0000  1B4C7561           header signature: "\27Lua" 改文件头就是改这里 #define LUA_SIGNATURE "\033Lua" 
    0004  51                 version (major:minor hex digits) 版本号,显眼的 LuaQ,5.2就是 LuaR
    0005  00                 format (0=official)
    0006  01                 endianness (1=little endian) 这4个字节应该不会改,可以作为定位依据
    0007  04                 size of int (bytes)          如果改了这边,就是放弃了完整的校验
    0008  04                 size of size_t (bytes)
    0009  04                 size of Instruction (bytes)
    000A  08                 size of number (bytes) 改数字类型就是这两个字节,double 0800, float 0400, int 0401, longlong 0801
    000B  00                 integral (1=integral)  是否整数
                             * number type: double
                             * x86 standard (32-bit, little endian, doubles)
                             ** global header end **
    
    000C                     ** function [0] definition (level 1) 顶层有且只有一个函数
                             ** start of function **
    000C  0F000000           string size (15) 函数对应文件名,标准字符串格式,前4个字节是长度,可以看出是大头big endian的
    0010  6C6F63616C657465+  "localete"       虽然 lua 字符串可以包含 \0 也不要求以 \0 结尾
    0018  73742E6C756100     "st.lua\0"       但 dump 时会加一个 \0结尾, load时会忽略
                             source name: localetest.lua
    001F  00000000           line defined (0)
    0023  00000000           last line defined (0)
    0027  00                 nups (0)
    0028  00                 numparams (0)
    0029  02                 is_vararg (2)
    002A  02                 maxstacksize (2)
                             * code:      指令区,当做过 OpCode 重定义 后,chunkspy 很可能在这里失败
    002B  03000000           sizecode (3) 指令数量
    002F  01000000           [1] loadk      0   0        ; R0 := "a"
    0033  64000000           [2] closure    1   0        ; R1 := closure(function[0]) 0 upvalues
    0037  1E008000           [3] return     0   1        ; return
                             * constants: 常量区开始
    003B  01000000           sizek (1)    常量数量
    003F  04                 const type 4 一个字节的常量类型,理论上有4种,luac只会出现 number=3 ,string=4,nil 和 boolean 不会出现
    0040  02000000           string size (2) 标准字符串格式
    0044  6100               "a\0"
                             const [0]: "a"
                             * functions: 子函数区,递归
    0046  01000000           sizep (1)    子函数数量
    
    004A                     ** function [0] definition (level 2) 第一个子函数
                             ** start of function **
    004A  00000000           string size (0)  函数对应文件名,空串,长度为0,只有长度,没有对应数据
                             source name: (none)
    004E  03000000           line defined (3)
    0052  05000000           last line defined (5)
    0056  00                 nups (0)
    0057  00                 numparams (0)
    0058  03                 is_vararg (3)
    0059  02                 maxstacksize (2)
                             * code:
    005A  03000000           sizecode (3)
    005E  65000000           [1] vararg     1   0        ; R1 to top := ...
    0062  5E000000           [2] return     1   0        ; return R1 to top
    0066  1E008000           [3] return     0   1        ; return
                             * constants: 常量区
    006A  00000000           sizek (0)    只有长度0,没有数据
                             * functions: 子函数区
    006E  00000000           sizep (0)    只有长度0,没有数据
                             * lines:     指令地址对应源文件行号, strip 时会清空,只留下数量0
    0072  03000000           sizelineinfo (3)
                             [pc] (line)
    0076  04000000           [1] (4)
    007A  04000000           [2] (4)
    007E  05000000           [3] (5)
                             * locals:     local 变量名, strip 时会清空,只留下数量0
    0082  01000000           sizelocvars (1)
    0086  04000000           string size (4) 标准字符串
    008A  61726700           "arg\0"
                             local [0]: arg
    008E  00000000             startpc (0)    跟着2个字节的变量作用范围
    0092  02000000             endpc   (2)
                             * upvalues:
    0096  00000000           sizeupvalues (0) upval 变量名, strip 时会清空,只留下数量0
                             ** end of function [0] definition (level 2) ** 一个函数结束了
    
                             * lines:         顶层函数的其他数据,格式见上面
    009A  03000000           sizelineinfo (3)
                             [pc] (line)
    009E  01000000           [1] (1)
    00A2  05000000           [2] (5)
    00A6  05000000           [3] (5)
                             * locals:
    00AA  02000000           sizelocvars (2)
    00AE  02000000           string size (2)
    00B2  6100               "a\0"
                             local [0]: a
    00B4  01000000             startpc (1)
    00B8  02000000             endpc   (2)
    00BC  02000000           string size (2)
    00C0  6600               "f\0"
                             local [1]: f
    00C2  02000000             startpc (2)
    00C6  02000000             endpc   (2)
                             * upvalues:
    00CA  00000000           sizeupvalues (0)
                             ** end of function [0] definition (level 1) ** 顶层函数结束,没了
    
    00CE                     ** end of chunk **
    


    luac格式,也可以参考 【原创】Lua脚本反编译入门之一 ,这篇主要分析的是5.2版,不过跟5.1差别不大。

    格式分析里我们可以看见 “改文件头”和“改数字类型”的影响。“OpCode 重定义”就不太容易看出了。所以,前面我们想方设法导出了 allopcodes.lua 的编译后数据,要拿来跟标准 lua 编译后的对比。
    allopcodes.lua 这个程序包含3个函数,顶层的一个是主函数,主函数包含的第一个函数,是我们要对比的对象。
    标准 lua 格式是这样的,66条指令,包含 lua 5.1 所有的38种指令。
    然后我们知道,lua指令格式是 4个字节,对 big endian/大头 ,标准lua就是这种,第1个字节的低6位,是操作符。
    那么,这样也就能对比出“OpCode 重定义”到底是怎么做的了。
    0063  42000000           sizecode (66)
    0067  C0000000           [01] move       3   0        ; R3 := R0
    006B  01010000           [02] loadk      4   0        ; R4 := 1
    006F  42018000           [03] loadbool   5   1   0    ; R5 := true
    0073  83010003           [04] loadnil    6   6        ; R6,  := nil
    0077  C4010000           [05] getupval   7   0        ; R7 := U0 , u1
    007B  05420000           [06] getglobal  8   1        ; R8 := g1
    007F  C6018203           [07] gettable   7   7   8    ; R7 := R7[R8]
    0083  07410000           [08] setglobal  4   1        ; g1 := R4
    0087  48018000           [09] setupval   5   1        ; U1 := R5 , u2
    008B  89018102           [10] settable   6   5   4    ; R6[R5] := R4
    008F  0A420001           [11] newtable   8   2   1    ; R8 := {} , array=2, hash=1
    0093  40020002           [12] move       9   4        ; R9 := R4
    0097  80028002           [13] move       10  5        ; R10 := R5
    009B  09420181           [14] settable   8   258 5    ; R8["x"] := R5
    009F  22420001           [15] setlist    8   2   1    ; R8[1 to 2] := R9 to R10
    00A3  4B824004           [16] self       9   8   258  ; R10 := R8; R9 := R8["x"]
    00A7  5C820001           [17] call       9   2   2    ; R9 := R9(R10)
    00AB  8C028101           [18] add        10  3   4    ; R10 := R3 + R4
    00AF  8D420105           [19] sub        10  10  5    ; R10 := R10 - R5
    00B3  8E820105           [20] mul        10  10  6    ; R10 := R10 * R6
    00B7  8FC20105           [21] div        10  10  7    ; R10 := R10 / R7
    00BB  90020205           [22] mod        10  10  8    ; R10 := R10 % R8
    00BF  91420205           [23] pow        10  10  9    ; R10 := R10 ^ R9
    00C3  92020005           [24] unm        10  10       ; R10 := -R10
    00C7  D3020005           [25] not        11  10       ; R11 := not R10
    00CB  D4028005           [26] len        11  11       ; R11 := #R11
    00CF  00030005           [27] move       12  10       ; R12 := R10
    00D3  40038005           [28] move       13  11       ; R13 := R11
    00D7  15430306           [29] concat     12  12  13   ; R12 := R12..R13
    00DB  17400102           [30] eq         0   4   5    ; R4 == R5, to [32] if true
    00DF  16400080           [31] jmp        2            ; to [34]
    00E3  58808102           [32] lt         1   5   6    ; R5 < R6, to [34] if false
    00E7  16400080           [33] jmp        2            ; to [36]
    00EB  19C00103           [34] le         0   6   7    ; R6 <= R7, to [36] if true
    00EF  16000280           [35] jmp        9            ; to [45]
    00F3  41030000           [36] loadk      13  0        ; R13 := 1
    00F7  81C30000           [37] loadk      14  3        ; R14 := 10
    00FB  C1030100           [38] loadk      15  4        ; R15 := 2
    00FF  60830080           [39] forprep    13  3        ; R13 -= R15; PC := 43
    0103  DA000000           [40] test       3       0    ; if R3 then to [42]
    0107  16000080           [41] jmp        1            ; to [43]
    010B  C0008002           [42] move       3   5        ; R3 := R5
    010F  5FC3FE7F           [43] forloop    13  -4       ; R13 += R15; if R13 <= R1
    4 then begin PC := 40; R16 := R13 end
    0113  16000280           [44] jmp        9            ; to [54]
    0117  45430100           [45] getglobal  13  5        ; R13 := ipairs
    011B  80030004           [46] move       14  8        ; R14 := R8
    011F  5C030101           [47] call       13  2   4    ; R13 to R15 := R13(R14)
    0123  16800080           [48] jmp        3            ; to [52]
    0127  DB410004           [49] testset    7   8   1    ; if R8 then R7 = R8 else to [51]
    012B  16000080           [50] jmp        1            ; to [52]
    012F  C0018004           [51] move       7   9        ; R7 := R9
    0133  61830000           [52] tforloop   13      2    ; R16, R17 := R13(R14,R15); if R16 ~= nil then R15 := R16 else PC := 54
    0137  1680FE7F           [53] jmp        -5           ; to [49]
    013B  65038001           [54] vararg     13  3        ; R13, R14 := ...
    013F  E4030000           [55] closure    15  0        ; R15 := closure(function[0]) 2 upvalues
    0143  00008006           [56] move       0   13       ; R0 := R13
    0147  00000007           [57] move       0   14       ; R0 := R14
    014B  00048007           [58] move       16  15       ; R16 := R15
    014F  45840100           [59] getglobal  17  6        ; R17 := k
    0153  85C40100           [60] getglobal  18  7        ; R18 := v
    0157  1C448001           [61] call       16  3   1    ;  := R16(R17, R18)
    015B  63030000           [62] close      13   ; SAVE all upvalues from R13 to top
    015F  45030200           [63] getglobal  13  8        ; R13 := f1
    0163  5D038000           [64] tailcall   13  1   0    ; R13 to top := R13()
    0167  5E030000           [65] return     13  0        ; return R13 to top
    016B  1E008000           [66] return     0   1        ; return
    


    通过上面做的文件分析对比,我们已经可以编译出 能够执行非标准luac的 lua 虚拟机了。

    但是如果你还想做 luac 格式转换的话,对于只用了“OpCode重定义”的,你可以载入,修改指令,写出。
    具体实现参考一下 luac.c ,里实现的 载入 和 写出。修改指令参考 【原创】Lua脚本反编译入门之一 , 记得递归修改所有的函数。
    还有载入时一定要跳过指令检查,也就是输出 bad code 的地方。这样改:
    lundump.c 的 LoadFunction , 注释下面一句
    IF (!luaG_checkcode(f), "bad code");
    


    “二 标准化 luac ”结束。现在应该能做到:
    编译出 能够执行非标准luac的 lua 虚拟机
    标准 luac 和 非标准 luac 之间格式转换的程序,应该能写了

    [培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

    收藏
    免费 0
    支持
    分享
    最新回复 (3)
    雪    币: 2105
    活跃值: (424)
    能力值: ( LV4,RANK:50 )
    在线值:
    发帖
    回帖
    粉丝
    2
    我顶啊 写的不错
    2014-10-12 19:16
    0
    雪    币: 71
    活跃值: (20)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    3
    很好,学习中,支持楼主
    2014-10-12 20:13
    0
    雪    币: 40
    活跃值: (14)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    4
    第二部分 标准化 luac 写完了
    2014-10-13 22:27
    0
    游客
    登录 | 注册 方可回帖
    返回
    //