st98 の日記帳


[ctf][seccon] SECCON 2019 国際決勝大会に参加しました

チーム Harekaze として hikalium さんhiww さんh_noson さんと 12 月 21 日から 22 日にかけて開催された SECCON 2019 国際決勝大会 (リンク先激重注意) に参加しました。最終的にチームで 1273 点 (攻撃ポイント 1100 点 + 防御ポイント 173 点) を獲得し、順位は参加 14 チーム中 11 位でした。

他のメンバーの write-up はこちら。

この大会では King of the Hill 形式で壱、弐、参、四、伍の 5 問、Jeopardy 形式で 5+1 問が出題されました。このうち私が挑戦した問題について write-up を書いてみます。

伍(5)

http://10.5.100.1/

防御ポイントのみの問題でした。

最初はアクセスすると HELLO WORLD! とだけ表示され、他のコンテンツはありませんでした。/neko のように適当なパスにアクセスすると返ってくるエラー画面の HTML でググって、HTTP レスポンスヘッダの情報と合わせて Apache 2.4.29 の裏で Bottle で書かれたアプリが動いているものと推測できました。待ち望んだ Web 問だ! Apache か Bottle の 1-day の脆弱性を使うのかな? 最近話題の脆弱性があったりするのかな? と喜んでいましたが、CTF 開始後 20 分ほどでサーバに修正があり、以下のようなコンテンツを返すようになりました。

Please execute game-server.py.

$ python3 game-server.py 1_eater 0.0.0.0 9999

source: http://10.5.100.1/file/snakesrc.zip
flag: http://10.5.100.1/flag

snakesrc.zip は対戦型のヘビゲームのソースコードでした。各自で以下のような AI (サンプルとして与えられた 1_eater.py) を書いてデプロイしろということでしょうか。

#!/usr/bin/env python3

import math
import copy
import random

class AI:
	
	def __init__(self):
		self.DIRECTION = { "UP": 0, "DOWN": 1, "RIGHT": 2, "LEFT": 3 }
		self.route = []
	
	def search_apples(self, _map, me):
		r = []
		for y in range(len(_map)):
			for x in range(len(_map[0])):
				if _map[y][x] == "APPLE":
					d = math.sqrt((abs(me[0] - x) ** 2) + (abs(me[1] - y) ** 2))
					r.append( (y, x, d) )
		return r
	
	def check_route(self, a, b, _map, me, _route):
		if self.route != []:
			return
		if a[0] == me[0] and a[1] == me[1]:
			pass
		else:
			if _map[a[1]][a[0]] != "EMPTY" and _map[a[1]][a[0]] != "APPLE":
				return
		if a[0] == b[0] and a[1] == b[1]:
			self.route = copy.deepcopy(_route)
			return
		if a[0] < b[0]:
			_route.append("RIGHT")
			self.check_route([a[0]+1, a[1]], b, _map, me, _route)
			_route.pop()
		if a[0] > b[0]:
			_route.append("LEFT")
			self.check_route([a[0]-1, a[1]], b, _map, me, _route)
			_route.pop()
		if a[1] < b[1]:
			_route.append("DOWN")
			self.check_route([a[0], a[1]+1], b, _map, me, _route)
			_route.pop()
		if a[1] > b[1]:
			_route.append("UP")
			self.check_route([a[0], a[1]-1], b, _map, me, _route)
			_route.pop()
		return
	
	def one_step(self, _map, _x, _y):
		if _map[_y][_x+1] == "EMPTY" or _map[_y][_x+1] == "APPLE":
			return self.DIRECTION["RIGHT"]
		if _map[_y][_x-1] == "EMPTY" or _map[_y][_x-1] == "APPLE":
			return self.DIRECTION["LEFT"]
		if _map[_y+1][_x] == "EMPTY" or _map[_y+1][_x] == "APPLE":
			return self.DIRECTION["DOWN"]
		if _map[_y-1][_x] == "EMPTY" or _map[_y-1][_x] == "APPLE":
			return self.DIRECTION["UP"]
		return random.randint(0, 3)

	def move(self, _map, snakes):
		_x, _y = snakes[0][0] #head pos
		r = self.search_apples(_map, [_x, _y])
		for (apple_y, apple_x, apple_d) in sorted(r, key=lambda x:x[2]):
			self.route = []
			self.check_route([_x, _y], [apple_x, apple_y], _map, [_x, _y], [])
			if self.route != []:
				return self.DIRECTION[self.route[0]]
		return self.one_step(_map, _x, _y)

