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

game

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

根据提示符去IDA的strings窗口找可以发现内存中存在这部分数据,但是没有交叉引用

说明该字符串在汇编层面是没有引用的

一般来说,用户添加的信息会放在一起,所以可以在提示字符串上下翻一翻,果然找到一点有意思的东西

这个\x1BLuaQ很引入注目,大概率就是lua逆向了
去查一下1B 4C 75 61,可以看到是Lua的字节码文件头
那么再用Lua搜一下

可以获得版本号

或者通过文件解析也可以知道后一个字节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即可

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

Rua!

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

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

稍微搜索一下就可以知道,go语言的入口package为main
因此可以直接搜索main

很显然这个main_main就是入口方法了

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

只是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即可

可以看到参数已经识别出来了….虽然好像有点长2333

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

而Go中

可以看到,字符串直接是无缝衔接的
引用的时候根据地址直接取
那么程序是怎么知道到哪里结束的呢?

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

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

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

本题里很明显InfoIntergration对读到的内容进行处理
往下翻一翻发现这里有个exit

关键判断在于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

查看交叉引用可知是Kernel32.dll的句柄

第二次则是GetTickCount的字符串

然后通过LazyCall调用

研究了一下可以通过脚本把调用约定跑回来,这样看起来就比较舒服了

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# 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))

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

不管他,继续往下看找输入数据流

上面那一大堆,其实核心部分就这一行异或加密

于是根据题目给出的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" --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"+`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(ns)):
    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

你可能感兴趣的:(CTF,CrackMe)