We participated in Paradigm CTF with KALOS team members and some friends (minaminao, taek, epist, gss1, pia).

 

During the competition, there were 2 challenges with the tag "cryptography" and I ended up getting first blood on both.

 

Oven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env python3
from Crypto.Util.number import *
import random
import os
import hashlib
 
FLAG = os.getenv("FLAG""PCTF{flag}").encode("utf8")
FLAG = bytes_to_long(FLAG[5:-1])
assert FLAG.bit_length() < 384
 
BITS = 1024
 
 
def xor(a, b):
    return bytes([i ^ j for i, j in zip(a, b)])
 
 
# This doesn't really matter right???
def custom_hash(n):
    state = b"\x00" * 16
    for i in range(len(n) // 16):
        state = xor(state, n[i : i + 16])
 
    for _ in range(5):
        state = hashlib.md5(state).digest()
        state = hashlib.sha1(state).digest()
        state = hashlib.sha256(state).digest()
        state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest()
 
    value = bytes_to_long(state)
 
    return value
 
 
def fiat_shamir():
    p = getPrime(BITS)
    g = 2
    y = pow(g, FLAG, p)
 
    v = random.randint(22**512)
 
    t = pow(g, v, p)
    c = custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t))
    r = (v - c * FLAG) % (p - 1)
 
    assert t == (pow(g, r, p) * pow(y, c, p)) % p
 
    return (t, r), (p, g, y)
 
 
while True:
    resp = input("[1] Get a random signature\n[2] Exit\nChoice: ")
    if "1" in resp:
        print()
        (t, r), (p, g, y) = fiat_shamir()
        print(f"t = {t}\nr = {r}")
        print()
        print(f"p = {p}\ng = {g}\ny = {y}")
        print()
    elif "2" in resp:
        print("Bye!")
        exit()
 
cs

 