ai = AI()
def main(d):
	return ai.move(d["map"], d["snakes"])

/flag にアクセスすると以下のような内容が返ってきました。

{
    "score": {
        "10.5.1.4:9999": 1500, 
        "10.5.1.14:9999": 1500, 
        "10.5.1.12:9999": 1500, 
        "10.5.1.15:9999": 1500, 
        "10.5.1.10:9999": 1500, 
        "10.5.1.13:9999": 1500, 
        "10.5.1.3:9999": 1500, 
        "10.5.1.5:9999": 1500, 
        "10.5.1.2:9999": 1500, 
        "10.5.1.1:9999": 1500, 
        "10.5.1.16:9999": 1500, 
        "10.5.1.7:9999": 1500, 
        "10.5.1.9:9999": 1500, 
        "10.5.1.6:9999": 1500, 
        "10.5.1.11:9999": 1500, 
        "10.5.1.8:9999": 1500
    }, 
    "result": {
        "1st": [
            "10.5.1.4:9999", 
            "e31f2f1879e2376949a8213635a58221"
        ]
    }
}

その時点で最も得点の高いチームが防御ポイントを得られるようです。…が、AI をデプロイしようにも我々のサーバである 10.5.1.11 への接続方法が全くわかりません。SSH の 22 番ポートが使えることはわかりましたが、パスワードがわかりませんでした。

先程の修正から 2 時間ほど経ってまたサーバに修正があり、各チームごとに用意された AI のデプロイ用サーバの認証情報が与えられました。

とりあえず 1_eater.py を眺めていたところ、周囲にりんごと何もないマスがあったときに同じ優先順位で扱っていることが気になり、以下のような修正を加えました。

ubuntu@snake-team-int-11:~$ diff -u 1_eater.py 2_test.py
--- 1_eater.py  2019-12-20 02:01:19.976000000 +0900
+++ 2_test.py   2019-12-21 15:34:01.378756860 +0900
@@ -49,14 +49,24 @@
                return
        def one_step(self, _map, _x, _y):
-               if _map[_y][_x+1] == "EMPTY" or _map[_y][_x+1] == "APPLE":
+               if _map[_y][_x+1] == "APPLE":
                        return self.DIRECTION["RIGHT"]
-               if _map[_y][_x-1] == "EMPTY" or _map[_y][_x-1] == "APPLE":
+               if _map[_y][_x-1] == "APPLE":
                        return self.DIRECTION["LEFT"]
-               if _map[_y+1][_x] == "EMPTY" or _map[_y+1][_x] == "APPLE":
+               if _map[_y+1][_x] == "APPLE":
                        return self.DIRECTION["DOWN"]
-               if _map[_y-1][_x] == "EMPTY" or _map[_y-1][_x] == "APPLE":
+               if _map[_y-1][_x] == "APPLE":
                        return self.DIRECTION["UP"]
+
+               if _map[_y][_x+1] == "EMPTY":
+                       return self.DIRECTION["RIGHT"]
+               if _map[_y][_x-1] == "EMPTY":
+                       return self.DIRECTION["LEFT"]
+               if _map[_y+1][_x] == "EMPTY":
+                       return self.DIRECTION["DOWN"]
+               if _map[_y-1][_x] == "EMPTY":
+                       return self.DIRECTION["UP"]
+
                return random.randint(0, 3)
        def move(self, _map, snakes):

これでしばらく防御ポイントを稼ぐことができましたが、こんな小手先の修正がいつまでも通用するわけがなく、1 時間と経たないうちに他のチームが防御ポイントを取り始めました。これ以降 Harekaze がサーバ伍の防御ポイントを取ることはありませんでした (すみません…)。

六(6) (Jeopardy)

[Crypto] Factor the flag

I hid the SECCON{} flag in a big prime number.

