Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 8930

SECCON CTF 13 Quals Writeup - misoの雑記

$
0
0

2024/11/23 14:00 (JST) ~ 2024/11/24 14:00 (JST)に開催されたSECCON CTF 13 QualsのWriteupです。

チームDouble Lariatで参加し、rev問のpackedとF is for Flagを解きました。

[rev] packed (93 pt / 119 solved)

問題名の通り、UPXでパックされたバイナリa.outが与えられます。実行してみると、入力した文字列がフラグと一致しているかを判定するシンプルなフラグチェッカーであることが確認できました。

$ ./packed
FLAG: SECCON{dummy}
Wrong...

upx -d a.outでアンパックできますが、アンパック後のバイナリは標準入力から文字列を受け取ってWrongを表示するだけの意味のないものであるため、アンパック前のバイナリを解析しました。

IDAでdisassembleし、エントリポイントから順に処理を追っていくと0x44EE1Dあたりのアドレスでread syscallを呼び出し、標準入力から得た文字列と何らかのバイト列でxorをとっていることが確認できます。

LOAD:000000000044EE0F                 push    rsp
LOAD:000000000044EE10                 pop     rsi
LOAD:000000000044EE11                 mov     edx, 80h
LOAD:000000000044EE16                 sub     rsi, rdx
LOAD:000000000044EE19                 xor     edi, edi
LOAD:000000000044EE1B                 xor     eax, eax
LOAD:000000000044EE1D                 syscall                 ; LINUX - sys_read
LOAD:000000000044EE1F                 cmp     eax, 31h ; '1'
LOAD:000000000044EE22                 jnz     loc_44EEC3
LOAD:000000000044EE28                 mov     ecx, eax
LOAD:000000000044EE2A                 pop     rdx
LOAD:000000000044EE2B                 pop     rsi
LOAD:000000000044EE2C                 lea     rdi, [rsp-90h]
LOAD:000000000044EE34
LOAD:000000000044EE34 loc_44EE34:                             ; CODE XREF: LOAD:000000000044EE3A↓j
LOAD:000000000044EE34                 lodsb
LOAD:000000000044EE35                 xor     [rdi], al
LOAD:000000000044EE37                 inc     rdi
LOAD:000000000044EE3A                 loopne  loc_44EE34
LOAD:000000000044EE3C                 call    sub_44EE72

また、その後に呼び出しているsub_44EE72ではxorをとった後のバイト列が、別のバイト列と一致しているかを確認しています。

LOAD:000000000044EE72 sub_44EE72      proc near               ; CODE XREF: LOAD:000000000044EE3C↑p
LOAD:000000000044EE72
LOAD:000000000044EE72 var_88          = byte ptr -88h
LOAD:000000000044EE72 arg_0           = qword ptr  8
LOAD:000000000044EE72
LOAD:000000000044EE72                 mov     ecx, 31h ; '1'
LOAD:000000000044EE77                 pop     rsi
LOAD:000000000044EE78                 lea     rdi, [rsp-8+var_88]
LOAD:000000000044EE80                 xor     edx, edx
LOAD:000000000044EE82
LOAD:000000000044EE82 loc_44EE82:                             ; CODE XREF: sub_44EE72+1B↓j
LOAD:000000000044EE82                 lodsb
LOAD:000000000044EE83                 cmp     [rdi], al
LOAD:000000000044EE85                 setnz   al
LOAD:000000000044EE88                 or      dl, al
LOAD:000000000044EE8A                 inc     rdi
LOAD:000000000044EE8D                 loopne  loc_44EE82
LOAD:000000000044EE8F                 test    edx, edx
LOAD:000000000044EE91                 jnz     short loc_44EEC3

gdbで動的解析し、それぞれのバイト列を取得してxorをとるとフラグが得られました。

key = [0x49f9830000004ae8, 0x374c8d4857534475, 0x39482feb5b565efd, 0x803cac5e563273ce, 0x7e8006778f3c0a72, 0x013ce82c06740ffe]
expected = [0x1c82cd4f43430fbb, 0x682ef87f240c1c25, 0x567848b43a092dcc, 0xdf0fcf6a3a422caa, 0x17e4371fd04e3a14, 0x7c0f8c1c652b3990]

