2020 AIS3 EOF Final - ARAY

Mon, Aug 23, 2021 8-minute read

題目:ARAY {FREE_FLAG}, ARAY2 {EASY_FLAG}, ARAY3 {REGEXP_FLAG}, ARAY4 {HARD_FLAG}

2020 AIS3 EOF Final 的 YARA 系列題,是時候搞定它ㄌ

Challenge Info

Yet Another Reversing Artifact 有 reverse.yac, yara, libyara.so 三個檔案,四題共用

ARAY {FREE_FLAG} (50)

Solution

strings 下去噴出一堆 flag

用眼睛看,或是 yara rule 檢查

1
2
./yara -C reverse.yac free_flag
# FREE_FLAG free_flag

ARAY2 {EASY_FLAG} (100)

肉眼觀察,找到明顯被拆散的 flag 片段XD

簡單寫個腳本解掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data = open('./reverse.yac', 'rb').read()[0x488FD:0x48AEF]
data = data.split(b'\x00')

flag = ''
for i, v in enumerate(data):
    if i & 1 == 0:
        flag += v.decode()

print(flag)
open('easy_flag', 'w').write(flag)

ARAY3 {REGEXP_FLAG} (200)

自己寫一個簡單的 regex rule,然後編譯

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
rule RegExpExample1
{
    strings:
        $re1 = /[Aa].{2}IS3}/
        $re2 = /CC/
        $re3 = /(bar|baz|quux)/
        $re4 = /[Ada].{2}IS3}/
        $re5 = /[Aavb].{6}IS3}/

    condition:
        all of them
}
1
./yarac ./test.yar ./test.yac`

用 hex editor 觀察,發現會以明文存起來,放在 \xA2RE_OPCODE_LITERAL) 後面、並用 \xADRE_OPCODE_MATCH) 隔開每條規則

又因為 flag 是正常的英文單字,所以湊出來就好了@@

隨便湊 flag 腳本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
data = open('./reverse.yac', 'rb').read()[0x1685C:0x16B36]
data = data.replace(b'\xA0', b'').replace(b'\xA2', b'')
data = data.split(b'\xAD')[:-1]
data[-4] += b'PP'
data[-2] += b'PP'
flag = b''

for i, v in enumerate(data):
    if i % 4 != 0:
        continue
    str_a = v[:-2]
    str_b = data[i+2][:-2]
    for a, b in zip(str_a, str_b):
        flag += bytes([a, b])
    if len(str_a) > len(str_b):
        flag += bytes([str_a[-1]])

print(flag)
open('regexp_flag', 'wb').write(flag)

原本覺得這是最難的一題,不像 ARAY4 可以直接 dump instruction,沒想到直接用看的就出來了QAQ

沒仔細看 yara regular expression engine 跟它 bytecode 是怎麼存的,目前知道在編譯規則的時候 regexp 會被建成 AST,可以用內建的 function yr_re_ast_print(re_ast) 把 AST 印出來


ARAY4 {HARD_FLAG} (300)

題目使用 v3.9.0 版本的 yara

1
2
3
4
5
6
7
git clone --branch 'v3.9.0' https://github.com/VirusTotal/yara.git

# compile
cd yara/
./bootstrap.sh
./configure
make -j

yara 執行 bytecode 的 function 是 yr_execute_code()libyara/exec.c),裡面有一個很大的 switch case 根據 1 byte opcode 做相對應的動作,是一個 stack based VM

可以 trace code 並印出一些資訊,來了解實際運作流程

經過一些嘗試,可以確定 hard flag 的部分會把輸入拿去做運算,然後跟特定數值做比較(OP_INT_EQ),如果一樣就進行下一個比較,否則結束(OP_JFALSE

修改兩個 instruction(OP_INT_EQ, OP_AND)強制通過檢查,這樣就能看到所有規則:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
      case OP_AND:
        pop(r2);
        pop(r1);

        if (is_undef(r1) || is_undef(r2))
          r1.i = 0;
        else
          r1.i = r1.i && r2.i;
        r1.i = 1; // add this line
        push(r1);
        break;
...

      case OP_INT_EQ:
        pop(r2);
        pop(r1);
        ensure_defined(r2);
        ensure_defined(r1);
        r1.i = r1.i == r2.i;
        r1.i = true; // add this line
        push(r1);
        break;
1
make -j && ./yara -C ../reverse.yac ../input

確定輸出有 HARD_FLAG ../input 代表成功繞過所有檢查


最後整理出所有規則,總共有 80 個檢查:

  • 19th:輸入不是 AIS3{You got my secret flag, but did you check which is real flag?}
  • 45th:輸入中存在 AIS3{}
  • 64th:輸入長度為 88
  • 76th:輸入要符合 $flag_format,不會逆 regex QQ,但我猜是類似這樣 AIS3\{.*\}
  • 其他:把輸入拿去做運算,結果要跟特定值一樣

之後還會載入 256 個 hash function(md5, sha1, sha256)然後算一堆 hash,就結果而言,能通過上面那 80 個規則就可以了

所以根據 instruction 生出相對應的 python code,用 z3 solver 解掉

最後修改的 exec.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
...

int op_init_cnt = 0;
int op_int_eq_cnt = 0;
int op_jfalse_cnt = 0;

#define DEBUG false

#define push(x)  \
    if (sp < stack_size) \
    { \
      stack[sp++] = (x); \
      if (DEBUG && op_init_cnt > 3) { \
        printf("# debug stack[%d]: [", sp); \
        for (int i=sp-1; i>=0; i--) printf("0x%lx (0x%x), ", stack[i], &stack[i]); \
        debug_puts("]"); \
      } \
    } \
    else \
    { \
      result = ERROR_EXEC_STACK_OVERFLOW; \
      stop = true; \
      break; \
    } \

...

void debug_puts(char* content) {
  if (op_init_cnt > 3) puts(content);
}

...

int yr_execute_code(
    YR_SCAN_CONTEXT* context)
{
  int64_t mem[MEM_SIZE];
  int32_t sp = 0;

  const uint8_t* ip = context->rules->code_start;

  YR_VALUE args[YR_MAX_FUNCTION_ARGS];
  YR_VALUE *stack;
  YR_VALUE r1;
  YR_VALUE r2;
  YR_VALUE r3;

  ...

  while(!stop)
  {
    opcode = *ip;
    ip++;

    switch(opcode)
    {
      ...

      case OP_PUSH:
        r1.i = *(uint64_t*)(ip);
        ip += sizeof(uint64_t);
        if (op_init_cnt > 3) {
          if(r1.s != 0xfffabadafabadaff && r1.s > 0x7efd2436a92) {
            printf("# OP_PUSH, 0x%hx, %s, '%s'\n", ip, r1.s->identifier, r1.s->string); // r1.s->string
          } else {
            printf("push(0x%lx)\n", r1.i);
          }
        }
        push(r1);
        break;

      ...

      case OP_JFALSE:
        if (op_init_cnt > 3) {
          switch (op_jfalse_cnt)
          {
          case 19:
          case 45:
          case 64:
          case 76:
            break;
          
          default:
            printf("s.add(stack[-1] == stack[-2]) # OP_INT_EQ %d\n", op_int_eq_cnt);
            break;
          }
          // puts("print(len(stack))");
          puts("clear()");
          printf("# [OP_JFALSE] %d\n\n", op_jfalse_cnt++);
        }
        pop(r1);
        push(r1);

        ip = jmp_if(is_undef(r1) || !r1.i, ip);
        break;

      ...

      case OP_BITWISE_XOR:
        debug_puts("xor() # OP_BITWISE_XOR");
        pop(r2);
        pop(r1);
        ensure_defined(r2);
        ensure_defined(r1);
        r1.i = r1.i ^ r2.i;
        push(r1);
        break;

      ...

      case OP_INIT_RULE:
        printf("# OP_INIT_RULE %d\n", op_init_cnt++);
        if (op_init_cnt > 3) {
          puts(
            "from z3 import *\n"
            "\n"
            "stack = []\n"
            "flag = [BitVec(f'f{i}', 64) for i in range(88)]\n"
            "\n"
            "\n"
            "def pop():\n"
            "    r1 = stack[-1]\n"
            "    stack.pop()\n"
            "    return r1\n"
            "\n"
            "\n"
            "def pop2():\n"
            "    r1, r2 = stack[-1], stack[-2]\n"
            "    stack.pop()\n"
            "    stack.pop()\n"
            "    return r1, r2\n"
            "\n"
            "\n"
            "def push(d):\n"
            "    stack.append(d)\n"
            "    # print(stack)\n"
            "\n"
            "\n"
            "def xor():\n"
            "    r1, r2 = pop2()\n"
            "    push(r1 ^ r2)\n"
            "\n"
            "\n"
            "def sub():\n"
            "    r1, r2 = pop2()\n"
            "    r2 -= r1\n"
            "    # if r2 < 0:\n"
            "    #     r2 += 0x100000000\n"
            "    push(r2)\n"
            "\n"
            "\n"
            "def add():\n"
            "    r1, r2 = pop2()\n"
            "    r2 += r1\n"
            "    # if r2 >= 0x100000000:\n"
            "    # r2 -= 0x100000000\n"
            "    push(r2)\n"
            "\n"
            "\n"
            "def minus():\n"
            "    r1 = pop()\n"
            "    # r1 = -r1 + 0x100000000\n"
            "    r1 = -r1\n"
            "    push(r1)\n"
            "\n"
            "\n"
            "def get(offset, size, endian):\n"
            "    offset = pop()\n"
            "    tmp = flag[offset:offset+size]\n"
            "    if endian == 'little':\n"
            "        tmp = tmp[::-1]\n"
            "    res = 0\n"
            "    for i, v in enumerate(tmp):\n"
            "        res = res | (v & 0xff) << ((size - i - 1) * 8)\n"
            "    push(res)\n"
            "\n"
            "\n"
            "def clear():\n"    
            "    stack.clear()\n"        
            "\n"
            "\n"
            "s = Solver()\n"
            "\n"
            "for i in range(len(flag)):\n"
            "    s.add(flag[i] > 0x20)\n"
            "    s.add(flag[i] < 0x7f)\n"
            );
        }
        memcpy(&init_rule_args, ip, sizeof(init_rule_args));
        #ifdef PROFILING_ENABLED
        current_rule = init_rule_args.rule;
        #endif
        if (RULE_IS_DISABLED(init_rule_args.rule))
          ip = init_rule_args.jmp_addr;
        else
          ip += sizeof(init_rule_args);
        break;

      ...

      case OP_FILESIZE:
        if (op_init_cnt > 3) {
          printf("push(88) # OP_FILESIZE %d\n", context->file_size);
        }
        r1.i = context->file_size;
        push(r1);
        break;

      ...

      case OP_INT8:
        pop(r1);
        printf("get(1, 'little')\n");
        r1.i = read_int8_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_INT16:
        pop(r1);
        printf("get(2, 'little')\n");
        r1.i = read_int16_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_INT32:
        pop(r1);
        printf("get(4, 'little')\n");
        r1.i = read_int32_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT8:
        pop(r1);
        printf("get(1, 'little')\n");
        r1.i = read_uint8_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT16:
        pop(r1);
        printf("get(2, 'little')\n");
        r1.i = read_uint16_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT32:
        pop(r1);
        printf("get(4, 'little')\n");
        r1.i = read_uint32_t_little_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_INT8BE:
        pop(r1);
        printf("get(1, 'big')\n");
        r1.i = read_int8_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_INT16BE:
        pop(r1);
        printf("get(2, 'big')\n");
        r1.i = read_int16_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_INT32BE:
        pop(r1);
        printf("get(4, 'big')\n");
        r1.i = read_int32_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT8BE:
        pop(r1);
        printf("get(1, 'big')\n");
        r1.i = read_uint8_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT16BE:
        pop(r1);
        printf("get(2, 'big')\n");
        r1.i = read_uint16_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      case OP_UINT32BE:
        printf("get(4, 'big')\n");
        pop(r1);
        r1.i = read_uint32_t_big_endian(context->iterator, (size_t) r1.i);
        push(r1);
        break;

      ...

      case OP_INT_EQ:
        if (op_init_cnt > 3) {
          op_int_eq_cnt += 1;
        }
        pop(r2);
        pop(r1);
        ensure_defined(r2);
        ensure_defined(r1);
        r1.i = r1.i == r2.i;
        r1.i = true; // add this line
        push(r1);
        break;

      ...

      case OP_INT_ADD:
        debug_puts("add() # OP_INT_ADD");
        pop(r2);
        pop(r1);
        ensure_defined(r2);
        ensure_defined(r1);
        r1.i = r1.i + r2.i;
        push(r1);
        break;

      case OP_INT_SUB:
        debug_puts("sub() # OP_INT_SUB");
        pop(r2);
        pop(r1);
        ensure_defined(r2);
        ensure_defined(r1);
        r1.i = r1.i - r2.i;
        push(r1);
        break;

      ...

      case OP_INT_MINUS:
        debug_puts("minus() # OP_INT_MINUS");
        pop(r1);
        ensure_defined(r1);
        r1.i = -r1.i;
        push(r1);
        break;

      ...

      default:
        // Unknown instruction, this shouldn't happen.
        assert(false);
    }

    ...
  }

  ...

  puts( "if s.check() == sat:\n"
        "    m = s.model()\n"
        "    for i in range(len(flag)):\n"
        "        print(chr(int(str(m[flag[i]]))), end='')\n"
        "else:\n"
        "    print('GGG')\n"
  );
  return result;
}

原本的 constrain 會把空白 filter 掉,這邊卡好久 = =

Flag

  1. FREE_FLAG:AIS3{This is the free flag for do nothing but run `strings` on the compiled rule file :D}
  2. EASY_FLAG:AIS3{Ok, now you just reversed the binary rule, but to make it harder, I have to add random string: xrmXfGydyUWk1Rirk3gZAN/tH08Ag+Hxrxus3n7kdjokmi54hcZOJmu6EwOvR1ISIBCK7KVYQSXNP1vfcKQ225Ok}
  3. REGEXP_FLAG:AIS3{Even strings shows nothing about the flag, but I think you are good at regular expression!}
  4. HARD_FLAG:AIS3{This is too hard, I want to go home. 7gy0kPXE1AUiMZLnAW06EKqummGSVcGh23Eq9tl9SW2fz}

Reference