1401111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111112220791111111111111111
1111111111111111111111111111122207911223089031988903088023088023088920012001200
2319889030879222080230880230890319887911122318879211992120012999912120013000013
0000131008920012001199121200120022089200130000119912119911121200120011992119912
1199121199121199121200130101000012001199121200120930009200130000119921199111121
2001200119921199121199121199121199121200130010208012002318879112120929999112120
9299991212103188892001200119912230890318889199121199121200130000131007911112119
9212092091991211992119912120013010111188791222079112129999121199121199121200130
0001200119911121200120012091992119921299992120013010099991112119911112129999121
1991211991212001300001200120012001209199223198889199212001209199213010099991112
1199212001299991212001300001300001300001200120012001299991111212091991212001209
1992130100999911121199122308903198890308802308802308892001200119922308903198879
2121031988903088011992130100999911121199111111111111111111111111111111111111111
2220791111111111111111111111111111111111111111111112220791111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111239593

flag in a big prime number. ということなので、とりあえずこの数値が素数かどうか factordb.com に投げて確認してみると、どうやらこれは 13 * 97 * 1111111111...13(1261 ケタ) の合成数であることがわかりました。1261 ケタの数値はほとんどが 19 で構成されています。126113 * 97 ということなので、この数値を 97 ケタごとに改行させてみましょう。

1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111991111111111111111111111111111111111111111111111111991111
1199911999991199911199911199911911191119111999991199911119911199911199911999991111111999911119111
1911191911111911191911191911191991191119111911111911191119911911191911191111191111111911191119111
1911111911111911111911111911191919191119111911111911191191911911191911191111911111111911191119111
1911111911111911111911111911191911991119111999911111191191911111191191911111911999911911191119111
1199911999911911111911111911191911191991111111191111911911911111911119111119111919191999911111991
1111191911111911111911111911191911191119111111191119111911911119111191911119111919191911111119111
1111191911111911111911111911191911191119111911191191111999991191111911191191111919191911111119111
1911191911111911191911191911191911191119111911191911111111911911111911191191111919191911111119111
1199911999991199911199911199911911191119111199911999991111911999991199911191111919191911111119111
1111111111111111111111111111111111111111991111111111111111111111111111111111111111111111111991111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111213

おっ、なにかがうっすらと見えますね。1 を半角スペースに置換してみましょう。

                                                                                                 
                                        99                                                 99    
  999  99999  999   999   999  9   9   9   99999  999    99   999   999  99999       9999    9   
 9   9 9     9   9 9   9 9   9 99  9   9   9     9   9   99  9   9 9   9     9       9   9   9   
 9     9     9     9     9   9 9 9 9   9   9     9   9  9 9  9   9 9   9    9        9   9   9   
 9     9     9     9     9   9 9  99   9   9999      9  9 9      9  9 9     9  9999  9   9   9   
  999  9999  9     9     9   9 9   9 99        9    9  9  9     9    9     9   9 9 9 9999     99 
     9 9     9     9     9   9 9   9   9       9   9   9  9    9    9 9    9   9 9 9 9       9   
     9 9     9     9     9   9 9   9   9   9   9  9    99999  9    9   9  9    9 9 9 9       9   
 9   9 9     9   9 9   9 9   9 9   9   9   9   9 9        9  9     9   9  9    9 9 9 9       9   
  999  99999  999   999   999  9   9   9    999  99999    9  99999  999   9    9 9 9 9       9   
                                        99                                                 99    
                                                                                              2 3

フラグが得られました。

SECCON{524287mP}

[QR,Rev,Pwn] QR Decoder

http://10.2.5.1:8182/

この問題には攻撃フラグが 2 つありました。

与えられた URL にアクセスすると、qrdecoder という x86_64 の ELF へのリンクと、QR コードをアップロードできるフォームが表示されました。

qrdecoder を Ghidra でデコンパイルすると、以下のような結果になりました。

int main(int argc,char **argv)

