SECCON CTF 2018 ( DOMESTIC )にチームBluemermaidで出て2位だった話

2018-12-23に開催されたSECCON CTF 2018の国内決勝に、チームBluemermaidとして h_nosonうさぎsrupとともに出場して、準優勝したようです。

ということで、一応Write-up的なものを書いておこうと思います。

概況

問題としては大きく三つ: 松島・天橋立・宮島に分かれており、最初は天橋立と宮島だけが開放されていて、松島は試合の途中で開放されました。 私は毎年のように、謎アーキテクチャバイナリ担当として雇われていた(予選ではそれだけしか解けなかった)のですが、残念ながらそういった問題は出なかったので、アセンブリコードゴルフができそうな3つ目の宮島を主にやっていました。

松島

松島は、ビデオポーカーの乱数に脆弱性があるので、それをついてうまくいい手を出すとフラグがもらえたりディフェンスキーワードをサブミットできるよ(プログラムのバイナリは与えるよ)というものでした。

これはうさぎさんとsrupさんがさくっと解いてくれたので詳細は各人に任せますが、どうも乱数に脆弱性がありすぎ?だったようで、バイナリを解析せずとも4カードやフルハウスを出せてしまったらしく、しかもディフェンスキーワードのサブミットフォームの送信先URLが常に同一だったため、一回解いてしまえばポーカーには一切触れず自動化できてしまうという代物だったようです。

天橋立

天橋立は、ディフェンスポイントのみがある問題でした。 XSS HELLというサイト名で、XSS脆弱性があるような任意のhtmlをアップロードできる仕組みになっていました。 そのhtmlはだれでもダウンロードでき、それに存在するXSSを他のチームは解くことができて、解かれてしまうとディフェンスポイントは入らないよーというルールでした。 具体的には、アップロード時の名前をディフェンスキーワードの文字列にすることで、その問題が解かれずに残っていると、ディフェンスポイントが入るという仕組みでした。

というわけで、人々は最初、そこそこ難しいペイロードを突っ込んだ時だけXSSが発動するようなhtmlをあげておいて、ディフェンスキーワードが変わったら自分たちで解いて新しくアップロードしなおせばいいのだと考えていました。 ところがなんと、あるチームがアップロードしたhtmlは、そのチームが自ら解くことはできず、一方でタイトルはアップロード履歴画面から任意に変更できるという仕組みになっていたのです。 …そうすると、何が起こったか。

どのチームも、そのチームだけが知っている特定のキーワードのハッシュ値をhtmlに書いておき、それが一致した時のみXSSを突かれたような挙動をする、ただのパスワード認証のようなhtmlを上げ始めたのです。 結局、「他のチームの問題を解く」という行為はほぼ不可能になり(SHA-256をじっと睨めば解ける方ならちがうかもしれませんが)、もはやディフェンスキーワードの更新を自動化するだけの作業だけが残ったのでした。

このゲーム性のなさは、作問者のYu Yagihashi氏も認識しており、コンセプトが崩壊したと悲しみの声を表明しておられました。

同時に、どのようにすればXSS HELLがコンセプト崩壊しなかったのか、意見を募集しておられるようですので、なにか思いついた方はつぶやいてみるのもよいのではないでしょうか。

宮島

というわけで、松島も天橋立も終わってしまった今となっては、もう宮島しか我々には残されていないわけです。

宮島はどのような問題だったかというと

  • ある与えられた要件を満たすような動作をするx86アセンブリのバイナリを作成して投げる
  • テストに合格すれば、フラグがもらえるので、アタックポイントがもらえる
  • バイナリと同時にディフェンスキーワードも送信する
    • 最も短いバイナリを最速でsubmitすることでディフェンスキーワードを書き込むことができる
    • 同じ長さのバイナリで2番目以降にsubmitした人はディフェンスキーワードを書き込めない
    • より短いバイナリを他のチームに投げられない限り、書き込まれたディフェンスキーワードのチームにディフェンスポイントが一定時間ごとに入る

というものでした。

画面としては以下のような感じ。

f:id:hikalium:20181224000347p:plain

これは最終問題なので、まあまあな感じですが、最初は

int型の引数a, bが渡されるので、その和を返す関数を実装してください

という感じのかんたんさで、かんたんであるが故にバイナリの短さの限界が見えてしまい、もはや問題予測&早解き大会となっていました。

ちなみに、上記のXorはおそらく下記のものが最短かと思われます(他のチームに先を越されてしまいましたが。)

0000000000000000 <func>:
   0:   87 ce                   xchg   %ecx,%esi

0000000000000002 <Lcmp>:
   2:   30 54 0f ff             xor    %dl,-0x1(%rdi,%rcx,1)
   6:   e2 fa                   loop   2 <Lcmp>
   8:   c3                      retq   

LOOP命令を使うのがミソです。SDMを読んでた甲斐がありましたね!

バイナリ早書き支援Makefile

最初からバイナリを書いてもいいんですが、間違えやすいので、こんなかんじで最初はcで書き、あとでasmで書いてみて、test.cとリンクしてテストするという感じで作業しました。

c:
    gcc -Os -c -o c.o c.c
    cp c.o func.o
    objdump -d c.o
    objdump -d c.o | cut -f 3
    objcopy -O binary -j .text c.o co
    od -An -t x1 co

asm:
    as -o asm.o asm.S
    cp asm.o func.o
    objdump -d asm.o
    objdump -d asm.o | cut -f 3
    objcopy -O binary -j .text asm.o co
    od -An -t x1 co

test:
    gcc -o test.bin func.o test.c
    ./test.bin

まあ、私より周りのプロのほうが早くてあまり役に立てませんでしたが…。

CLIからバイナリ送信

バイナリ送信がwebフォーム経由だったのですが、ディフェンスキーワードをいれたり、コードをコピペしてEnterを押すのは面倒だったので、CLIでできるように準備だけはしました。暇だったので。(だって最速AC取られたら30分後まで勝ち目はないんだもの…)。

const puppeteer = require('puppeteer');

(async () => {
  var key = process.argv[2];
  var code = process.argv[3];
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.dismiss();
  });
  await page.goto('http://miyajima.pwn.ja.seccon/');
  await page.type('input', key);
  await page.type('textarea', code);
  const elementHandle = await page.$('button');
  await elementHandle.press('Enter');
  const element = await page.$(".form__attacking-flag");
  if(element){
    const flag = await page.evaluate(element => element.value, element);
    console.log("FLAG: " + flag);
  }
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();

ぐーぐるくろーむのpuppeteerっていうのを使うと、けっこうお気軽にWebブラウザ操作の自動化ができておいしいです。べんり。

github.com

最初はLighthouseを使おうとしたのですが、どうもうまくいかなかったのでこちらを使いました。 ただ、結構インターネットの海にある使ってみたよ記事は古いAPIを使っていたりしてうまくいかないことが多かったです。 困った時はちゃんとAPIドキュメントを見ると楽かもしれません。

感想

少し問題数が少なかったような気がする。特に、アタックポイントにほとんど差がついていなかったと思うので、もう少し問題数を増やすとか、解ききれない難しいものを出してもらえるとよかったかもしれない。 また、問題に穴が多かったというか、ゲーム性が少なく、早押しクイズのような感じになってしまっていた部分が例年より多かったように感じた。

