欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

180823 逆向-网鼎杯(2-2)

程序员文章站 2022-05-19 13:39:01
...

game

IDA打开main函数啥有用的信息都看不到
运行一下发现是个八数码问题,要求解10000000次

根据提示符去IDA的strings窗口找可以发现内存中存在这部分数据,但是没有交叉引用
180823 逆向-网鼎杯(2-2)
说明该字符串在汇编层面是没有引用的

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

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

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

那么把脚本抓下来然后反编译即可
稍微搜了一下,这个脚本没有指示长度的字节,是类似于pyc那样递归解析的。
因此需要尽可能多的dump,我抓了0x1500字节时即可解析了
反编译工具
使用方法:

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[x][y]
  borad[x][y] = borad[xx][yy]
  borad[xx][yy] = 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[math.random(4)])
  end
end
function display()
  local s = ""
  for x = 1, 3 do
    for y = 1, 3 do
      s = s .. "| " .. borad[x][y] .. " "
    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[math.floor(i / 3) + 1][i % 3 + 1] ~= i + 1 then
        f = false
        break
      end
    end
    f = f and borad[3][3] == 0
  until f
  if f then
    times = times + 1
    math.randomseed(times)
    for i = 1, #secret do
      secret[i] = bit.bxor(secret[i], 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来跑了
于是去官网下个lua,很小,但是编译有点困难orz
找到的编译好的binary,但是不带bit库,于是又去github上找了xor的lua实现拿来跑

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)
    math.randomseed(times)
    for i = 1, #secret do
      secret[i] = xor(secret[i], 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即可

180823 逆向-网鼎杯(2-2)

后来找到了带库的luaforwin安装包,bit模块确实肉眼可见的快,不过感觉也得跑个好几十分钟吧……orz

Rua!

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

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

稍微搜索一下就可以知道,go语言的入口package为main
因此可以直接搜索main
180823 逆向-网鼎杯(2-2)
很显然这个main_main就是入口方法了

这里符号似乎都保留了,从Read和Write就能猜到意思了,不过这里的参数都没有识别出来—-读写总要有句柄或是文件名吧~
从这个函数的声明也可以看出来第一个参数应该就是文件名
void __cdecl main_ReadAll(string *filePath1, __uint8 *_r1, error_0 *_r2)
那么查看汇编可以发现确实有把字符串放到寄存器里的
180823 逆向-网鼎杯(2-2)
只是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; // [rsp+28h] [rbp-18h]
void *retaddr; // [rsp+40h] [rbp+0h]

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

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

于是对着函数按y,将其的声明改为__usercall即可
180823 逆向-网鼎杯(2-2)
可以看到参数已经识别出来了….虽然好像有点长2333

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

而Go中
180823 逆向-网鼎杯(2-2)
可以看到,字符串直接是无缝衔接的
引用的时候根据地址直接取
那么程序是怎么知道到哪里结束的呢?

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

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

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

本题里很明显InfoIntergration对读到的内容进行处理
往下翻一翻发现这里有个exit
180823 逆向-网鼎杯(2-2)
关键判断在于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[0])),
        uintptr(unsafe.Pointer(fmtp)),
        1000, 2000, 3000)

两次写入分别是模块和函数名
然而直接看比较困难,还是要把调用约定改回来,或者就要硬怼汇编啦
可以看到第一次的参数是main_kernel
180823 逆向-网鼎杯(2-2)
查看交叉引用可知是Kernel32.dll的句柄
180823 逆向-网鼎杯(2-2)
第二次则是GetTickCount的字符串
180823 逆向-网鼎杯(2-2)
然后通过LazyCall调用

研究了一下可以通过脚本把调用约定跑回来,这样看起来就比较舒服了
180823 逆向-网鼎杯(2-2)
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[ref] = 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[ea])
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))

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

不管他,继续往下看找输入数据流
180823 逆向-网鼎杯(2-2)
上面那一大堆,其实核心部分就这一行异或加密

于是根据题目给出的rua文件解密,得到一个binary
解密脚本:

with open("rua", "rb") as f:
    data = f.read()

decode_data = [data[0] for i in range(len(data))]
# print(len(decode_data))
for i in range(len(data)-1,0, -1):
    # print(i)
    decode_data[i] = (((data[i-1]^i^data[i])&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那么舒服,不过还是有*的
反编译器

-- 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_3] = 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[i] = input[i]^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 = [1, 31, 83, 10, 35, 47, 33, 127, 41, 120, 23, 110, 5, 2, 81, 63, 35, 103, 12, 58, 82, 125, 85, 102, 73, 6, 113, 9, 38, 127, 10, 3, 4, 73, 67, 83, 93, 50, 49, 4, 116, 27, 111, 70, 124, 81, 48, 122, 30, 67, 30, 115, 48, 53, 96, 46, 49, 29, 10, 76, 87, 56, 106, 5, 85, 33, 96, 109, 22, 97, 56, 52, 19, 66, 60, 63, 11, 107, 113, 71, 66, 13, 47, 92, 1, 17, 11, 114, 12, 41, 81, 84, 63, 46, 102, 55, 3, 28, 108, 51]
test1 = ""
test2 = ""
for i in range(len(s)):
    r = (ord(s[i])-32)
    r1 = r | 0b100000
    r = r^l[i]
    r1 = r1^l[i]
    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[n])&95)+32==ord(s[n])):
            print(chr(i),end='')
            # n += 1
            # break

    print()
    n += 1