{
  int iVar1;
  char *__file;
  FILE *__stream;
  char str [256];
  stat sStack200;
  int fd;
  FILE *file;
  
  __file = __xpg_basename(*argv);
  if (argc == 2) {
    __file = argv[1];
    iVar1 = stat(__file,&sStack200);
    if (iVar1 == 0) {
      sprintf(str,"zbarimg %s",__file);
      __stream = popen(str,"r");
      if (__stream == (FILE *)0x0) {
        perror(str);
        iVar1 = 1;
      }
      else {
        str[0] = 0;
        fread(str,1,sStack200.st_size & 0xffffffff,__stream);
        fclose(__stream);
        printf("The decoded string is: %s\n",str + 8);
        iVar1 = strcmp(str,"QR-Code:Hello, world!\n");
        if (iVar1 == 0) {
          fd = open("./flag1.html",0);
          if (fd < 0) {
            iVar1 = 1;
          }
          else {
            read(fd,str,0x100);
            close(fd);
            printf("%s",str);
            iVar1 = 0;
          }
        }
        else {
          iVar1 = 0;
        }
      }
    }
    else {
      perror(__file);
      iVar1 = 1;
    }
  }
  else {
    fprintf(stderr,"Usage: %s <filename>\n",__file);
    iVar1 = 1;
  }
  return iVar1;
}

zbarimg (QR コードなどを読み込んでくれるツール) にアップロードした画像を投げて、これの出力した結果が QR-Code:Hello, world! であれば 1 つ目のフラグを出力してくれるようです。Hello, world! という内容の QR コードを生成して投げるとフラグが得られました。

SECCON{8182 means decimal ASCII code of Q and R}

2 つ目のフラグについてはこの問題を Pwn すればよかったようですが、解けませんでした。OS コマンドインジェクションかと思いきや、コマンドライン引数として与えられるファイル名は /tmp/php… (str が使い回されていることから A のように適当に短い内容の QR コードを投げると確認できる) のように index.php$_FILES['qrimage']['tmp_name'] がそのまま渡されており操作できないことがわかります。また、スタックバッファオーバーフローについても h_noson さんによれば難しいとのことでした。想定解法が気になります。

[Binary] Bad Mouse

(Digispark の画像)

No claim no return. Firmware dump list is as follows:

chall.hex

この問題文に加え、各チームに 1 つずつこの Intel HEX 形式で記述されたバイナリが書き込まれた Digispark が配布されました。私がこの問題に取り組み始めたのは 1 日目の夜で、問題サーバに接続する必要がない問題だろうと考え、ホテルに持ち帰って頑張っていました。

とりあえずこれを USB ポートに (本来は素直に挿し込むべきではありませんが、作問者だと推測していた takesako さんを信じて) 挿し込んでみると、Digispark に載っている LED を点滅させながらマウスカーソルが動き始めました。ペイントソフトを起動してもう一度挿し直してみると flag… と書いている様子が確認できました…が、動きがどんどん遅くなっていきます。一昨年の国際大会で出題された BadUSB のマウス版でしょうか。

もう一度挿し直して観察していると、明滅の切り替わる間隔がほぼなし → 100ms → 500ms → 1000ms と変わっていることに気づきました。

では Ghidra を使って静的解析を頑張っていきましょう。chall.hexAVR8 for an Atmega 256 という Language でインポートしておきます。

どこから手をつけてよいかわからず関数を片っ端から眺めていると、以下のような怪しい関数が見つかりました。

void FUN_code_0004b2(uint uParm1,undefined2 uParm2,undefined2 uParm3,undefined2 uParm4,
                    undefined2 uParm5,undefined2 uParm6)