とはいえ、解くこと自体は面白かったし、SECCONはSECCONなのでよかったと思います。 来年ももっと進化したSECCONを楽しみにしています!(精進します…。)

共に戦った参加者のみなさん、運営のみなさま、関係者の皆様、ありがとうございましたー!

自作OSでできる!NVDIMMのつかいかた

これは、自作OS Advent Calendar 2018 の7番目の素数日の記事です。

はじめに

みなさん、NVDIMMって知っていますか?知っている人はぜひ仲良くなりましょうー。

NVDIMMとは、Non-Volatile DIMMの略で、要するにDIMMスロットに刺さる不揮発性の記憶モジュールのことです。 通常のDRAM DIMMは、電源を切るとデータが消えてしまう揮発性の記憶素子なのですが、なんとNVDIMMは電源を切ってもデータが消えません。すごいね!

(NVDIMMの実現方法にはいくつか種類があって…という、NVDIMM自体の細かい話はここではしません。)

さて、自作OSを書いている皆様はよくわかると思うのですが、自作OSで何らかのデータを保存するのはとても大変です。メモリにあるデータは電源を切ると消えてしまいますから、HDDやSSDやSDカードに書き出さないといけません。そうすると、まずはそれらのデバイスのドライバを書かなくてはいけませんし、しかもこれらのデバイスはブロック単位でしか書き込みができませんから、何らかのファイルシステムのような仕組みも用意しなくてはいけません。…大変ですね。

そういうわけで、多くのみなさんはデータを保存する機構を実装せず、電源を切ったらデータは全部消えてしまってもいいという割り切りをすることになります。これはかなしい。

ところが!NVDIMMはCPUからみたとき、メモリとして認識されるので、ドライバなしにCPUから直接読み書きすることができます。つまり、DIMMにマッピングされたアドレスに書いたデータは、起動後もそのまま読み出せるのです!

ということで、この記事では、自作OSでNVDIMMを使うにはどうすればいいかを解説していきます。

検証環境

残念ながら、NVDIMMの実機を個人で買うのはすこしたいへんです。なので、代わりにQEMUのNVDIMMエミュレーションを利用することにします。

利用したバージョンはコミット7c69b7c849をソースビルドしたものです。

QEMU emulator version 3.0.50 (v3.0.0-1143-g7c69b7c849)

コマンドラインオプション

公式ドキュメントは以下にあります。

qemu/docs/nvdimm.txt

これを参考に、QEMUの起動コマンドラインオプションは以下のようにしました。

-bios $(OVMF) \
-machine q35,nvdimm -cpu qemu64 -smp 4 \
-monitor stdio \
-m 8G,slots=2,maxmem=10G \
-drive format=raw,file=fat:rw:mnt -net none \
-object memory-backend-file,id=mem1,share=on,mem-path=pmem.img,size=2G \
-device nvdimm,id=nvdimm1,memdev=mem1

これらのオプションのうち、NVDIMMをエミュレーションする上で重要なポイントは以下の通りです。

  • -machinenvdimm を追加する。
  • -m のサイズはDRAMのサイズを設定する。
    • slots は、(メインのDRAMスロット数+NVDIMMスロット数)に設定する。
      • (今回の場合 1 + 1 = 2)
    • maxmem は、(メインのDRAM容量 + NVDIMMの容量)に設定する。
      • (今回の場合 8G + 2G = 10G)
  • -object-device の組で、ひとつのファイルバックエンドNVDIMMデバイスを作成できる。
    • -objectid と、-devicememdevの値を一致させる。
    • -mem-path にはqemu-imgで作成したデータイメージのパスを指定する。
      • ここでは、qemu-img create pmem.img 2G と実行して作成されたイメージを利用した。
    • -sizeには、上記データイメージ作成時に渡したパラメータと同じサイズを指定する。

また、今回はUEFIを利用するため、-biosにはOVMFのコミットb9cee524e6からビルドしたbiosイメージを指定しています。

Linux以外でのエミュレーションができなかった問題

ちなみに、Linux以外でNVDIMMエミュレーション(正確にはhostmem-file)が正しく動作しない問題があったので、私がパッチを送っておきました。すでにmasterにはマージされているので、最新のQEMUコンパイルしてご利用ください。

https://github.com/qemu/qemu/commit/d5dbde4645fe56a1bcd678f85fa26c5548bcf552

実装の指針

さて、これでエミュレーションの準備は整いました。つぎは実装の計画を立てましょう。

いかにしてNVDIMMのマップされている物理アドレスを取得するか

NVDIMMはメモリバスの配下に接続されているので、物理アドレス空間のどこかにNon-volatileなメモリ空間が生い茂っているはずです。 しかし、私たちはそれがどこにあるのかまだ知りません。知るためにはどうすればよいか…ACPIのNFITを読みます。

ACPI NFIT

ACPI Revision 6.0 より、NFITというPlatform Capabilities Structureが追加されました。 NFITとは、NVDIMM Firmware Interface Table の略です。このテーブルを参照することで、実行中のプラットフォーム上にあるNVDIMMの情報を取得できます。

packed_struct ACPI_NFIT {
  char signature[4];
  uint32_t length;
  uint8_t revision;
  uint8_t checksum;
  uint8_t oem_id[6];
  uint64_t oem_table_id;
  uint32_t oem_revision;
  uint32_t creator_id;
  uint32_t creator_revision;
  uint32_t reserved;
  uint16_t entry[1];
};

今回はそのNFITに含まれる情報の中でも、SPA(System Physical Address) Range Structures に知りたい情報があります。

packed_struct ACPI_NFIT_SYSTEM_PHYSICAL_ADDRESS_RANGE_STRUCTURE {
  uint16_t type;
  uint16_t length;
  uint16_t spa_range_structure_index;
  uint16_t flags;
  uint32_t reserved;
  uint32_t proximity_domain;
  uint64_t address_range_type_guid[2];
  uint64_t system_physical_address_range_base;
  uint64_t system_physical_address_range_length;
  uint64_t address_range_memory_mapping_attribute;
};

話はわかった。ところで、そのNFITっていうのはどうやったら読めるの?

NFITへのポインタは、ACPIのXSDT(eXtended System Descriptor Table)に格納されています。

packed_struct ACPI_XSDT {
  char signature[4];
  uint32_t length;
  uint8_t revision;
  uint8_t checksum;
  uint8_t oem_id[6];
  uint64_t oem_table_id;
  uint32_t oem_revision;
  uint32_t creator_id;
  uint32_t creator_revision;
  void* entry[1];
};

XSDTへのポインタはRSDT(Root System Description Table)に格納されています。

packed_struct ACPI_RSDT {
  char signature[8];
  uint8_t checksum;
  uint8_t oem_id[6];
  uint8_t revision;
  uint32_t rsdt_address;
  uint32_t length;
  ACPI_XSDT* xsdt;           // <<< HERE!
  uint8_t extended_checksum;
  uint8_t reserved;
};

で、このRSDTへのポインタは…EFI Configuration Table にあります。(EFI_ACPI_TABLE_GUIDから引ける。)

というわけで、実際の順番としては

EFIConfigurationTable 
-> ACPI_RSDT 
-> ACPI_XSDT 
-> ACPI_NFIT 
-> SPARangeStructure

