
'CTF' 카테고리의 다른 글
| ACSC 2024: Strange Machine (4 solves) (0) | 2024.03.31 |
|---|---|
| CODEGATE 2023 Finals - The Leakers (1 solve) (1) | 2023.08.25 |
| ACSC 2023 Writeups (0) | 2023.02.26 |
| HackTM CTF Writeup (0) | 2023.02.22 |
| BlackHat MEA Finals (0) | 2022.11.21 |

| ACSC 2024: Strange Machine (4 solves) (0) | 2024.03.31 |
|---|---|
| CODEGATE 2023 Finals - The Leakers (1 solve) (1) | 2023.08.25 |
| ACSC 2023 Writeups (0) | 2023.02.26 |
| HackTM CTF Writeup (0) | 2023.02.22 |
| BlackHat MEA Finals (0) | 2022.11.21 |
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.
|
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(2, 2**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
t = 93427683041905342461173547022600938643986887324972032834291939142561139490252265979618477433690413153065906221518481848227204925109596682649424431709625280133746760813834179099858484483652348113289609400674325409313902686091936601023976041128497642819529787946866157016217668015647432593957212819330706662552
r = 118742916848068745441234425121114897870051115198012668332112268094669581171444207495264796187702240967886273172282936547873545351825602051457018801135490983227564157979548997162173927440795170927696473299845487953514965643146540163956783643164820892016837035894476678034631205573666641222349277397600109205124
p = 126549310493151963326469876679724603661026366918265538391139061164994266707511395259083420603797826570096038735637035531704734213415007496749352216356840971790966322793226620694976878837854388424723443760043794854992519120259435122442271146978894991534180108105270279798030108380275673703977920882358759270081
g = 2
y = 101749117219274577198619316763589342740512542428910664337482178675253076795143579139493733233731229068585593656754291831105189837876269162333709022495477051005897166911586115461217424920314778304390479804373263955110732503700846048681418836722374515970914402818776396927744121752161147483063673287114906716300
c = custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t))
c //= 15
lmao = r % 15
r = (r - r % 15) // 15
M = (p - 1) // 15
print(GCD(c, M))
lst = solve(c, M, (-r) % M , ((1 << 512) // 15 - r + M) % M, 0, 1 << 384)
print(lst)
print(long_to_bytes(10803675063719384436548220153547010608867399889700922150272564339681282264952460761794626241561264720352594960927090))
|
cs |
The codebase is quite large, so I can't upload it all here. A quick summary -
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, 4, 1, "");
_mint(msg.sender, 5, 1, "");
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(), 0x07, 0x80, 0x60, 0x80, 0x40)) { revert(0, 0) }
P_x := mload(0x80)
P_y := mload(0xa0)
mstore(0x80, Gx)
mstore(0xa0, Gy)
mstore(0xc0, r)
mstore(0xc0, keccak256(0xc0, 0x20))
if iszero(staticcall(gas(), 0x07, 0x80, 0x60, 0x80, 0x40)) { revert(0, 0) }
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(), 0x07, 0x00, 0x60, 0x40, 0x40)) { revert(0, 0) }
}
mstore(0x00, Q_x)
mstore(0x20, Q_y)
if iszero(staticcall(gas(), 0x07, 0x00, 0x60, 0x40, 0x40)) { revert(0, 0) }
rand := mload(0x40)
mstore(0x40, 0x80)
}
}
}
|
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(1, 0);
}
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(), 0x05, 0, pointer, 0xc0, pointer, 0x20)) {
revert(0, 0)
}
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(0, 0)}
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(0, 0)}
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 |
| 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
| 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 |
제가 참가한 보안감사 리포트가 공개되었습니다 :)
| Paradigm CTF 2023: "Cryptography Challenges" (0) | 2023.10.30 |
|---|---|
| Scroll's Security Measure Seminar (0) | 2023.10.25 |
| ZKP Security Seminar @ SpearbitDAO (1) | 2023.07.27 |
| A fun story on "Membership Proofs" (0) | 2022.12.07 |
| DFX Finance Attack Overview (0) | 2022.11.16 |
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
| 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
| Multilinear PCS from Univariate PCS (0) | 2023.12.01 |
|---|---|
| Brakedown Overview (0) | 2023.10.13 |
| [Axiom OS Project] Implementing Poseidon2 & AES-ECB for Verifiable Encryption (0) | 2023.06.14 |
| ZK Applications (0) | 2023.03.03 |
| Polynomials and Elliptic Curves in ZK (0) | 2023.02.27 |
|
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"
P = PolynomialRing(ZZ, 'x')
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(2411, 83)
for _ in range(8):
task2(8501, 2125)
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.
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.
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
|
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
P = PolynomialRing(ZZ, 'x')
x = P.gen()
# r = process(['sage', '../prob/for_organizer/task.sage'])
r = 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(2411, 83)
for _ in tqdm(range(8)):
solve_task2(8501, 2125)
print(r.recvline())
en = time.time()
print(en - st)
|
cs |
| 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 |
스트리밍
음악 기기
구매처
Hip Hop (Western)
Hip Hop (Japan)
Warp Records
City Pop
Japanese
J-Jazz
Animation Related
Japanese Rhythm Game or Doujin Music
Touhou Project
Misc
| 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 |