2025TSCTF-Grilhook

.apk、lua+加密注入.so、hook

题目是.false后缀的文件,放入die后发现它其实就是一个apk文件,放到jadx中打开,
26e5f86c8741cbc670fee5b27835c3a7
打开后先到res/AndroidManifest.xml下找package=和android:name=,发现是
com.lynnette.girlhook.MainActivity,将.换位/,逻辑代码的路径,进入后就是图中所示
将原题文件复制一个副本,然后将后缀改为zip并解压,拿到其中lib目录下的.so文件再放到IDA中反编译,因为这两个(.dex和.so)文件是.apk执行的文件,所以我都看(其实一开始只看.dex)然后得到了诱饵flag
815a8495c49c7de8207aeabb29c41ba3

打开后遵循抓大放小原则,直接看有没有循环、判断、函数调用。发现三个调用了三个函数,其中hook_java_mothe显然是劫持了.dex,再看上面的what_is_this,在ai的帮助下终于看懂了,这是嵌入了一个加密后的lua脚本(在上文中已解释),继续看代码,发现申请了一个1437字节的空间v0,双击secret这个参数,发现刚好有1440字节的数据,事实上就是加密后的lua脚本,观察这几个参数,ai说是chacha20加密算法,因为chacha20需要的其中三个关键参数32 字节密钥、12 字节 nonce、一个计数器(1u)这里刚好对上了,然后用第三方库进行解密

  import struct
  from Crypto.Cipher import ChaCha20

  # ═══════ 第 1 步:从 .so 里把密文抠出来 ═══════
  SO   = r"E:/下载/TS3 - 副本/lib/arm64-v8a/libgirlhook.so"
  raw  = open(SO, "rb").read()

  VA   = 0x18EC40          # IDA 里 secret 的地址
  SIZE = 1440              # 你数出来的长度

  def va2off(f, va):
      """虚拟地址 → 文件偏移(ELF 通用,以后都能用)"""
      e_phoff     = struct.unpack_from("<Q", f, 0x20)[0]
      e_phentsize = struct.unpack_from("<H", f, 0x36)[0]
      e_phnum     = struct.unpack_from("<H", f, 0x38)[0]
      for i in range(e_phnum):
          o = e_phoff + i * e_phentsize
          if struct.unpack_from("<I", f, o)[0] != 1:        # 只看 PT_LOAD 段
              continue
          p_offset = struct.unpack_from("<Q", f, o + 0x08)[0]
          p_vaddr  = struct.unpack_from("<Q", f, o + 0x10)[0]
          p_filesz = struct.unpack_from("<Q", f, o + 0x20)[0]
          if p_vaddr <= va < p_vaddr + p_filesz:
              return p_offset + (va - p_vaddr)

  off    = va2off(raw, VA)
  secret = raw[off : off + SIZE]
  print(f"[1] 抠出密文 {len(secret)} 字节(文件偏移 {hex(off)})")

  # ═══════ 第 2 步:ChaCha20 解密 ★ 核心就这 4 行 ═══════
  key   = b"youareclosetoit_youareclosetoit_"   # 32 字节
  nonce = b"continueabcd"                        # 12 字节

  c = ChaCha20.new(key=key, nonce=nonce)
  c.seek(64)                    # ★ 计数器从 1 开始 = 跳过第 1 个 64 字节块
  plain = c.decrypt(secret)

  # ═══════ 第 3 步:输出 ═══════
  open(r"E:/1/lua_script.txt", "wb").write(plain)
  print("[2] 解密完成,前 60 字节:")
  print(plain[:60].decode("utf-8", "replace"))

以上代码即为作者写的lua代码,分析后发现它劫持了.dex中的两个函数,第一个check_if_correct(劫持的代码即23行fun1)被劫持后永远返回0,所以!check_if_correct恒为1,所以最后那个if中的三目要区check_again。
再看劫持check_again的代码,他就是fun_enter,被写在lua中,

local function xor_byte(a, b)
    local result = 0
    for i = 0, 7 do
        local x = a % 2
        local y = b % 2
        result = result + ((x ~ y) << i)
        a = math.floor(a / 2)
        b = math.floor(b / 2)
    end
    return result
end

function xor_encrypt(str, key)
    local result = {}
    for i = 1, #str do
        local c = string.byte(str, i)
        c = (c + 1) % 255
        local k = string.byte(key, (i - 1) % #key + 1)
        result[i] = string.char(xor_byte(c, k))
    end
    return table.concat(result)
end

-- base64 encode
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function base64_encode(data)
    return ((data:gsub('.', function(x)
        local r,bits='',string.byte(x)
        for i=8,1,-1 do r=r..(bits % 2^i - bits % 2^(i-1) > 0 and '1' or '0') end
        return r
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if #x < 6 then return '' end
        local c=0
        for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
        return b:sub(c+1,c+1)
    end)..({ '', '==', '=' })[#data % 3 + 1])
end

function encrypt(plain, key)
    return base64_encode(xor_encrypt(plain, key))
end

function func_enter(args)
    local strobj = args[1]
    local str = getJavaStringContent(strobj)
    local encrypted = encrypt(str, "W0WY0U4R3S0G00D2F1NDTH3K3Y")
    local e1 = createJavaString(strobj, encrypted)
    args[1] = e1
    return true, args, 0
end~ 

以上即lua脚本,分析可知它将用户的输入异或加密后取代了.dex里那个check_again,然后再与 ("AmQTDHd7fy5CIAQyRUokVD1RJ3VlJFM4WTE+CBcn")
比较是否相同,用这个字符串按照lua再异或一下就能得出flag了

posted @ 2026-09-23 23:06  Yyytt  阅读(4)  评论(0)    收藏  举报