whklhh 发表于 2018-8-23 16:36

网鼎杯第二场Reverse

本帖最后由 whklhh 于 2018-8-30 10:31 编辑

刚结束的第二场网鼎杯,有两个lua的逆向(外加一个go的宿主程序),Solves也比较少,感觉还是比较值得深挖的
如果有理解错的还请dalao不吝赐教~
### game
IDA打开main函数啥有用的信息都看不到
运行一下发现是个八数码问题,要求解10000000次

根据提示符去IDA的strings窗口找可以发现内存中存在这部分数据,但是没有交叉引用
![](https://i.imgur.com/JkV1plU.png)
说明该字符串在汇编层面是没有引用的


一般来说,用户添加的信息会放在一起,所以可以在提示字符串上下翻一翻,果然找到一点有意思的东西
![](https://i.imgur.com/EXW6Vgx.png)
这个\x1BLuaQ很引入注目,大概率就是lua逆向了
去查一下`1B 4C 75 61`,可以看到是Lua的字节码文件头
那么再用Lua搜一下
![](https://i.imgur.com/lUNVGGi.png)
可以获得版本号

或者通过文件解析也可以知道后一个字节`51`代表版本号

由此可见这个程序是lua打包生成的exe,实际上就是自带一个Lua解释器和脚本

那么把脚本抓下来然后反编译即可
稍微搜了一下,这个脚本没有指示长度的字节,是类似于pyc那样递归解析的。
因此需要尽可能多的dump,我抓了0x1500字节时即可解析了
[反编译工具](https://sourceforge.net/projects/unluac/)
使用方法:
> java -jar unluac.jar xxx.lua

```
require("bit")
borad = {
{
    1,
    2,
    3
},
{
    4,
    5,
    6
},
{
    7,
    8,
    0
}
}
sx = 3
sy = 3
function swap_chess(x, y, xx, yy)
local t = borad
borad = borad
borad = t
end
function move_chess(d)
if d == "S" and sx == 1 or d == "W" and sx == 3 or d == "D" and sy == 1 or d == "A" and sy == 3 then
    return
end
if d == "S" then
    swap_chess(sx, sy, sx - 1, sy)
    sx = sx - 1
elseif d == "W" then
    swap_chess(sx, sy, sx + 1, sy)
    sx = sx + 1
elseif d == "D" then
    swap_chess(sx, sy, sx, sy - 1)
    sy = sy - 1
elseif d == "A" then
    swap_chess(sx, sy, sx, sy + 1)
    sy = sy + 1
end
end
function randomize()
local d = {
    "W",
    "S",
    "A",
    "D"
}
math.randomseed(os.time())
for i = 1, 1000 do
    move_chess(d)
end
end
function display()
local s = ""
for x = 1, 3 do
    for y = 1, 3 do
      s = s .. "| " .. borad .. " "
    end
    s = s .. "|\n"
    if x ~= 3 then
      s = s .. "-------------\n"
    end
end
s = s .. "\n"
io.write(s)
end
secret = {
171,
201,
244,
200,
118,
100,
138,
190,
170,
159,
94,
91,
42,
184,
8,
98,
198,
134,
110,
165,
108,
219,
117,
179,
180,
179,
221,
144,
167,
155
}
print("i want to play a game with u")
io.read()
print("finish this game 10000000 times and i'll show u the flag, trust me")
print("use WSAD/wsad to move, ctrl+z to quit")
io.read()
times = 0
total = 10000000
while times < total do
randomize()
f = false
os.execute("cls")
print("times: " .. times .. "/" .. total)
display()
repeat
    io.write("> ")
    s = io.read()
    if s == nil then
      break
    end
    for i = 1, string.len(s) do
      move_chess(string.upper(string.sub(s, i, i)))
    end
    os.execute("cls")
    print("times: " .. times .. "/" .. total)
    display()
    f = true
    for i = 0, 7 do
      if borad ~= i + 1 then
      f = false
      break
      end
    end
    f = f and borad == 0
until f
if f then
    times = times + 1
    math.randomseed(times)
    for i = 1, #secret do
      secret = bit.bxor(secret, math.random(255))
    end
else
    os.execute("cls")
    break
end
end
if times == total then
os.execute("cls")
print("congrats!")
flag = ""
for i, v in ipairs(secret) do
    flag = flag .. string.char(v)
end
print(flag)
end
```
找一下flag,可以发现它是将secret与随机数逐字节异或,循环了10000000次,这个数字有点夸张……估计是出题人怕有算法大佬直接硬解题目吧23333


到了这里,由于用到了巨量随机数,因此必须使用lua来跑了
试了一下online,果然时间不够orz
于是去官网下个lua,很小,但是编译有点困难orz
找到的编译好的binary,但是不带bit库,于是又去github上找了xor的lua实现拿来跑
刚开始下的latest,lua5.3,怎么跑都不对,后来发现各个版本的随机数都不一样,我就惊他喵了个呆了
于是根据之前收集到的信息,找来lua5.1跑即可
```
function xor(a,b)
local r = 0
local f = math.floor
for i = 0, 31 do
    local x = a / 2 + b / 2
    if x ~= f(x) then
      r = r + 2^i
    end
    a = f(a / 2)
    b = f(b / 2)
end
return r
end

secret = {
171,
201,
244,
200,
118,
100,
138,
190,
170,
159,
94,
91,
42,
184,
8,
98,
198,
134,
110,
165,
108,
219,
117,
179,
180,
179,
221,
144,
167,
155
}

for times=1, 10000000 do
    print(times)
    times = times + 1
    math.randomseed(times)
    for i = 1, #secret do
      secret = xor(secret, math.random(255))
    end
end
if times == total then
   
    print("congrats!")
    flag = ""
    for i, v in ipairs(secret) do
      -- print(v)
      flag = flag .. string.char(v)
    end
    print(flag)
end
```
估计是由于xor的实现问题非常慢,大概需要三四十分钟才能跑出flag

如果是自己编译的windows-lua的话,会自带bit模块,那个bxor应该会很快
只需要在头部添加
`require("bit")`
将xor改为bit.bxor即可

![](https://i.imgur.com/iNbnlA5.png)
>后来找到了带库的luaforwin安装包,bit模块确实肉眼可见的快,不过感觉也得跑个好几十分钟吧……orz


###Rua!
反编译exe,按照那些函数来看是GO语言编译出的
IDA对GO语言的支持相对而言比较差,对参数、结构的解析都比较差强人意
比方说首先自己识别的main函数就是错的,仍然是Go的Runtime初始化部分

我没有学过go语言,所以不清楚它的结构
不过从函数命名来看,应该是把方法所属的类或者是package体现在名称上了

稍微搜索一下就可以知道,go语言的入口package为main
因此可以直接搜索main
![](https://i.imgur.com/hbidSM3.png)
很显然这个main_main就是入口方法了

这里符号似乎都保留了,从Read和Write就能猜到意思了,不过这里的参数都没有识别出来----读写总要有句柄或是文件名吧~
从这个函数的声明也可以看出来第一个参数应该就是文件名
`void __cdecl main_ReadAll(string *filePath1, __uint8 *_r1, error_0 *_r2)`
那么查看汇编可以发现确实有把字符串放到寄存器里的
![](https://i.imgur.com/hwLQawU.png)
只是IDA没有识别出来

对于这题而言,看到这里当然就差不多了---不过如果是大一些的程序,还是需要Hex-rays的辅助的,因此最好还是修正咯

以Read为例
` main_ReadAll(v1, v2, v0);`的v1、v2、v0从栈声明可以看出分别对应的是rdi, rsi, rdx寄存器
>error_0 *v0; // rdx
string *v1; // rdi
__uint8 *v2; // rsi
error_0 *_r2; //
void *retaddr; //

而从上面的汇编可以看出来,它的参数应该在栈里
也就是说,是传参的分析错误
我们知道传参是由调用约定规定的
这个程序是x64的,默认情况下x64会使用__fastcall--前4个参数使用寄存器来传

知道问题在哪就很好改了,不过查了一下x64下的__stdcall、__cdecl、__fastcall实质上都是一样的……
还好IDA自己定义了一个__usercall,没有受这些影响

于是对着函数按y,将其的声明改为__usercall即可
![](https://i.imgur.com/oGiM8g5.png)
可以看到参数已经识别出来了....虽然好像有点长2333

这个实际上也是因为Go语言的不同
我们知道在C中以\0作为字符串结尾,因此IDA也是这么识别的

而Go中
![](https://i.imgur.com/q0DWYpN.png)
可以看到,字符串直接是无缝衔接的
引用的时候根据地址直接取
那么程序是怎么知道到哪里结束的呢?

注意看ReadALL的第二个参数,0xD就是第一个参的长度了

解决参数传递的问题后,还有一个返回值的问题……
从直接的汇编可以看出来,ReadAll调用完以后返回值是从栈里取出来的---这就让IDA头大了
目前的IDA认定返回值存在寄存器中,对于这种栈中返回值就没法识别了

因此..还是只能猜啦╮(╯_╰)╭

本题里很明显InfoIntergration对读到的内容进行处理
往下翻一翻发现这里有个exit
![](https://i.imgur.com/zdKW0ri.png)
关键判断在于v17的值
Hex-rays里面它又是无头变量了,不过大概能猜出来是LazyProc的返回值吧
关于LazyProc查一下可以知道它是通过模块名和函数名来调用函数的方法

于是再往上看,可以发现用`runtime_writeBarrier`做了两次写入
比较类似这个形式
```
fmtp, _ := syscall.BytePtrFromString("%d %d %d")

a, _, _ := GetDLL(t, "user32.dll").Proc("wsprintfA").Call(
                  uintptr(unsafe.Pointer(&buf)),
                  uintptr(unsafe.Pointer(fmtp)),
                  1000, 2000, 3000)
```
两次写入分别是模块和函数名
然而直接看比较困难,还是要把调用约定改回来,或者就要硬怼汇编啦
可以看到第一次的参数是main_kernel
![](https://i.imgur.com/RBi5hy7.png)
查看交叉引用可知是`Kernel32.dll`的句柄
![](https://i.imgur.com/WfuD6Ox.png)
第二次则是GetTickCount的字符串
![](https://i.imgur.com/Tr1VL8k.png)
然后通过LazyCall调用

研究了一下可以通过脚本把调用约定跑回来,这样看起来就比较舒服了
![](https://i.imgur.com/WFQ3135.png)
v2和srca是同一个变量,通过改变结构体的类型还可以让IDA把Name、l(应该是library的意思?)、Name.len都识别出来

脚本如下,注意仅对汇编界面光标所在函数内调用过的函数有效

```
def find_all_callees(start_ea):
    callees = {}
    fname = GetFunctionName(start_ea)
    end_ea = FindFuncEnd(start_ea)

    all_refs = set([])
    # For each ins in the function.
    addr = start_ea
    while(addr<end_ea):
      # We are only interested in call
      if GetDisasm(addr).find("call")==-1 :
            addr += idaapi.decode_insn(addr)
            continue
      refs = CodeRefsFrom(addr, 0)
      for ref in refs:
            callees = GetFunctionName(ref)
      addr += idaapi.decode_insn(addr)
    return callees

# main
ea = ScreenEA()
callees = find_all_callees(ea)
print"===========func========="
for ea in callees:
    print '    0x%08X: %s' % (ea, callees)
for ea in callees:
    type = get_type(ea)
    #print(type),
    type = type.replace("__cdecl", "__usercall"+" "+ GetFunctionName(ea).replace(".","_")) + ";"
    print(type),
    print(idc.SetType(ea, type))
```

这个函数是获取程序运行时间,与我们的输入没有关系
貌似是个反调试?

不管他,继续往下看找输入数据流
![](https://i.imgur.com/vWj1ATF.png)
上面那一大堆,其实核心部分就这一行异或加密

于是根据题目给出的rua文件解密,得到一个binary
解密脚本:
```
with open("rua", "rb") as f:
    data = f.read()

decode_data = for i in range(len(data))]
# print(len(decode_data))
for i in range(len(data)-1,0, -1):
    # print(i)
    decode_data = (((data^i^data)&0xff))
print(data)
# print(decode_data)
str = b""
for i in decode_data:
    str += bytes((i,))
print(str)
with open("rua_decode", "wb") as f:
    f.write(str)
```
搜一下文件头`1B 4C 4A`,又是lua~不过是luajit编译的字节码了,反编译起来没有lua那么舒服,不过还是有轮子的
[反编译器](https://github.com/bobsayshilol/luajit-decomp)
```
-- BYTECODE -- test.lua:0-0
function someFunc0()
var_0_1 = "bit" --var_0_1 STRING-STRING
require(var_0_1)
var_0_0 = io.read()
str = var_0_0
var_0_0 = "gs2mx}t>{-v<pcp>" --var_0_0 STRING-STRING
unk = var_0_0
var_0_0 = "" --var_0_0 STRING-STRING
ret = var_0_0
var_0_0 = {}
Barray = var_0_0
var_0_1 = 0 --var_0_1 NUMBER-NUMBER
math.randomseed(var_0_1)
var_0_0 = 0 --var_0_0 NUMBER-NUMBER
var_0_1 = string.len(str)
var_0_1 = var_0_1 -1 --var_0_1 NUMBER-NUMBER
var_0_2 = 1 --var_0_2 NUMBER-NUMBER
for var_0_3 = var_0_0,var_0_1,var_0_2 do --location 0025, loop ends at 0054-1
var_0_8 = str
var_0_9 = var_0_3 +1 --var_0_9 NUMBER-NUMBER
var_0_7 = str.byte(var_0_8, var_0_9)
var_0_9 = 128 --var_0_9 NUMBER-NUMBER
var_0_8 = math.random(var_0_9) --var_0_8 REPLACE-REPLACE
var_0_6 = bit.bxor(var_0_7, var_0_8)
var_0_7 = 95 --var_0_7 NUMBER-NUMBER
var_0_5 = bit.band(var_0_6, var_0_7)
var_0_5 = var_0_5 +32 --var_0_5 NUMBER-NUMBER
Barray = var_0_5
var_0_5 = string.char(unknown0)
var_0_4 = var_0_4 .. var_0_5
ret = var_0_4
end --location 0053, loops back to 0026-1
if ret == unk then
--jump to 0062 (if previous if statement is false) --0062 JMP-JMP
var_0_1 = "Bingo" --var_0_1 STRING-STRING
print(var_0_1)
--jump to 0065 (if previous if statement is false) --0065 JMP-JMP
--location 0062--0062 LOCATION-LOCATION
var_0_1 = "GG" --var_0_1 STRING-STRING
print(var_0_1)
--location 0065--0065 LOCATION-LOCATION
return
end


```
反编译效果不是很好,不过几个函数都能拿到,再加上本身逻辑不是很复杂,还是不太难的
核心算法其实就几句
```
math.randomseed(0)
for i=1 , string.len(input) do
      r = input^math.random(128)&95+32
end
```
最后要求r与给定字符串相等,值得注意的是给定字符串无论是反编译的结果还是官方luajit.exe反汇编出的结果,都是截断长度不够的
只能自己去binary中扒字符串,而输入长度也未知╮(╯_╰)╭所以只能往长了取

然后反运算即可
这里还有同样长度的随机数需要拿出,我刚开始用luajit生成随机数死活算不对,后来又搞个lua51跑了一下随机数发现居然不一样,再算就对了
PS: lua51 52 53 luajit 每个的随机数都不一样,我真是惊了个呆了……
```
s = """gs2mx}t>{-v<pcp>"+`v>19*%j=|g ;p{/w="tdg?*!!#%$)j*}."""
l =
test1 = ""
test2 = ""
for i in range(len(s)):
    r = (ord(s)-32)
    r1 = r | 0b100000
    r = r^l
    r1 = r1^l
    if(r<127 and r>32):
      print(chr(r),end='')
    elif(r1<127 and r1>32):
      print(chr(r1),end='')
    else:
      print('**', end='')
```
反运算的时候注意原运算中有一个&,因此解码的时候会出现两个可能

还有更简单的方法就是逐字节爆破23333虽然同样会有多解,但是省事的多
```
n = 0
while(n<len(s)):
    for i in range(32, 127):
      if(((i^l)&95)+32==ord(s)):
            print(chr(i),end='')
            # n += 1
            # break
   
    print()
    n += 1
```





whklhh 发表于 2018-8-27 20:17

lanlana 发表于 2018-8-26 10:44
大佬可不可以问下game那道题目   dump 0x1500字节
具体是咋弄的呀

很多方法啊,比如OD可以dump整个进程下来,也可以找到虚拟地址到物理地址的映射后从binary里扒出来,还可以用IDA脚本

whklhh 发表于 2018-8-30 10:29

goobey 发表于 2018-8-29 15:27
楼主,你好!
请问game.exe的lua脚本部分数据否被修改过?我这还原出来的数据无法运行,game.exe也无法运 ...

。。。。是改过orz 尝试patch的 忘了 不好意思 我等下重新传

LIJIAMMM 发表于 2018-8-23 16:58

感谢大佬的分享,学习东西

xiaofengzi 发表于 2018-8-23 17:51

谢谢分析,回头自己分析下,共同进步

打ち上げ花火 发表于 2018-8-23 17:53

感谢分享,新人学习中

demon_lin 发表于 2018-8-23 18:32

谢谢,学习了。

mayl8822 发表于 2018-8-23 18:37

感谢分享

nalansitan 发表于 2018-8-23 20:29

感谢大佬分享

QiNuo 发表于 2018-8-23 20:37

感谢分享

z236293824 发表于 2018-8-23 21:28

膜拜大神

wazl8890 发表于 2018-8-23 22:25

很不错,学习了
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 网鼎杯第二场Reverse