{
  byte bVar1;
  byte bVar2;
  byte bVar3;
  char cVar4;
  undefined2 uVar5;
  undefined2 uVar6;
  undefined2 uVar7;
  undefined2 uVar8;
  undefined2 uVar9;
  undefined2 uVar10;
  
  cVar4 = read_volatile_1(DAT_mem_0067);
  write_volatile_1(DAT_mem_0067,cVar4 + 1U);
  uParm1 = uParm1 & 0xff00 | (uint)(byte)(cVar4 + 1U) & 0xff01;
  FUN_code_00063f();
  uVar10 = uParm3;
  uVar9 = uParm4;
  uVar8 = uParm5;
  uVar7 = uParm6;
  uVar6 = R7R6;
  uVar5 = R5R4;
  bVar1 = read_volatile_1(PRR1);
  bVar2 = read_volatile_1(OSCCAL);
  if ((byte)(R1 + (bVar1 < 0xd)) <= bVar2) {
    Z = DAT_code_00000f & 0xff;
    if ((char)Z == '=') {
      if (bVar2 < (byte)(R1 + (bVar1 < 100))) {
        uParm2 = 100;
      }
      else {
        uParm2 = 500;
        if ((byte)(R1 + (bVar1 < 0x96)) <= bVar2) {
          uParm2 = 1000;
          if ((byte)(R1 + (bVar1 < 200)) <= bVar2) {
            uParm2 = 5000;
            if ((byte)(R1 + (bVar1 < 0xfa)) <= bVar2) {
              uParm2 = 10000;
              bVar1 = (bVar1 < 0x2c) + 1;
              if (bVar1 <= (byte)(bVar2 - bVar1)) {
                uParm2 = 20000;
              }
            }
          }
        }
      }
      goto LAB_code_00041a;
    }
  }
  uParm2 = 10;
LAB_code_00041a:
  uParm1 = 0;
  uParm4 = uParm2;
  uParm3 = 0;
  W = FUN_code_0005e0();
  uParm6 = uParm1;
  uParm5 = W;
  while( true ) {
    if ((char)(R15R14._1_1_ +
              (R1 < (byte)((char)R15R14 + (R1 < (byte)(R13R12._1_1_ + (R1 < (byte)R13R12)))))) <=
        (char)R1) break;
    W = FUN_code_0005e0();
    R5R4 = uParm1;
    R7R6 = W;
    R5R4._0_1_ = (byte)uParm1;
    R13R12._0_1_ = (byte)R13R12 - (byte)R5R4;
    R5R4._1_1_ = (char)(uParm1 >> 8);
    bVar2 = R5R4._1_1_ + ((byte)R13R12 < (byte)R5R4);
    bVar1 = (char)((uint)uParm4 >> 8) - bVar2;
    R7R6._0_1_ = (char)W;
    bVar3 = (char)R7R6 + (bVar1 < bVar2);
    bVar2 = (char)R15R14 - bVar3;
    R7R6._1_1_ = (char)((uint)W >> 8);
    uParm4 = CONCAT11(bVar1 + R9R8._1_1_ + CARRY1((byte)R13R12,(byte)R9R8),(byte)R13R12 +(byte)R9R8
                     );
    uParm3 = CONCAT11(((char)((uint)uParm3 >> 8) - (R7R6._1_1_ + (bVar2 < bVar3))) + R11R10._1_1_+
                      CARRY1(bVar2,(byte)R11R10),bVar2 + (byte)R11R10 + CARRY1(bVar1,R9R8._1_1_));
    W = FUN_code_0003c0();
    uParm5 = R7R6;
    uParm6 = R5R4;
  }
  R5R4 = uVar5;
  R7R6 = uVar6;
  uParm6 = uVar7;
  uParm5 = uVar8;
  uParm4 = uVar9;
  uParm3 = uVar10;
  return;
}

1005001000 とまさに明滅の切り替わる間隔の数値が、なんらかのカウントが大きくなるごとに順番に uParm2 へ代入されています。これを小さな数値に変えればフラグが書かれる速度が遅くなることはなくなるのではないでしょうか。

例えば、uParm2 = 500; に相当する命令列は以下のようになっています。

     code:0004d5 44  ef           ldi        R20 ,0xf4
     code:0004d6 51  e0           ldi        R21 ,0x1
     code:0004d7 60  e0           ldi        R22 ,0x0
     code:0004d8 70  e0           ldi        R23 ,0x0

冒頭 4 バイトを uParm2 = 100; に相当する 44 e6 50 e0 (ldi R20,0x64; ldi R21, 0x0) に書き換えます。これを繰り返した結果、以下のような diff が出来上がりました。

$ diff -u chall.hex modified.hex 
--- chall.hex   2019-12-21 20:33:06.589007891 +0900
+++ modified.hex        2019-12-21 20:33:09.701092417 +0900
@@ -152,11 +152,11 @@
 :1009700081E085D180916500909166008D30910570
 :1009800068F0EEE1F0E0E491ED3341F48436910556
 :1009900060F444E650E060E070E004C04AE050E0FB
