Automotive CTF Japan決勝writeup

https://vicone.com/jp/automotive-ctf

チームTeam Oneで出場して2位。賞金8万円＆アメリカのデトロイトで開催される決勝の出場権獲得。

![IMG_7316.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/ca77d1f6-d0de-8f4e-bf23-011057ce72c7.jpeg)

![vicone.ctfd.io_scoreboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/c5a8f025-412f-53e9-10e1-62dc4fcec8ff.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/22c7e2f5-0067-345a-c89a-107e03fe3fa7.png)

私が関わった部分のwriteup。

## Misc

### I am an EV Charger! (1000, Reverse)

> 誰かがインターネット上にファームウェアを流出させました。EV充電器のブランドを特定してください。

ルーターとかならともかく、EV充電器のファームウエアなんて知らんがな。

プロダクトIDの文字列とか入ってないのかな。

```shell-session
$ strings ev-charger.bin | fgrep -i prod
 :
prodcertprovd
prodcert
prodpriv
mqtts://prod-mqtt.emporiaenergy.com:8883
prod
[0;32mI (%u) %s: load [prod] rsa key
prod_log
[0;32mI (%u) %s: prod logs %s
 :
```

入ってなかったけど、たまたまURLが出てきたのでググって終わり。First blood。

`bh{emporia}`

## RAMN

大半の問題がこのジャンル。RAMNとはこれ。

![IMG_7318.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/72bcd4d9-fb2c-4908-00a3-6c7ff488dc82.jpeg)

https://github.com/ToyotaInfoTech/RAMN

要は、これは車。鍵も付いているし、スライダはアクセルとブレーキだし、ダイヤルはハンドル。こいつをハックできるなら、車をハックして操作するというハッカー漫画みたいなこともできますよ、という話らしい。

### [ECU C] Where? (2000, CAN, Steganography)

> CAN ID 0x0ABのメッセージのタイミングにフラグが隠されています。
>
> 注意:
>
> フラグは "bh{" で始まるASCII文字列です。
> 1分間のCANメッセージログにフラグを取得するために必要なすべてが含まれています。

CAN通信のダンプを取ってもらった。

ID 0x0ABのものだけをフィルタ。

```0ab.txt
 (1726189354.081649)  can0  0AB   [4]  00 01 0B 17               '....'
 (1726189354.274629)  can0  0AB   [4]  00 01 0B 19               '....'
 (1726189354.474804)  can0  0AB   [4]  00 01 0B 1B               '....'
 (1726189354.574577)  can0  0AB   [4]  00 01 0B 1C               '....'
 (1726189354.674538)  can0  0AB   [4]  00 01 0B 1D               '....'
 (1726189354.774896)  can0  0AB   [4]  00 01 0B 1E               '....'
 (1726189354.874836)  can0  0AB   [4]  00 01 0B 1F               '....'
 (1726189355.074720)  can0  0AB   [4]  00 01 0B 21               '...!'
 (1726189355.174899)  can0  0AB   [4]  00 01 0B 22               '..."'
 (1726189355.520222)  can0  0AB   [4]  00 01 0B 25               '...%'
 (1726189355.616121)  can0  0AB   [4]  00 01 0B 26               '...&'
 (1726189355.712412)  can0  0AB   [4]  00 01 0B 27               '...''
 (1726189356.000619)  can0  0AB   [4]  00 01 0B 2A               '...*'
 (1726189356.096836)  can0  0AB   [4]  00 01 0B 2B               '...+'
  :
```

「タイミング」と言っているので、各ID 0xABの通信と直前のID 0x0ABの通信との時間差をプロット。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/3ff8ceaf-bc74-8f7c-dcd8-e744cedae0ca.png)

だいたい0.1秒単位。まあ、0.1秒ごとに通信があったら `1` で、無かったら `0` でしょう。

```solve.py
D = [float(l[2:19]) for l in open("0ab.txt")]
D = [D[i+1]-D[i] for i in range(len(D)-1)]
D2 = ""
for d in D:
    D2 += "0"*int(d*10-.5)
    D2 += "1"
print(D2)
```