defu64(x):
    buf = []
    for _ inrange(8):
        buf.append(x & 0xff)
        x >>= 8returnbytes(buf)

key = b"".join(u64(x) for x in key)
expected = b"".join(u64(x) for x in expected)
flag = [a ^ b for a, b inzip(key, expected)]
print("".join(chr(x) for x in flag))
$ python3 solve.py
SECCON{UPX_s7ub_1s_a_g0od_pl4c3_f0r_h1din6_c0d3}

[rev] F is for Flag (227 pt / 16 solved)

与えられたバイナリFを実行してみると、packedと同様に入力した文字列がフラグと一致しているかを確認するフラグチェッカーであることが分かります。

$ ./F
FLAG: SECCON{dummy}
"Wrong"

はじめにGhidraでdecompileし、main関数の処理を見てみました。

undefined8 main(void)

{
  basic_ostream<> *this;
  long in_FS_OFFSET;
  undefined local_8fa;
  undefined local_8f9;
  ...
  local_760 = &local_8ee;
  local_758 = &local_8f1;
  local_750 = &local_8f6;
  local_748 = &local_8ed;
  local_740 = &local_8f5;
  std::__cxx11::basic_string<>::basic_string(flag);
                    /* try { // try from 001056ed to 0010570a has its CatchHandler @ 00106ea7 */
  std::operator<<((basic_ostream *)std::cout,"FLAG: ");
  std::operator>>((basic_istream *)std::cin,(basic_string *)flag);
  local_8e8 = 0;
  std::variant<>::variant<>(local_5e8,&local_8e8);
  local_8ec = 0xb7e9a2a4;
  std::variant<>::variant<>(local_618,&local_8ec);
                    /* try { // try from 00105770 to 00105774 has its CatchHandler @ 00106b23 */
  main::{lambda(std::variant<>,std::variant<>)#19}::operator()
            (local_5b8,&local_8f1,local_618,local_5e8);
  local_8e4 = 0x1904c652;
  std::variant<>::variant<>(local_588,&local_8e4);
                    /* try { // try from 001057b7 to 001057bb has its CatchHandler @ 00106afc */
  main::{lambda(std::variant<>,std::variant<>)#19}::operator()
            (local_558,&local_8f1,local_588,local_5b8);
  local_8e0 = 0xbe8afe4d;
  std::variant<>::variant<>(local_528,&local_8e0);
                    /* try { // try from 001057fe to 00105802 has its CatchHandler @ 00106ad5 */
  main::{lambda(std::variant<>,std::variant<>)#19}::operator()
            (local_4f8,&local_8f1,local_528,local_558);
  local_8dc = 0xbd18775a;
  std::variant<>::variant<>(local_4c8,&local_8dc);
                    /* try { // try from 00105845 to 00105849 has its CatchHandler @ 00106aae */
  main::{lambda(std::variant<>,std::variant<>)#19}::operator()
            (local_498,&local_8f1,local_4c8,local_4f8);
  local_8d8 = 0x82841cf4;
  std::variant<>::variant<>(local_468,&local_8d8);
                    /* try { // try from 0010588c to 00105890 has its CatchHandler @ 00106a87 */
  main::{lambda(std::variant<>,std::variant<>)#19}::operator()
            (local_438,&local_8f1,local_468,local_498);
  local_8d4 = 0xd2c1d5af;
  std::variant<>::variant<>(local_408,&local_8d4);
  ...
  {lambda(std::variant<>,std::function<>,std::function<>)#1}::operator()
            ((function<> *)local_48,&local_8f2,(long)local_78,(long)local_198,(long)local_168);
                    /* try { // try from 001067ed to 00106803 has its CatchHandler @ 00106dba */
  this = (basic_ostream<> *)operator<<(std::cout,local_48);
  std::basic_ostream<>::operator<<(this,std::endl<>);
  std::variant<>::~variant(local_48);
  std::variant<>::~variant(local_78);
  std::variant<>::~variant(len_valid);
  std::variant<>::~variant(local_108);
  std::variant<>::~variant(local_138);
  std::variant<>::~variant(local_d8);
  std::function<>::~function((function<> *)local_1f8);
  std::function<>::~function((function<> *)local_1c8);
  std::function<>::~function((function<> *)local_198);
  std::function<>::~function((function<> *)local_168);
  std::variant<>::~variant(local_648);
  std::variant<>::~variant(local_678);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)flag);
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */__stack_chk_fail();
  }
  return0;
}

std::variantインスタンスが大量に生成されていることが特徴的です。また、main::{lambda{std::variant<>, std::variant<>}#<ID>}::operator()という名前の関数が大量に呼び出されていることが確認できます。この関数は、std::variant同士の演算に関する処理だと推測できます。以下にIDに対応する関数の詳細を示します。ここで、vから始まる引数はstd::variant型を表しています。また、簡単のため第一および第二引数は省略しています。

ID引数詳細
1v1,v2 result = v1 + v2
3v1,v2 result = v1 * v2
4v1(string) result = len(v1)
6v1,v2 result = v1 ^ v2
7v1,v2 result = v1 | v2
8v1,v2 result = v1 & v2
9v1,v2 result = (v1 << v2)
10v1,v2 result = (v1 >> v2)
11v1,v2 result = (v1 >> (0x20 - v2)) | (v1 << v2)
17v1,v2 result = (v1 == v2)
17v1,v2 result = (v1 != v2)
19v1,v2 result = Cons(v1, v2)

上記の関数と似た名前の関数にmain::{lambda{std::variant<>, std::function<>, std::function<>}#1}::operator()も存在します。こちらは第三引数にstd::variant、第四および第五引数にstd::functionを受け取ります。そして、第三引数のstd::variantが0なら第四引数の関数を、1なら第五引数の関数を実行します。

この情報を基にmain関数を解析すると、以下のPythonコードと同等の処理を行っていることが確認できました。

Variant = Union[int, str, Cons]

@dataclassclassCons:
    val: Variant
    nxt: Variant

defmain():
    flag = input("FLAG: ")
    
    # 0x570b ~ 0x63c8
    arr1 = Cons(0x11793013, Cons(0x88d2ec9b, ...))
    arr2 = Cons(0x3, Cons(0xe, ...))
    
    iflen(flag) == 0x40:
        wrong = check(flag, arr2, ...)
    else:
        wrong = 1if wrong:
        ret = '"Wrong"'else:
        ret = "Correct"print(ret)

上記コードにおけるcheck関数は、0x6704で生成しているstd::functionに対応しています。このstd::functionのコンストラクタから次の関数を辿っていくと、関数の中身を特定することができました。

  • _Function_handler<>::_M_invoke
  • __invoke_r<>
  • __invoke_impl<>
  • main::{lambda()#1}::operator()

解析を進めた結果、この関数の3つのfix<>::operator()で入力した文字列の変換およびフラグのチェックを行っていることが判明しました。

{lambda()#1} * __thiscall main::{lambda()#1}::operator()({lambda()#1} *this,undefined8 *param_1)

{
  undefined8 *puVar1;
  undefined8 uVar2;
  undefined8 *puVar3;
  long in_FS_OFFSET;
  fix<> local_88 [48];
  fix<> local_58 [40];
  long local_30;
  
  local_30 = *(long *)(in_FS_OFFSET + 0x28);
  puVar1 = (undefined8 *)*param_1;
  uVar2 = param_1[4];
  puVar3 = (undefined8 *)param_1[1];
  fix<>::operator()(local_88,(undefined8 *)param_1[2],param_1[3]);
                    /* try { // try from 0010551b to 0010551f has its CatchHandler @ 00105579 */
  fix<>::operator()(local_58,puVar3,local_88);
                    /* try { // try from 00105534 to 00105538 has its CatchHandler @ 00105564 */
  fix<>::operator()((function<> *)this,puVar1,local_58,uVar2);
  std::variant<>::~variant((variant<> *)local_58);
  std::variant<>::~variant((variant<> *)local_88);
  if (local_30 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */__stack_chk_fail();
  }
  return this;
}

fix<>::operator()の処理を追っていくと、次のような形式の関数が多く見つかります。

function<> *
main::{lambda(auto:1,std::variant<>,std::variant<>)#2}::operator()
          (function<> *param_1,undefined8 *param_2,undefined8 param_3,undefined8 param_4,
          long param_5)

{
  undefined8 uVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  undefined8 local_150;
  undefined8 *local_148;
  function<> *local_140;
  undefined4 local_134;
  undefined8 local_130;
  undefined8 *local_128;
  undefined8 local_120;
  undefined8 local_118;
  undefined8 local_110;
  undefined8 local_108;
  long local_100;
  undefined8 local_f8;
  function<> local_e8 [32];
  function<> local_c8 [32];
  variant<> local_a8 [48];
  variant<> local_78 [48];
  variant<> local_48 [40];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  uVar1 = *param_2;
  local_128 = &local_150;
  local_120 = param_2[2];
  local_118 = param_2[3];
  local_110 = param_2[4];
  local_f8 = param_2[5];
  local_150 = param_3;
  local_148 = param_2;
  local_140 = param_1;
  local_108 = param_4;
  local_100 = param_5;
  _ZNSt8functionIFSt7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4C onsEEEvEEC2IZZ4mainENKUlT_SA_SA_E0_clISt17reference_wrapperIK3fixISF_EEEESA_SE_SA_SA_EUlvE0_vEEOSE _((undefined (*) [16])local_c8,&local_128);
  local_130 = param_4;
  _ZNSt8functionIFSt7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4C onsEEEvEEC2IZZ4mainENKUlT_SA_SA_E0_clISt17reference_wrapperIK3fixISF_EEEESA_SE_SA_SA_EUlvE_vEEOSE_((undefined (*) [16])local_e8,&local_130);
  uVar2 = local_148[1];
  local_134 = 8;
  std::variant<>::variant<>(local_78,&local_134);
                    /* try { // try from 0010c3e1 to 0010c3e5 has its CatchHandler @ 0010c4ae */
  std::variant<>::variant(local_a8,param_5);
                    /* try { // try from 0010c3fb to 0010c3ff has its CatchHandler @ 0010c496 */
  {lambda(std::variant<>,std::variant<>)#17}::operator()(local_48,uVar2,local_a8,local_78);
                    /* try { // try from 0010c422 to 0010c426 has its CatchHandler @ 0010c481 */
  {lambda(std::variant<>,std::function<>,std::function<>)#1}::operator()
            (local_140,uVar1,(long)local_48,(long)local_e8,(long)local_c8);
  std::variant<>::~variant(local_48);
  std::variant<>::~variant(local_a8);
  std::variant<>::~variant(local_78);
  std::function<>::~function(local_e8);
  std::function<>::~function(local_c8);
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */__stack_chk_fail();
  }
  return local_140;
}

この形式の関数は、{lambda(std::variant<>,std::variant<>)#17}などで何らかの条件式を評価し、その条件の成立に応じて関数を実行するようになっています。解析の結果、条件が成立しなかった場合の関数(上のコードではlocal_c8)の中で自身を再帰的に呼び出すようになっており、実質的にwhileループのような処理を行っていることが分かりました。

これを基に3つのfix::operator()の解析を進めると、上から順番に以下の処理を行っていることが確認できました。

  1. 入力された文字列を4バイトごとに分割し、リストを作成する
  2. 1で作成したリストに対して様々な演算を行い、暗号化する
  3. 2で暗号化した結果を暗号化されたフラグと比較する

これらの処理をPythonで書き直すと以下のようになります。

from Crypto.Util.number import bytes_to_long
import copy

defrot(x, n):
    return ((x >> (0x20 - n)) | (x << n)) & 0xffffffffdefhoge(chunks, idx):
    temp = rot(chunks[(idx + 3) % 0x10], 0x1d)
    temp ^= rot(chunks[(idx + 2) % 0x10], 0x11)
    temp ^= rot(chunks[(idx + 1) % 0x10], 7)
    temp ^= chunks[idx % 0x10]
    return temp

defsplit_to_chunks(x):
    chunks = []
    for i inrange(0, len(x), 4):
        chunks.append(x[i:i+4])
    return chunks

# flag = input("FLAG: ")# vals = Cons(0x3, Cons(0xe, ...))defcheck(flag, vals):
    #  fix<>::operator()(local_88,(undefined8 *)param_1[2],param_1[3]);
    chunks = [bytes_to_long(x) for x in split_to_chunks(flag)]
    
    # fix<>::operator()(local_58,puVar3,local_88);for k inrange(8):
        for i inrange(0x10):
            result = 0for j inrange(8):
                x = 4*j
                y = (chunks[i] >> x) & 0xf
                z = vals[y]
                result |= (z << x)
            chunks[i] = result * 0x4e6a44b9
            chunks[i] &= 0xffffffff

        prev = copy.deepcopy(chunks)
        for i inrange(0x10):
            a = 0x10 + i < 0x10 + k
            b = 0xc + k < 0x10 + i
            c = a & b

            d = i < 0x10 + k
            e = 0xc + k < i
            f = d & e
            g = c | f

            ifnot g:
                chunks[i] = hoge(prev, i) & 0xffffffff# fix<>::operator()((function<> *)this,puVar1,local_58,uVar2);
    exp_chunks = [
        0x11793013,
        0x88d2ec9b,
        0x2c0cccdd,
        0x97aef869,
        0x34bc7c60,
        0xf86c82d7,
        0x927b5bd9,
        0xd5689a8c,
        0x451f151a,
        0x0f389c4a,
        0xd2c1d5af,
        0x82841cf4,
        0xbd18775a,
        0xbe8afe4d,
        0x1904c652,
        0xb7e9a2a4
    ]
    for c, e inzip(chunks, exp_chunks):
        if c != e:
            return1return0

この処理の逆の処理を実装し、実行するとフラグが得られました。

from Crypto.Util.number import *

defrot(x, n):
    return ((x >> (0x20 - n)) | (x << n)) & 0xffffffffdefhoge(chunks, idx):
    temp = rot(chunks[(idx + 3) % 0x10], 0x1d)
    temp ^= rot(chunks[(idx + 2) % 0x10], 0x11)
    temp ^= rot(chunks[(idx + 1) % 0x10], 7)
    temp ^= chunks[idx % 0x10]
    return temp

vals = [0x3, 0xe, 0x1, 0xa, 0x4, 0x9, 0x5, 0x6, 0x8, 0xb, 0xf, 0x2, 0xd, 0xc, 0, 0x7]
chunks= [
    0x11793013,
    0x88d2ec9b,
    0x2c0cccdd,
    0x97aef869,
    0x34bc7c60,
    0xf86c82d7,
    0x927b5bd9,
    0xd5689a8c,
    0x451f151a,
    0x0f389c4a,
    0xd2c1d5af,
    0x82841cf4,
    0xbd18775a,
    0xbe8afe4d,
    0x1904c652,
    0xb7e9a2a4
]

for k inrange(7, -1, -1):
    first = (13 + k) & 0xffor i inrange(0x10):
        idx = (first - 1 - i) & 0xfif idx in [first, (first+1)&0xf, (first+2)&0xf]:
            continue
        chunks[idx] = hoge(chunks, idx) & 0xfffffffffor i inrange(0x10):
        chunks[i] = (chunks[i] * pow(0x4e6a44b9, -1, 0xffffffff+1)) % (0xffffffff+1)
        
    for i inrange(0x10):
        result = 0for j inrange(8):
            x = 4*j
            y = (chunks[i] >> x) & 0xf
            z = vals.index(y)
            result |= (z << x)
        chunks[i] = result & 0xffffffffprint(b"".join(bytes.fromhex(f"{x:08x}")[::-1] for x in chunks))
$ python3 solve.py
b'SECCON{fUnCt10n4l_pRoGr4mM1n6_1s_pR4c7iC4lLy_a_pUr3_0bfu5c4T1oN}'

感想ですが、シンボル名が長すぎてgdbの画面が非常に見づらくなり、動作を追うのに苦労しましたが、面白い問題だったと思います。

苦しい


Viewing all articles
Browse latest Browse all 8930

Latest Images

Trending Articles