-:1009A00060E070E088E690E045CF44EF51E060E021
-:1009B00070E086399105B0F348EE53E060E070E0F6
-:1009C000883C910578F348E853E160E070E08A3FA5
-:1009D000910540F340E157E260E070E08C329140D5
-:1009E00008F340E25EE460E070E0DCCFCF93DF9399
+:1009A00060E070E088E690E045CF44E650E060E021
+:1009B00070E086399105B0F344E650E060E070E0F6
+:1009C000883C910578F344E650E060E070E08A3FA5
+:1009D000910540F344E650E060E070E08C329140D5
+:1009E00008F344E650E060E070E0DCCFCF93DF9399
 :1009F0008091640066E0B4D2492F20916500309167
 :100A00006600B901680F711D87FD7A95FB01E05EF4
 :100A1000FF4FE49121EC3FEF261B370BE20FF32F42

変更を加えた後のバイナリを書き込もうとしましたが、環境構築で詰まってしまいました。Digispark を持っていればこれを書き込んで試してもらいたいとチームメンバーに助けを求めたところ、hikalium さんが持っているとのことで試してもらいました。1 時間ほどすると (本当に申し訳ないです、ありがとうございました!)、フラグの全体 (画像は hikalium さんの write-up を参照ください) が得られたとのことでした。

SECCON{379eaX85bTa99c695b36855i4Ycfa5b5}

[Hardware] mimura

It’s a hardware challenge.

この大変シンプルな問題文に加え、各チームに 1 つずつ STM32 の載ったボード (たぶんこれ) 等が配布されました。私がこの問題に取り組み始めたのは 2 日目で、その時点で hikalium さんが既にファームウェアをダンプしており、後は頑張ってバイナリを読むだけという状態でした。

これがどのような挙動をするかについても hikalium さんによって確認されており、MicroUSB のケーブルで接続するとシリアル通信で以下のようにパスワードを聞かれることがわかっていました。

Unlock Pass:*******
Checking..
Invalid.
Unlock Pass:****
Checking..
Invalid.
This Device was frozen due to prevent to steal information by attacker.

何度もパスワードを間違えてしまうと This Device was frozen due to prevent to steal information by attacker. と怒られ、一度接続し直さないと再度パスワードを入力することができなくなってしまいます。

では objdump と Ghidra を使って静的解析を頑張っていきましょう。objdump (全部入り) では all-objdump firmware2.bin -D -b binary -m arm -M force-thumb > firmware2.dis で逆アセンブルした結果を適当なファイルに出力しておきます。Ghidra ではバイナリをインポートする際に Language として ARM Cortex / Thumb little endian を選んでおきます。