```shell-session
$ python3 solve.py
01011111011001110011010001010100010001010111110101100010011010000111101101000110
01001100001100110100010101001111101000110011011000110010101100011011100110100111
11011001110011010001001010001000101011111010110001001101000011110110100011001001
10000110011010001010101111101000110011011000110010101100101011010011010111110110
01110010101000101010001000101011011101011000100110100001101101101000110010011000
01010011010001010101111101000110011011000110010101100101011100110101111101100111
00110100010101000100010101111101011000100110100001111011010001100100110000110011
01000101010011111010001100110110001100101011001010111001101001111101100111001101
00010010100010001010111110101100010011010000111101101000110010011000011001101000
10101011111010001100110110001100101011001010110100110101111101100111001
```

バイト列に変換。

```
_g4TE}bh{FL3EO£62±¹§ÙÍ¯¬MhÉh«èÍ¬­5ör¢¢+u¡¶SE_Flees_g4TE}bh{FL3EO£62²¹§ÙÍ¯¬MhÉh«èÍ¬­5ö9
```

フラグの文字列は見えるが、化けている。プロットしたのを見ると分かるように、時間がぶれているからな……。これだから物理は。

1ビットずつずらすとこうなる。

```
0: _g4TE}bh{FL3EO£62±¹§ÙÍ¯¬MhÉh«èÍ¬­5ör¢¢+u¡¶SE_Flees_g4TE}bh{FL3EO£62²¹§ÙÍ¯¬MhÉh«èÍ¬­5ö9
1: ¾Îh¨úÄÐöfFlecsO³%_XÑÑWÑYZkìåEDVëCm0¦¾ØÊÊæ¾Îh¨úÄÐöfFleesO³%_XÑÑWÑYZkì9
2: }ÑQõ¡í0Í>ØÊÆæg4J"¾±4=£&¢¯£62²´×ÙÊ­Ö&Ú2aM}±Í}ÑQõ¡í0Í>ØÊÊæg4J"¾±4=£&¢¯£62²´×Ù
3: û9¢¢+ëCÚ2a*}±Í>ÎhE}bh{FL3E_Fleei¯³[¬M´dÂ*ú3c++û9¢¢+ëCÚ2a*}±Í>ÎhE}bh{FL3E_Fleei¯³	
4: ösEDWÖ&´dÃ4Tú3c+}Ñ(úÄÐöf¾ØÊÊÓ_g**"·XhÉ4UôfÆVW5ösEDWÖ&´dÃ4Tú3c++}Ñ(úÄÐöf¾ØÊÊÓ_g
5: ìæ¯¬MhÉh©ôfÆV74û9¢Qõ¡í0Í}±¦¾ÎTTEn±46Ñh«èÍ¬®kìæ¯¬MhÉh©ôfÆVW4û9¢Qõ¡í0Í}±¦¾Î
6: ÙÍ_XÑÑSèÍ¬niösD¢+ëCÚ2a*ú3c++M}¨¨Ýbhm£&ÑWÑY\×ÙÍ_XÑÑSèÍ¬®iösD¢+ëCÚ2a*ú3c++M}
7: ³*"¾±4=£&¢§ÑXÜÓìæDWÖ&´dÃ4UôfÆVVû9QQºÄÐÛFL)¢¯£62²¹¯³*"¾±4=£&¢§ÑY\ÓìæDWÖ&´dÃ4UôfÆVVû9
```

ぐっと睨んで、 `bh{FL3E_Flees_g4TE}` が通った。First blood。

`bh{FL3E_Flees_g4TE}`

### [ECU B] SecurityAccess (1200, UDS, Reverse)

> ECUはData Identifier 0xFFFFにフラグを保持しています。ただし、始めに認証が必要です。

「Cryptoだよ～」と仕事が降ってきた。この認証というのがチャレンジ＆レスポンスで、4バイトのチャレンジに対して、4バイトのレスポンスを返す必要があるらしい。4バイトのレスポンスを何個か渡すから、そこから法則を見つけて、さらにチャレンジを導けと。レスポンスはランダムなバイト列にしか見えない。無理でしょ。

