ISITDTU-CTF 2021 Quals

Mở đầu

Đã khá lâu rồi team tụi mình (HCMUS.Twice) mới có dịp tái hợp lại để chơi CTF như này. Team mình đã rất cố gắng (hầu như mọi người không ngủ hoặc ngủ rất ít trong quá trình thi) và, cuối cùng tụi mình hoàn thành ở rank 4.

LOVESEA

Đây là một bài RE khá thú vị. Team mình đã first blood câu này. Để tóm tắt lại thì:

  • Bài này yêu cầu chúng ta phải cài game Cửu âm chân kinh.
  • Sau đó copy file challenge của ban tổ chức vào thư mục game.
  • Chạy game và bấm 3 (numpad) để nhập flag.

Bảng nhập flag

Đây là nội dung file challenge của ban tổ chức:

\---res
    |   lua.package
    |   skin.package
    |
    \---auto
            3.lua

Trong đó file lua.packageskin.package khá lớn, mình đoán đây là file resource của game (các skin nhân vật, hình ảnh, …). Còn file 3.lua khá nhỏ, chưa tới 200kb, và nó có extension .lua nên có thể đoán đây là file source code Lua.

Mở file 3.lua bằng text editor, ta thấy nó không phải là file text bình thường mà là file nhị phân.

Để xác định chính xác đây là file gì, ta dùng lệnh file trên linux:

$ 3.lua
3.lua: Lua bytecode, version 5.1

Kết quả nói rằng đây là Lua bytecode, có nghĩa file này đã được compile. Ta phải tìm cách decompile nó về dạng source code.

luadec

Sau một hồi search google, mình tìm thấy tool luadec dùng để decompile lua bytecode. Respository này không có sẵn binary nên mình đem về compile lại. Mình sử dụng docker Ubuntu 20.04.

Sau khi compile xong, ta chạy thử:

root@chall2:~$ ./luadec/luadec/luadec 3.lua
./luadec/luadec/luadec: 3.lua: bad header in precompiled chunk

Search “bad header” trong thư mục source code ra được đoạn code này:

static void LoadHeader(LoadState* S)
{
 char h[LUAC_HEADERSIZE]; // LUAC_HEADERSIZE = 12
 char s[LUAC_HEADERSIZE];
 luaU_header(h);
 LoadBlock(S,s,LUAC_HEADERSIZE);
 IF (memcmp(h,s,LUAC_HEADERSIZE)!=0, "bad header");
}

void luaU_header (char* h)
{
 int x=1;
 memcpy(h,LUA_SIGNATURE,sizeof(LUA_SIGNATURE)-1);
 h+=sizeof(LUA_SIGNATURE)-1; // #define LUA_SIGNATURE "\033Lua"
 *h++=(char)LUAC_VERSION;    // #define LUAC_VERSION 0x51
 *h++=(char)LUAC_FORMAT;     // #define LUAC_FORMAT 0x00
 *h++=(char)*(char*)&x;				/* endianness */
 *h++=(char)sizeof(int);     // 0x04
 *h++=(char)sizeof(size_t);  // 0x08 (64 bit)
 *h++=(char)sizeof(Instruction); // 0x04
 *h++=(char)sizeof(lua_Number);  // 0x08
 *h++=(char)(((lua_Number)0.5)==0); // 0x00
}

Đoạn code trên đọc 12 byte đầu từ file của chúng ta, rồi so sánh với 12 byte hardcode. Ta thử đọc 12 byte từ file 3.lua:

root@chall2:~$ xxd -g1 -l12 ./3.lua
00000000: 1b 4c 75 61 51 00 01 04 04 04 08 00              .LuaQ.......

Ta build lại bản 32-bit. Chạy lại:

root@chall2:~$ ./luadec/luadec/luadec 3.lua
./luadec/luadec/luadec: 3.lua: unexpected end in precompiled chunk

Vẫn bị lỗi, tuy nhiên lần này là lỗi unexpected end chứ không phải lỗi bad header như vừa nãy. Ít nhất là chúng ta cũng đã sửa được cái gì đó.

Đoạn code gây ra lỗi ở đây:

static void LoadBlock(LoadState* S, void* b, size_t size)
{
 size_t r=luaZ_read(S->Z,b,size);
 IF (r!=0, "unexpected end");
}

size_t luaZ_read (ZIO *z, void *b, size_t n) {
  while (n) {
    size_t m;
    if (luaZ_lookahead(z) == EOZ)
      return n;  /* return number of missing bytes */
    m = (n <= z->n) ? n : z->n;  /* min. between n and z->n */
    memcpy(b, z->p, m);
    z->n -= m;
    z->p += m;
    b = (char *)b + m;
    n -= m;
  }
  return 0;
}