どこから静的解析を始めるかの手がかりとして、先程シリアル通信で送られたメッセージを使いましょう。バイナリで This Device was frozen … を探すと 0x8492 にこの文字列が配置されていることがわかりました。firmware2.dis でこれを参照している命令がないか雑に探してみましたが見つかりません。それならばとさらにバイナリで 92 84 を探すと、0x4a8 に一箇所見つかりました。これを参照している命令がないか探してみると、以下のような命令列が見つかりました。

     42a:	491f      	ldr	r1, [pc, #124]	; (0x4a8)
     42c:	4817      	ldr	r0, [pc, #92]	; (0x48c)
     42e:	f001 ff80 	bl	0x2332

0x2332 はなにか出力周りの関数でしょうか。とりあえずこの命令列が含まれる関数を Ghidra でデコンパイルし、出力関数っぽい部分の引数が参照している文字列を注釈として加えると以下のようになりました。

void FUN_00000378(void)

{
  int *piVar1;
  int *piVar2;
  int iVar3;
  int iVar4;
  int iVar5;
  uint uVar6;
  
  thunk_FUN_0000169c(DAT_00000488);
  iVar3 = thunk_FUN_00000cc4(DAT_0000048c);
  if (iVar3 != 0) {
    uVar6 = (uint)*DAT_00000490;
    if (*DAT_00000490 == 0) {
      iVar4 = FUN_000006c2(DAT_0000048c);
      piVar2 = DAT_0000049c;
      iVar3 = DAT_00000498;
      piVar1 = DAT_00000494;
      if (*DAT_00000494 < 1) {
        FUN_00002332(DAT_0000048c,DAT_000004a8); /* 0x4a8: "This Device was frozen due to prevent to steal information by attacker." */
        FUN_0000448c(0x20,uVar6);
        FUN_000044ec(10);
        FUN_0000448c(0x20,1);
        FUN_000044ec(0x28);
        FUN_0000448c(0x20,uVar6);
        FUN_000044ec(10);
        FUN_0000448c(0x20,1);
        FUN_000044ec(0x28);
        FUN_0000448c(0x20,uVar6);
        FUN_000044ec(10);
        FUN_0000448c(0x20,1);
        return;
      }
      if ((iVar4 == 0xd) || (iVar4 == 10)) {
        FUN_0000448c(0x20,0);
        FUN_000044ec(10);
        FUN_0000448c(0x20,1);
        iVar4 = DAT_00000498;
        *(undefined *)(iVar3 + *piVar2) = 0;
        iVar3 = FUN_000001a8(iVar4);
        if (iVar3 == 0) {
          FUN_00002332(DAT_0000048c,DAT_000004a0); /* 0x4a0: "Invalid." */
          FUN_000044ec(3000);
          iVar3 = *piVar1 + -1;
          *piVar1 = iVar3;
          if (0 < iVar3) {
            FUN_00002310(DAT_0000048c,DAT_000004a4); /* 0x4a4: "Unlock Pass:" */
          }
        }
        else {
          FUN_00000200();
        }
        *piVar2 = 0;
      }
      else {
        FUN_00000878(DAT_0000048c,0x2a);
        iVar5 = *piVar2 + 1;
        *(undefined *)(iVar3 + *piVar2) = (char)iVar4;
        *piVar2 = iVar5;
        *(undefined *)(iVar3 + iVar5) = 0;
      }
      if (0x1d < *piVar2) {
        *DAT_0000049c = 0x1e;
        return;
      }
    }
  }
  return;
}

FUN_000001a8(iVar4) の返り値が 0 であれば Invalid. と出力し、そうでない場合には FUN_00000200 を呼び出しています。恐らく FUN_000001a8 で入力されたパスワードが正しいかどうかチェックしているのでしょう。デコンパイルします。

uint FUN_000001a8(char *pcParm1)

{
  FUN_0000231a(DAT_000001f8);
  FUN_00002332(DAT_000001f8,DAT_000001fc);
  if ((((pcParm1[8] == 'a') && (pcParm1[9] == 'm')) &&
      ((uint)(byte)pcParm1[6] == (uint)(byte)pcParm1[2])) &&
     ((((uint)(byte)pcParm1[10] == (uint)(byte)pcParm1[6] && ((uint)(byte)pcParm1[10] * 10 ==0x3f2)
       ) && (pcParm1[0xb] == '!')))) {
    return (uint)(*pcParm1 == 'O');
  }
  return 0;
}

それっぽい関数が出てきました。この条件に当てはまる文字列は O?e???e?ame! のような形式ですが、アンロック用のパスワードであるという状況から考えるに Open Sesame! でしょう。

このパスワードが正しいか実際にシリアル通信で入力して確認します。開けゴマ!

************
Checking..
Unlocking..Okay.

正しかったようです! …が、フラグは出力されず Windows のエクスプローラなどで USB メモリとしてアクセスしてみても空のままでした。


競技中はここで時間切れでした。パスワードを特定してから静的解析を続け、なんとかしてフラグを復号できないか試行していました。以下のように 1f 8b … と gzip を書き込んでいることから FUN_00000200 がフラグの復号処理と判断していました。

void FUN_00000200(void)

{
  int iVar1;
  undefined4 uVar2;
  undefined4 uVar3;
  undefined4 *puVar4;
  undefined4 *puVar5;
  undefined4 *puVar6;
  undefined4 *puVar7;
  undefined4 local_3c [10];
  undefined4 uStack20;
  
  uVar3 = DAT_000002d8;
  uVar2 = DAT_000002d4;
  *DAT_000002d0 = 1;
  FUN_00002310(uVar3,uVar2);
  iVar1 = DAT_000002dc;
  *(undefined *)(DAT_000002dc + 0x2003) = 0;
  *(undefined *)(iVar1 + 0x2004) = 0;
  *(undefined *)(iVar1 + 0x2005) = 0;
  *(undefined *)(iVar1 + 0x2006) = 0;
  *(undefined *)(iVar1 + 0x2007) = 0;
  *(undefined *)(iVar1 + 0x2008) = 0;
  *(undefined *)(iVar1 + 0x200e) = 0xf6;
  *(undefined *)(iVar1 + 0x200b) = 0x76;
  *(undefined *)(iVar1 + 0x2009) = 3;
  *(undefined *)(iVar1 + 0x2001) = 0x8b;
  *(undefined *)(iVar1 + 0x2000) = 0x1f;
  *(undefined *)(iVar1 + 0x2002) = 8;
  *(undefined *)(iVar1 + 0x200a) = 0xb;
  *(undefined *)(iVar1 + 0x200c) = 0x75;
  *(undefined *)(iVar1 + 0x200d) = 0x76;
  *(undefined *)(iVar1 + 0x200f) = 0xf7;
  puVar6 = local_3c;
  puVar7 = DAT_000002e0 + 10;
  puVar4 = puVar6;
  puVar5 = DAT_000002e0;
  do {
    uVar2 = puVar5[1];
    *puVar4 = *puVar5;
    puVar4[1] = uVar2;
    puVar4 = puVar4 + 2;
    puVar5 = puVar5 + 2;
  } while (puVar5 != puVar7);
  *(undefined *)puVar4 = *(undefined *)puVar5;
  puVar4 = DAT_000002e4;
  do {
    uVar2 = *puVar6;
    uVar3 = puVar6[1];
    puVar6 = puVar6 + 2;
    *puVar4 = uVar2;
    puVar4[1] = uVar3;
    uVar3 = DAT_000002e8;
    uVar2 = DAT_000002d8;
    puVar4 = puVar4 + 2;
  } while (puVar6 != &uStack20);
  *(undefined *)puVar4 = *(undefined *)puVar6;
  FUN_00002332(uVar2,uVar3);
  return;
}

これを Python に移植して実行してみましたが、どこかで間違えているようで出力された gzip ファイルを展開してみても SECCON{YOU_CAN_ とフラグのごく一部しか得られませんでした。

# coding: utf-8
import struct

u16 = lambda x: struct.unpack('<H', x)[0]
u32 = lambda x: struct.unpack('<I', x)[0]

with open('firmware2.bin', 'rb') as f:
    s = list(f.read())

for i, c in enumerate(b'\x1f\x8b\x08\0\0\0\0\0\0\3\x0b\x76\x75\x76\xf6\xf7'):
    s[8 + 0x2000 + i] = c

local_3c = [None for _ in range(11)]

puVar6 = 0
puVar7 = u16(bytes(s[0x2e0:0x2e0+2])) + 10
puVar4 = 0
puVar5 = u16(bytes(s[0x2e0:0x2e0+2]))

while puVar5 != puVar7:
    uVar2 = s[puVar5 + 1]
    local_3c[puVar4] = s[puVar5]
    local_3c[puVar4 + 1] = uVar2
    puVar4 += 2
    puVar5 += 2

local_3c[puVar4] = s[puVar5]
puVar4 = u16(bytes(s[0x2e4:0x2e4+2]))

while puVar6 != 10:
    uVar2 = local_3c[puVar6]
    uVar3 = local_3c[puVar6 + 1]
    puVar6 += 2
    s[puVar4] = uVar2
    s[puVar4 + 1] = uVar3
    uVar3 = u16(bytes(s[0x2e8:0x2e8+2]))
    uVar2 = u16(bytes(s[0x2d8:0x2d8+2]))
    puVar4 += 2

s[puVar4] = local_3c[puVar6]

with open('result.gz', 'wb') as f:
    f.write(bytes(s[0x2008:puVar4+1]))

CTF の終了後に行われた懇親会で作問者のみむらさんにお話を伺ったところ、パスワードが特定できれば本当にあと少しで、パスワードの入力後 (フラグの復号処理後) に FTK Imager などで開けばよいだけとのことでした。実際にやってみると、確かに以下のように gzip ファイルが書き出されていることが確認できました。

sugoi

これを展開するとフラグが得られました。

SECCON{YOU_CAN_ANALYSE_HARDWARE_DEVICE}

いろいろ

このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