あらためて問題を見ると、添付ファイルがあった。添付ファイルが無くてもある程度進められる問題は添付ファイルを見逃しがち。

添付ファイルはこれ。

```ECUB_security_access.txt
08001c10 <RAMN_UDS_SecurityAccess>:
{
 8001c10:	b530      	push	{r4, r5, lr}
 8001c12:	b083      	sub	sp, #12
 8001c14:	4604      	mov	r4, r0
	if( size <  2U )
 8001c16:	2901      	cmp	r1, #1
 8001c18:	d90a      	bls.n	8001c30 <RAMN_UDS_SecurityAccess+0x20>
		switch(data[1]&0x7F)
 8001c1a:	7843      	ldrb	r3, [r0, #1]
 8001c1c:	f003 037f 	and.w	r3, r3, #127	@ 0x7f
 8001c20:	2b01      	cmp	r3, #1
 8001c22:	d00a      	beq.n	8001c3a <RAMN_UDS_SecurityAccess+0x2a>
 8001c24:	2b02      	cmp	r3, #2
 8001c26:	d031      	beq.n	8001c8c <RAMN_UDS_SecurityAccess+0x7c>
			RAMN_UDS_FormatNegativeResponse(data, UDS_NRC_SFNS);
 8001c28:	2112      	movs	r1, #18
 8001c2a:	f7ff ff09 	bl	8001a40 <RAMN_UDS_FormatNegativeResponse>
  :
```

チャレンジ＆レスポンス処理のコードの逆アセンブル。この記事を書いている今気が付いたけど、この問題のジャンルはUDS&Reverseで、Cryptoではないな。

アセンブルコードを読むのが面倒だな。いっそバイナリならそのままGhidraに投げられるのに……と思ったけど、Cのソースコードも付いている。ただし、肝心の部分は隠されている。

```ECUB_security_access.txt
 :
	udsSessionHandler.defaultSAhandler.currentSeed = RAMN_RNG_Pop32();
 8001c46:	f7ff fec1 	bl	80019cc <RAMN_RNG_Pop32>
 8001c4a:	4a38      	ldr	r2, [pc, #224]	@ (8001d2c <RAMN_UDS_SecurityAccess+0x11c>)
 8001c4c:	60d0      	str	r0, [r2, #12]
	 //?????? EXPECTED ANSWER TO SEED ????????????
 8001c4e:	eb00 0340 	add.w	r3, r0, r0, lsl #1
 8001c52:	f503 5391 	add.w	r3, r3, #4640	@ 0x1220
 8001c56:	3314      	adds	r3, #20
 8001c58:	f483 437f 	eor.w	r3, r3, #65280	@ 0xff00
 8001c5c:	f083 03ff 	eor.w	r3, r3, #255	@ 0xff
	udsSessionHandler.defaultSAhandler.currentKey  = ????????????
 8001c60:	6113      	str	r3, [r2, #16]
	answer[2] = (uint8_t)(udsSessionHandler.defaultSAhandler
  :
```

この5命令を復元すれば良いらしい。はい。

```py
chall = 1234
print(hex((chall*3+0x1220+0x20)&0xffffffff^0xffff))
```

でも、「本当にこれで合っているのか？」という気がする。足している値とか、xorしている値とかがもう少しランダムっぽい値ならいかにもだけど……。

私「自信は無いけど、これ試してみてください」
laysakuraさん「通りませんね」
私「エンディアンを変えてみたらどうです？」
laysakuraさん「通りませんね」
私「はい……」

良く見たら、 `20` は16進数ではなく、10進数だった。

正しくはこれで、これなら認証が通り、フラグが手に入れられたらしい。

```py
chall = 1234
print(hex((chall*3+0x1220+0x20)&0xffffffff^0xffff))
```

実質私が解いたようなものだからと、フラグをもらって私がサブミットした。ありがとうございます。アセンブルコードを読むより、CANとかUDSとかのプロトコルを把握するほうが大変だと思う。