Rõ ràng là hàm luaZ_read đã xử lý gì đó trên bytecode, nhưng bị lỗi. Có thể là vì bytecode đã được encrypt theo một cách nào đó. Đến đây mình không biết làm gì nữa vì, game dùng driver anti-cheat nên mình không debug được. Mình đã thử tìm tới cách dump process bằng code kernel (và hi vọng là file lua đã được decrypt sẽ nằm trên memory của process) nhưng kết quả là:

FML

Không hiểu sao trên máy mình lại bị lỗi trong Capcom.sys. Trên máy ảo mình thì lại chạy vô tư nhưng mình lại không thể cài game trong máy ảo vì nó quá nặng.

Hint từ BTC

Đến 7h tối trong ngày, BTC cho một hint mới, bảo rằng ta cần phải xem qua file fxcore.dll. Đến đây mình lập tức mở IDA, tìm hàm LoadBlock.

char __usercall LoadBlock@<al>(LoadState *a1@<eax>, int _b@<ebx>, int _size@<edi>)
{
  unsigned int v4; // ebp
  char result; // al
  int b; // [esp+0h] [ebp-10h]
  unsigned int size; // [esp+4h] [ebp-Ch]
  int v8; // [esp+14h] [ebp+4h]

  v4 = luaZ_read((unsigned int *)a1->Z, b, size);
  result = sub_10005EA0(a1->L, (char *)_b, _size, v8);
  if ( v4 )
    ERROR(a1, "unexpected end");
  return result;
}

(Mình đã rename và thêm struct vào để nhìn cho dễ). Nếu so sánh với hàm LoadBlock trong source code thì ta thấy có thêm 1 hàm lạ. Hàm này làm gì?

char __cdecl sub_10006FB0(void *a1, char *a2, unsigned int size, int a4)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
  v4 = 0;
  if ( a4 && size )
  {
    do
    {
      result = aSnailgame[v4 % 9];  // "snailgame"
      a2[v4++] ^= result;
    }
    while ( v4 < size );
  }
  return result;
}

Vậy là sau khi luaZ_read, các byte sẽ được xor với key snailgame. Đến đây mình code thêm vào hàm LoadBlock như sau:

static void LoadBlock(LoadState* S, void* b, size_t size)
{
 size_t r=luaZ_read(S->Z,b,size);
 const char* key = "snailgame";
 char* bb = (char*)(b);
 for (size_t i = 0; i < size; ++i) {
    bb[i] ^= key[i % 9];
 }
 IF (r!=0, "unexpected end");
}

Sau đó chạy lại, ta nhận được kết quả:

key = "Good Job"
local a = loadstring((function(b, c)
  -- function num : 0_0
  bxor = function(d, e)
    -- function num : 0_0_0
    local f = {
{0, 1}
, 
{1, 0}
}
    local g = 1
    local h = 0
    while 1 do
      if d > 0 or e > 0 then
        h = h + (f[d % 2 + 1])[e % 2 + 1] * g
        d = (math.floor)(d / 2)
        e = (math.floor)(e / 2)
        g = g * 2
        -- DECOMPILER ERROR at PC35: LeaveBlock: unexpected jumping out IF_THEN_STMT

        -- DECOMPILER ERROR at PC35: LeaveBlock: unexpected jumping out IF_STMT

      end
    end
    return h
  end

  -- more code ...

Chạy đoạn code trên thì thấy bị đứng lại, đó là vì đoạn loop ngay phía trên không hề có điều kiện dừng (mình nghĩ là do decompiler đã bị lỗi khi process đoạn này). Đến đây mình sửa lại thành:

    while d > 0 or e > 0 do
        h = h + (f[d % 2 + 1])[e % 2 + 1] * g
        d = (math.floor)(d / 2)
        e = (math.floor)(e / 2)
        g = g * 2
    end
    return h

Mình sửa như vậy vì bài Warm up code y chang như vậy. Sau khi sửa, nó sẽ decrypt và chạy đoạn code dưới đây:

-- a lot of code ...

flag_encoded = "A2SCaoBIKyr0qQ5e8RKFGYNNoB3ejBk9mDVaVOkbcZSDHpZgcvw/5sgftkOYPbijRB1vUg=="
key = "lovesea1505"

local dialog = nx_execute("util_gui", "util_get_form", "form_common\\form_input_name", true, false)
dialog.info_label.Text = nx_function("ext_utf8_to_widestr", "Nhập Flag")
dialog.name_edit.Text = nx_widestr("")
dialog:ShowModal()
local res, text = nx_wait_event(100000000, dialog, "input_name_return")
if res == "ok" then
    plaintext = nx_string(text)
    ciphertext = RC4(key, plaintext)
    ciphertext = base64.encode(ciphertext)
    if ciphertext == flag_encoded then
        SendNotice("Correct!", 1)
    else
        SendNotice("Wrong!", 1)
    end
end

Code trên sẽ check RC4(input, key) == flag_encoded.

Flag: ISITDTU{Base64_RC4_Lua5.1_And_Sea_Make_A_Good_Night}