と辿っていけば、NVDIMMのマップされているアドレス system_physical_address_range_base がわかるわけです。

実装

さあ、これでもうあとは実装するだけですね!

ということで、実装してみた例がこちらです。

github.com

軽く実装について説明します。

src/liumos.cc のMainForBootProcessor()が、起動後に最初に実行される関数です。

この関数内の、

  ACPI_RSDT* rsdt = static_cast<ACPI_RSDT*>(
      EFIGetConfigurationTableByUUID(&EFI_ACPITableGUID));
  ACPI_XSDT* xsdt = rsdt->xsdt;

という部分で、EFIConfigurationTableからRSDTを取得し、そこからXSDTを取得しています。

そして、以下のようにしてXSDTからNFITを見つけ出し、

  ACPI_NFIT* nfit = nullptr;
  ...
  for (int i = 0; i < num_of_xsdt_entries; i++) {
    const char* signature = static_cast<const char*>(xsdt->entry[i]);
    if (IsEqualStringWithSize(signature, "NFIT", 4))
      nfit = static_cast<ACPI_NFIT*>(xsdt->entry[i]);
   ...
  }

NFITが存在していれば、適当にSPARange structureを見つけ出して、適宜書き込んだり読み込んだりしてみてその結果を表示しています。

 if (nfit) {
    PutString("NFIT found\n");
    PutStringAndHex("NFIT Size", nfit->length);
    PutStringAndHex("First NFIT Structure Type", nfit->entry[0]);
    PutStringAndHex("First NFIT Structure Size", nfit->entry[1]);
    if (static_cast<ACPI_NFITStructureType>(nfit->entry[0]) ==
        ACPI_NFITStructureType::kSystemPhysicalAddressRangeStructure) {
      ACPI_NFIT_SPARange* spa_range = (ACPI_NFIT_SPARange*)&nfit->entry[0];
      PutStringAndHex("SPARange Base",
                      spa_range->system_physical_address_range_base);
      PutStringAndHex("SPARange Length",
                      spa_range->system_physical_address_range_length);
      PutStringAndHex("SPARange Attribute",
                      spa_range->address_range_memory_mapping_attribute);
      PutStringAndHex("SPARange TypeGUID[0]",
                      spa_range->address_range_type_guid[0]);
      PutStringAndHex("SPARange TypeGUID[1]",
                      spa_range->address_range_type_guid[1]);

      uint64_t* p = (uint64_t*)spa_range->system_physical_address_range_base;
      PutStringAndHex("\nPointer in PMEM Region: ", p);
      PutStringAndHex("PMEM Previous value: ", *p);
      (*p)++;
      PutStringAndHex("PMEM value after write: ", *p);

      uint64_t* q = reinterpret_cast<uint64_t*>(page_allocator.AllocPages(1));
      PutStringAndHex("\nPointer in DRAM Region: ", q);
      PutStringAndHex("DRAM Previous value: ", *q);
      (*q)++;
      PutStringAndHex("DRAM value after write: ", *q);
    }
  }

(本当はきちんと見つけ出さなければいけないのですが、QEMUの場合NFIT中の0番目のエントリに運良くSPARangeStructureがあったので手抜きしています。ごめんなさい!)

ね!テーブルをたどるだけの簡単なお仕事でしょ!

実行結果

リポジトリのこのハッシュをクローンしてきてmake runすると、最初にpmem.imgが作成されてからQEMUが起動します。