`bh{GU4RDeD_M4ILBoX}`

### [ECU B] RAM peak (2000, UDS)

> RAMにはReadMemoryByAddressサービスで読み取れるフラグがあります。フラグの長さは17文字です。

↑の問題が、我々のチームの最後から2問目。残りはこの1問。コンテスト時間は9時30分から16時の6時間半で、この時点で12時半。2位とも差を付けて1位。全完優勝余裕ですわ……と思っていたけれど、これが解けず……。

```ramn_uds.c
 :
            if (memsize > 0xFFE)
            {
                RAMN_UDS_FormatNegativeResponse(data, UDS_NRC_ROOR);
            }
            else if ((RAMN_MEMORY_CheckAreaReadable(addr, (addr + (uint32_t)memsize)) != 0U) && (memsize < 0xFFF))
            {
                //use rx buffer to copy
                uds_answerData[0] = data[0] + 0x40; //positive response
                for(uint32_t i = 0; i < memsize; i++ )
                {
                    uds_answerData[i+1] = (volatile uint8_t)*((volatile uint8_t*)(addr+i));
                }
                *uds_answerSize = (uint16_t)memsize+1;
            }
#if defined(ENABLE_MINICTF) && defined(TARGET_ECUD)
            else if ((addr == 0x01234567) && memsize <= (sizeof(FLAG_UDS_4)-1))
            {
                uds_answerData[0] = data[0] + 0x40; //positive response
                for(uint32_t i = 0; i < memsize; i++ )
                {
                    uds_answerData[i+1] = (volatile uint8_t)*((volatile uint8_t*)(FLAG_UDS_4+i));
                }
                *uds_answerSize = (uint16_t)memsize+1;
            }
#endif
            else
            {
                RAMN_UDS_FormatNegativeResponse(data, UDS_NRC_ROOR);
            }
 :
```

https://github.com/ToyotaInfoTech/RAMN/blob/5479b6c20f30c735f43dd8906f362049ee2ebe6c/firmware/RAMNV1/Core/Src/ramn_uds.c#L402

オリジナルのRAMNもちょっとしたCTFのようなことができるようになっていて、アドレス0x01234567を読むとフラグが返ってくる。このアドレス読んでも当然NG。オリジナルでどこからこのアドレスをどうやって導くかというと、単に問題文に書かれている。

オリジナルは、読めるアドレスなら読める。0xffffffffから読もうとすると固まることに気が付いた。無限ループしそうな箇所は `RAMN_MEMORY_CheckAreaReadable` くらいしかないので、この問題でも `RAMN_MEMORY_CheckAreaReadable` は生きていそう？　そう、コードなりメモリなりがダンプできれば、フラグでなくても良いよね。でも、コードやデータがありそうなアドレスを読んでも成功しない。ランダム化されていたりするのだろうか。ページ単位くらいで走査してみる？　でも、ページサイズ4KBだとしても、32bitのアドレス空間だと約100万通りになるな……。

ところで、問題文の「peak」とは？　メモリのreadとwriteのことをピークとポークと言うらしい（へー）が、このピークのスペルはPEEK。peakに何か意味がある……？

https://ja.wikipedia.org/wiki/PEEK%E3%81%A8POKE

とかしているうちに時間切れ。

コンテスト終了後に聞いたところによると、オリジナルのRAMNでRAMが配置されるアドレスを1バイトずつずらして読んでいけば、フラグが読めたらしい。それも試さなかったっけ……。謎。

## 雑感

CTFに詳しい人がこの記事を読むと、「お前たいしたことしてないな」と言われそう。自分でもそう思う。このCTFは、CTFっぽい部分は特に難しくない（と私は思う）。一方で、CANとかUDSとか車の通信関連の部分が分からん。環境を構築するだけでも一苦労。しかし、チームメイトの車に詳しい人からすると、その辺はたいしたことなくて、CTF部分が難しいらしい。車に詳しい人とCTFに詳しい人でチームを組むと強い。