チームTeam Oneで出場して2位。賞金8万円&アメリカのデトロイトで開催される決勝の出場権獲得。
私が関わった部分のwriteup。
Misc
I am an EV Charger! (1000, Reverse)
誰かがインターネット上にファームウェアを流出させました。EV充電器のブランドを特定してください。
ルーターとかならともかく、EV充電器のファームウエアなんて知らんがな。
プロダクトIDの文字列とか入ってないのかな。
$ 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とはこれ。
要は、これは車。鍵も付いているし、スライダはアクセルとブレーキだし、ダイヤルはハンドル。こいつをハックできるなら、車をハックして操作するというハッカー漫画みたいなこともできますよ、という話らしい。
[ECU C] Where? (2000, CAN, Steganography)
CAN ID 0x0ABのメッセージのタイミングにフラグが隠されています。
注意:
フラグは "bh{" で始まるASCII文字列です。
1分間のCANメッセージログにフラグを取得するために必要なすべてが含まれています。
CAN通信のダンプを取ってもらった。
ID 0x0ABのものだけをフィルタ。
(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の通信との時間差をプロット。
だいたい0.1秒単位。まあ、0.1秒ごとに通信があったら 1
で、無かったら 0
でしょう。
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)
$ python3 solve.py
01011111011001110011010001010100010001010111110101100010011010000111101101000110
01001100001100110100010101001111101000110011011000110010101100011011100110100111
11011001110011010001001010001000101011111010110001001101000011110110100011001001
10000110011010001010101111101000110011011000110010101100101011010011010111110110
01110010101000101010001000101011011101011000100110100001101101101000110010011000
01010011010001010101111101000110011011000110010101100101011100110101111101100111
00110100010101000100010101111101011000100110100001111011010001100100110000110011
01000101010011111010001100110110001100101011001010111001101001111101100111001101
00010010100010001010111110101100010011010000111101101000110010011000011001101000
10101011111010001100110110001100101011001010110100110101111101100111001
バイト列に変換。
_g4TE}bh{FL3EO£62±¹§Ùͯ¬MhÉh«èͬ5ör¢¢+u¡¶SE_Flees_g4TE}bh{FL3EO£62²¹§Ùͯ¬MhÉh«èͬ5ö9
フラグの文字列は見えるが、化けている。プロットしたのを見ると分かるように、時間がぶれているからな……。これだから物理は。
1ビットずつずらすとこうなる。
0: _g4TE}bh{FL3EO£62±¹§Ùͯ¬MhÉh«èͬ5ör¢¢+u¡¶SE_Flees_g4TE}bh{FL3EO£62²¹§Ùͯ¬MhÉh«èͬ5ö9
1: ¾Îh¨úÄÐöfFlecsO³%_XÑÑWÑYZkìåEDVëCm0¦¾ØÊÊæ¾Îh¨úÄÐöfFleesO³%_XÑÑWÑYZkì9
2: }ÑQõ¡í0Í>ØÊÆæg4J"¾±4=£&¢¯£62²´×ÙÊÖ&Ú2aM}±Í}ÑQõ¡í0Í>ØÊÊæg4J"¾±4=£&¢¯£62²´×Ù
3: û9¢¢+ëCÚ2a*}±Í>ÎhE}bh{FL3E_Fleei¯³[¬M´dÂ*ú3c++û9¢¢+ëCÚ2a*}±Í>ÎhE}bh{FL3E_Fleei¯³
4: ösEDWÖ&´dÃ4Tú3c+}Ñ(úÄÐöf¾ØÊÊÓ_g**"·XhÉ
4UôfÆVW5ösEDWÖ&´dÃ4Tú3c++}Ñ(úÄÐöf¾ØÊÊÓ_g
5: ì毬MhÉh©ôfÆV74û9¢Qõ¡í0Í}±¦¾ÎTTEn±46Ñh«èͬ®kì毬MhÉ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バイトのレスポンスを何個か渡すから、そこから法則を見つけて、さらにチャレンジを導けと。レスポンスはランダムなバイト列にしか見えない。無理でしょ。
あらためて問題を見ると、添付ファイルがあった。添付ファイルが無くてもある程度進められる問題は添付ファイルを見逃しがち。
添付ファイルはこれ。
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のソースコードも付いている。ただし、肝心の部分は隠されている。
:
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命令を復元すれば良いらしい。はい。
chall = 1234
print(hex((chall*3+0x1220+0x20)&0xffffffff^0xffff))
でも、「本当にこれで合っているのか?」という気がする。足している値とか、xorしている値とかがもう少しランダムっぽい値ならいかにもだけど……。
私「自信は無いけど、これ試してみてください」
laysakuraさん「通りませんね」
私「エンディアンを変えてみたらどうです?」
laysakuraさん「通りませんね」
私「はい……」
良く見たら、 20
は16進数ではなく、10進数だった。
正しくはこれで、これなら認証が通り、フラグが手に入れられたらしい。
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位。全完優勝余裕ですわ……と思っていたけれど、これが解けず……。
:
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);
}
:
オリジナルのRAMNもちょっとしたCTFのようなことができるようになっていて、アドレス0x01234567を読むとフラグが返ってくる。このアドレス読んでも当然NG。オリジナルでどこからこのアドレスをどうやって導くかというと、単に問題文に書かれている。
オリジナルは、読めるアドレスなら読める。0xffffffffから読もうとすると固まることに気が付いた。無限ループしそうな箇所は RAMN_MEMORY_CheckAreaReadable
くらいしかないので、この問題でも RAMN_MEMORY_CheckAreaReadable
は生きていそう? そう、コードなりメモリなりがダンプできれば、フラグでなくても良いよね。でも、コードやデータがありそうなアドレスを読んでも成功しない。ランダム化されていたりするのだろうか。ページ単位くらいで走査してみる? でも、ページサイズ4KBだとしても、32bitのアドレス空間だと約100万通りになるな……。
ところで、問題文の「peak」とは? メモリのreadとwriteのことをピークとポークと言うらしい(へー)が、このピークのスペルはPEEK。peakに何か意味がある……?
とかしているうちに時間切れ。
コンテスト終了後に聞いたところによると、オリジナルのRAMNでRAMが配置されるアドレスを1バイトずつずらして読んでいけば、フラグが読めたらしい。それも試さなかったっけ……。謎。
雑感
CTFに詳しい人がこの記事を読むと、「お前たいしたことしてないな」と言われそう。自分でもそう思う。このCTFは、CTFっぽい部分は特に難しくない(と私は思う)。一方で、CANとかUDSとか車の通信関連の部分が分からん。環境を構築するだけでも一苦労。しかし、チームメイトの車に詳しい人からすると、その辺はたいしたことなくて、CTF部分が難しいらしい。車に詳しい人とCTFに詳しい人でチームを組むと強い。