$ make run
make -C src
make[1]: Nothing to be done for `default'.
qemu-img create pmem.img 2G
Formatting 'pmem.img', fmt=raw size=2147483648
mkdir -p mnt/EFI/BOOT
cp src/BOOTX64.EFI mnt/EFI/BOOT/
qemu-system-x86_64 -bios ovmf/bios64.bin -machine q35,nvdimm -cpu qemu64 -smp 4 -monitor stdio -m 8G,slots=2,maxmem=10G -drive format=raw,file=fat:rw:mnt -net none -object memory-backend-file,id=mem1,share=on,mem-path=pmem.img,size=2G -device nvdimm,id=nvdimm1,memdev=mem1
QEMU 3.0.50 monitor - type 'help' for more information
(qemu)

画面としてはこのような感じになります。

f:id:hikalium:20181217181025p:plain
最初の起動

今回は比較のため、PMEM領域に含まれるアドレスと、DRAM領域に含まれるアドレスにある8バイト整数を起動毎にそれぞれインクリメントしていくように実装しました。

ひとまず最初は、どちらも運良く0で初期化されていたので、それぞれインクリメントしたら1になっていますね。

では、qemuのコンソールにqと打ち込んで終了させ、もう一度make runしてみましょう。

すると…!

f:id:hikalium:20181217231241p:plain
2回目の起動

DRAMとPMEM、どちらも最初の起動時と同じポインタを読み書きしているのですが、DRAMでは最初の起動時にインクリメントした1は忘れ去られてまた0からやりなおしになっている一方、PMEMでは前回のインクリメント結果である1が再起動後も残っていて、今度は2が書き込まれました!

念の為もう一回再起動してみると…

f:id:hikalium:20181217231640p:plain
3回目の起動

やっぱりDRAMはデータが消えてしまっていますが、PMEMは残っていますね!すごい!

まとめ

というわけで、NVDIMMを使えば、自作OSでも簡単にデータを保存して再起動後にも残しておけるということがわかりました! とはいえ今回は簡単な説明しかしておらず、実装は手抜きですし、キャッシュをフラッシュしなければデータが消える可能性があるなど、細かい点で注意しなければならないことが山積みです。

これらを考慮しつつ、liumOSはNVDIMMを有効活用した新しいOSを目指して開発をしてゆきますので、今後にご期待ください!

では皆様、よいOS自作ライフを!

参考文献

編集履歴

  • ACPI SpecおよびUEFI Specについて、最新バージョンを参照するよう参考文献を変更しました。
  • ACPI SpecにNFITが追加されたのは6.2Aではなく6.0からとの指摘を受けましたので、該当箇所を修正しました。

compilium v2 (in progress)の工夫点

この記事は、言語実装アドベントカレンダーの7日目にあたります。

ちなみに、これを書いている現在は2018年12月7日の12:00です。果たして今日中に記事を公開することはできるのでしょうか…。

compilium v2とは

github.com

compiliumは、hikaliumが開発しているC11準拠(予定)のCコンパイラです。C言語で書かれています。

compilium v2では、v1での反省点およびセキュリティ・キャンプ全国大会2018のCコンパイラ自作ゼミでの知見をもとに、よりわかりやすく単純でインクリメンタルに開発できることを目標に開発しています。

この記事では、これらの得られた知見とともに、それらが実際にどのようにcompilium v2に活かされているかを紹介します。

最初からレジスタマシンとしてアセンブリを出力する

当初、Cコンパイラゼミでは、スタックマシンとしてアセンブリを出力するよう受講者のみなさんにお願いしていました。

これは、レジスタ割り当てを最初から考慮するのは大変であり、スタックマシンならばシンプルに実装できるという予想に基づいたものでした。

実際、スタックマシンを採用したことで、式のコード生成については、かなりスムーズに習得してもらうことができました。

一方で、スタックマシンの弱点としては、アセンブリレベルでのデバッグが非常に困難になることが挙げられます。 これは、出力の多くが本質的な計算ではなくpush/popに占められることと、各計算の間で値がどのように伝播するかわかりづらいことが原因でした。

そこで、レジスタマシンの可能性を再考してみたところ、式に閉じたレジスタマシンであれば、複雑な式を与えない限り、単純なコードでもうまく生成でき、出力も自然に近いアセンブリになることがわかりました。

簡単なレジスタマシンの実装

式(Expression)に閉じたレジスタマシン、つまり、異なる式の間で値を引き継いだりしないようなものは容易に実装することができます。 具体的には、値であれば新しくレジスタを確保し、演算子であればその各オペランドに対してレジスタ割り当てを実行したのち、戻り値を戻すためのレジスタ一つをのぞいてすべて解放する、という手順を行えばよいです。

戻り値を戻すためのレジスタとして、演算子の左側のオペランドレジスタを利用することにした場合、2 + 3 * 5は、下の図のように割り当てることができます(R, G, Bはそれぞれレジスタ名です)。

f:id:hikalium:20181207192720g:plain
レジスタ割り当てのようす

実際には、この式は2つのレジスタで計算可能な式(3*5を先に計算すればよい)なのですが、そんなことは気にしたら負けです。 とにかく単純に実装することがcompiliumの目的ですからね!

利用可能なレジスタ

SystemV ABI において、保存せずに破壊しても問題ない汎用レジスタは、以下のものになります。

rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11

このうち、

  • raxは戻り値の格納に使われる
  • rdxはmul, div系で変更される可能性がある(MUL r/m64: RDX:RAX ← RAX ∗ r/m64))
  • rcxはシフト系演算でオペランドとして利用される(シフト系演算のシフト量は、レジスタで指定する場合CLしか使えない)

ことから、単純のため利用しないことにすると、気軽に自由に使えるレジスタ

rsi, rdi, r8, r9, r10, r11

の6つになります。

なぜこれで大体うまくいくのか

f:id:hikalium:20181207200845p:plain
レジスタ割り当ての例

上の図を参照してもらえればわかるように、ASTが右に伸びるほど、必要なレジスタの個数が増えていることがわかります。

ASTが右に伸びるということは、右側のほうを先に計算する必要がある、つまり、右側の演算子の優先順位が高いか、同じ優先順位でも右結合の場合か、もしくはかっこで包まれている場合に限られます。

C言語二項演算子で右結合性のものはassignment-operatorのみであり、これを連続して使うような場合はあまり多くありません。

したがって、優先順位の異なる演算子や括弧をひとつの式の中で多用しないかぎり、このアルゴリズムでも大体うまくいくわけです。

うまくいかなかったらどうするかって?ソースを書き換えちゃいましょう!変数を使えばどんな式もコンパイルできる形に確実に書き直せますよ!

エラーメッセージの改善

compilium v1では、エラー時にその行番号を報告することしかできませんでした。 しかし、式のテストは1行にソースが押し込められてしまうことから、行番号は大して役にたちませんでした。 そこで、compilium v2では、エラー発生時に該当トークンの前後を表示できるように実装されました。

{int *a; return sizeof(a);}
                ^^^^^^
Error: Generate: Not implemented unary prefix op

assignment-expression のパーストリック

assignment-expression:
    conditional-expression
    unary-expression assignment-operator assignment-expression

assignment-expressionが右再帰であることは先ほど触れましたが、右再帰とはいっても単純な右再帰ではありません。 再帰の終端点では conditional-expression が許されるのに対し、再帰途中の左辺には unary-expressionしか許されません。 パーサを書いていて、ここをどう処理するか悩んでいるみなさんも多いのではないでしょうか。(もしかしてみんな悩まない!?)

compilium v2の実装にあたり、この問題の解決策をいくつか考えてみました。

方法1: unary-expressionを先読みする

conditional-expressionは、cast-expressionの場合を除いて、先頭にunary-expressionがくる。

そして、cast-expressionの場合に左側に来る要素はunary-expressionとして認識される可能性がない( type-name ) ので、unary-expressionを先読みして直後にassignment-operatorがくるか調べ、来なければそのunary-expressionを渡しつつconditional-expressionをパースすれば良い。

  • 利点:
    • 文法に沿ったパース結果を得られる
  • 欠点:
    • パーサの一部のみ引数が増える

方法2: 試してみてだめだったら巻き戻す

compilium v1で採用していた方法

  • 利点
    • 文法に沿ったパース結果を得られる
  • 欠点
    • 巻き戻すのはださい(このためだけにTokenStreamの位置を保存しなければいけない)
    • 効率が悪い(2回同じことをすることになる可能性がある)

方法3: conditional-expressionとして読んでから、読んだトークンのクラスを判定する

  • 利点:
    • 文法に沿ったパース結果を得られる
    • パーサの引数の数を統一できる
  • 欠点:
    • ASTのタイプを注意深く設計しなければならない
    • unary-expression以下のASTタイプを新たに実装した際に判定条件に追加しなければならない
      • unary-expression, postfix-expression, primary-expression がunary-expressionに含まれる。

方法4: 単純な右再起になるよう構文を変更してしまう

assignment-expression:
    conditional-expression
    conditional-expression assignment-operator assignment-expression
    ^^^^^^^^^^^^^^^^^^^^^^

9ccで採用されている方法 compilium v2でもこれを採用することにした。

  • 利点:
    • パーサの記述が楽
  • 欠点:
    • 構文にあてはまらない入力をパースできてしまう
      • a+1 = 2 のような構文は本来パースできない
    • ただし意味解析で除去できる(左辺がlvalueかどうかチェックすればよい)
    • 副作用として、条件演算子を左辺値として用いることができるようになる。
      • これはgcc拡張である
      • しかもこの構文はC++ではvalid(つまりこっちのほうが自然)

というわけで、皆さんも方法4を採用するといいと思います!(楽なので)

主要な構造体をTokenとASTNodeのみに集約

compilium v1 では、型がついていないと不安だし、メモリを効率よく使いたいという思いから、ASTの各タイプごとに構造体を用意していました。

しかし、これは機能追加ごとのコード量が増大する要因となり、またほぼ同一内容のコードがソース中に増える原因となりました。

compilium v2 では、思い切ってASTおよび型情報を単一の構造体ASTNodeに押し込めることにしました。

これは、メモリ効率の低下を招きますが、まあ昨今みんなRAMは32GB以上積んでいると思われますので問題ないでしょう…。

(やっぱり無理して書くよりも気楽に書いた方がいいですよ。)

おわりに

本来はもっと真面目にもっと詳細に語りたかったのですが、ちょっと疲れてしまってゆるふわになってしまいました。ごめんなさい…。

特に型の話についてはもう少し語りたいので、12/23のC言語アドベントカレンダーでまた書きたいと思います。

私もコンパイラ初心者なので、何か指摘や質問等あればぜひTwitterのほうまでお願いします!

年内にはv2がセルフホストできるようにがんばりますので、どうか生暖かい目で見守っていてください…。

今年度セキュリティキャンプ2018のCコンパイラゼミに参加してくださったみなさん、そして一緒に講師を担当していただいたRuiさんには、実装方法など様々な面でよい刺激をいただきました。ありがとうございます。

ということで、

みなさんもCコンパイラ書きましょうね?

楽しいぞ!

参考文献

SECCON 2018 Online CTF Writeup

https://score-quals.seccon.jp/team/115

チーム Bluemermaid として参加しました。総合17位、国内6位だったようです。 私がsubmitしたフラグは1つでした。(2つ解けたのだけど、1つは少し前に他の方が解いていた…。はやい。)

解けた問題

Special Device File

毎度おなじみ謎アーキでした。

$ file runme_8a10b7425cea81a043db0fd352c82a370a2d3373 
runme_8a10b7425cea81a043db0fd352c82a370a2d3373: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped

と言ってもaarch64ですが。

まずは毎度おなじみ環境構築をします。私はUbuntu 18.04でやりました。(事前にやっておけ。) 全部のアーキをビルドしていると試合が終わってしまうので、必要なアーキだけtargets.shに書いておくとよいでしょう。

そんなわけで、とりあえずシミュレータで実行してみると

/usr/local/cross2-gcc494/bin/aarch64-elf-run runme_8a10b7425cea81a043db0fd352c82a370a2d3373 

トラップで落ちます。まあきっと未実装なのでしょう。

みんな大好きobjdumpをみてみると、

000000000000140c <__exit>:                           
    140c:       d4200000        .word   0xd4200000   
    1410:       d65f03c0        ret               
                                                      
0000000000001414 <__read>:                           
    1414:       d28000c8        mov     x8, #0x6                        // #6
    1418:       d45e0000        .word   0xd45e0000             
    141c:       d65f03c0        ret                            
                                                    
0000000000001420 <__write>:                           
    1420:       d28000a8        mov     x8, #0x5                        // #5
    1424:       d45e0000        .word   0xd45e0000         
    1428:       d65f03c0        ret                   
                                                     
000000000000142c <__open>:                           
    142c:       d2800028        mov     x8, #0x1                        // #1
    1430:       d45e0000        .word   0xd45e0000    
    1434:       d65f03c0        ret                  
                                                      
0000000000001438 <__close>:                                                  
    1438:       d2800048        mov     x8, #0x2                        // #2    
    143c:       d45e0000        .word   0xd45e0000          
    1440:       d65f03c0        ret                         
    1444:       d503201f        nop                        
    1448:       d503201f        nop                         

こんな感じでたいへん怪しいです。きっと0xd45e0000という命令を実装しないといけないのでしょう。

ここで、シミュレータをトレース付きで実行してみると、

/usr/local/cross-gcc494/aarch64-elf/bin/run --trace-syscall=on  runme_8a10b7425cea81a043db0fd352c82a370a2d3373

どこでこけているのかわかるようになります。

具体的には、cross-gcc494/toolchain/gdb-7.12.1/sim/aarch64/simulator.cの中のhandle_halt関数内です。(このソースは1万5000行くらいあって、ファイル分けてほしかったなあという気持ちになった。)

static void
handle_halt (sim_cpu *cpu, uint32_t val)
{
  ....
  switch (aarch64_get_reg_u32 (cpu, 0, NO_SP))
    {
    case AngelSVC_Reason_HeapInfo:
    ...
    case AngelSVC_Reason_Rename:
    case AngelSVC_Reason_Elapsed:
    default:
      TRACE_SYSCALL (cpu, " HLT [Unknown angel %x]",
         aarch64_get_reg_u32 (cpu, 0, NO_SP));
      sim_engine_halt (CPU_STATE (cpu), cpu, NULL, aarch64_get_PC (cpu),
           sim_stopped, SIM_SIGTRAP);
    }
  aarch64_set_reg_u64 (cpu, 0, NO_SP, result);
}

この関数の下方、AngelSVC_XXXでswitch-caseのdefaultで落ちていました。ということで、適当にここにハンドラを作ってあげればよさそうです。

syscall番号は、先のobjdump disasより、x8レジスタに代入されていることがわかっているので、引数はレジスタ0から入ると仮定していろいろみてみたところ、なんと/dev/xorshift64といういかにもなデバイスをopenしていることがわかりました。 最初のwriteはシードを設定するのでしょう。そして、後続のreadで乱数を取得すると予想できます。 というわけで、こんな感じで書いてみるとよさそうです。

    default:
      {     
        static uint64_t seed = 0;
        uint64_t syscall_id = aarch64_get_reg_u64 (cpu, 8, SP_OK);
        if(syscall_id == 1) {
          uint64_t pathname = aarch64_get_reg_u64 (cpu, 0, SP_OK);
          uint64_t flags = aarch64_get_reg_u64 (cpu, 1, SP_OK);
          uint64_t mode = aarch64_get_reg_u64 (cpu, 1, SP_OK);
          printf("open \n");
          printf("pathname:%s \n", aarch64_get_mem_ptr (cpu, pathname));
          break;
        } else if(syscall_id == 2) {
          printf("close\n");
          break;
        } else if(syscall_id == 5) {
          printf("write\n");
          uint64_t buf = aarch64_get_reg_u64 (cpu, 1, SP_OK);
          uint64_t size = aarch64_get_reg_u64 (cpu, 2, SP_OK);
          printf("buf:%lX \n", buf); 
          printf("size:%lX \n", size);
          seed = aarch64_get_mem_u64 (cpu, buf); 
          printf("seed:%lX \n", seed);
          break;
        } else if(syscall_id == 6) {
          uint64_t buf = aarch64_get_reg_u64 (cpu, 1, SP_OK);
          uint64_t size = aarch64_get_reg_u64 (cpu, 2, SP_OK);
          printf("read\n");
          printf("seed:%lX \n", seed);
          printf("buf:%lX \n", buf); 
          printf("size:%lX \n", size);
          aarch64_set_mem_u64(cpu, buf, xorshift64_2(&seed));
          printf("seed:%lX \n", seed);
          break;
        }     
        printf("not implemented syscall %lX\n", syscall_id);
      }     
      

      TRACE_SYSCALL (cpu, " HLT [Unknown angel %x]",
         aarch64_get_reg_u32 (cpu, 0, NO_SP));
      sim_engine_halt (CPU_STATE (cpu), cpu, NULL, aarch64_get_PC (cpu),
           sim_stopped, SIM_SIGTRAP);

注意しておきたいのは、readは戻り値ではなく、メモリに読み込んだ値を書き込んで返すということです。当たり前なのですが、私はうっかり戻り値に収まる大きさだったので戻り値として乱数を返してしまっており、時間を少し無駄にしました。(そのぶんaarch64のアセンブリを読めるようになったのでよしとしよう)。

ちなみにxorshift64_2は、試行錯誤の結果以下のコードでうまくいきました。(試行錯誤の過程が関数名に現れている。)

uint64_t xorshift64_2(uint64_t *state) {
  uint64_t x = *state;
      x = x ^ (x << 13);
      x = x ^ (x >> 7);
      x = x ^ (x << 17);
      *state = x; 
      return x;
}

xorshiftは、よい周期性をもつシフトパラメータがいくつも存在するので、どれが選ばれるかは実装依存なのですが、このパラメータはxorshiftの論文内で例として用いられていたものでした。

Xorshift RNGs George Marsaglia ∗The Florida State University

余談ですが、論文内のxorshift32の実装例のところに

It uses one of my favorite choices, [a, b, c] = [13, 17, 5]

と書いてあって、めっちゃわかるー、ってなっていました。64bit版もきれいでよい。

まあ、こんな感じでデバイスの実装自体は終わりですが、文字出力のほうも既存のsyscallの実装にかぶっていたのでよしなにしてあげます。(同関数の上方です。)

    case AngelSVC_Reason_Open:
      {
        /* Get the pointer  */
        /* uint64_t ptr = aarch64_get_reg_u64 (cpu, 1, SP_OK);.  */
        /* FIXME: For now we just assume that we will only be asked
           to open the standard file descriptors.  */
        static int fd = 0;
        result = fd ++;

        //TRACE_SYSCALL (cpu, " AngelSVC: Open file %d", fd - 1);
        uint64_t p0 = aarch64_get_reg_u64 (cpu, 0, SP_OK);
        uint64_t p1 = aarch64_get_reg_u64 (cpu, 1, SP_OK);
        uint64_t p2 = aarch64_get_reg_u64 (cpu, 2, SP_OK);
        uint64_t p19 = aarch64_get_reg_u64 (cpu, 19, SP_OK);
        //printf("p0:%lx p1:%lx p2:%lx sp:%lx p19:%lx\n", p0, p1, p2, aarch64_get_reg_u64 (cpu, 31, SP_OK), p19);
        printf ("%c", aarch64_get_mem_u8(cpu, p1));
      }
      break;

(いろいろdeadcodeがありますが気にしないでください。)

これでシミュレータをコンパイルして再度実行してあげるとフラグを得られました。

SECCON{UseTheSpecialDeviceFile}

Special Instructions

これも謎アーキです。cross-gccのサンプルをfileしたときの出力と、与えられたファイルのfileを比較した結果、アーキはmoxieとわかりました。

$ file runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be 
runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be: ELF 32-bit MSB executable, *unknown arch 0xdf* version 1 (SYSV), statically linked, not stripped

*unknown arch 0xdf* いい響きだ。

さて、というわけで適当に実行してみると、

This program uses special instructions.
SETRSEED: (Opcode:0x16)
RegA -> SEED
GETRAND: (Opcode:0x17)
xorshift32(SEED) -> SEED
SEED -> RegA

と出てから、SIGILLで落ちてしまいます。

cross-gcc494/toolchain/gdb-7.12.1/sim/moxie/interp.c にソースがあるので適当に読むと、該当する命令番号にご丁寧にも/*bad*/と書かれているのがわかりました。というわけで、適当に実装しちゃいましょう。

      case 0x16: /* bad */
        {    
          int a = (inst >> 4) & 0xf; 
          xorstate = cpu.asregs.regs[a];
          //printf("SETRSEED(%X)\n", xorstate);
        }    
        break;
      case 0x17: /* bad */
        {    
          int a = (inst >> 4) & 0xf; 
          //printf("GETRAND SEED=%X\n", xorstate);
          cpu.asregs.regs[a] = XorShift(&xorstate);
        }    
        break;

変数 xorstate は適当に関数sim_engine_runの先頭でstatic宣言しておきます。

void
sim_engine_run (SIM_DESC sd,
    int next_cpu_nr, /* ignore  */
    int nr_cpus, /* ignore  */
    int siggnal) /* ignore  */
{
  word pc, opc;
  unsigned short inst;
  sim_cpu *scpu = STATE_CPU (sd, 0); /* FIXME */
  address_word cia = CPU_PC_GET (scpu);
  static int xorstate = 0;

で、問題はxorshiftです。論文で示されているパラメータは64セットあるのですが、十数個試しても引っかかりません。こまった。

というわけで、適当にパラメタ化してソルバを書いてやりました。

int shift_table[9 * 9 * 3] = {
 1, 3,10, 1, 5,16, 1, 5,19, 1, 9,29, 1,11, 6, 1,11,16, 1,19, 3, 1,21,20, 1,27,27,
 2, 5,15, 2, 5,21, 2, 7, 7, 2, 7, 9, 2, 7,25, 2, 9,15, 2,15,17, 2,15,25, 2,21, 9,
 3, 1,14, 3, 3,26, 3, 3,28, 3, 3,29, 3, 5,20, 3, 5,22, 3, 5,25, 3, 7,29, 3,13, 7,
 3,23,25, 3,25,24, 3,27,11, 4, 3,17, 4, 3,27, 4, 5,15, 5, 3,21, 5, 7,22, 5, 9,7 ,
 5, 9,28, 5, 9,31, 5,13, 6, 5,15,17, 5,17,13, 5,21,12, 5,27, 8, 5,27,21, 5,27,25,
 5,27,28, 6, 1,11, 6, 3,17, 6,17, 9, 6,21, 7, 6,21,13, 7, 1, 9, 7, 1,18, 7, 1,25,
 7,13,25, 7,17,21, 7,25,12, 7,25,20, 8, 7,23, 8,9,23 , 9, 5,1 , 9, 5,25, 9,11,19,
 9,21,16,10, 9,21,10, 9,25,11, 7,12,11, 7,16,11,17,13,11,21,13,12, 9,23,13, 3,17,
13, 3,27,13, 5,19,13,17,15,14, 1,15,14,13,15,15, 1,29,17,15,20,17,15,23,17,15,26,
};

uint32_t XorShift(uint32_t *state)
{
  *state ^= *state << shift_table[SHIFT_INDEX * 3 + 0];
  *state ^= *state >> shift_table[SHIFT_INDEX * 3 + 1];
  *state ^= *state << shift_table[SHIFT_INDEX * 3 + 2];
  return *state;
}

SHIFT_INDEXを定義してビルド&実行をぶん回すスクリプトを用意して

for i in {0..80}
do
echo index=$i
touch ~/repo/cross-gcc494/toolchain/gdb-7.12.1/sim/moxie/interp.c && \
        make -s -C ~/repo/cross-gcc494/build/gdb/moxie-elf/sim CFLAGS+="-DSHIFT_INDEX=$i " &> /dev/null && \
        cp ~/repo/cross-gcc494/build/gdb/moxie-elf/sim/moxie/run /usr/local/cross-gcc494/moxie-elf/bin/ && \
        /usr/local/cross-gcc494/moxie-elf/bin/run runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be | grep SECCON && exit
done

実行すると

$ ./solve.sh 
index=0
...
index=74
SECCON{MakeSpecialInstructions}

74番目は...13,17,15でしたね。うつくしい…。

ということで、フラグを得ました。(ところが十数分前にsrupさんがsubmitしていたのでした。きっとソルバを書き始めようとしていた頃でしょう。ちょっとくやしい。)

解けなかった問題

q-escape

megumishさんから教えていただいた

あたりを参考にしつつ、情報収拾をしている段階で時間切れでした。

$ ./qemu-system-x86_64 -device help 2>&1 | grep cydf
name "cydf-vga", bus PCI, desc "Cydf CLGD 54xx VGA"
name "isa-cydf-vga", bus ISA
$ nm qemu-system-x86_64 | grep cydf | grep ' d '
0000000000f5a580 d isa_cydf_vga_properties
0000000000f5a460 d pci_vga_cydf_properties
$ nm qemu-system-x86_64 | grep mmio | grep cydf
000000000068da40 t cydf_mmio_blt_read
000000000068ec70 t cydf_mmio_blt_write
0000000000a99bc0 r cydf_mmio_io_ops
000000000068dd90 t cydf_mmio_read
000000000068f9f0 t cydf_mmio_write
00:02.0 Class 0300: 1234:1111
/ # cat /proc/iomem | grep 00:02.0
  fc000000-fcffffff : 0000:00:02.0
  febc0000-febc0fff : 0000:00:02.0

これはがんばれば解けそうな気がするのでぜひやりたい。

Needle in a haystack

場所の特定はした。大阪の梅田ですね。(「新梅田研修センター」という看板が目立っていた。) 画面中央のビルは実在しなさそうなので怪しかったのですが、動画処理は得意ではないので撤退しました。

まとめ

今回は謎アーキ問題がそんなに多くなかったし、そこまで頭をひねるというわけでもなかったのは少し残念かもしれない。 とはいえ、単純なところで引っかかって時間を無駄にしてしまったり、そもそも準備不足で環境構築に時間を浪費する場面が多かったので、本戦に行く際には改善してゆきたい。 あと風邪を引いてしまったようなので、きちんと休んで回復したい。

運営の皆さん、面白い問題をありがとうございました!

ISUCON8 本戦に出てまあまあだった話

(この記事は本戦後の眠い頭を無理に回して書いています。どうか温かい目で読んであげてください…。)

これまでのあらすじ

前回は、ISUCON8予選に出てガチャを引いたら運良く本戦に行けることになった!という話を書きました。

hikalium.hatenablog.jp

というわけで今回は、本戦でどのようなことをしたのか、そしてその結果などをまとめてゆきたいと思います。

試合開始前

椅子CONではなく机CONだった

開場より少し早く会場に着いてしまったので、エレベーターホールで受付開始まで待っていたのですが、すでにかなりの人数が集まっていたのでびっくり。

入場開始するも、チームメイトのうさぎさん・megumishさんがまだ着いていなかったので、長蛇の列を横目に傍観していた。

列も捌けてきたし、まだ二人が来ないので入場して会場のあるフロアへ。

「机は各チームひとつくらいで」とのことだったので周囲を見回して見たところ、すでにけっこう埋まっていた。

埋まっていないもっとも多くある机は、3人だとちょうどよいか少し肩身がせまいかも、という感じだった。

広めの机はすでに確保されていたし、空いている広い机は椅子に座って作業するには適していない低い机だったので、まあしょうがない。

(もちろんたくさんのチームを招くために場所を確保するのはたいへんだったと思うのでありがたい限りだが、もう少し各チームの机が広かったらうれしいなあと思った。)

ストーリー説明

仮想椅子取引所ISUCOINというこれまた面白そうなテーマの背景ストーリーが説明された。あの人気SNS、ISUBATAと連携!とか、過去問をやっていてわかるネタがあって、良く練られていると感じた。

試合の中身

私はインフラ要員として雇われた感じなので、アプリのアルゴリズムそのものではなく、環境やDB, Webサーバの設定最適化を主に担当した。

環境としては、2cpu, 1G mem のサーバーが4台与えられて、初期状態では各サーバでDB, Webサービス共に動作していた。

目新しかった点は、サービスがDocker内で動いていたということである。Dockerよくわからん!となったので、とりあえず当初は、Dockerからアプリを引き剥がす作業に取り組むことになった。

megumish氏はアプリ本体、私はDBとWebサーバをひっぺがす作業をした。

今回はアプリがHTTPSで動作していたため、当初何も考えずにcertbotで新しくグローバルのドメイン用にLetsEncryptで証明書を取ってきたりしたのだが、なんとベンチマーカはグローバルIPではなくベンチマーカIPを対象にアクセスしており、そのIPにはグローバルのドメインとは別のドメインが割り当てられていたため、証明書エラーでベンチマークが落ちてしまった。これは、よくよくアプリのディレクトリを確認すると、Docker内のnginxが参照していた証明書が置いてあったため、それを証明書として指定してやればうまくいった。(ちょっと時間を無駄にしてしまった。)

同時に、アプリの実装を、デフォルトのgolangからPythonに変更する作業もした。ここまでの作業をしてdstatをつかってなんとなく状況を測定してみたところ、やはりCPUを食いつぶしていたので、次はDBを別サーバに分離することにした。

このあたりだったか、うさぎさんがさくっとLIMIT 1の修正などを追加してくれて、これにより得点が700前後->2000超になった。

https://github.com/kmyk/isucon8-final/pull/6

アプリサーバとDBサーバを分けてからベンチマークを再度かけてみたところ、DBサーバのCPU使用率が高く、律速しているような雰囲気だった。

ここでうさぎさんが、ローソク足の計算を毎回履歴を走査して計算することなく、アプリ起動時にキャッシュするようにすればよいのではないかと気づき実装してくれた。

https://github.com/kmyk/isucon8-final/pull/9

この修正により、ベンチマーク中のDBサーバのCPU利用率は、ベンチ開始時にどかんと上がり、すぐに低くなって低いまま走る、という感じになった。

この、ベンチ開始時に急にCPU利用率が上がるという現象は、今後ロードバランシングする上で問題になりそうだったので、そもそも初期データに対するローソク足の計算は事前にやっておいてテーブルの初期データに挿入しておけばいいという話になり、その変更が入った。

https://github.com/kmyk/isucon8-final/pull/12

これにより、DBの利用率はかなり落ちて、またシェアボタンも確率的に出すようにしたところ( https://github.com/kmyk/isucon8-final/pull/10 )、アプリが律速してきたような印象だったため、ロードバランシングをするようにしてみた。

https://github.com/kmyk/isucon8-final/pull/13

当初は2台でロードバランシングしていたが、それでも1台のサーバに対してベンチマークをかけて2台のサーバのCPU利用率が上がるのは、見ていて非常に楽しかった。

と、ここに来て、連続でベンチを2回走らせると必ず事前チェックでfailするという問題が明らかになり、どうも初期に投入した、settingsのシングルトン化がバグっていたということがわかり、revertするなどした。

megumish氏はこの裏で、ログ送信を即時ではなくキューイングして非同期に行う方法を模索していたのだが、それがなかなかうまくいかずこの辺りでチームの人々に疲れが相当見え始めた。

結局最後は、全4台でアプリサーバをロードバランスしつつ、DBサーバはひとつという構成に落とし込んで、ガチャを回しつつ再起動テストに備える(ただし実際に再起動はしないで最後は祈る)ということをした。

結果

f:id:hikalium:20181020235815p:plain

f:id:hikalium:20181020235848p:plain

明らかに得点が伸び悩んだので、これはかなり厳しい結果だったのではないかと誰もが覚悟していたが、予想に反して全30チーム中12位の最終7475点(途中最高は7752点)、学生チームだけで見れば第4位と、そこまでひどくない成績を残せたようで、チームメンバー一同少し心がやわらぎました。

そして、なんと優勝は学生チームの「最大の敵は時差」で、35312点でした。すごい!おめでとうございます。

社会人チームの「takedashi」は、最大で5万点超えを出していたようですが、再起動後に動作が不調で減点の結果、2位となったようです。残念(でもすごい)。

講評や反省

試合結果発表後、問題作成者による講評があったのですが、DBのパーティションを解除しましたか?したほうが高速だったよ、という話を聞いて、それは気づかなかったなあ、まだまだ勉強が足りないな、と思いました。

また、私がPythonにそんなに慣れていないこともあって直接ロジックの改良に加われなかったこと、非同期処理や排他制御に関する知識の不足、そもそも練習不足なども多くあったなあと、反省する点が多く見つかりました。

もちろん、これまで何度か練習をしたことで得た知見を活かせていた部分もあったので、もっとこれからもいろいろ知識を身につけてゆけばよいのですが。がんばってゆきたい!

感想

  • 高レイヤ、まだまだ知らないことが多すぎる
  • 非同期・マルチスレッドの知識が薄かったので深めたい
  • ISUCON楽しい!

学生生活延長することになったし、来年も本戦に出られるよう精進します。次こそは優勝するぞ!

f:id:hikalium:20181020231226j:plain

ISUCON8の予選に出てみた

概要

  • CTFつながりでうさぎさんに声をかけていただき、megumishさんと3人で参加した。
  • チーム名は「受験生の仇」
  • 13980点で学生枠8位で通過できた。

とにかくうさぎさんのギャンブル運の賜物である。

isucon.net

なにをしたの

基本的にインフラまわりをしていた。

突然のh2oが登場してびっくりするなどした。h2o速いですね。

nginxに慣れていてログ取りとかもこれで用意していたので、最初はnginxに移行することを試みたが、微妙に遅い上にcsvファイルを上手く転送できずよくわからなくてあきらめた。

代わりにh2oのログフォーマットを書き換えて、alpで使えるようになおした。これでやっと定量的にパフォーマンスをみることができるようになった。

あとpythonのプロファイラも事前の練習で出来上がっていたものを使えるようにセットアップしたりしていた。あまり使わなかったけれど。

最初は一台のサーバでWebサーバもDBサーバも動かしていたけれど、htopとかdstatでみていた感じ、メモリが飽和していて苦しそうだったので分けてあげることにした。

分けたことで、DBサーバにメモリをたくさん食べさせてあげることができてDBもうれしそうな悲鳴をあげていた。

初めてMySQLTunerをつかってみた。だいたいはおすすめ通りでいい感じだったが、一部おすすめ通りだとメモリが少なすぎるのかDBが死んだので適宜調整した。

github.com

あとは、アプリ側の小さな修正をしたり、二人のプルリクをレビュー&マージしたり、ロードバランシングできないかなーと試すなどしていた(firewalldさんの存在を忘れかけていて頭をひねっていた)。

最後はみんながベンチを投げた時のDBサーバの負荷を見て「あ、落ちたね」「終わった」とか呟くボットになっていました。

結果

最後にうさぎさんがスロットを回し続けたおかげでなんとか13980点に到達した。最後の賭けが外れていたら本戦には行けていなかった。

一応再起動のことを考えてsystemctlまわりの設定は見直していたものの、追試で落ちないことを祈り続けていた。祈るだけじゃなくて検証したいものですね。

感想

運良く本戦に行けることになったので、次は実力で勝負したいところ。

バイナリ問が出るといいなー。(これはCTFではない。)

あと、学生チームが強い。(昨年同じチームで出て予選敗退した人が今回別のチームで出ていたのだが、我々よりずっと高得点を叩き出していて、学生なのに普通の枠で通過しており、さすがだと思った。)

みなさまどうかお手柔らかにお願いいたします…。

チームメンバーの参加記

kimiyuki.net

思い立っておうちネットワークにdnsmasqを建てた話

概要

  • ホストとipの結びつきや、固定ipの利用状況の管理が ~/.ssh/config 頼りだったので改善したかった
  • とりあえず善は急げでdnsmasqを建てた
  • Ubuntu 18.04ではsystemd-resolvedが53をLISTENしていて手間取った

環境

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04 LTS"

動機

ふとRaspberry pi で遊びたくなったんです。そんな日もありますよね。

IPアドレスどれにしていたかなーと思いつつ、WiresharkDHCPのパケットを見張って特定して無理やりSSHするなどして、とりあえずアクセスはできました。

あとのことも考えて、そうだIPアドレスを固定しようと思った時に、気づいたわけです。

「どのアドレスが空いてるんだっけ?」

まあ、手元の端末の ~/.ssh/configを参照すればだいたい書いてあったので、そこから適当に選んでIPを振ったのはよかったのですが、本当はかっこよくDNSでシュッと引けたら気分がいいですよね?そうしましょう!

やったこと

dnsmasq を使うことにしました。bindっていう規模でもなかったので。aptですぐ入るのが便利。

sudo apt install dnsmasq

ところがインストール直後に不穏なメッセージが。

dnsmasq: failed to create listening socket for port 53: アドレスは既に使用中です

え?いつの間にDNSサーバーが!?誰だ建てたのは!外部からの侵入か?と冷や汗をかいたが犯人は systemd-resolved でした。 (127.0.0.1:53 だけだったので、外からはアクセスできませんでしたが。)

以下の記事も同じような状況に遭遇して同じような反応をしていました。

qiita.com

結局 systemd-resolvedをdisableすることにしました。以下の投稿の通りにすればOKだった。

askubuntu.com

設定は基本的に以下で述べられている例を参考にすれば良いと思います。

int128.hatenablog.com

また、 /etc/hostsをdnsmasq用に使うと、dnsmasq自体を走らせているサーバーのホストについて127.0.0.1を返すことになります。これでは悲しいので、/etc/dnsmasq.conf

no-hosts
addn-hosts=/etc/hosts-dnsmasq

と記述し、/etc/hostsは読み込まず、代わりに/etc/hosts-dnsmasqを利用するように設定しました。

おまけ:DHCPサーバにDNSサーバとドメイン名を設定する(Ciscoルーターの場合)

ここまで設定したら、端末側のDNSサーバ設定もDHCP経由で自動的に設定してあげたいですよね。私のネットワークでは、CiscoルーターDHCPサーバーになっているので、その設定も変更しておくことにします。

ルータのバージョンは以下の通りです。

Cisco IOS Software, C800M Software (C800M-UNIVERSALK9-M), Version 15.7(3)M2, RELEASE SOFTWARE (fc2)

手順

  1. show ip dhcp pooldhcp poolの名前を確認しておきます。
  2. configure terminal に入って、ip dhcp pool <dhcp pool名>を実行して該当poolの設定に入ります。
  3. dns-server <DNSサーバのアドレス> domain-name <ネットワークのドメイン名> としたのち、exitしてwrite memします。

これで、各端末のDHCPリースを更新すれば、ホスト名だけでsshしたりできるようになっているはずです。やったね!

まとめ

  • dnsmasqをインストールして、かんたんな内部向けDNSサーバを建てた。
  • 新しいUbuntuでは、systemd-resolvedがすでにPort 53をLISTENしているので、これをdisableにした。
  • /etc/dnsmasq.confはこんな感じ。
domain-needed
bogus-priv
local=/local.hikalium.com/
no-hosts
addn-hosts=/etc/hosts-dnsmasq
expand-hosts
domain=local.hikalium.com
  • DHCPサーバの設定もいじっておきましょう!