Snippets
Crypto 100
公開鍵のBit数が256bit(=32byte)で、Base64をデコードすると32byteなのでどう考えてもRSAで暗号化されている。
この公開鍵のModulsを見てみると、下位1bitが0すなわち偶数となっており二つの素数の積としては明らかにおかしいことが分かった。 ‘‘‘ $ openssl rsa -text -pubin < PublicKey.pem Public-Key: (256 bit) Modulus: 00:b6:2d:ce:9f:25:81:63:57:23:db:6b:18:8f:12: f0:46:9c:be:e0:cb:c5:da:cb:36:c3:6e:0c:96:b6: ea:7b:fc Exponent: 65537 (0x10001) writing RSA key -----BEGIN PUBLIC KEY----- MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhALYtzp8lgWNXI9trGI8S8EacvuDLxdrL NsNuDJa26nv8AgMBAAE= -----END PUBLIC KEY-----
問題文に‘Where is the lost 1bit?‘とあることから下位1bitが反転していると推定出来たので、Modulusは82401872610398250859431855480217685317486932934710222647212042489320711027709であると分かった。これをFactorDBに投げると素因数分解の結果が帰ってきたので、それを元に[秘密鍵を生成し](https://bitbucket.org/snippets/nomeaning777/eg5z8)解読したらフラグを得ることが出来た。
## Crypto 200
First ACな問題
Keyのうち2文字が欠けているのでそれについては全探索を行なった。
CBCモードなので、最後の16byteをECBとして解読した時の結果と平文の最後16byteのXORを取ると、前のブロックの暗号化結果が分かる。 前のブロックの暗号化結果の先頭3文字が判明しているので、一致するものを探索し、さらに前のブロック前のブロックを求めることによりIVを求めた。
```ruby
require 'facets'
require 'openssl'
e = 'fe1.........................9ec3307df037c689300bbf2812ff89bc0b49'
e = [e[-32..-1]].pack("H*")
key = '5d6I9pfR7C1JQt'
k = [*10..127].map{|a|a.chr}
mes = 'The message is protected by AES!'
for c in k
for c2 in k
kk = key + c + c2
enc = OpenSSL::Cipher.new("AES-128-ECB")
enc.decrypt
enc.padding = 0
enc.key = kk
dec = enc.update(e)
dec ^= mes[-16..-1]
next if dec.unpack("H*")[0][0,3] != 'fe1'
dec = enc.update(dec)
dec ^= mes[0,16]
p dec
end
end
Crypto 300
Zipファイルのパスワードは‘change it‘(icchyrさんが教えてくれた)であり、解読するとRSAで暗号化されたAESの鍵と、その鍵で暗号化された次の問題のファイル(Zip)が含まれている。RSAの鍵は既知のもので、そのヒントがReadme.txtに書いてある。これが再帰的にLevel5まで続く。
RSAの鍵はそれぞれのレベルについて次のようなものであった。 * Level2: 昔のApacheのmod_sslに付属していたsnakeoilのkey * Level3: Wireshark付属のkey * Level4: Python付属のKey * Level5: debian-ssh 付属のKey
Crypto 400
画像ファイルは画像データとmetadata1/2がそれぞれドキュメントファイル・公開鍵となっていた。 ドキュメントファイルはOfficeの元だが暗号化されており、最後の方に使われている公開鍵とそれによって暗号化されたAESの共通鍵が含まれていた。
画像ファイルに含まれる公開鍵とドキュメントに含まれる公開鍵についてGCDを取ることで素因数分解が出来たので、秘密鍵を作り出すことができ、それによってAESの鍵を取得した。後は暗号化されている部分についてAES-256-CBCで解読した。
Crypto 500
52^3全てを試す。
require 'base64'
alphabet = [*'A'..'Z'] + [*'a'..'z']
strs = []
for a in alphabet
for b in alphabet
for c in alphabet
strs << Base64.encode64(a + b + c).split.join
strs << Base64.encode64(a + b).split.join
strs << Base64.encode64(a).split.join
end
end
end
strs = strs.sort.uniq
b64chars = alphabet + [*'0'..'9'] + %W(+ /)
b64chars.sort.each do |c|
next if strs.select{|a| a[0] == c}.count > 1
next if strs.select{|a| a[1] == c}.count > 1
next if strs.select{|a| a[2] == c}.count > 1
next if strs.select{|a| a[3] == c}.count > 1
print c
end
Programming 100
ブラウザのスクリーンショット画像から最も少ない色の場所を自動的に選択するプログラムを作成した。
require 'capybara'
require 'capybara/poltergeist'
require 'rmagick'
include Magick
def solve(src)
img = ImageList.new('hoge.png')
w = img.rows
h = img.columns
cnt = Hash.new{0}
w.times do |x|
h.times do |y|
p = img.pixel_color(x,y)
r, g, b = p.red / 257, p.green / 257, p.blue / 257
next if r== 255 && g==255 && b==255
cnt[[r,g,b]] += 1
end
end
target = cnt.to_a.sort_by{|a,b|b}[0][0]
minx = 999999
maxx = 0
miny = 999999
maxy = 0
w.times do |x|
h.times do |y|
p = img.pixel_color(x,y)
r, g, b = p.red / 257, p.green / 257, p.blue / 257
if [r,g,b] == target
minx = [minx, x].min
maxx = [maxx, x].max
miny = [miny, y].min
maxy = [maxy, y].max
end
end
end
return p([(maxx+minx)/2, (miny+maxy)/2])
end
s = Capybara::Session.new(:poltergeist)
s.visit 'http://ctfquest.trendmicro.co.jp:43210/click_on_the_different_color'
while true
s.save_screenshot 'hoge.png'
cur = s.current_path
s.driver.click *solve(src)
sleep 1 while cur == s.current_path
puts "OK"
end
Programming 200
数式が1行に一つ渡されるのでevalした結果を返す。ローマ数字や英語による数字はそれぞれroman-numeralsやnumbers_in_wordsといったgemによって元の数字に戻した。 (Rubyはこの手のgemが多いので便利です)
require 'roman-numerals'
require 'ctf'
require 'numbers_in_words'
require 'numbers_in_words/duck_punch'
TCPSocket.open(*ARGV) do |s|
s.echo = true
while true
input =s.expect('=')[0].split(/=/)[0].gsub(',','')
input = input.gsub(/[A-Z]+/) do |t|
RomanNumerals.to_decimal(t).to_s
end
input = input.gsub(/[a-z ]+/) do |t|
if t.strip == ''
t
else
t.in_numbers.to_s
end
end
puts "Input #{input}"
s.print eval(input).to_s + "\n"
end
end
Programming 300
グラフを縮約しつつdijkstra法。縮約後のグラフについてO(2^∣V∣&|V|^2|E| log |E| )ぐらい。縮約後のグラフは大体|V| <= 15であった。
グラフの縮約は次のようなルールで行なった。 * 2つの隣接する頂点がそれぞれただの通路であり、次数が2になら1つにまとめる * 1つの頂点について、それが通路であり、次数が1なら消去する。
class Node
attr_accessor :neighbor, :cost, :name, :visited
attr :type
def initialize(type)
@neighbor = []
@type = type
@cost = 1
@visited = false
end
def add(node,direction)
if self.neighbor.include?(node)
STDERR.puts 'already'
return
end
if direction == 'D'
self.neighbor <<= [node, ?D]
node.neighbor <<= [self, ?U]
else
self.neighbor <<= [node, ?R]
node.neighbor <<= [self, ?L]
end
end
def to_s
"node(#{name}:#{type})"
end
end
sum = 0
while gets
w,h,c = $_.split.map(&:to_i)
map = Array.new(h){gets.chomp}
start = nil
node = Array.new(h){Array.new(w)}
h.times do |y|
w.times do |x|
if map[y][x] == '.'
node[y][x] = Node.new(:plain)
start = node[y][x]
elsif map[y][x] == 'S'
node[y][x] = Node.new(:start)
elsif map[y][x] == 'C'
node[y][x] = Node.new(:check)
elsif map[y][x] == 'G'
node[y][x] = Node.new(:goal)
elsif map[y][x] == 'E'
node[y][x] = Node.new(:energy)
end
if node[y][x]
node[y][x].name = "Node(#{y},#{x})"
end
end
end
h.times do |y|
w.times do |x|
if node[y][x] && node[y+1][x]
node[y][x].add(node[y+1][x], 'D')
end
if node[y][x] && node[y][x+1]
node[y][x].add(node[y][x+1], 'R')
end
end
end
nodes = []
h.times do |y|
w.times do |x|
nodes << node[y][x] if node[y][x]
end
end
while true
update = false
nodes.each do |node|
break if update
if node.type == :plain
if node.neighbor.size <= 1
node.neighbor[0][0].neighbor.reject!{|n,dir| n == node}
nodes.delete node
update = true
break
end
if node.neighbor.size == 2
node.neighbor.each do |n2,dir|
next if n2.type != :plain
next if n2.neighbor.size != 2
# 隣接するノードの
n2dir = n2.neighbor.select{|n,dir| n == node}[0][1]
n1dir = dir
n2.neighbor.reject!{|n,dir| n == node}
#node.neighbor.reject!{|n,dir| n == n2}
node.cost += n2.cost
didir = n2.neighbor[0][0].neighbor.select{|n,dir| n== n2}[0][1]
n2.neighbor[0][0].neighbor.reject!{|n,dir| n== n2}
n2.neighbor[0][0].neighbor << [node, didir + n2dir]
node.neighbor.select{|nv,t| nv == n2}[0][1] += n2.neighbor[0][1]
node.neighbor.select{|nv,t| nv == n2}[0][0] = n2.neighbor[0][0]
update = true
break
end
end
end
end
break unless update
end
nodes.each.with_index do |node,i|
node.name = i
end
require 'pqueue'
que = PQueue.new{|a,b| a[0] < b[0]}
start = nodes.select{|a|a.type == :start}[0].name
que.push([0,start,1 << start])
dp = {}
back = {}
dp[[start, 1<< start]] = 0
ans = nil
mask = 0
nodes.each do |i|
mask |= 1 << i.name if i.type == :check
end
while que.top
tmp = que.pop
cost = tmp[0]
cur = tmp[1..-1]
if nodes[cur[0]].type == :goal && (cur[1] & mask) == mask
str = ''
until cur == [start,1 << start]
str = back[cur][0] + str
cur = back[cur][1]
end
puts str
sum += cost
break
end
next if dp[cur] < cost
nodes[cur[0]].neighbor.each do |n, dir|
if (cur[1] >> n.name & 1) == 1
ncost = cost + n.cost
else
ncost = cost
ncost -= 20 if n.type == :energy
end
nvisited = cur[1] | (1 << n.name)
if dp.key?([n.name, nvisited]) == false || dp[[n.name,nvisited]] > ncost
dp[[n.name,nvisited]] = ncost
back[[n.name,nvisited]] = [dir, cur]
que.push([ncost, n.name, nvisited])
end
end
end
# require 'gviz'
# Gviz.new.graph do
# nodes.each do |node|
# add node.neighbor.map{|a|a[0].name} => node.name
# node node.name, label: "#{node.type} #{node.cost} #{node.name}"
# node.neighbor.each do |n,dir|
# edge "#{node.name}_#{n.name}".to_sym, label: dir
# end
# end
# save :graph, :png
# end
end
puts sum
Programming 400
両幅探索をすることで、手数の長さNに対して2^|N/2|ぐらいで計算することが出来る。55手程度で終わるらしいのでこの方法で十分高速に解くことが出来た。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
#include <unordered_map>
using namespace std;
uint32_t xor128(void) {
static uint32_t x = 123456789;
static uint32_t y = 362436069;
static uint32_t z = 521288629;
static uint32_t w = 88675123;
uint32_t t;
t = x ^ (x << 11);
x = y; y = z; z = w;
return w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
}
const int N = 8;
vector<int> G[N * 2];
vector<int> pos;
vector<int> res(100);
unsigned long long hav[17][16];
unsigned long long hash2(int cur) {
unsigned long long r = 0;
for(int i = 0; i < N * 2; i++) {
r ^= hav[i][pos[i]];
}
r ^= hav[2*N][cur];
return r;
}
unordered_map<unsigned long long, string> min_depth;
void search(int cur, int depth, int max_depth, int back) {
if(cur == 0) {
bool ok = true;
for(int i = 1; i < N * 2 - 1; i++) {
if(pos[i] != i) {ok = false;break;}
}
if(ok) {
for(int i = 0; i < depth; i++) {
cout << (char)('A' +res[i]);
}
cout << endl;
exit(0);
}
}
unsigned long long hv = hash2(cur);
if(min_depth.count(hv)) {
for(int i =0 ; i < depth; i++) {
cout << (char)('A' +res[i]);
}
cout << min_depth[hv] << endl;
exit(0);
}
if(max_depth == depth)return;
for(int i = 0; i <G[cur].size(); i++) {
if(G[cur][i] == back) continue;
res[depth] = G[cur][i];
swap(pos[cur], pos[G[cur][i]]);
search(G[cur][i], depth + 1, max_depth, cur);
swap(pos[cur], pos[G[cur][i]]);
}
}
string cur_str;
void search2(int cur, int depth, int max_depth, int back) {
unsigned long long hv = hash2(cur);
if(depth == max_depth) {
if(min_depth.count(hv) == 0) {
min_depth[hv] = cur_str;
}
min_depth[hv] = min(min_depth[hv], cur_str);
}
if(max_depth == depth)return;
for(int i = 0; i <G[cur].size(); i++) {
if(G[cur][i] == back) continue;
swap(pos[cur], pos[G[cur][i]]);
cur_str = (char)('A' + cur) + cur_str;
search2(G[cur][i], depth + 1, max_depth, cur);
swap(pos[cur], pos[G[cur][i]]);
cur_str = cur_str.substr(1);
}
}
int main() {
for(int i =0 ;i < 17; i++) {
for(int j = 0; j < 16; j++) {
hav[i][j] = (((unsigned long long)xor128()) << 32) | xor128();
}
}
for(int i = 0; i < N; i++) {
if(i > 0) {
G[i].push_back(i + N - 1);
G[i+ N - 1].push_back(i);
}
if(i < N - 1) {
G[i].push_back(i + N + 1);
G[i + N + 1].push_back(i);
}
G[i].push_back(i+ N);
G[i + N].push_back(i);
}
for(int i = 0; i < N * 2; i++) {
sort(G[i].begin(),G[i].end());
pos.push_back(i);
}
swap(pos[0],pos[N*2-1]);
search2(0, 0, 27, -1);
pos.clear();
for(int i = 0; i < N * 2; i++) {
sort(G[i].begin(),G[i].end());
pos.push_back(i);
}
for(int depth = 0; depth < 80; depth++) {
cout << "DEPTH: " << depth << endl;
search(N * 2 - 1, 0, depth, -1);
}
}
Programming 500
シミュレーション。なんで答えがあったのか分かってないので略
Misc 200
長いランダムな文字列をある程度たくさん送ると、学習が一定の結果になってランダム要素が減るということを利用して、長い文字列の後ろに勝てそうな文字を加えていって30回連続勝利するまで回した。
rps = File.read('rps.dat').strip # 4500文字程度のRPSからなるランダムな文字列
require 'json'
win = {'R' => 'P', 'P' => 'S' , 'S' => 'R'}
cur = 'PSPPSPPSRSSSRRRRRSSSRRPPSRSSSR'
while true
clen = cur.size
res = JSON.load(`curl -s 'http://ctfquest.trendmicro.co.jp:8181/98cd98a1894676b9/bf9b62aa00e7986fa75ef400f06d57e5/ai_rps.py?hands=#{rps}#{cur}R'`)
p [:fisrt, res['ai_hands'][-1]]
cur += win[res['ai_hands'][-1]]
res = JSON.load(`curl -s 'http://ctfquest.trendmicro.co.jp:8181/98cd98a1894676b9/bf9b62aa00e7986fa75ef400f06d57e5/ai_rps.py?hands=#{rps}#{cur}'`)
h = res['ai_hands'][-(cur.size)..-1]
p [:hands, res['ai_hands'][-1]]
p res['consecutive_wins']
p cur
if res['consecutive_wins'] >= 30
File.write('hoge', res.to_json)
end
sleep 10
end
Misc 300
Operaで開いて、画像を一度読み込んだ後、キャッシュした画像のみを利用する設定に変更し、その画像の文字をひたすら入力した。手で入力するのは疲れるので次のようなスクリプトを使った。
for t in {0..499}
do
xte 'mousemove 2200 950'
xte 'mouseclick 1'
sleep 0.05
xte 'keydown Shift_L'
xte 'key E'
xte 'key F'
xte 'key C'
xte 'key H'
sleep 0.2
xte 'keyup Shift_L'
xte 'key Return'
done
Offensive 200
Androidのプログラムを逆コンパイルする。jadxというツールを教えてもらった。dex2jarがやっていることを含めて自動で逆コンパイルしてくれるので便利。
逆コンパイルすると謎のBase64を生成している部分があり、それを読んでみると次のようなプログラムであることが分かり、その結果がフラグであった。
require 'base64'
data = 'Q'
a = 'Q'
a += '2'
old_a = a
a = '9'
a += 'u'
string = old_a + a
string += 'Z3'
data += '2xpY2tz'
string += 'Jh'
string += 'dH'
string += 'Nf'
string += 'MT'
b64 = string + "BN" + data
p Base64.decode64(b64)
Comments (0)
You can clone a snippet to your computer for local editing. Learn more.