Weather

TCPサーバの接続情報とファイルが与えられた。
このファイルを7-Zipで開くと、以下のファイルが得られた。

A file and information to connect to a TCP server were given.
Opening the file with 7-Zip, I found these files:

Device Datasheet Snippets.pdf (以下、「PDF」) はシステムの構造についての説明、 firmware.c はサーバのプログラムのソースコードのようであった。

Device Datasheet Snippets.pdf (I'll call this as "the PDF") looked like a document about the structure of the system, and firmware.c looked like a source code for the server program.

デバイスへのアクセス Accessing the devices

PDFより、今回のシステムでは、 プロセッサと各種センサおよびEEPROMがI2Cで繋がっていることがわかる。
このEEPROMには、プロセッサ用のプログラムが格納されていそうである。
なお、センサのI2CアドレスはPDFに載っているが、EEPROMのI2Cアドレスは載っていない。

firmware.c を読むと、

The PDF tells that the processor is connected to the sensors and the EEPROM via I2C in the system for this challenge.
The EEPROM should have the program for the processor.
Note that the I2C addresses for the sensors are on the PDF, but the address for the EEPROM isn't.

Reading firmware.c, I found that we can read from I2C devices by specifying the address to read and the number of bytes to read in this order after "r" like this:

r 101 4

のように、「r」に続いて読み込むアドレスと読み込むバイト数を順に指定することで、I2Cデバイスからの読み込みを行えることがわかる。
また、

Also, we can write to I2C devices by specifying the address to write, the number of bytes to write, the data to write in this order after "w" like this:

w 101 4 11 22 33 44

のように、「w」に続いて書き込むアドレス、書き込むバイト数、書き込むデータを順に指定することで、I2Cデバイスへの書き込みを行えることがわかる。

ここで、指定したアドレスは、port_to_int8 関数によって数値に変換される。
この関数では、まず is_port_allowed 関数でアドレスを表す文字列をチェックし、 チェックを通過した場合のみ str_to_uint8 関数で文字列を数値に変換して返す。

is_port_allowed 関数では、指定した文字列が許可リストにある文字列のいずれかで始まるかどうかをチェックしている。
また、str_to_uint8 関数では、オーバーフローやラップアラウンドのチェックをしない単純な処理で十進文字列を8ビットの数値に変換している。
したがって、許可リストにある文字列 (例えば 101) で始め、その後の数字列をうまく選ぶことで、0~127の任意のアドレスを指定することができる。

例えば、101248000101 で始まっているため、アドレスの指定として利用可能である。
さらに、256で割った余りが0なので、末尾の 000 の部分をアクセスしたいアドレスに置き換えることで、簡単に任意のアドレスを指定できる。
これを利用し、0~127の全I2Cアドレスへのアクセスを試みる以下のプログラムを作成・実行した。

Now note that the specified address is converted to an integer by the function port_to_int8.
This function first checks the strings that represent the addresses using the function is_port_allowed, and then converts the strings to integers using the function str_to_uint8 only if it passed the check.

The function is_port_allowed checks if the strings start from either one of the strings in the allow list.
Also, the function str_to_uint8 simply converts decimal strings to 8-bit integers without checking for overflows/wraparounds.
Therefore, we can specify arbitrary addresses from 0 to 127 by starting from a string in the allow list (101, for example) and properly selecting numbers to add after that.

For example, we can use 101248000 as the address because this starts from 101.
Moreover, since the remainder of this value divided by 256 is 0, we can easily specify arbitrary addresses by replacing the last part 000 with the address to specify.
I wrote this program that tries accessing all I2C addresses from 0 to 127 using this value, and executed that.

port_brute.pl

この結果、センサのI2CアドレスとしてPDFに載っているアドレスに加えて、アドレス33へのアクセスが有効であることがわかった。
したがって、このアドレス33がEEPROMのI2Cアドレスであると推測できた。

アドレス33は、101 で始まり、256で割った余りが33になる 101153 とも表すことができる。
101248033 よりも短いため、今後はこれを用いる。

As a result, I found that the I2C address 33 is available in addition to the addresses that are on the PDF as the addresses for the sensors.
Therefore, I guessed that the address 33 is the I2C address for the EEPROM.

We can also use 101153, which begins from 101 and the remainder divided by 256 is 33, to specify the address 33.
I'll use this from here because this is shorter than 101248033.

EEPROMのダンプ Dumping the EEPROM

PDFより、今回使用されているEEPROMは以下の手順でI2Cから読み出すことができることがわかる。

  1. page index を書き込むことにより、読み出すページを選択する。
  2. データを読み出す。最大64バイトを読み出すことができる。

1ページの大きさは64バイトである。
page index の指定方法は明示されていないようであるが、試した結果何番目のページを読み出したいかを0-originの1バイトで指定すればよさそうだった。
また、今回使用されているEEPROMは CTF-55930D であり、これは64ページあることがわかる。

これらを踏まえ、EEPROM全体の内容を読み出す以下のプログラムを作成・実行した。

The PDF is telling that the EEPROM used in this challenge can be read via I2C in this way:

  1. Select the page to read by writing "page index".
  2. Read data. We can read 64 bytes at most.

The size of a page is 64 bytes.
How to specify "page index" didn't look clearly specified. Some experiments showed that putting the index of the page to read (the first page is 0th) as a single byte should work.
The PDF is also telling that the EEPROM used in this challenge is CTF-55930D, which has 64 pages.

Based on these information, I wrote this program to read the whole contents from the EEPROM and executed that.

read_eeprom.pl

読み出した結果、EEPROMのデータは以下のようになっているようだった。

The contents of EEPROM read were:

EEPROMの解析 Analyzing the EEPROM

EEPROMの内容を読み出すことができたので、これを解析し、flagの取得に繋げたい。
文字列はEEPROMの内容と firmware.c の内容を紐づける特徴となりそうなので、まずはこれを足がかりとした解析を試みた。

例えば、firmware.c 中の関数 i2c_status_to_error では、以下のように文字列が連続で使われている。

Now I succeeded to read out the contents of EEPROM. What to do next is analyzing this and find some ways to get the flag.
I decided to begin with using strings for analysis because they looks useful to determine which part of the EEPROM corresponds to each parts of firmware.c.

For example, this function i2c_status_to_error in firmware.c has multiple strings used in row.

const char *i2c_status_to_error(int8_t err) { switch (err) { case 0: return "i2c status: transaction completed / ready\n"; case 1: return "i2c status: busy\n"; case 2: return "i2c status: error - device not found\n"; case 3: return "i2c status: error - device misbehaved\n"; } return "i2c status: unknown error\n"; }

EEPROMから読み出したデータに strings --radix=x コマンドをかけた結果のうち、これらの文字列に対応する部分は以下のようになった。

This is the part of the result of strings --radix=x command used to the EEPROM data that corresponds to the strings.

889 "i2c status: transaction completed / ready 8b5 i2c status: busy 8c7 i2c status: error - device not found 8ed i2c status: error - device misbehaved 914 i2c status: unknown error

これらのアドレスをEEPROMのデータから探したところ、このあたりにビッグエンディアンで入っていた。

I searched for these addresses from the EEPROM data. As a result, I found them (stored in big-endian) in this area.

000000E0 E1 05 75 FE 01 80 F4 8F 82 22 AF 82 BF 00 02 80 |..u......"......| 000000F0 0F BF 01 02 80 11 BF 02 02 80 13 BF 03 1E 80 15 |................| 00000100 90 08 8A 75 F0 80 22 90 08 B5 75 F0 80 22 90 08 |...u.."...u.."..| 00000110 C7 75 F0 80 22 90 08 ED 75 F0 80 22 90 09 14 75 |.u.."...u.."...u| 00000120 F0 80 22 AD 82 AE 83 AF F0 8D 82 8E 83 8F F0 12 |..".............|

この部分のデータをよく見ると、0x100~0x122において、90 XX XX 75 F0 80 22 というデータが繰り返されていることがわかる。
(XX XX はそれぞれの文字列のアドレスをビッグエンディアンで表したものである)
さらに、0x0EC~0x0FFのデータは BF YY ZZ 80 WW というパターンの繰り返しになっており、WW の次のバイトからWWバイト後が90 XX XXになっていることがわかった。
これらを i2c_status_to_error 関数の処理と照らし合わせ、これらのデータは以下の意味を持つ機械語であると推測した。

ここで、プロセッサの名前が CTF-8051μC であり、「8051」が入っていることから、8051の命令セットが使われているのではないかと考えた。
8051の命令セットについては、例えば以下のページに情報がある。

Carefully looking at data in this area, I found that a pattern 90 XX XX 75 F0 80 22 is repeated from 0x100 to 0x122.
(XX XX is the addresses of each strings, represented in big-endian)
Moreover, I found a pattern BF YY ZZ 80 WW repeated from 0x0EC to 0x0FF, and that the pattern 90 XX XX begins at WW bytes ahead from the byte next to WW.
Comparing these findings with what the function i2c_status_to_error does, I guessed that these patterns are machine codes that has these meanings:

Here, I guessed that the instruction set of 8051 is used here because the name of the processor is CTF-8051μC, which contains "8051".
We can found information about the instruction set of 8051 on, for example, these pages:

これらのページを参照すると、75 F0 80 2275 F0 80 (値0x80をアドレス0xF0にストアする) と 22 (RET) に分割でき、
推測した機械語の意味と合っていそうであることがわかる。
そこで、以下の逆アセンブラを利用し、EEPROMのデータ全体の逆アセンブルを行った。

Referring these pages, I found that the instruction set matches with the guessed meanings of machine code,
dividing 75 F0 80 22 into 75 F0 80 (store the value 0x80 to the address 0xF0) and 22 (RET).
Seeing this, I disassembled the whole data in the EEPROM using this disassembler:

BiPOM Electronics - D52 8051/8052 Disassembler

その結果、全体を自然に逆アセンブルすることができたので、8051の命令セットが使われていると仮定して進めることにした。

As a result, the whole data is smoothly disassembled. Therefore, I decided to assume that the instruction set for 8051 is used to proceed to the next step.

FlagROMの読み出し Reading the FlagROM

使われている命令セットが推測できたので、残る仕事はFlagROMの内容を読み出すことである。
PDFによれば、FlagROMは256バイトのROMであり、アドレス用のレジスタにセットした位置のデータがデータ用のレジスタから読めるようである。

I2C経由でEEPROMの内容を書き換えることで、実行するプログラムを書き換えることができるはずである。
ただし、ビットを1から0にすることはできるが、0から1にすることはできないようである。
幸い、EEPROMの後半に0xFFが連続している部分があるので、ここには任意のプログラムを書くことができる。
そこで、既存のプログラムをうまく書き換え、この部分に書いたプログラムを実行する方法を考えることにした。

逆アセンブル結果を調べた結果、出力する文字列のアドレスをセットしてから呼び出されていることなどから、
X0123serial_print 関数に相当しそうであることがわかった。
さらに、その冒頭部分は以下のようになっていた。

Now I guessed the instruction set used. What to do next is to read the FlagROM out.
The PDF tells that the FlagROM is a 256-byte ROM, and that data whose position is specified via the address register is available via the data register.

It should be possible to change the program to execute by changing contents of the EEPROM via I2C.
Note that changing bits from 1 to 0 is possible, but changing bits from 0 to 1 looks impossible.
Fortunately there is a region with continuous 0xFF in the latter part of the EEPROM and we can write arbitrary programs there.
Therefore, I decided to find a way to change the existing program to have it execute programs written to the region.

Reading the disassembled code, I found that X0123 should correspond to the function serial_print.
One of the reasons is that the label is called after setting the addresses of strings to print.
This is the first part of the location:

X0123: mov r5,dpl ; 0123 ad 82 -. mov r6,dph ; 0125 ae 83 .. mov r7,b ; 0127 af f0 /p

このうち 83 af f0 という部分は、適切にビットを1から0にすることで、02 0a 40 にすることができる。
これはアドレス0xA40に実行を移すLJMP命令であり、0xFFが連続している部分に実行を移すことができる。

次に、このアドレス0xA40に書き込む、FlagROMの内容を出力するプログラムを作成した。
serial_print 関数のプログラムを参考に以下のプログラムを書き、手動で機械語に変換した。

The piece 83 af f0, which exists in this area, can be changed to 02 0a 40 by properly changing some bits from 1 to 0.
This is the LJMP instruction to jump to the address 0xA40, so it can execute the region with continuous 0xFF.

After finding this, I created a program for writing to this address 0xA40 to print the contents of the FlagROM.
Referring to the program of the function serial_print, I wrote this program and converted to machine code manually.

7d 00 mov r5, #0 flagLoop: 8d ee mov 0eeh, r5 waitLoop: e5 f3 mov a, 0f3h 60 fc jz waitLoop e5 ef mov a, 0efh f5 f2 mov 0f2h, a 0d inc r5 bd 00 f2 cjne r5, #0, flagLoop 22 ret

PDFより、EEPROMにI2C経由で以下のデータを書き込むことで、EEPROMに書き込みを行うことができることがわかる。

これに基づき、作成したプログラムをCyberChefで書き込み用のバイト列に変換した。

The PDF tells that we can write to the EEPROM by writing the following data to the EEPROM via I2C:

Based on this, I converted the program I wrote to a sequence of bytes to use for writing using CyberChef.

Find / Replace, From Hex, NOT, To Decimal - CyberChef

以下が、これを用いて作成した、このプログラムを0xA40から書き込むコマンドである。

This is the command to write this program from 0xA40, created using the result of this conversion.

w 101153 22 41 165 90 165 90 130 255 114 17 26 12 159 3 26 16 10 13 242 66 255 13 221

さらに、以下が serial_print 関数の冒頭部分を0xA40にジャンプするように書き換えるコマンドである。
83 af f0 の部分を書き換えるだけでなく、その前の ae00 (NOP) に書き換えることで、LJMP命令を実行させている。

This is the command to modify the beginning part of the function serial_print to have it jump to 0xA40.
This command will not only modify the piece 83 af f0 but also change ae placed before the piece to 00 (NOP) to have it execute the LJMP instruction.

w 101153 46 4 165 90 165 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 253 245 191

これらのコマンドを以下のように続けて実行することで、flagが得られた。

I obtained the flag using these commands in this way:

== proof-of-work: disabled == Weather Station ? w 101153 22 41 165 90 165 90 130 255 114 17 26 12 159 3 26 16 10 13 242 66 255 13 221 i2c status: transaction completed / ready ? w 101153 46 4 165 90 165 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 253 245 191 CTF{DoesAnyoneEvenReadFlagsAnymore?} CTF{DoesAnyoneEvenReadFlagsAnymore?}
CTF{DoesAnyoneEvenReadFlagsAnymore?}

Google Capture the Flag 2022