TokyoWesterns CTF 5th 2019 Write-up
はじめに
2019/08/31 ~ 2019/09/02に開催されたTokyoWesterns CTF 5th 2019にチーム(NekochanNano!)で参加しました.
成績
チームとしては6問解いて107位でした (1005チーム中).
今回は自分が解いた2問のWrite-upを書きます.
Easy Crack Me [Rev, 88pts, 185solved]
Cracking is easy for you.
> file easy_crack_me easy_crack_me: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=7e80b79602fcfd8121e9f4d9d26bb01a81f4afe5, stripped
> ./easy_crack_me ./bin flag_is_here > ./easy_crack_me flag incorrect
アプローチ:Ghidraでデコンパイル + 気合で解読 + 制約条件に基づいて全探索
とりあえずida
で開いてみますが,これを静的解析するのはしんどいなという気持ちになり,すぐに方針をangr
に切り替えました.
しかし,上手くいきませんでした…(終)
以下が上手く行かなかったangr
ソルバです.
問題点があれば教えてほしいです.
(strchr
のせいなのか?)
import angr length = 0x27 p = angr.Project('./easy_crack_me') flag = claripy.BVS('flag', length*8) state = p.factory.entry_state(args=[p.filename, flag]) for i, x in enumerate(flag.chop(8)): if i < 6: state.add_constraints(x == b'TWCTF{'[i]) elif i == 0x25: state.add_constraints(x == b'5') elif i == 7: state.add_constraints(x == b'f') elif i == 0xb: state.add_constraints(x == b'8') elif i == 0xc: state.add_constraints(x == b'7') elif i == 0x17: state.add_constraints(x == b'2') elif i == 0x1f: state.add_constraints(x == b'4') elif i == 0x26: state.add_constraints(x == b'}') else: state.add_constraints(x >= b' ') state.add_constraints(x <= b'~') simgr = p.factory.simulation_manager(state) simgr.explore(find=0x400e10, avoid=[0x400777,0x400dfc]) try: simstate = simgr.found[0] print(simstate.posix.dumps(1)) print(simstate.solver.eval(flag, cast_to=bytes)) except Exception as e: print(e)
angr
で解けなかったので再度方針を静的解析に切り替えました.
まずはmain
関数をGhidra
でデコンパイルしてソースコードを解析していきます.
undefined8 FUN_00400747(int iParm1,long lParm2) { char cVar1; char *__s; int iVar2; undefined8 uVar3; size_t sVar4; char *pcVar5; long lVar6; undefined8 *puVar7; long in_FS_OFFSET; byte bVar8; int local_1b8; int local_1b4; int local_1b0; uint local_1ac; int local_1a8; int local_1a4; int local_1a0; uint local_19c; int local_198; int local_194; int local_190; int local_18c; char *local_188; undefined8 local_168; undefined8 local_160; undefined8 local_158; undefined8 local_150; undefined8 local_148; undefined8 local_140; undefined8 local_138; undefined8 local_130; undefined8 local_128; undefined8 local_120; undefined8 local_118; undefined8 local_110; undefined8 local_108; undefined8 local_100; undefined8 local_f8; undefined8 local_f0; undefined8 local_e8; undefined8 local_e0; undefined8 local_d8; undefined8 local_d0; undefined8 local_c8; undefined8 local_c0; undefined8 local_b8; undefined8 local_b0; undefined8 local_a8 [16]; undefined8 local_28; undefined8 local_20; long local_10; bVar8 = 0; local_10 = *(long *)(in_FS_OFFSET + 0x28); if (iParm1 == 2) { __s = *(char **)(lParm2 + 8); sVar4 = strlen(__s); if (sVar4 != 0x27) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } iVar2 = memcmp(__s,"TWCTF{",6); if ((iVar2 != 0) || (__s[0x26] != '}')) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } local_e8 = 0; local_e0 = 0; local_d8 = 0; local_d0 = 0; local_c8 = 0; local_c0 = 0; local_b8 = 0; local_b0 = 0; local_28 = 0x3736353433323130; local_20 = 0x6665646362613938; local_1b8 = 0; while (local_188 = __s, local_1b8 < 0x10) { while (pcVar5 = strchr(local_188,(int)*(char *)((long)&local_28 + (long)local_1b8)), pcVar5 != (char *)0x0) { *(int *)((long)&local_e8 + (long)local_1b8 * 4) = *(int *)((long)&local_e8 + (long)local_1b8 * 4) + 1; local_188 = pcVar5 + 1; } local_1b8 = local_1b8 + 1; } iVar2 = memcmp(&local_e8,&DAT_00400f00,0x40); if (iVar2 != 0) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } local_168 = 0; local_160 = 0; local_158 = 0; local_150 = 0; local_148 = 0; local_140 = 0; local_138 = 0; local_130 = 0; local_1b4 = 0; while (local_1b4 < 8) { local_1b0 = 0; local_1ac = 0; local_1a8 = 0; while (local_1a8 < 4) { local_1b0 = local_1b0 + (int)__s[(long)local_1a8 + (long)(local_1b4 << 2) + 6]; local_1ac = local_1ac ^ (int)__s[(long)local_1a8 + (long)(local_1b4 << 2) + 6]; local_1a8 = local_1a8 + 1; } *(int *)((long)&local_168 + (long)local_1b4 * 4) = local_1b0; *(uint *)((long)&local_148 + (long)local_1b4 * 4) = local_1ac; local_1b4 = local_1b4 + 1; } local_128 = 0; local_120 = 0; local_118 = 0; local_110 = 0; local_108 = 0; local_100 = 0; local_f8 = 0; local_f0 = 0; local_1a4 = 0; while (local_1a4 < 8) { local_1a0 = 0; local_19c = 0; local_198 = 0; while (local_198 < 4) { local_1a0 = local_1a0 + (int)__s[(long)(local_198 << 3) + (long)local_1a4 + 6]; local_19c = local_19c ^ (int)__s[(long)(local_198 << 3) + (long)local_1a4 + 6]; local_198 = local_198 + 1; } *(int *)((long)&local_128 + (long)local_1a4 * 4) = local_1a0; *(uint *)((long)&local_108 + (long)local_1a4 * 4) = local_19c; local_1a4 = local_1a4 + 1; } iVar2 = memcmp(&local_168,&DAT_00400f40,0x20); if ((iVar2 != 0) || (iVar2 = memcmp(&local_148,&DAT_00400f60,0x20), iVar2 != 0)) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } iVar2 = memcmp(&local_128,&DAT_00400fa0,0x20); if ((iVar2 != 0) || (iVar2 = memcmp(&local_108,&DAT_00400f80,0x20), iVar2 != 0)) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } lVar6 = 0x10; puVar7 = local_a8; while (lVar6 != 0) { lVar6 = lVar6 + -1; *puVar7 = 0; puVar7 = puVar7 + (ulong)bVar8 * 0x1ffffffffffffffe + 1; } local_194 = 0; while (local_194 < 0x20) { cVar1 = __s[(long)local_194 + 6]; if ((cVar1 < '0') || ('9' < cVar1)) { if ((cVar1 < 'a') || ('f' < cVar1)) { *(undefined4 *)((long)local_a8 + (long)local_194 * 4) = 0; } else { *(undefined4 *)((long)local_a8 + (long)local_194 * 4) = 0x80; } } else { *(undefined4 *)((long)local_a8 + (long)local_194 * 4) = 0xff; } local_194 = local_194 + 1; } iVar2 = memcmp(local_a8,&DAT_00400fc0,0x80); if (iVar2 != 0) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } local_190 = 0; local_18c = 0; while (local_18c < 0x10) { local_190 = local_190 + (int)__s[(long)((local_18c + 3) * 2)]; local_18c = local_18c + 1; } if (local_190 != 0x488) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } if ((((__s[0x25] != '5') || (__s[7] != 'f')) || (__s[0xb] != '8')) || (((__s[0xc] != '7' || (__s[0x17] != '2')) || (__s[0x1f] != '4')))) { puts("incorrect"); /* WARNING: Subroutine does not return */ exit(0); } printf("Correct: %s\n",__s); uVar3 = 0; } else { fwrite("./bin flag_is_here",1,0x12,stderr); uVar3 = 1; } if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return uVar3; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
これを気合で解読すると以下の処理を行っていることが分かります.
- 文字列長の確認 (0x27文字)
- flagフォーマットの確認 (
TWCTF{*f***87**********2*******4*****5}
) 0123456789abcdef
の出現回数の確認 ([3, 2, 2, 0, 3, 2, 1, 3, 3, 1, 1, 3, 1, 2, 2, 3]
)- 7文字目以降から4文字区切りで総和をとって確認 (
[0x15e, 0xda, 0x12f, 0x131, 0x100, 0x131, 0xfb, 0x102]
) - 7文字目以降から4文字区切りで総XORをとって確認 (
[0x52, 0x0c, 0x01, 0x0f, 0x5c, 0x05, 0x53, 0x58]
) - 7文字目以降から8文字飛ばしで4文字毎に総和をとって確認 (
[0x129, 0x103, 0x12b, 0x131, 0x135, 0x10b, 0xff, 0xff]
) - 7文字目以降から8文字飛ばしで4文字毎に総XORをとって確認 (
[0x01, 0x57, 0x07, 0x0d, 0x0d, 0x53, 0x51, 0x51]
) - 7文字目以降から1文字飛ばしで総和をとって確認 (
0x488
) - 7文字目以降が[a-f]または[0-9]のどちらに含まれるか確認 (
[a-f] -> 0x80
,[0-9] -> 0xff
,[0x80, 0x80, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0x80, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0xff, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0xff, 0x80, 0xff]
)
あとはこれらの制約条件に基づいて全探索すれば解けます.
# flag = 'TWCTF{*f***87**********2*******4*****5}' flag = '*f***87**********2*******4*****5' appearance = [3, 2, 2, 0, 3, 2, 1, 3, 3, 1, 1, 3, 1, 2, 2, 3] main_constraint =[0x80, 0x80, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0x80, 0xff, 0xff, 0x80, 0xff, 0xff, 0x80, 0xff, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0xff, 0x80, 0xff] plus_constraint1 = [0x15e, 0xda, 0x12f, 0x131, 0x100, 0x131, 0xfb, 0x102] xor_constraint1 = [0x52, 0x0c, 0x01, 0x0f, 0x5c, 0x05, 0x53, 0x58] plus_constraint2 = [0x129, 0x103, 0x12b, 0x131, 0x135, 0x10b, 0xff, 0xff] xor_constraint2 = [0x01, 0x57, 0x07, 0x0d, 0x0d, 0x53, 0x51, 0x51] def flag_check(flag): total = 0 for i in range(0x10): total += ord(flag[i * 2]) if total != 0x488: return False for i in range(8): plus_check = 0 xor_check = 0 for j in range(i,32,8): plus_check += ord(flag[j]) xor_check ^= ord(flag[j]) if plus_constraint2[i] != plus_check or xor_constraint2[i] != xor_check: return False return True def appearance_check(flag): for i,c in enumerate('0123456789abcdef'): if flag.count(c) > appearance[i]: return False return True def candidates_search(s, d, base): if d == 4: plus_check = 0 xor_check = 0 for c in s: plus_check += ord(c) xor_check ^= ord(c) if plus_constraint1[base // 4] == plus_check and xor_constraint1[base // 4] == xor_check: yield s else: if flag[base + d] != '*': yield from candidates_search(s + flag[base + d], d + 1, base) else: if main_constraint[base + d] == 0x80: for c in 'abcdef': yield from candidates_search(s + c, d + 1, base) else: for c in '012456789': yield from candidates_search(s + c, d + 1, base) def flag_search(s, d, candidates_list): if d == 8: if flag_check(s): yield s else: for h in candidates_list[d]: if appearance_check(s + h): yield from flag_search(s + h, d + 1, candidates_list) candidates_list = [] for base in range(0,32,4): x = [] for candidate in candidates_search('', 0, base): x.append(candidate) candidates_list.append(x) print('TWCTF{{{}}}'.format(next(flag_search('', 0, candidates_list))))
> python solve.py TWCTF{df2b4877e71bd91c02f8ef6004b584a5}
TWCTF{df2b4877e71bd91c02f8ef6004b584a5}
動けばいいやと思って書いてるので必然的にクソコードになる
Simple Logic [Crypto, 95pts, 167solved]
Simple cipher is always strong.
encrypt.rb
require 'securerandom' require 'openssl' ROUNDS = 765 BITS = 128 PAIRS = 6 def encrypt(msg, key) enc = msg mask = (1 << BITS) - 1 ROUNDS.times do enc = (enc + key) & mask enc = enc ^ key end enc end def decrypt(msg, key) enc = msg mask = (1 << BITS) - 1 ROUNDS.times do enc = enc ^ key enc = (enc - key) & mask end enc end fail unless BITS % 8 == 0 flag = SecureRandom.bytes(BITS / 8).unpack1('H*').to_i(16) key = SecureRandom.bytes(BITS / 8).unpack1('H*').to_i(16) STDERR.puts "The flag: TWCTF{%x}" % flag STDERR.puts "Key=%x" % key STDOUT.puts "Encrypted flag: %x" % encrypt(flag, key) fail unless decrypt(encrypt(flag, key), key) == flag # Decryption Check PAIRS.times do |i| plain = SecureRandom.bytes(BITS / 8).unpack1('H*').to_i(16) enc = encrypt(plain, key) STDOUT.puts "Pair %d: plain=%x enc=%x" % [-~i, plain, enc] end
output
Encrypted flag: 43713622de24d04b9c05395bb753d437 Pair 1: plain=29abc13947b5373b86a1dc1d423807a enc=b36b6b62a7e685bd1158744662c5d04a Pair 2: plain=eeb83b72d3336a80a853bf9c61d6f254 enc=614d86b5b6653cdc8f33368c41e99254 Pair 3: plain=7a0e5ffc7208f978b81475201fbeb3a0 enc=292a7ff7f12b4e21db00e593246be5a0 Pair 4: plain=c464714f5cdce458f32608f8b5e2002e enc=64f930da37d494c634fa22a609342ffe Pair 5: plain=f944aaccf6779a65e8ba74795da3c41d enc=aa3825e62d053fb0eb8e7e2621dabfe7 Pair 6: plain=552682756304d662fa18e624b09b2ac5 enc=f2ffdf4beb933681844c70190ecf60bf
アプローチ:keyを下位bitから求めていく
encrypt.rb
の本質的な暗号化処理は以下のようになります.
enc = (enc + key) & mask enc = enc ^ key
一見シンプルな暗号化処理ですが,enc + key
の際に発生する繰り上がりがその後のenc ^ key
に影響を与えてしまうため,key
の特定が難しくなっているように見て取れます.
しかし,ネックになっているのが繰り上がりなのであれば繰り上がりが発生しない最下位bitからkey
を徐々に確定していくことでkey
の特定が可能になると考えられます.
そこで,output
として与えられたPair1~6
のplain
の下位1byteを暗号化した際にenc
の下位1byteと一致するようなkey
を求め,そこから徐々にkey
を確定させていくソルバを書きました.
ROUNDS = 765 def encrypt(msg, key, mask): enc = msg for _ in range(ROUNDS): enc = (enc + key) & mask enc = enc ^ key return enc def decrypt(msg, key): enc = msg mask = (1 << 128) - 1 for _ in range(ROUNDS): enc = enc ^ key enc = (enc - key) & mask return enc enc_flag = 0x43713622de24d04b9c05395bb753d437 msgs = [0x29abc13947b5373b86a1dc1d423807a ,0xeeb83b72d3336a80a853bf9c61d6f254,0x7a0e5ffc7208f978b81475201fbeb3a0,0xc464714f5cdce458f32608f8b5e2002e,0xf944aaccf6779a65e8ba74795da3c41d,0x552682756304d662fa18e624b09b2ac5] encs = [0xb36b6b62a7e685bd1158744662c5d04a,0x614d86b5b6653cdc8f33368c41e99254,0x292a7ff7f12b4e21db00e593246be5a0,0x64f930da37d494c634fa22a609342ffe,0xaa3825e62d053fb0eb8e7e2621dabfe7,0xf2ffdf4beb933681844c70190ecf60bf] fixed_keys = [0x09] # Brute-force search in advance for bits in range(16,136,8): print('BITS: {}'.format(bits)) fixed_bits = bits - 8 mask = (1 << bits) - 1 keys = [] print('fixed_keys: {}'.format(list(map(hex,fixed_keys)))) for fixed_key in fixed_keys: for x in range(0,256): check = True for i in range(6): enc = encrypt(msgs[i] & mask, (x << fixed_bits) + fixed_key, mask) if enc != (encs[i] & mask): check = False if check: keys.append((x << fixed_bits) + fixed_key) fixed_keys = keys for key in fixed_keys: dec = decrypt(enc_flag, key) print('TWCTF{{{}}}'.format(hex(dec)[2:]))
> python solve.py BITS: 16 fixed_keys: ['0x9'] BITS: 24 fixed_keys: ['0x5509', '0xd509'] BITS: 32 fixed_keys: ['0x73d509', '0xf3d509'] BITS: 40 fixed_keys: ['0x7773d509', '0xf773d509'] BITS: 48 fixed_keys: ['0x3e7773d509', '0xbe7773d509'] BITS: 56 fixed_keys: ['0x153e7773d509', '0x953e7773d509'] BITS: 64 fixed_keys: ['0x3a153e7773d509', '0xba153e7773d509'] BITS: 72 fixed_keys: ['0x29ba153e7773d509', '0xa9ba153e7773d509'] BITS: 80 fixed_keys: ['0x57a9ba153e7773d509', '0xd7a9ba153e7773d509'] BITS: 88 fixed_keys: ['0x4957a9ba153e7773d509', '0xc957a9ba153e7773d509'] BITS: 96 fixed_keys: ['0x494957a9ba153e7773d509', '0xc94957a9ba153e7773d509'] BITS: 104 fixed_keys: ['0x5c494957a9ba153e7773d509', '0xdc494957a9ba153e7773d509'] BITS: 112 fixed_keys: ['0x6bdc494957a9ba153e7773d509', '0xebdc494957a9ba153e7773d509'] BITS: 120 fixed_keys: ['0x1aebdc494957a9ba153e7773d509', '0x9aebdc494957a9ba153e7773d509'] BITS: 128 fixed_keys: ['0x521aebdc494957a9ba153e7773d509', '0xd21aebdc494957a9ba153e7773d509'] TWCTF{ade4850ad48b8d21fa7dae86b842466d}
TWCTF{ade4850ad48b8d21fa7dae86b842466d}
まとめ
- Crypto, Revの簡単な問題は徐々に解けるようになってきた気がする
- Web, Pwnはチームメイトに任せっぱなしで問題すら見てないのでwrite-up読んで勉強します
Happy!
とmeow
を解きたかった…