So we are given a random signature oracle. To be precise, we know $t, r, p, g, y$ such that $$c = \text{hash}(g, y, t), \quad r \equiv (v - c \cdot flag) \pmod{p-1}$$ The key part I used is that we know $c, r, p$ and that $flag < 2^{384}$ as well as $v < 2^{512}$. In other words, we are finding the $0 < flag < 2^{384}$ such that $c \cdot flag + r \pmod{p-1}$ is less than $2^{512}$. This is a classic task suitable for lattice reduction (a bit overkill, but it is) and similar tricks have appeared in CTFs so much that I have a whole repository on this (https://github.com/rkm0959/Inequality_Solving_with_CVP)

 

A good heuristic to why this would work is that the "probability" of a value $flag$ satisfying $c \cdot flag + r \pmod{p-1} < 2^{512}$ would be around $2^{512} / p$, which is far less than $1/2^{384}$ as $p$ is 1024 bits. This means that there would be a unique $flag$ that satisfies our needs. 

 

To understand how to solve this "linear inequality with modulo" task, see https://rkm0959.tistory.com/188 (korean).

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from sage.all import * 
from pwn import *
from Crypto.Util.number import GCD
 
def ceil(n, m): # returns ceil(n/m)
    return (n + m - 1// m
 
def is_inside(L, R, M, val): # is L <= val <= R in mod M context?
    if L <= R:
        return L <= val <= R
    else:
        R += M
        if L <= val <= R:
            return True
        if L <= val + M <= R:
            return True 
        return False
 
def optf(A, M, L, R): # minimum nonnegative x s.t. L <= Ax mod M <= R
    if L == 0:
        return 0
    if 2 * A > M:
        L, R = R, L
        A, L, R = M - A, M - L, M - R
    cc_1 = ceil(L, A)
    if A * cc_1 <= R:
        return cc_1
    cc_2 = optf(A - M % A, A, L % A, R % A)
    return ceil(L + M * cc_2, A)
 
# check if L <= Ax (mod M) <= R has a solution
def sol_ex(A, M, L, R):
    if L == 0 or L > R:
        return True
    g = GCD(A, M)
    if (L - 1// g == R // g:
        return False
    return True
 
## find all solutions for L <= Ax mod M <= R, S <= x <= E:
def solve(A, M, L, R, S, E):
    # this is for estimate only : if very large, might be a bad idea to run this
    print("Expected Number of Solutions : ", ((E - S + 1* (R - L + 1)) // M + 1)
    if sol_ex(A, M, L, R) == False:
        return []
    cur = S - 1
    ans = []
    num_sol = 0
    while cur <= E:
        NL = (L - A * (cur + 1)) % M
        NR = (R - A * (cur + 1)) % M
        if NL > NR:
            cur += 1
        else:
            val = optf(A, M, NL, NR)
            cur += 1 + val
        if cur <= E:
            ans.append(cur)
            # remove assert for performance if needed
            assert is_inside(L, R, M, (A * cur) % M)
            num_sol += 1
    print("Actual Number of Solutions : ", num_sol)
    return ans
 
# conn = remote("oven.challenges.paradigm.xyz", 1337)
# conn.interactive()
 
 
import hashlib
from Crypto.Util.number import *
 
def xor(a, b):
    return bytes([i ^ j for i, j in zip(a, b)])
 
 
# This doesn't really matter right???
def custom_hash(n):
    state = b"\x00" * 16
    for i in range(len(n) // 16):
        state = xor(state, n[i : i + 16])
 
    for _ in range(5):
        state = hashlib.md5(state).digest()
        state = hashlib.sha1(state).digest()
        state = hashlib.sha256(state).digest()
        state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest()
 
    value = bytes_to_long(state)
 
    return value
 
= 93427683041905342461173547022600938643986887324972032834291939142561139490252265979618477433690413153065906221518481848227204925109596682649424431709625280133746760813834179099858484483652348113289609400674325409313902686091936601023976041128497642819529787946866157016217668015647432593957212819330706662552
= 118742916848068745441234425121114897870051115198012668332112268094669581171444207495264796187702240967886273172282936547873545351825602051457018801135490983227564157979548997162173927440795170927696473299845487953514965643146540163956783643164820892016837035894476678034631205573666641222349277397600109205124
 
= 126549310493151963326469876679724603661026366918265538391139061164994266707511395259083420603797826570096038735637035531704734213415007496749352216356840971790966322793226620694976878837854388424723443760043794854992519120259435122442271146978894991534180108105270279798030108380275673703977920882358759270081
= 2
= 101749117219274577198619316763589342740512542428910664337482178675253076795143579139493733233731229068585593656754291831105189837876269162333709022495477051005897166911586115461217424920314778304390479804373263955110732503700846048681418836722374515970914402818776396927744121752161147483063673287114906716300
 
= custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t))
 
//= 15 
lmao = r % 15
= (r - r % 15// 15 
 
= (p - 1// 15 
 
print(GCD(c, M))
lst = solve(c, M, (-r) % M , ((1 << 512// 15 - r + M) % M, 01 << 384)
 
print(lst)
 
print(long_to_bytes(10803675063719384436548220153547010608867399889700922150272564339681282264952460761794626241561264720352594960927090))
 
cs

 

 

Dragon Tyrant

The codebase is quite large, so I can't upload it all here. A quick summary - 

  • The goal of the challenge is to slay the dragon
  • To slay the dragon, one has get an NFT with max attack/defense, and predict the dragon's moves 60 times in a row.
  • The dragon's moves, as well as the basic stats of a generated NFT, is done with a weird elliptic curve based RNG.
  • To get max attack/defense, one can buy items from an item shop
  • However, getting the max attack sword legit is too expensive
  • but it suffices to buy items from a shop that has the same extcodehash as the provided one. 

Let's start with getting the max attack and defense. We just need matching extcodehash - so whatever we do in the constructor doesn't matter, as long as we return the same code. Therefore we can do some nice tricks as follows.

 

1
2
3
4
5
6
7
8
9
10
11
contract ItemShop2 is ItemShop {
    constructor(bytes memory code) {
        // write this
        _itemInfo[4= ItemInfo({name"A", slot: EquipmentSlot.Weapon, value: type(uint40).max, price: 0}); // example
        _itemInfo[5= ItemInfo({name"B", slot: EquipmentSlot.Shield, value: type(uint40).max, price: 0}); // example
        _mint(msg.sender, 41"");
        _mint(msg.sender, 51"");
        assembly { return ( add(code, 0x20), mload(code)) }
    }
}
 
cs

 

Now we can equip this accordingly to get max attack/defense. We now have to predict the moves. 

When the off-chain entity resolves the randomness, it first generates the random values for the mints' traits.

Only after that, it generates the random value for the dragon's move. This can be seen in the code below.

 

1
2
3
4
5
6
7
8
9
function resolveRandomness(bytes32 seed) external override {
        if (msg.sender != address(factory.randomnessOperator())) {
            revert Unauthorized(msg.sender);
        }
 
        _lastOffchainSeed = seed;
        uint256 nextRound = _resolveMints();
        _resolveFight(nextRound);
    }
cs

 

We can also fetch the traits, as the NFT contract provides a public function for it. This implies that we can fetch all the previously generated values for the off-chain provided seed. In other words, if we can compute $\text{rand}(seed, i + 1)$ from $\text{rand}(seed, i)$, we would be done. 

 

1
2
3
function _generateRandomness(uint256 round) internal view returns (bytes32 rand) {
        rand = randomness.generate(_lastOffchainSeed, round + 1);
    }
cs

 

Now let's look at the randomness itself. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
 
import "./OwnedUpgradeable.sol";
import "./Interfaces.sol";
 
contract Randomness is IRandomness {
    error AlreadySet();
    // https://eips.ethereum.org/EIPS/eip-197 Y^2 = X^3 + 3
    // a generator of alt_bn128 (bn254)
 
    uint256 public constant Gx = 1;
    uint256 public constant Gy = 2;
    uint256 public immutable Px;
    uint256 public immutable Py;
    uint256 public immutable Qx;
    uint256 public immutable Qy;
    uint256 public constant fieldOrder =
        uint256(21888242871839275222246405745257275088696311157297823662689037894645226208583);
    uint256 public constant groupOrder =
        uint256(21888242871839275222246405745257275088548364400416034343698204186575808495617);
 
    constructor() {
        uint256 P_x;
        uint256 P_y;
        uint256 Q_x;
        uint256 Q_y;
 
        bytes32 r = bytes32(uint256(0x123456789));
 
        assembly {
            mstore(0x80, Gx)
            mstore(0xa0, Gy)
            mstore(0xc0, r)
            if iszero(staticcall(gas(), 0x070x800x600x800x40)) { revert(00) }
            P_x := mload(0x80)
            P_y := mload(0xa0)
 
            mstore(0x80, Gx)
            mstore(0xa0, Gy)
            mstore(0xc0, r)
            mstore(0xc0, keccak256(0xc00x20))
            if iszero(staticcall(gas(), 0x070x800x600x800x40)) { revert(00) }
            Q_x := mload(0x80)
            Q_y := mload(0xa0)
        }
 
        Px = P_x;
        Py = P_y;
        Qx = Q_x;
        Qy = Q_y;
    }
 
    /// @notice Generates a sequence of random numbers from an initial seed
    /// @param seed The initial seed
    /// @param rounds The round to generate
    /// @return rand The generated randomness for the round
    function generate(bytes32 seed, uint256 rounds) external view override returns (bytes32 rand) {
        uint256 Q_x = Qx;
        uint256 Q_y = Qy;
        uint256 P_x = Px;
        uint256 P_y = Py;
        assembly {
            mstore(0x00, P_x)
            mstore(0x20, P_y)
            mstore(0x40, seed)
            for { let i := 0 } lt(i, rounds) { i := add(i, 1) } {
                if iszero(staticcall(gas(), 0x070x000x600x400x40)) { revert(00) }
            }
            mstore(0x00, Q_x)
            mstore(0x20, Q_y)
            if iszero(staticcall(gas(), 0x070x000x600x400x40)) { revert(00) }
            rand := mload(0x40)
            mstore(0x400x80)
        }
    }
}
 
cs

 

So it first takes $r$ as 0x123456789 and sets $P = r \cdot G$ and $Q = \text{hash}(r) \cdot G$. Then, the randomness is generated by iterating $seed \leftarrow (seed \cdot P).x$ for $round$ number of times and finishing with $out \leftarrow (seed \cdot Q).x$. 

 

Now we see the idea - since we know the $(seed \cdot Q).x$ as the output, one can recover $seed \cdot Q$ where $seed$ is the result of $round$ iterations. Since we know the discrete-logarithm relations between $P$ and $Q$, this means that we can also recover $seed \cdot P$ - and taking the $x$ coordinate here would be the result of $round + 1$ iterations. Now doing $out \leftarrow (seed \cdot Q).x$ once again would be sufficient to get the randomness result for $\text{rand}(seed, round + 1)$. We have our theoretical exploit! Now on to the implementation.

 

To do this in a smart contract, the only hard part would be to recover the full elliptic curve point from the $x$ coordinate alone. To do this you need a modular square root, but since $p \equiv 3 \pmod{4}$ it's easy, (no Tonelli-Shanks) simply raise to $(p+1)/4$th power.  

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
 
import {Script, console2} from "forge-std/Script.sol";
import "../test/Counter.t.sol";
 
import "../src/ItemShop.sol";
import "../src/ItemShop2.sol";
import "../src/Challenge.sol";
 
 
contract Exploiter {
    Challenge public challenge;
    NFT public nft;
    ItemShop public itemShop;
    Factory public factory;
    ItemShop2 public itemShop2;
    Exploiter public lmao;
    uint256 public cnter = 0;
     uint256 public constant fieldOrder =
        uint256(21888242871839275222246405745257275088696311157297823662689037894645226208583);
    uint256 public constant groupOrder =
        uint256(21888242871839275222246405745257275088548364400416034343698204186575808495617);
 
    function exploit_part_1(address chall) external {
        challenge = Challenge(chall);
        factory = challenge.FACTORY();
        nft = challenge.TOKEN();
        itemShop = challenge.ITEMSHOP();
        itemShop2 = new ItemShop2(address(itemShop).code);
        itemShop2.setApprovalForAll(address(nft), true);
        address[] memory myself = new address[](1);
        myself[0= address(this);
        nft.batchMint(myself);
    }
 
    function exploit_part_2() external {
        address[] memory myself = new address[](1);
        myself[0= address(this);
        nft.batchMint(myself);
        nft.fight(10);
    }
 
    function onERC721Received(address sender, address from, uint256 tokenId, bytes memory data) external returns (bytes4) {
        cnter += 1;
        if(cnter == 1) {
            nft.equip(tokenId, address(itemShop2), 4);
            nft.equip(tokenId, address(itemShop2), 5);
        }
        return this.onERC721Received.selector;
    }
 
    function modExp(uint256 _b, uint256 _e, uint256 _m) public returns (uint256 result) {
        assembly {
            let pointer := mload(0x40)
 
            mstore(pointer, 0x20)
            mstore(add(pointer, 0x20), 0x20)
            mstore(add(pointer, 0x40), 0x20)
 
            mstore(add(pointer, 0x60), _b)
            mstore(add(pointer, 0x80), _e)
            mstore(add(pointer, 0xa0), _m)
 
            let value := mload(0xc0)
 
            if iszero(call(gas(), 0x050, pointer, 0xc0, pointer, 0x20)) {
                revert(00)
            }
 
            result := mload(pointer)
            mstore(0x40, pointer)
        }
    }
 
    function getInput(FighterVars calldata attacker, FighterVars calldata attackee) external returns (uint256 inputs) {
        Trait memory trait = nft.traits(2);
 
        uint256 prev_rand = 0;
        prev_rand += uint256(trait.rarity);
        prev_rand += uint256(trait.strength) << 16;
        prev_rand += uint256(trait.dexterity) << 56;
        prev_rand += uint256(trait.constitution) << 96;
        prev_rand += uint256(trait.intelligence) << 136;
        prev_rand += uint256(trait.wisdom) << 176;
        prev_rand += uint256(trait.charisma) << 216;
 
        // recover the elliptic curve point
        uint256 cube_3_3 = modExp(prev_rand, 3, fieldOrder) + 3;
        uint256 y_val = modExp(cube_3_3, (fieldOrder + 1/ 4, fieldOrder);
 
        uint256 interim;
        assembly {
            interim := mload(0x40)
        }
 
        assembly {
            let pointer := mload(0x40)
            mstore(pointer, prev_rand)
            mstore(add(pointer, 0x20), y_val)
            mstore(add(pointer, 0x40), 0x200ac28172d3dfaf595636a5d34fc6a98f3168b32317278ab95d95792e3b4f8f)
            if iszero(staticcall(gas(), 0x07, pointer, 0x60, pointer, 0x40)) { revert(00)}
            interim := mload(pointer)
            mstore(0x40, pointer)
        }
 
        uint256 Qx =   2771061477252358712132284804733770040260252456558485434530149143843066948317;
        uint256 Qy = 21636887117896825552852388732829976843920120171647088092176094089927511555925;
        assembly {
            let pointer := mload(0x40)
            mstore(pointer, Qx)
            mstore(add(pointer, 0x20), Qy)
            mstore(add(pointer, 0x40), interim)
            if iszero(staticcall(gas(), 0x07, pointer, 0x60, pointer, 0x40)) { revert(00)}
            interim := mload(pointer)
            mstore(0x40, pointer)
        }
 
        return type(uint256).max - interim;
    }
 
 
    function onERC1155Received(address, address, uint256, uint256, bytes calldata)
        external
        pure
        returns (bytes4)
    {
        return this.onERC1155Received.selector;
    }
 
    function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
        external
        pure
        returns (bytes4)
    {
        return this.onERC1155BatchReceived.selector;
    }
}
 
 
contract CounterScript is Script {
    Challenge challenge;
    NFT nft;
    ItemShop itemShop;
    Factory factory;
    ItemShop2 itemShop2;
    Exploiter lmao;
    uint256 public constant fieldOrder =
        uint256(21888242871839275222246405745257275088696311157297823662689037894645226208583);
    uint256 public constant groupOrder =
        uint256(21888242871839275222246405745257275088548364400416034343698204186575808495617);
 
    function setUp() public {}
 
    function run() public {
        vm.startBroadcast();
        challenge = Challenge(0x4c2f201aFd08F986BDeed4907C263795c1510F75);
 
        /*lmao = new Exploiter();
        console2.log(address(lmao));
 
        lmao.exploit_part_1(address(challenge));
        vm.stopBroadcast();*/
 
 
        
        lmao = Exploiter(0xf25888e0B386Ed0739B0d2D77CE68B6e1E0583b5);
        console2.log(lmao.cnter());
        
        lmao.exploit_part_2();
        console2.logBool(challenge.isSolved());
        
        vm.stopBroadcast();
        
    }
}
 
cs

 

'Blockchain Security' 카테고리의 다른 글

Curta / Plonky2x Audit Report  (0) 2024.02.27
zkSummit11 @ Athens  (0) 2024.02.24
Scroll's Security Measure Seminar  (0) 2023.10.25
Scroll zkEVM Audit Report  (0) 2023.10.17
ZKP Security Seminar @ SpearbitDAO  (1) 2023.07.27

https://twitter.com/Scroll_ZKP/status/1716781095138861158

 

X에서 Scroll 📜 님

This week, we'll be hosting an online webinar to dive into our security measures, and you're invited! We'll be joined by auditing collaborators from @immunefi, @zellic_io, @trailofbits, and @OpenZeppelin. 🗓 Save the date: October 25th, at 9:00AM EST. ht

twitter.com

 

'Blockchain Security' 카테고리의 다른 글

zkSummit11 @ Athens  (0) 2024.02.24
Paradigm CTF 2023: "Cryptography Challenges"  (0) 2023.10.30
Scroll zkEVM Audit Report  (0) 2023.10.17
ZKP Security Seminar @ SpearbitDAO  (1) 2023.07.27
A fun story on "Membership Proofs"  (0) 2022.12.07

https://github.com/kalos-xyz/Publications/blob/main/audit-reports-english/Scroll_zkEVM_-_Audit_Report.pdf

https://github.com/kalos-xyz/Publications/blob/main/audit-reports-english/Scroll_zkEVM_-_Part_2_-_Audit_Report.pdf

 

제가 참가한 보안감사 리포트가 공개되었습니다 :)

https://infossm.github.io/blog/2023/09/16/Brakedown/

 

Brakedown Overview

이 내용은 https://eprint.iacr.org/2021/1043 의 요약입니다. 이 논문의 목표는 Linear Code를 기반으로 한 Linear-Time PCS를 준비하고 이를 Spartan에 적용하여 Linear-Time Field-Agnostic SNARK를 얻는 것입니다. Spartan 계

infossm.github.io

 

'Cryptography' 카테고리의 다른 글

Folding Part 1: ProtoStar  (0) 2023.12.01
Multilinear PCS from Univariate PCS  (0) 2023.12.01
Monolith Hash Function  (0) 2023.09.30
[Axiom OS Project] Implementing Poseidon2 & AES-ECB for Verifiable Encryption  (0) 2023.06.14
ZK Applications  (0) 2023.03.03

https://infossm.github.io/blog/2023/07/14/Monolith/

 

Hash Functions Monolith for ZK Applications: May the Speed of SHA-3 be With You

이 내용은 https://eprint.iacr.org/2023/1025.pdf 의 요약입니다. ZK Friendly Hash Function의 필요성 해시함수는 굉장히 많은 곳에서 사용되고 있습니다. 그런만큼 ZKP 상에서도 해시함수의 계산에 대한 증명을

infossm.github.io

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/sage
import random
import hashlib
import os
import signal 
 
signal.alarm(1800)
 
def PoW():
    prefix = os.urandom(8)
    print(prefix.hex())
    answer = bytes.fromhex(input().strip())
    assert len(answer) == 24
    result = hashlib.sha256(prefix + answer).digest()
    assert result[:3== b"\x00\x00\x00"
 
= PolynomialRing(ZZ, 'x')
= P.gen()
 
def convolution(n, f, g):
    return (f * g) % (x ** n - 1)
 
def balance_mod(f, q):
    tt = f.coefficients(sparse = False)
    ret = 0
    for i in range(len(tt)):
        cc = int((tt[i] + q // 2) % q) - q // 2
        ret += cc * (x ** i)
    return ret
 
def random_poly(n, v1, v2):
    ret = v1 * [1+ v2 * [-1+ (n - v1 - v2) * [0]
    random.shuffle(ret)
    return P(ret)
 
def invert_prime(n, f, p):
    T = P.change_ring(GF(p)).quotient(x ** n - 1)
    ret = P(lift(1 / T(f)))
    return balance_mod(ret, 3)
 
def pad(n, arr):
    while len(arr) < n:
        arr.append(0)
    return arr
 
def encode(n, arr):
    res = 0
    for i in range(n):
        assert -1 <= arr[i] <= 1
        res += (arr[i] + 1* (3 ** i)
    return res 
 
def task1(n, D):
    random.seed(int.from_bytes(os.urandom(32), "big"))
    f = random_poly(n, n // 3 + 1, n // 3)
    f3 = invert_prime(n, f, 3)
 
    random.seed(int.from_bytes(os.urandom(32), "big"))
    sel1 = random.sample(range(n), D)
    random.seed(int.from_bytes(os.urandom(32), "big"))
    sel2 = random.sample(range(n), D)
 
    coef_original = pad(n, f.coefficients(sparse = False))
    coef_inverse = pad(n, f3.coefficients(sparse = False))
 
    for i in range(D):
        coef_original[sel1[i]] = 0
        coef_inverse[sel2[i]] = 0
    
    print(sel1)
    print(sel2)
    print(encode(n, coef_original))
    print(encode(n, coef_inverse))
 
    assert int(input()) == encode(n, pad(n, f.coefficients(sparse = False)))
    assert int(input()) == encode(n, pad(n, f3.coefficients(sparse = False)))
 
def task2(n, D):
    random.seed(int.from_bytes(os.urandom(32), "big"))
    f = random_poly(n, n // 3 + 1, n // 3)
    f3 = invert_prime(n, f, 3)
    
    seed = int(input())
    random.seed(seed)
 
    sel1 = random.sample(range(n), D)
    sel2 = random.sample(range(n), D)
 
    coef_original = pad(n, f.coefficients(sparse = False))
    coef_inverse = pad(n, f3.coefficients(sparse = False))
 
    for i in range(D):
        coef_original[sel1[i]] = 0
        coef_inverse[sel2[i]] = 0
    
    print(sel1)
    print(sel2)
    print(encode(n, coef_original))
    print(encode(n, coef_inverse))
 
    assert int(input()) == encode(n, pad(n, f.coefficients(sparse = False)))
    assert int(input()) == encode(n, pad(n, f3.coefficients(sparse = False)))
 
PoW()
for _ in range(8):
    task1(241183)
for _ in range(8):
    task2(85012125)
 
flag = open("flag.txt""r").read()
print(flag)
cs

 

We are given two polynomials $f, f_v$ such that $f \cdot f_v \equiv 1 \pmod{x^n - 1}$, but some $D$ of the coefficients are erased. We have to recover $f, f_v$ completely, in a relatively fast and reliable fashion. The erasure positions are also given by the server.

 

For the first task, $(n, D) = (2411, 83)$ and the erasure positions are completely random. 

For the second task, $(n, D) = (8501, 2125)$ and the erasure positions can be controlled by a user provided seed. 

 

Task 1

By setting a variable for each erased coefficient, we will have a system of $n$ quadratic equations over $2D$ variables in $\mathbb{F}_3$. However, the interesting part is that some of the quadratic equations are actually just linear. For example, if we denote $S_1$ and $S_2$ as the set of erased coefficient's degree in $f$ and $f_v$ respectively, we can see that the equation arising from computing the coefficient of $x^k$ in $f \cdot f_v \pmod{x^n - 1}$ will be simply linear if there are no $u \in S_1$ and $v \in S_2$ such that $u + v \equiv k \pmod{n}$. 

 

By collecting these equations and solving the linear system, we will be closer to finding the solutions for the $2D$ variables.

However, after implementing this you can see that there will be a nontrivial kernel, of size around 40 to 50. 

 

This can be resolved in two ways. 

  • The author's intended solution is to modify the given system as a system of $n$ quadratic equations over $K$ variables, where $K$ is the size of the kernel. This can be done simply by expressing the solution set of the $2D$ variables as a single solution added with a vector in the span of the computed kernel basis. As $K$ is much smaller than $2D$, we can actually solve this quadratic equation system by linearization. In other words, we can regard all quadratic terms as a separate linear variable, and solve the linear system over $\mathcal{O}(K^2)$ variables. This fails if $K$ is large, but such probability is small enough so that you can just try repeatedly.  
  • soon-haari's solution works by selecting variables so that fixing it will add as many linear equations as possible, then brute forcing them. Apparently, brute forcing around 3 to 7 variables makes it sufficient to solve everything with a linear system. This was considered by the author as well, but was considered to be of similar difficulty. Congratulations to soon-haari for the solve!

 

Task 2

From solving task 1, it should be clear that the goal should be to create as many linear equations as possible, and the best way to do it is by making the erased coefficients consecutive in their positions. Note that $D = n/4$. Now how do we do that?

 

Looking at the sample implementation, we can see that the random sampling works by 

  • selecting a random number below $n - i$
  • taking the value at that index
  • swapping with the value at position $n - i - 1$ so it's not selected again
1
2
3
4
5
6
7
8
if n <= setsize:
    # An n-length list is smaller than a k-length set.
    # Invariant:  non-selected at pool[0 : n-i]
    pool = list(population)
    for i in range(k):
        j = randbelow(n - i)
        result[i] = pool[j]
        pool[j] = pool[n - i - 1]  # move non-selected item into vacancy
cs

 

The first idea is that our consecutive selections should be between $3n/4$ and $n$ - this is because if we try to pick everything from the front, the whole swapping process with the elements from the back makes everything very complicated. By picking everything at the back, the swapping process doesn't matter. Our goal is that for each $0 \le i < 2D$, the $i$th randbelow call should return a value $x$ such that $$n - D \le x < n - (i \pmod{D})$$ To do this efficiently, we need to minimize the number of bits we constrain from the randbelow results.

 

This can be done by finding $t, e$ such that $$n - D \le t \cdot 2^e < (t + 1) \cdot 2^e \le n - (i \pmod{D})$$ and maximizing $e$. Now, it suffices to constrain that the randbelow result is equal to $t$ after being shifted right $e$ bits. 

 

With this constraint in mind, finding the random seed is a relatively standard & well-known part. Check 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
from sage.all import * 
from pwn import * 
import os, time
from tqdm import tqdm
 
master_seed = 60024794038789808135493353697686616908493063225185396948889991711002019881012260998892909674537714109874172336760379200731611344495379266686257170624293028615555926994680615436128867836845735901814299032623034279895233846538952693992950207880205125875448087855836772541821353895968629329852908028951482292046429634872159757554409659745842900181726502284855318484066778535471595292549152348723264514710398644809539021921578114599851865111153331428784733149637717776626956886869295212600036738540930509437369722973201969837169465390106295804865432397396740692302797829400696350801197455057637221980617059842112641232969233787118965053967785222933031549323453023915303961246528225916509962869051725570255571786695958227355527242709785066976431806251198382777530714413831824130431722178073034419423922524991559359620024288713328086359403414933794337286671654435418265857597949112504278406015704720441696610732129686376522250690959070825600070450708353622473071494975779294350695405558303914530098365686912502766894493961489506186331053557290449360412540423892253756102487513806029093829893795865411528046974478581984437329446430455654183382959047805022370700975782191280373163986285947944114008525799059163331940037604447345980775790916325310887097685957858367094306509865450306900924968543630281114344776628409464077904562522114091218110199532185567329195460558546578726337659844292296095730138395687853467464686825377362140525018332064336498037333745669506410885086785171181512953442077146559888185321193265161347864107827758406549956890513622009827054983736589580372571827584944816635338237441722836404122046830295098128877031939911864423063671844869052268187006757023952046083313268542630040867977783706755657741531230633393603995284157154279750477826730638935203116032077204427304499208757288061746193707867350153416770764821598920213730711114965590743331727312353459097800387002408397658122900984840808246371821451013961389731787227009157398692371099660858182271036744448166340400895784346173287333244495255001791730081569336839520662381405144290409455974037142516983228317936699727842606081642240578844883094341186246580921199983915842081314638611291380152731368056730650469228559623662052531729887982086548223154333361363334651179104285034483322425275792671395416574805801066174116333550082050898468922185218775818163991247447213512072133729905831026979204387449740237107250035767499446745374366910625527910665062130250560706824973131871130316743119025554746764821434156674317934199946146598426673815806872877856328467376086085059620030855489151545303043307029412045887534894425887717583117437877318171620007894084301274965980741305242376340156346230267114570705189461331266648670135971622540649186222606301719344988665012528312715455324776650769063545393847491422947171743363094737848164203527815500665056795598783088145237029472339734427963849835022178538693065735805207641271044631695148529356782873975936571285135245429745146108036491430498162500763656245871098506267158087440237750159751664863211065697979948218429442546498813889729910223944717520161604727418685340138991981985619225849416335059973922841803883389393526433881970295102025473967271242147333800098274311358630381328801550779263853582690762954190377450436922016091335950930865947431088321016971794583463543506805777276891066135310863296397756985031896218023594355682462504506734940500365812292755553197773898889660699577561530145492927609494642174649101705826746672666183294279559291981779160964522297534960255063618332466976614501183228998682729197968575051178082566996628571473801354118883690941507931744401374987244372715844496021747653016503181871565513365490311937754665491041784007233799634349717657539855346128371782526748645254711640975576359645995419371902147356469195156372805778603944262661098290749863028288989422941985200606780481844930898792872531924601622394832468102957298372210031427732699319934781107988301247600593511823195668971916883951609735878765097692081037005879736118141874324737540398705286288988681436765122193427285686784758267811646782109984193537493517035025336405137273109568869777622232503118830613665850700496192371337485618910625319419657584661501620375974836832411786322507731977027575625783250266414378787148900895983949856500564564779297830591427575079297862649141244375485805801593463197364935921063290595220319427752540805812417257433276354627419044442018101969592668555978348415720913743417010710043519932066341337349002366227396881340998080393401376080508879416483875634105463190312058370838700500096751599290858489939320163312460446194287943741486626465044167916764309453265867227749001640757524516320238197501217997176854839242256247737322846205881431327708198117910578431481983383453719767475910167265170827225409678070198207927333097671611616080671517652067086557349582167623754660313732259984661354739151828000954072261669966765373367625577786880673420597700680012679943342785274712575950904993112141660047795983847702513444872812047642230003916360543076277750525218857399197096056760230787156929311820632995878912539801079019446915728253563406722059879474176881696819163446675139576322957663476859383520766444528469597928076769925748138660178853642739598139933326858259371267626938125840719239557084994647877357315029370116939357392564073911953692514212255988095253356674531690329478518006539330700869866952658773265849967085817409436103360317583749880904176992380298324586178020697936498010899061947345939204960967177164006648986295804937160615262813770879693445967104259390803516524422802908637470251797969593148039038477540080340954624195569529253847629154075274257199861147125055216614817003693519884916983205377401473762967508445819467333797312883675275497717519827470702088770584690428550650158813144401296875683289360897009594467643245700773535411913797121904382044029865707340324833895400600974009981197992647586507846704616021687940777933174900551897716451821657379278722485819954967117168842871510763631682184488735137792120697666469419973009451057094047507237995094164031163468702
 
= PolynomialRing(ZZ, 'x')
= P.gen()
 
# r = process(['sage', '../prob/for_organizer/task.sage'])
= remote("52.79.59.27"17776)
 
def solvePoW():
    PoW_start = time.time()
    prefix = bytes.fromhex(r.recvline().strip().decode())
    cnt = 0
    while True:
        cnt += 1
        if cnt % 1000000 == 0:
            print(cnt)
        ret = os.urandom(24)
        result = hashlib.sha256(prefix + ret).digest()
        if result[:3== b"\x00\x00\x00":
            r.sendline(ret.hex())
            break
    PoW_end = time.time()
    print("PoW", PoW_end - PoW_start)
 
def balance_mod(f, q):
    tt = f.coefficients(sparse = False)
    ret = 0
    for i in range(len(tt)):
        cc = int((tt[i] + q // 2) % q) - q // 2
        ret += cc * (x ** i)
    return ret
 
def pad(n, arr):
    while len(arr) < n:
        arr.append(0)
    return arr
 
def encode(n, arr):
    res = 0
    for i in range(n):
        assert -1 <= arr[i] <= 1
        res += (arr[i] + 1* (3 ** i)
    return res 
 
def decode(n, v):
    ret = [0* n 
    for i in range(n):
        ret[i] = v % 3 - 1
        v = v // 3 
    return ret
 
def read_input(n, D):
    s1 = r.recvline()
    s2 = r.recvline()
    if b"Traceback" in s1:
        for _ in range(20):
            print(r.recvline())
    sel1 = eval(s1)
    sel2 = eval(s2)
    coef_original = decode(n, int(r.recvline().strip()))
    coef_inverse = decode(n, int(r.recvline().strip()))
    return sel1, sel2, coef_original, coef_inverse
 
def solve_init(n, D, sel1, sel2, coef_original, coef_inverse):
    mat, vec = [], []
    isok = [1 for _ in range(n)]
    for i in range(D):
        for j in range(D):
            isok[(sel1[i] + sel2[j]) % n] = 0
    
    idxs1 = [-1* n 
    idxs2 = [-1* n 
    for i in range(D):
        idxs1[sel1[i]] = i 
        idxs2[sel2[i]] = i
    
    for i in tqdm(range(n)):
        if isok[i] == 1:
            coefs = [0* (2 * D)
            val = 0
            if i == 0:
                val = 1
            for j in range(n):
                if idxs1[j] != -1:
                    coefs[idxs1[j]] = coef_inverse[(i - j) % n]
                if idxs2[(i - j) % n] != -1:
                    coefs[idxs2[(i - j) % n] + D] = coef_original[j]
                if idxs1[j] == -1 and idxs2[(i - j) % n] == -1:
                    val -= coef_original[j] * coef_inverse[(i - j) % n]
            mat.append(coefs)
            vec.append(val % 3)
    
    M = Matrix(GF(3), mat)
    v = vector(GF(3), vec)
    sol = M.solve_right(v)
 
    kernel = M.right_kernel().basis()
    return sol, kernel 
 
def solve_task1(n, D):
    sel1, sel2, coef_original, coef_inverse = read_input(n, D)
 
    sol, kernel = solve_init(n, D, sel1, sel2, coef_original, coef_inverse)
 
    idxs1 = [-1* n 
    idxs2 = [-1* n 
    for i in range(D):
        idxs1[sel1[i]] = i 
        idxs2[sel2[i]] = i
 
    fvar = len(kernel)
    print(fvar)
    tot = fvar + (fvar * (fvar - 1)) // 2 + fvar
 
    idxs = [[0 for _ in range(fvar)] for _ in range(fvar)]
    for i in range(fvar):
        idxs[i][i] = i
    
    cur = fvar 
    for i in range(fvar):
        for j in range(i + 1, fvar):
            idxs[i][j] = cur
            cur += 1
    
    def single(x):
        return fvar + fvar * (fvar - 1// 2 + x
 
    def wow(x1, x2):
        if x1 == x2:
            return x1
        else:
            if x1 > x2:
                x1, x2 = x2, x1
            return idxs[x1][x2]
 
    mat = []
    vec = []
 
    for i in tqdm(range(n)):
        coefs = [0* tot
        val = 0
        if i == 0:
            val = 1
        for j in range(n):
            # [j] * [i - j]
            if idxs1[j] == -1 and idxs2[(i - j) % n] == -1:
                val -= coef_original[j] * coef_inverse[(i - j) % n]
            if idxs1[j] == -1 and idxs2[(i - j) % n] != -1:
                idx2 = idxs2[(i - j) % n]
                val -= coef_original[j] * sol[idx2 + D]
                for k in range(fvar):
                    coefs[single(k)] += coef_original[j] * kernel[k][idx2 + D]
            if idxs1[j] != -1 and idxs2[(i - j) % n] == -1:
                idx1 = idxs1[j]
                val -= coef_inverse[(i - j) % n] * sol[idx1]
                for k in range(fvar):
                    coefs[single(k)] += coef_inverse[(i - j) % n] * kernel[k][idx1]
            if idxs1[j] != -1 and idxs2[(i - j) % n] != -1:
                idx1 = idxs1[j]
                idx2 = idxs2[(i - j) % n]
                val -= sol[idx1] * sol[idx2 + D]
                for k in range(fvar):
                    coefs[single(k)] += sol[idx1] * kernel[k][idx2 + D]
                    coefs[single(k)] += sol[idx2 + D] * kernel[k][idx1]
                for k1 in range(fvar):
                    for k2 in range(fvar):
                        coefs[wow(k1, k2)] += kernel[k1][idx1] * kernel[k2][idx2 + D]
        mat.append(coefs)
        vec.append(val)
 
    M = Matrix(GF(3), mat)
    v = vector(GF(3), vec)
    final_sol = M.solve_right(v)
 
    fins = [0* (2 * D)
 
    for i in range(2 * D):
        fins[i] += sol[i]
    
    for i in range(2 * D):
        for j in range(fvar):
            fins[i] += final_sol[single(j)] * kernel[j][i]    
 
    recover_f = 0
    recover_f3 = 0
    for i in range(n):
        if i in sel1:
            recover_f += fins[sel1.index(i)] * (x ** i)
        else:
            recover_f += coef_original[i] * (x ** i)
    
    for i in range(n):
        if i in sel2:
            recover_f3 += fins[sel2.index(i) + D] * (x ** i)
        else:
            recover_f3 += coef_inverse[i] * (x ** i)
    
    recover_f = balance_mod(recover_f, 3)
    recover_f3 = balance_mod(recover_f3, 3)
 
    r.sendline(str(encode(n, pad(n, recover_f.coefficients(sparse = False)))))
    r.sendline(str(encode(n, pad(n, recover_f3.coefficients(sparse = False)))))
 
def solve_task2(n, D):
    r.sendline(str(master_seed))
    sel1, sel2, coef_original, coef_inverse = read_input(n, D)
 
    for i in range(D):
        assert n - n // 4 <= sel1[i] < n 
        assert n - n // 4 <= sel2[i] < n
 
    sol, kernel = solve_init(n, D, sel1, sel2, coef_original, coef_inverse)
 
    print("task2 kernel"len(kernel))
 
    recover_f = 0
    recover_f3 = 0
    for i in range(n):
        if i in sel1:
            recover_f += sol[sel1.index(i)] * (x ** i)
        else:
            recover_f += coef_original[i] * (x ** i)
    
    for i in range(n):
        if i in sel2:
            recover_f3 += sol[sel2.index(i) + D] * (x ** i)
        else:
            recover_f3 += coef_inverse[i] * (x ** i)
 
    recover_f = balance_mod(recover_f, 3)
    recover_f3 = balance_mod(recover_f3, 3)
 
    r.sendline(str(encode(n, pad(n, recover_f.coefficients(sparse = False)))))
    r.sendline(str(encode(n, pad(n, recover_f3.coefficients(sparse = False)))))
 
st = time.time()
 
solvePoW()
 
for _ in tqdm(range(8)):
    solve_task1(241183)
 
for _ in tqdm(range(8)):
    solve_task2(85012125)
 
print(r.recvline())
 
en = time.time()
 
print(en - st)
cs

 

'CTF' 카테고리의 다른 글

ACSC 2024: Strange Machine (4 solves)  (0) 2024.03.31
Paradigm CTF 2023 2nd Place  (0) 2023.10.31
ACSC 2023 Writeups  (0) 2023.02.26
HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21

스트리밍

  • Youtube Premium
  • Spotify

 

음악 기기

  • AT-LP60X + MR4
  • 에듀플레이어 CD Player 

구매처

  • Tower Records Shibuya
  • Hyundai Card Vinyl & Plastic
  • Doujin Stores
  • Misc. Abroad Vinyl Stores

 

Hip Hop (Western)

  • Kendrick Lamar
    • GKMC (CD + Vinyl) 
    • TPAB (CD + Vinyl)
    • DAMN (CD)
    • Mr. Morale & the Big Steppers (CD)
  • Kanye West
    • MBDTF (Vinyl)
    • Kids See Ghosts (CD + Vinyl)
  • Tyler, the Creator
    • Cherry Bomb (Vinyl)
    • Flower Boy (CD + Vinyl)
    • IGOR (Vinyl)
    • CALL ME IF YOU GET LOST (CD)
  • N.W.A.
    • Straight Outta Compton (CD)
  • Madvillian
    • Madvillainy (Vinyl)

Hip Hop (Japan)

  • Samurai Champloo - Departure (Vinyl)
  • Modal Soul (Vinyl)
  • Metaphorical Music (Vinyl)
  • Luv(sic) Hexalogy (CD)
  • Hydeout Productions 2nd Collection (Vinyl)

Warp Records

  • Aphex Twin
    • Selected Ambient Works 85-92 (Vinyl)
    • Selected Ambient Works Volumn II (CD)
    • Richard D James Album (CD)
    • drukqs (CD)
    • Syro (CD)
    • Collapse (CD)
    • Blackbox Life Recorder 21f (Vinyl)
    • Girl/Boy EP (Vinyl)
  • Squarepusher
    • Feed Me Weird Things (CD, 25th Anniversary Version)
    • Be Up a Hello (CD)
    • Lamental EP (CD)
  • Flying Lotus
    • Cosmogramma (CD)
    • Until the Quiet Comes (CD)
    • You're Dead! (CD)

City Pop

  • Mignonne - Taeko Ohnuki (CD)
  • Heaven Beach - Anri (CD)

Japanese 

  • だから僕は音楽を辞めた - Yorushika (CD)
  • Your Name - RADWIMPS (CD)
  • Lemon - Kenshi Yonezu (CD)
  • 極彩色 - Reol (CD)
  • 世界はiに満ちている - CHiCO with HoneyWorks (CD)

J-Jazz

  • Scenery - Fukui Ryo (Vinyl)
  • Casiopea - Casiopea (CD)
  • Mint Jams - Casiopea (CD)

Animation Related

  • COWBOY BEBOP (CD)
  • Evangelion Finally (CD)
  • Howl's Moving Castle (Vinyl)

Japanese Rhythm Game or Doujin Music 

  • See You Again, HOLLOWOOD - t+pazolite (CD)
  • DJ Genki Best Collection - DJ Genki (CD)
  • CRYSTAR Soundtrack - Sakuzyo (CD)
  • AD:PIANO VI (CD)
  • Thank You - Hoshina Haru (CD)
  • "Cutie" Strikes Back - t+pazolite (CD)
  • TANO*C Short Collection (CD)

Touhou Project

  • ZUN's Music Collection vol 1~6 (CD)
  • FELT
    • Sevens Head (CD)
    • Rising Nebula (CD)
    • Darkness Brightness (CD)
  • Tokyo Active NEETs
    • Orchestral CDs - Compilation, TH07, TH08, TH09, TH10, TH11, TH12, TH13, TH14, TH16 (CD)

Misc

  • Currents - Tame Impala (CD)
  • WORLDS - Porter Robinson (CD)
  • Nurture - Porter Robinson (CD)
  • Donuts - J Dilla (Vinyl)
  • Endtroducing... - DJ Shadow (Vinyl)
  • Super Mario Bros 30th Anniversary (CD)
  • Random Access Memories 10th Anniversary Edition (Vinyl)
  • Hans Zimmer x Alan Walker Time (Vinyl)
  • Untrue - Burial (Vinyl)
  • OKNOTOK 1997 2017 (CD)
  • Kid A - Radiohead (Vinyl)
  • In Rainbows - Radiohead (Vinyl)
  • Discovery - Daft Punk (Vinyl)
  • Music has right to children - Boards of Canada (Vinyl)

 

'취미' 카테고리의 다른 글

EVERYTHING WE NEED IS ALREADY HERE  (0) 2024.12.16
CALL ME IF YOU GET LOST  (0) 2021.07.29
공부를 2주동안 하지 않은 이야기  (0) 2020.07.18
Squarepusher - Beep Street  (0) 2019.11.27
2019/11/16 ~ 2019/11/17 CHUNITHM 성과 정리  (0) 2019.11.17

https://www.youtube.com/watch?v=8wsR7o0rOxU 

 

'Blockchain Security' 카테고리의 다른 글

Scroll's Security Measure Seminar  (0) 2023.10.25
Scroll zkEVM Audit Report  (0) 2023.10.17
A fun story on "Membership Proofs"  (0) 2022.12.07
DFX Finance Attack Overview  (0) 2022.11.16
CODEGATE 2022: A Survey on Price Oracle Attacks  (0) 2022.11.05