Snippets

Masafumi Yabu Trend Micro CTF 2015 Writeup

Created by Masafumi Yabu

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)

HTTPS SSH

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