Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
binprotect — x64 PE bin2bin 난독화 도구로, 바이너리에 섹션을 추가하지 않습니다 | Kitploit
도구/GitHubGitHub/noahware/binprotect
Static AnalysisDynamic Analysis (Sandboxing)Reverse EngineeringMalware AnalysisBinary Analysis
GitHubnoahware/binprotect

binprotect

x64 PE bin2bin 난독화 도구로, 바이너리에 섹션을 추가하지 않습니다

저장소 보기
304362일 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

빌드 지침

빌드 지침은 4. 빌드를 참조하세요.

목차

  1. 소개
  2. 바이너리 재작성기
    • 2.1. 상대 주소 추적
    • 2.2. 역어셈블리
      • 2.2.1. 기본 블록 분할
      • 2.2.2. 간접 제어 흐름
        • 2.2.2.1. 점프 테이블
          • 2.2.2.1.1. 경계가 있는 점프 테이블
          • 2.2.2.1.2. 경계가 없는 점프 테이블
          • 2.2.2.1.3. 다양한 유형의 점프 테이블
        • 2.2.2.2. 예외 사례
      • 2.2.3. 함수
      • 2.2.4. 'Noreturn' 호출 처리
    • 2.3. 예외 지원
      • 2.3.1. 언와인드 지원
        • 2.3.1.1. 구현
      • 2.3.2. 예외 정보 파싱
        • 2.3.2.1. SEH/C_SCOPE_TABLE
        • 2.3.2.2. FuncInfo3 및 FuncInfo4
      • 2.3.3. RTTI 및 ThrowInfo 파싱
        • 2.3.3.1. RTTI
        • 2.3.3.2. ThrowInfo
  3. 난독화
    • 3.1. 가상 머신
      • 3.1.1. 언와인드 지원
    • 3.2. 불투명 조건 블록
    • 3.3. 제어 흐름 평탄화
    • 3.4. 선형 치환
    • 3.5. 혼합 부울 산술
  4. 빌드
  5. 사용법
  6. 약어
  7. 크레딧

1. 소개

이 난독화 도구는 bin2bin 방식으로, 이미 컴파일된 실행 가능한 바이너리를 가져와 난독화 패스를 적용하여 재생성합니다. 이는 원본 소스 코드에 접근할 필요 없이 애플리케이션을 보호하는 데 사용될 수 있습니다. 현재는 x64 PE(이식 가능한 실행 파일) 파일만 지원되지만, 향후 다른 바이너리 형식(예: ELF)에 대한 지원을 추가할 계획이 있습니다.

현재 잘 알려진 모든 bin2bin 난독화 도구는 바이너리 끝에 섹션을 추가하여 난독화된 코드나 데이터를 그 안에 배치합니다. 이는 기존 섹션의 내용을 변경하지 않고 바이너리의 원래 레이아웃을 유지하기 위함입니다. 이 방법은 대부분의 RVA(상대 주소)가 유효하게 유지되므로 관리하기가 훨씬 쉽습니다.

이 프로젝트는 bin2bin에 독특한 접근 방식을 취하는데, 난독화된 코드나 데이터는 바이너리의 원래 섹션 내에 삽입됩니다. 이를 위해서는 애플리케이션의 모든 RVA를 추적해야 합니다. 이 접근 방식의 장점은 다음과 같습니다.

  • 추가 실행 가능 섹션이 바이너리에 추가되지 않으므로 악성 코드 분석에 덜 의심됩니다. 참고: 이는 순전히 교육 및 연구 목적으로 테스트되었습니다.
  • 출력 실행 가능 바이너리의 크기가 줄어듭니다. 섹션 크기를 조정할 수 있으므로 원래 난독화되지 않은 코드를 바이너리에서 지울 수 있기 때문입니다.
  • 리버스 엔지니어가 섹션만으로 난독화된 코드와 난독화되지 않은 코드를 분리할 수 없으므로 분석이 더 어려워집니다. 각 루틴이 난독화되었는지 확인해야 합니다.

이 문서는 실행 가능한 바이너리의 재작성과 구현된 난독화 기술을 모두 설명합니다. 다음 난독화 기술이 구현되었습니다.

  • 가상 머신.
  • 불투명 조건.
  • 제어 흐름 평탄화.
  • 선형 치환.
  • 혼합 부울 산술.

또한 이 프로젝트는 예외 지원(C++ 예외 및 SEH)을 제공하며 예외 처리가 있는 함수를 난독화할 수 있습니다.

바이너리 내 코드를 역어셈블하고 발견하는 데 도움을 주기 위해 심볼 파일(PDB 및 MAP 모두)을 선택적으로 허용합니다. 심볼 파일을 제공하는 것은 필수가 아니지만 복잡한 바이너리에서 역어셈블리에 도움이 됩니다. 예외 지원 및 제어 흐름 평탄화와 같은 일부 기능은 심볼 파일이 제공되어야 합니다.

2. 바이너리 재작성기

바이너리 재작성기는 실행 가능한 바이너리를 가져와 내부의 코드나 데이터를 변경하여 변경 사항이 적용된 출력 바이너리를 생성합니다.

2.1. 상대 주소 추적

난독화된 코드가 바이너리의 원래 섹션에 직접 삽입되므로 프로그램의 상대 주소를 추적하여 이에 대한 모든 참조를 조정할 수 있도록 해야 합니다. 이는 코드와 데이터가 삽입된 후에도 참조가 동일한 위치를 가리키도록 하기 위함입니다. 그렇지 않으면 데이터나 코드가 잘못된 위치에서 액세스되어 출력 바이너리의 동작이 변경되고 심각한 불안정이 발생할 수 있습니다.

상대 주소에 대한 참조가 발견될 때마다(예: rip 상대 피연산자를 포함하는 명령어 또는 PE 데이터 디렉터리) 추적 목록에 추가되어 재작성 마지막에 업데이트됩니다. 참조가 발생하는 RVA(참조를 업데이트할 위치를 알기 위해)와 참조되는 RVA(참조를 어떤 RVA로 업데이트할지 알기 위해)가 추적됩니다.

역어셈블러가 rip 상대 명령어를 찾을 때마다 난독화 마지막에 업데이트할 참조 목록에 추가합니다. 이렇게 하면 해당 명령어들이 모두 원래 가리키던 위치를 계속 가리키게 됩니다. 점프 테이블과 같은 다른 상대 명령어 사례도 업데이트할 참조로 추가됩니다.

바이너리에 바이트가 삽입되거나 제거될 때마다 추적된 모든 RVA를 조정해야 합니다. 예를 들어, 다음은 바이트 삽입 핸들러입니다:```cpp
void binwrite::binary_t::insert(const rva_t rva, const std::span data, const bool inclusive)
{
buffer_.insert_range(buffer_.begin() + rva.value(), data);

root@kitploit:~
update_rvas(rva, static_cast<rva_t::size_type>(data.size()), inclusive);  

}

root@kitploit:~
`update_rvas`는 바이너리에서 발생한 변경 사항을 반영하기 위해 추적된 모든 RVA가 업데이트되는 곳입니다. 다음은 이 과정의 다이어그램입니다:

![](https://assets.kitploit.com/production/public/readmes/8091/12aaec3746c267c932ab3804b13df50c08a463c403417bf6ba4a52f39d12f48a.png)

그림 1. 상대 주소 추적.

삽입된 데이터(파란색)는 현재 데이터(회색)를 이동시킵니다. 명령어가 참조하는 RVA(주황색)는 삽입된 데이터(파란색)를 고려하여 동일한 메모리를 가리키도록 업데이트됩니다.

## 2.2. 디스어셈블리

잠재적인 코드 항목(내보내기, 진입점, 코드 섹션을 가리키는 재배치 등)은 모두 디스어셈블리 큐에 추가됩니다. 기호 파일이 있는 경우, 기호 파일이 설명하는 모든 함수도 디스어셈블리 큐에 추가됩니다. 큐의 각 항목은 개별 기본 블록으로 처리됩니다.

기본 블록은 분기가 없는 명령어 그룹입니다. 즉, 제어 흐름 명령어(예: 점프, 리턴, 인터럽트)에서 종료됩니다. 기본 블록은 호출에서 종료되지 않습니다. 대부분의 경우 호출된 함수가 반환될 것으로 예상되기 때문입니다. 일부 함수는 반환되지 않으며(예: _CxxThrowException), 앞으로 'noreturn' 호출이라고 부릅니다.

디스어셈블리 큐의 기본 블록이 처리될 때, 각 명령어는 위에서부터 다음 중 하나가 발생할 때까지 디스어셈블됩니다:

- 이미 분석된 다른 기본 블록에 도달하여 중복이 발생합니다. "기본 블록 분할" 참조.
- 종결 명령어가 발견됨(점프, 리턴, 인터럽트).
- 명령어 디스어셈블 실패.
- 코드 패딩이 발견됨.

다음은 디스어셈블리 및 디스어셈블리 큐 진입의 다이어그램입니다(다이어그램에서 코드 패딩 검사는 생략됨). 이 과정은 디스어셈블리 큐가 비워질 때까지 반복됩니다.

![](https://assets.kitploit.com/production/public/readmes/8091/7fe76b27e9f1f6ef620bc3aca182b85f9075c1dd31441e565099c05cfa396098.png)

그림 2. 디스어셈블리 처리.

### 2.2.1 기본 블록 분할

두 기본 블록이 중복되면 그중 하나를 분할해야 합니다. 이렇게 하면 두 블록이 동일한 명령어를 기술하는 것을 방지합니다. 예:```asm  
wcslen proc  
    or      rax, 0FFFFFFFFFFFFFFFFh  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  
    retn  
wcslen endp  

이것은 와이드 문자열의 길이를 구하는 wcslen의 구현입니다. 첫 번째 명령어 'or rax, FFFFFFFFFFFFFFFF'가 기본 블록의 시작으로 디스어셈블되면, 'retn'까지 디스어셈블을 계속합니다.

'jnz loc_140001078'는 루프를 형성하기 위해 위로 점프합니다. 이는 참조로 추가되며, jnz의 대상과 fallthrough 분기(다음 명령어)가 디스어셈블 큐에 추가됩니다. 명령어가 이미 분석된 블록의 중간으로 점프하므로, 새 블록을 형성하고 다시 'retn'까지 디스어셈블하는 것은 중복된 표현을 초래하기 때문에 불가능합니다.

'jnz'(조건부 점프)는 또한 fallthrough 분기를 따라 새로운 기본 블록을 생성합니다. 이제 다음과 같은 4개의 기본 블록이 있습니다:

Block A:```asm
or rax, 0FFFFFFFFFFFFFFFFh
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078

root@kitploit:~
블록 B:```asm  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  

블록 C:

root@kitploit:~
from src.modules.blockchain import Block
from src.modules.transaction import Transaction

# Example usage: Create a new block with some transactions
transactions = [
    Transaction("address1", "address2", 10.0),
    Transaction("address2", "address3", 5.0)
]
genesis_block = Block(0, "0" * 64, transactions)
print(genesis_block.hash)
``````asm  
    retn  

Block D:```asm
retn

root@kitploit:~
이는 기본 블록의 잘못된 표현입니다. 동일한 명령어가 4개의 블록에 걸쳐 중복되기 때문입니다. 이는 현재 디스어셈블리와 겹치는 모든 기본 블록을 분할함으로써 수정할 수 있습니다. 더 이상 중복된 명령어가 없을 것입니다. 각 중복 지점에서 명령어의 중복 표현을 생성하는 대신, 기존 명령어가 새로운 기본 블록으로 전송되기 때문입니다. 분할을 사용한 올바른 표현은 다음과 같습니다:

Block A:```asm  
    or      rax, 0FFFFFFFFFFFFFFFFh  

블록 B:```asm
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078

root@kitploit:~
블록 C:```asm  
    retn  

2.2.2. 간접 제어 흐름

2.2.2.1. 점프 테이블

점프 테이블은 switch 문의 핸들러 주소를 저장하는 데 사용됩니다. switch 문의 각 case에 대해 많은 if 문/조건부 점프를 사용하는 대신, case 문 핸들러의 주소 테이블을 보관합니다. 다음은 예시입니다 (LLVM/CLANG 바이너리):```cpp
std::int32_t sub_140004080(const std::int32_t a1)
{
std::int32_t result;

switch ( a1 )
{
case 0:
result = 9;
break;
case 1:
result = 4;
break;
case 2:
result = 3;
break;
case 3:
result = 1;
break;
default:
result = 0;
break;
}

return result;
}

root@kitploit:~
이 switch 문은 다음과 같은 어셈블리로 컴파일됩니다:```asm  
; ecx = a1  
cmp     ecx, 3 ; check if above bounds, must be default case  
ja      short def_140004097 ; goto default if a1 above 3  
mov     ecx, ecx  
mov     eax, ecx  
lea     rcx, jpt_140004097  
movsxd  rax, ds:(jpt_140004097 - 140004100h)[rcx+rax*4] ; select correct index of jump table for a1  
add     rax, rcx  
jmp     rax ; goto index specified - the case handler

jpt_140004097:  
dd offset loc_140004099 - 140004100h ; address of handler for first case  
dd offset loc_1400040D5 - 140004100h ; address of handler for second case  
dd offset loc_1400040BD - 140004100h ; address of handler for third case  
dd offset loc_1400040C9 - 140004100h ; address of handler for fourth case  

'a1' 값은 case 문의 최대값과 비교되며, 그보다 크면 바로 기본 핸들러로 이동합니다. a1이 case 범위 내에 있으면 점프 테이블의 해당 항목에 접근하여 계산된 핸들러 주소로 점프합니다.

점프 테이블 항목과 점프 테이블에 대한 참조는 추적되어 그대로 유지됩니다.

2.2.2.1.1. 제한된 점프 테이블

점프 테이블이 switch 문이 사용할 수 있는 모든 값 범위를 설명하지 않는 경우, 제한된 테이블을 사용하여 한계를 확인합니다. 이는 범위를 벗어나면 switch를 기본 문으로 리디렉션하기 위한 것입니다. case 문의 개수를 파싱하기 위해 비교 명령어를 확인하여 항목 수를 찾습니다. 예를 들어, 'cmp ecx, 3' 명령어는 점프 테이블의 항목 수가 3임을 나타냅니다.

2.2.2.1.2. 무제한 점프 테이블

점프 테이블이 case 문이 될 수 있는 모든 가능한 값을 채우는 경우(예: UINT8 유형의 경우 UINT8 최소값부터 UINT8 최대값까지), 한계 확인이 없는 무제한 테이블을 사용합니다. 이는 컴파일러가 점프 테이블이 모든 가능한 값을 포함한다는 것을 알기 때문입니다. 점프 테이블 항목 수를 암시하는 비교 명령어가 없으므로 항목을 브루트 포스해야 합니다. 테이블의 기준점에서 코드 섹션 내의 유효한 RVA를 점진적으로 확인하고 각 유효한 항목을 추적합니다. 이는 다른 데이터/명령어를 점프 테이블 항목으로 파싱할 수 있기 때문에 안전하지 않으므로, 가능한 경우 제한된 점프 테이블 확인이 사용됩니다.

2.2.2.1.3. 다양한 유형의 점프 테이블

바이너리 재작성기는 MSVC(다중 레벨 테이블 포함), LLVM/CLANG 및 GCC로 빌드된 바이너리의 점프 테이블을 지원합니다.

MSVC 점프 테이블은 일반(normal)과 다중 레벨(multi-level)의 두 가지 형태가 있습니다. MSVC의 일반 점프 테이블은 RVA의 배열입니다. 각 RVA는 case 문 핸들러를 가리킵니다.

MSVC의 다중 레벨 테이블은 핸들러를 공유하는 많은 수의 case 문이 있는 switch 문에 사용됩니다. 다중 레벨 버전은 2개의 테이블을 가지며, 하나는 핸들러 RVA 배열이고 다른 하나는 case 값을 첫 번째 테이블의 인덱스와 매칭합니다. 이는 첫 번째 테이블에서 RVA의 반복을 방지합니다. 각 case 값은 4바이트 RVA 대신 1바이트 인덱스만 설명하면 되기 때문입니다. 다음은 다중 레벨 점프 테이블의 예입니다:```asm
lea rdx, cs:140000000h
movsxd rax, edi ; load value
movzx eax, ds:(byte_1400023D8 - 140000000h)[rdx+rax] ; get handler index by value
mov ecx, ds:(jpt_14000209D - 140000000h)[rdx+rax*4] ; get RVA of handler by handler index
add rcx, rdx
jmp rcx

jpt_14000209D dd offset loc_14000209F - 140000000h
dd offset loc_1400020AB - 140000000h
dd offset loc_1400020B7 - 140000000h
dd offset loc_1400020CF - 140000000h
dd offset loc_1400020E7 - 140000000h
dd offset loc_1400020F3 - 140000000h
dd offset loc_1400020FF - 140000000h
dd offset loc_14000210B - 140000000h
dd offset loc_140002117 - 140000000h
dd offset loc_140002123 - 140000000h
dd offset loc_1400020C3 - 140000000h
dd offset loc_14000213B - 140000000h
dd offset loc_140002147 - 140000000h
dd offset loc_140002153 - 140000000h
dd offset loc_14000216B - 140000000h
dd offset loc_140002177 - 140000000h
dd offset loc_140002183 - 140000000h
dd offset loc_14000219B - 140000000h
dd offset loc_1400021A7 - 140000000h
dd offset loc_1400021B3 - 140000000h
dd offset loc_1400021BF - 140000000h
dd offset loc_1400021CB - 140000000h
dd offset loc_1400021D7 - 140000000h
dd offset loc_1400021E3 - 140000000h
dd offset loc_1400021EF - 140000000h
dd offset loc_1400021FB - 140000000h
dd offset loc_140002207 - 140000000h
dd offset loc_140002213 - 140000000h
dd offset loc_14000221F - 140000000h
dd offset loc_14000222B - 140000000h
dd offset loc_140002237 - 140000000h
dd offset loc_140002243 - 140000000h
dd offset loc_14000224F - 140000000h
dd offset loc_14000225B - 140000000h
dd offset loc_140002267 - 140000000h
dd offset loc_140002273 - 140000000h
dd offset loc_14000227F - 140000000h
dd offset loc_14000228B - 140000000h
dd offset loc_140002294 - 140000000h
; ... more handler addresses

byte_1400023D8:
db 0, 2Bh, 1, 2, 2Bh, 3, 2Bh, 4, 2Bh, 5, 6, 7, 8, 9, 0Ah, 0Bh, 0Ch, 0Dh
db 2Bh, 0Eh, 0Fh, 10h, 2Bh, 11h, 12h, 13h, 14h, 15h, 2Bh, 16h, 2Bh, 17h
db 18h, 19h, 1Ah, 1Bh, 1Ch, 2Bh, 1Dh, 1Eh, 1Fh, 20h, 21h, 22h, 2Bh, 23h
db 24h, 25h, 26h, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh
; ... more indexes to handler table

root@kitploit:~
LLVM은 테이블의 기준 주소에 대한 오프셋을 포함하는 테이블을 사용합니다. 테이블 기준 주소에 항목이 설명하는 오프셋을 더하면 핸들러 주소를 계산할 수 있습니다.

GCC의 점프 테이블은 DIR64 재배치 배열입니다. 각 재배치는 case 문의 주소를 가리킵니다. 재배치는 이미 추적되므로 이러한 점프 테이블 항목은 이미 수정되었습니다. 런타임 시, 이러한 재배치 항목은 기준 주소만큼 오프셋되므로 각 항목을 역참조하여 case 문의 주소를 얻을 수 있습니다.

#### 2.2.2.2. 경계 사례

지원해야 하는 다른 형태의 간접 제어 흐름이 있습니다. 예를 들어, FuncInfo3 C++ 예외를 사용하는 CLANG 바이너리의 경우, 계속 주소(continuation address)가 레지스터 rax에 로드된 후 호출자에게 반환됩니다. 호출자는 계속 주소로 점프합니다.```asm  
lea rax, [rip+X]  
retn  

lea가 가리키는 주소가 코드 섹션에 있더라도 실제로 코드라는 보장은 없습니다. 점프 테이블과 문자열은 캐시 지역성을 위해 바이너리의 코드 섹션에 배치될 수 있습니다. 올바른 코드 경로를 발견하고 디스어셈블하여 그 안의 RVA 참조를 추적할 수 있어야 하며(또한 이를 난독화할 수 있도록) 이러한 경우를 식별하는 것이 중요합니다. 이것들은 '위험한' 참조입니다.

점프 테이블이 디스어셈블리 큐에 잘못 추가되는 것을 방지하기 위해 디스어셈블러는 위험한 참조를 큐에 추가하기 전에 먼저 점프 테이블인지 확인합니다.

lea를 통해 문자열이 잘못 디스어셈블리 큐에 추가되는 것을 방지하기 위해 디스어셈블러는 추가적인 정합성 검사를 적용하여 기본 블록으로 디스어셈블을 시도합니다(예: 위험한 참조 중 하나인 경우 종결자 명령어가 있어야 함). 위험한 참조 블록에서 이러한 정합성 검사 중 하나라도 실패하면 해당 블록 전체가 무시되고 데이터로 간주됩니다.

심볼 파일이 제공되면 데이터 심볼은 위험한 참조로 추가되지 않습니다.

2.2.3. Functions

일부 난독화 패스는 어떤 기본 블록이 어떤 함수에 속하는지 알아야 합니다. 이러한 이유로 모든 기본 블록은 해당 함수에 할당됩니다. 심볼 파일을 파싱하여 모든 함수 주소를 찾고 목록에 배치합니다.

각 함수에 대해 다음 단계가 수행됩니다:

  • 함수의 진입 기본 블록을 가져옵니다(함수 시작 주소의 기본 블록).
  • 이 진입 블록을 함수에 할당합니다.
  • 기본 블록의 모든 종료 지점을 찾습니다(폴스루 분기, 대상 분기).
  • 각 종료 기본 블록에 대해, 다른 함수의 진입 블록이 아닌 경우 함수에 할당합니다(기본 블록 RVA != 모든 함수 RVA). 이 마지막 두 단계는 발견된 모든 기본 블록에 대해 반복됩니다.
  • 발견된 블록의 모든 점프 테이블을 파싱하고 그 대상 기본 블록을 함수에 할당합니다.

2.2.4. 'Noreturn' call handling

Noreturn 함수는 반환되지 않습니다. noreturn 호출이 발생하면 호출이 기본 블록을 종료하지 않기 때문에 기본 블록은 계속 디스어셈블됩니다. 바이너리는 호출 이후를 실행할 것으로 예상되지 않으므로 컴파일러가 기본 블록을 종료하는 적절한 코드를 삽입하지 않았습니다. 앞서 언급한 디스어셈블 검사가 이러한 경우를 포착하여 기본 블록을 종료할 것입니다.```asm
sub_140006310 proc
sub rsp, 38h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+38h+var_8], rax
mov [rsp+38h+pExceptionObject], 2Ah
lea rdx, __TI1H
lea rcx, [rsp+38h+pExceptionObject]
call _CxxThrowException
db 0CCh
sub_140006310 endp
algn_14000633D:
align 20h

root@kitploit:~
예를 들어, 이 noreturn _CxxThrowException 호출에서 컴파일러는 INT3 명령어 형태의 패딩을 삽입했으며, 이는 패딩이자 종료 명령어로 모두 포착됩니다. noreturn 호출 후에 UD2 명령어가 삽입되는 등 다른 경우들도 처리됩니다.

이는 탐지하는 완벽한 방법은 아니며, [CodeDefender 팀도 논의한 바](https://codedefender.io/blog/2024/07/02/) 있지만, 지나치게 진행된 디스어셈블리를 바로잡기 위한 대책이 마련되어 있습니다. 점프 테이블 항목이 기본 블록 내에서 발견되면, 기본 블록이 분할되어 점프 테이블에 우선순위가 부여됩니다. 이렇게 하면 noreturn 호출 후에 점프 테이블 항목이 명령어로 디스어셈블되는 것을 방지할 수 있습니다. 다음 명령어가 유효한 코드라면, 다음 주소의 기본 블록이 디스어셈블/처리될 때 기본 블록이 어차피 분할됩니다.

## 2.3. 예외 지원

### 2.3.1. 언와인드 지원

난독화 도구는 난독화 패스에서 스택 할당을 사용합니다. 이를 통해 사용하는 레지스터의 값을 저장(예: push rax)하고 난독화 패스가 완료된 후 복원하여 레지스터가 손상되지 않도록 합니다. 프레임 포인터는 스택의 특정 위치를 가리키는 레지스터입니다.

모든 스택 할당은 함수의 언와인드 코드로 설명되어야 합니다(프레임 포인터가 없는 경우). 이를 통해 OS 예외 핸들러가 스택을 역추적하여 반환 주소로 이동하고 애플리케이션의 예외 핸들러를 검색할 수 있습니다. 이러한 스택 할당은 함수 시작 지점으로부터의 거리 제한 때문에 프롤로그(함수 시작 부분)에 있어야 합니다. 프롤로그 외부에서 스택 할당이 이루어지면 언와인드 코드가 이를 설명할 수 없으며 OS가 언와인드할 수 없게 되어 예외 지원이 중단됩니다.

프레임 포인터가 사용되는 경우, OS는 rsp 레지스터에서 스택을 역추적할 필요 없이 프레임 포인터에서 역추적할 수 있습니다. 이는 난독화 도구가 프롤로그 외부에서 스택 할당을 언와인드 코드로 설명하지 않고도 만들 수 있음을 의미합니다.

#### 2.3.1.1 구현

리라이터는 프레임 포인터 레지스터가 없는 모든 런타임 함수에 이를 삽입합니다. 이렇게 하면 난독화 도구가 프롤로그 외부에서 스택 할당을 한 후에도 OS가 스택을 언와인드할 수 있습니다.

다음은 함수의 프롤로그와 에필로그에 적용된 변경 사항의 예시입니다:

원본 함수 프롤로그:```asm  
DriverEntry proc  
    sub     rsp, 38h  

수정된 함수 프롤로그:```asm
DriverEntry proc
push rbp
push rbp
sub rsp, 38h
lea rbp, [rsp]
lea rbp, [rsp]

root@kitploit:~
원본 함수 에필로그:```asm  
    add     rsp, 38h  
    ren  
DriverEntry endp  

수정된 함수 에필로그:```asm
add rsp, 38h
pop rbp
pop rbp
retn DriverEntry endp

root@kitploit:~
![](https://assets.kitploit.com/production/public/readmes/8091/5974475161fee46781ae597219071d6b2580577065820981370676998617fc1e.png)

그림 3. 프레임 포인터 삽입 전 스택 레이아웃.

비휘발성 레지스터 rbp는 리라이터(rewriter)에 의해 프레임 포인터로 사용됩니다. 비휘발성 레지스터는 보존되어야 하므로, rbp의 값은 프롤로그에서 푸시(push)되고 언와인드 코드로 설명됩니다(운영체제 언와인더가 rbp의 원래 값을 복원할 수 있도록). rbp 레지스터는 스택을 16바이트로 재정렬하기 위해 2번 푸시되는데, 두 번째 푸시는 순전히 재정렬 목적입니다.

프롤로그에서 rbp 값이 두 번 푸시되었으므로, 함수 끝에서 스택 포인터를 원래 값으로 되돌리기 위해 반드시 팝(pop)되어야 합니다. 이는 반환 주소가 반환 명령어를 위해 [rsp]에 있도록 하기 위함입니다.

이러한 푸시 명령어에 대해 각각의 언와인드 코드가 삽입되어 운영체제가 프롤로그에 더 많은 스택 할당이 있음을 알 수 있습니다(프레임 포인터를 언와인드할 대상). 이것이 에필로그에 2개의 팝이 있는 이유입니다.

해당 종료 기본 블록/에필로그를 찾기 위해, 함수의 모든 기본 블록을 검색하여 'ret' 또는 현재 함수 외부로 이동하는 점프를 찾습니다. 간접 점프(예: jmp rcx)도 점프 테이블을 제외하고는 현재 함수 외부로 이동하는 것으로 간주합니다. 모든 종료 기본 블록을 그룹화한 후, 2개의 팝 명령어를 삽입하여 함수를 떠날 때 프롤로그에서 푸시의 효과가 되돌려지도록 합니다.

프롤로그 끝에 있는 'lea rbp, [rsp]'는 운영체제에 스택의 구체적인 위치를 알려주어 rsp 대신 그 위치에서 언와인드할 수 있도록 합니다. 이 프레임 포인터 설정 명령어에 대한 각각의 언와인드 정보와 코드가 삽입됩니다.

![](https://assets.kitploit.com/production/public/readmes/8091/206fc3b3bf2154dd4519f51575034b9bbc539068c6a375b5144bf43549cbaeae.png)

그림 4. 프레임 포인터 삽입 후 잘못된 스택 레이아웃.

또 다른 고려 사항은 스택 포인터 앞에 위치한 스택 인수입니다. 함수 내에서 로컬 할당 이후(반환 주소 슬롯과 같거나 그 이후)에 스택에 접근하는 경우, 해당 참조를 업데이트해야 합니다. 푸시가 스택을 16바이트 조정하기 때문에, 그 이후의 모든 참조된 데이터도 16바이트만큼 이동되어야 합니다. 위 다이어그램은 스택 참조가 정렬되지 않고 수정이 필요한 모습을 보여줍니다.

예를 들어 'mov rax, [rsp+0x90]'은 푸시가 실행된 후에도 동일한 스택 슬롯에 접근할 수 있도록 0x10(10진수: 16)이 더해집니다. 수정된 명령어는 다음과 같습니다: 'mov rax, [rsp+0xA0]'.

명령어가 로컬 스택 할당을 넘어서 접근하는 경우, 스택에서 수행된 푸시를 건너뛰기 위해 16만큼 조정됩니다. 때로는 스택 포인터가 다른 레지스터로 이동되고 다른 레지스터를 통해 접근되는데, 이 경우 해당 레지스터를 모니터링하고 스택 포인터와 동일한 방식으로 조정합니다.

또 다른 경우는 예외에 대한 catch 핸들러입니다. rdx에 EstablisherFrame의 주소(예외 발생 시 프레임 포인터 레지스터의 값과 동일)를 전달받습니다. catch 핸들러는 rdx를 통해 예외 함수의 스택에 접근하므로, rdx도 추적 및 조정되어야 합니다.

아래는 스택 참조 조정으로 수정된 후의 스택 레이아웃 다이어그램입니다.

![](https://assets.kitploit.com/production/public/readmes/8091/bc9c8db3ac3b9fdc1c651e8def9b9824570e442c04fd8807d4d715b1519c85ba.png)

그림 5. 프레임 포인터 삽입 후 수정된 스택 레이아웃.

예외 지원을 위해서는 난독화기에 심볼 파일을 제공해야 합니다. 바이너리의 심볼에 대해 가능한 한 많은 정보가 필요하기 때문입니다.

### 2.3.2. 예외 정보 파싱

리라이터는 바이너리의 언와인드 정보를 파싱하여 예외 핸들러 정보를 찾습니다. 발견된 모든 RVA도 추적됩니다. 지원되는 예외 핸들러 정보 유형은 다음과 같습니다:

- SEH/C_SCOPE_TABLE(C 스타일 예외).
- FuncInfo3 및 FuncInfo4 유형의 C++ 예외.

#### 2.3.2.1. SEH/C_SCOPE_TABLE

이 형식은 다음 테이블 항목들의 배열입니다:```cpp  
struct c_scope_table_entry_t  
{  
	std::uint32_t begin_rva; // where exception-throwing range begins  
	std::uint32_t end_rva; // where exception-throwing range ends  
	std::uint32_t handler_rva; // the handler type/rva (normally 1)  
	std::uint32_t target_rva; // the catch handler rva  
};

struct c_scope_table_t  
{  
	std::uint32_t entry_count;  
	c_scope_table_entry_t table[1];  
};  

The begin/end RVAs describe what range of code can throw an exception. The target RVA describes the catch handler which processes the exception when it happens.

2.3.2.2. FuncInfo3 and FuncInfo4

Used for C++ exceptions, the main difference between FH3 and FH4 is that FH4 uses a compressed format to try to save memory. They share the following descriptors:

  • Unwind map - list of C++ objects that need to be destroyed as well as the offset of the object from the frame.
  • Try block map - list of catch handlers and the types that each can catch (e.g. std::runtime_error).
  • IP2State map - describes the state of objects depending on what the current instruction pointer is/ the offset in the function.

FH3 specifics:

  • The continuation address in the try block map is kept in code and returned by the catch handler in rax (e.g. lea rax, continuation_address).

FH4 specifics:

  • Stores map information in a compressed integer format to save space.
  • The try block map contains the continuation address encoded in the FH4 info structure.

Only MSVC and CLANG/LLVM are supported for C++ exceptions. GCC is not supported for C++ exceptions as it uses a different format than FH3/FH4.

2.3.3. RTTI and ThrowInfo parsing

Runtime type information (RTTI) and throw info are structures used to introspect C++ types at runtime, including for throwing exceptions. These structures contain many RVAs and hence must be tracked for stability purposes.

2.3.3.1. RTTI

The 'try block' map in the C++ exception descriptors have type information to know if they catch the thrown type. This type information is called RTTI, and also describes other things about the type, such as:

  • Virtual function tables.
  • Type name.
  • Inheriting classes.

For classes without virtual functions, all that is generated is a type descriptor:```cpp
struct type_descriptor_t
{
std::uint64_t vftable_address; // this is a DIR64 relocation
std::uint64_t unk;
char name[1];
};

root@kitploit:~
이는 데이터 섹션에서 멤버 필드 'vftable_address'에 있는 DIR64 재배치를 스캔하여 발견되며, 이것이 실제 가상 함수 테이블인지 확인됩니다.

가상 함수가 있는 클래스의 경우, 완전 객체 로케이터와 클래스 계층 구조 설명자가 생성됩니다. 클래스 계층 구조 설명자는 기본 클래스들의 배열을 포함합니다.```cpp  
struct complete_object_locator_t  
{  
	std::uint32_t signature;  
	std::uint32_t offset;  
	std::uint32_t constructor_offset;  
	std::uint32_t type_rva;  
	std::uint32_t hierarchy_rva;  
	std::uint32_t self_rva;  
};

struct hierarchy_descriptor_t  
{  
	std::uint32_t signature;  
	std::uint32_t attributes;  
	std::uint32_t base_class_count;  
	std::uint32_t base_class_list_rva;  
};

struct base_class_array_t  
{  
	std::uint32_t class_rvas[1];  
};

struct base_class_descriptor_t  
{  
	std::uint32_t type_rva;  
	std::uint32_t element_count;  
	std::uint32_t member_displacement;  
	std::uint32_t unk;  
	std::uint32_t unk1;  
	std::uint32_t attributes;  
	std::uint32_t hierarchy_rva;  
};  

이를 찾기 위해 데이터 섹션에서 DIR64 재배치를 스캔하여 완전한 객체 로케이터를 가리키는 항목을 찾습니다. DIR64 재배치의 대상에 대해 검사가 수행되어 완전한 객체 로케이터를 가리키는지 확인합니다(예: self_rva가 클래스 베이스의 RVA를 가리키는지, 타입 디스크립터와 계층 디스크립터가 올바르게 파싱되는지 등).

2.3.3.2. ThrowInfo

ThrowInfo는 예외 객체가 처리된 후 이를 소멸시키는 방법과 던져진 타입을 설명하는 데 사용됩니다. 던져진 예외 타입의 상속된 클래스들은 catchable type array(RTTI 참조 포함)에 기술되어 예외 핸들러가 catch 문과 일치시킬 수 있도록 합니다.

ThrowInfo는 데이터 섹션에서 catchable type array의 내용을 이전에 발견된 RTTI 정보와 대조하여 스캔됩니다. catchable type의 모든 RVA와 ThrowInfo가 추적 목록에 추가됩니다.```cpp
struct throw_info_t
{
std::uint32_t attributes;
std::uint32_t pmfn_unwind; // address of exception object destructor
std::uint32_t forward_compat;
std::uint32_t catchable_type_array;
};

struct catchable_type_array_t
{
std::uint32_t count;
std::uint32_t type_rvas[1];
};

struct catchable_type_t
{
std::uint32_t attributes;
std::uint32_t rva_type;
std::uint32_t mdisp;
std::uint32_t pdisp;
std::uint32_t vdisp;
std::uint32_t size_of_thrown_object;
std::uint32_t optional_copy_constructor_rva;
};

root@kitploit:~
# 3. 난독화

## 3.1. 가상 머신

이 기술은 x86-64 아키텍처의 명령어를 가상 CPU 아키텍처의 명령어로 변환합니다. 이는 리버스 엔지니어가 원래 명령어가 수행하는 작업을 분석하기 전에 먼저 가상 CPU 아키텍처를 이해해야 하므로 분석하기가 훨씬 어렵습니다.

이 구현은 다양한 명령어를 난독화할 수 있도록 가상 머신 핸들러를 생성하는 일반적인 접근 방식을 사용하므로 각 x86-64 명령어에 대한 핸들러를 하드코딩할 필요가 없습니다.

![](https://assets.kitploit.com/production/public/readmes/8091/b21a90f7d3e6c1b79b5bf81f447d00bbf7bc2623987832b1dfdc306936cdd17d.png)
![](https://assets.kitploit.com/production/public/readmes/8091/b3cf262b57cbc3bd3c53fc312e2ad2feb94f68ed77d5a9111879fdf9f52676eb.png)  
![](https://assets.kitploit.com/production/public/readmes/8091/2d5615dbae58ca45fbdaa91f79534d1690a190e6104b87dfc9409ce8a7c91a85.png)  
![](https://assets.kitploit.com/production/public/readmes/8091/996397440d85c087eb220250937ada234e83d7968618a4d69534513e27d675c2.png) 

그림 6. 가상 머신 아키텍처.

가상화되는 원래 명령어 시퀀스는 가상 머신의 진입 블록에 대한 호출로 대체됩니다.

가상 머신에 진입할 때, 모든 범용 레지스터(rsp 제외)는 스택에 푸시됩니다. rflags 레지스터도 스택에 푸시됩니다. 이러한 스택 슬롯은 가상 레지스터로 사용되며, 각각 원래 레지스터에 해당합니다(따라서 rax의 슬롯은 rax 대신 사용됩니다). 이러한 가상 레지스터의 스택 내 순서는 무작위화되므로 각 가상 머신 핸들러의 레지스터 레이아웃이 변경됩니다.

범용 레지스터를 가상 스택 레이아웃에 배치하는 push 명령어도 무작위화되며, 'sub rsp, 8; mov [rsp] reg' 또는 'push reg'가 될 수 있습니다. 이는 가상 머신 진입에 대한 시그니처를 구축하기 어렵게 하기 위한 것입니다.

하드웨어 레지스터는 x86-64 아키텍처의 범용 레지스터입니다. 이제 하드웨어 레지스터가 가상 CPU 상태로 스택에 저장되었으므로, 이를 자유롭게 덮어쓸 수 있습니다. 가상 머신 상태는 가상 머신 스텁이 사용할 수 있는 현재 사용 가능한 하드웨어 레지스터 목록을 추적합니다. 스텁이 완료되면 해당 하드웨어 레지스터는 목록에 다시 추가되어 재사용될 수 있습니다.

이제 애플리케이션이 가상 머신에 진입했으며, 대상 명령어에 대한 핸들러로 실행을 넘겨야 할 차례입니다. 대상 명령어는 이 CPU 아키텍처로 가상화되는 x86-64 명령어입니다.

먼저, 대상 명령어의 피연산자를 스택에 로드해야 합니다. 피연산자의 값은 사용 가능한 하드웨어 레지스터에 로드됩니다. 그런 다음 피연산자의 값은 난독화되어 스택에 푸시됩니다. 이는 명령어 핸들러에 대한 선행 블록(이전 핸들러 또는 첫 번째 핸들러인 경우 가상 머신 진입 블록)에서 수행됩니다.

피연산자 값에 적용되는 난독화는 다음과 같습니다:

- 피연산자 값에 대한 무작위 16비트 숫자 xor 연산.
- 피연산자 값에 대한 1의 보수 부정 연산.

즉시 피연산자의 경우, 값이 알려져 있으므로 이 난독화는 난독화 시간에 수행될 수 있으므로 런타임에 계산되지 않으므로 역공학이 더 어렵습니다.

특정 레지스터가 필요한 숨겨진 피연산자(예: 'rep movsb'의 경우 rsi 및 rdi)는 무작위 하드웨어 레지스터 대신 해당 특정 레지스터에 로드됩니다.

명령어 핸들러 블록에서 피연산자는 스택에서 팝되고 난독화가 해제됩니다. 피연산자의 원래 값을 얻기 위해 역연산이 실행됩니다.

원래 명령어가 rflags 레지스터에서 읽는 경우, 명령어를 실행하기 전에 rflags가 스택 컨텍스트에서 로드됩니다.

원래 명령어가 rflags 레지스터에 쓰는 경우, 명령어를 실행하기 전에 rflags가 스택 컨텍스트에서 로드됩니다. 원래 명령어가 실행된 후, 업데이트된 rflags는 스택 컨텍스트에 다시 기록됩니다.

이를 통해 가상화된 명령어가 원래 명령어와 정확히 동일한 플래그 동작을 가지도록 보장합니다.

명령어 핸들러 블록 내에서 원래 x86-64 명령어는 난독화가 해제된 피연산자를 사용하도록 인코딩됩니다. 원래 명령어가 실행되면 결과 피연산자가 난독화되어 스택에 푸시됩니다.

이것이 마지막 명령어 핸들러인 경우, 다음 기본 블록은 가상 머신 종료 블록이 됩니다. 그렇지 않은 경우 다음 블록은 다음 핸들러 블록이 됩니다.

다음 기본 블록은 난독화된 결과 피연산자를 스택에서 팝하고 동일한 난독화 해제 과정을 적용합니다. 결과 값은 원래 대상에 기록됩니다. 이는 스택 컨텍스트의 가상 레지스터이거나 원래 명령어에 설명된 메모리의 특정 위치일 수 있습니다.

이 프로세스는 기본 블록의 명령어를 가상화할 수 없을 때까지(예: rsp 레지스터를 사용하는 명령어) 반복됩니다.

그런 다음 가상 머신 컨텍스트를 언로드하여 가상화되지 않은 코드로 전환해야 합니다. 모든 가상 레지스터는 스택에서 해당 범용 레지스터로 팝됩니다. 수정된 rflags 레지스터도 스택에서 팝하여 복원됩니다. 이제 가상 머신 종료 블록이 호출자로 반환됩니다.

### 3.3.1 언와인드 지원

가상화된 명령어가 예외를 발생시키는 경우, OS는 호출자에서 적절한 예외 핸들러를 찾기 위해 가상 머신 컨텍스트 밖으로 언와인드할 수 있어야 합니다.

이를 가능하게 하려면 바이너리에 언와인드 정보를 추가하여 가상 머신 함수의 스택 레이아웃을 OS가 알 수 있도록 해야 합니다.

프레임 포인터가 rbp에 로드되는데, 이는 가상 머신 핸들러가 프롤로그 외부에서 스택 할당을 사용하기 때문입니다. 이는 가상 머신 핸들러가 rbp를 '사용 가능한' 하드웨어 레지스터로 사용하는 것을 허용하지 않음을 의미합니다.

사용된 가상 머신의 모든 하드웨어 레지스터는 스택 컨텍스트에 푸시되므로 해당 푸시에 대한 언와인드 코드가 삽입됩니다.

그런 다음 가상 머신 함수에 대한 런타임 함수가 예외 디렉터리에 삽입됩니다. 이제 가상 머신은 언와인드 가능합니다.

## 3.2. 불투명 조건문 블록

이 기술은 잘못된 데이터 흐름을 가진 가짜 기본 블록으로 분기하는 분기를 생성하여 리버스 엔지니어를 혼란스럽게 합니다.

불투명 조건문은 true 또는 false로만 평가되는 문장입니다.

기본 블록은 복제되어 불투명 조건문 if 문으로 감싸집니다. 블록 중 하나는 데이터 흐름이 왜곡되어 유사하지만 올바르지 않게 됩니다.```cpp  
if (opaque_statement_always_true)  
{  
    … original basic block  
}  
else  
{  
    … incorrect but similar basic block  
}  

역공학자를 더욱 혼란시키기 위해, 모든 명령어 피연산자가 수집되어 무작위화됩니다. 각 명령어는 수집된 목록에서 무작위로 선택된 피연산자로 재컴파일됩니다. 이렇게 하면 복제된 블록의 동작이 원본과 다르게 됩니다.

블록들의 위치도 무작위로 뒤섞이므로, 메모리 상의 물리적 위치만으로는 어느 것이 올바른 분기인지 알 수 없습니다.

분기 선택에 필요한 조건도 무작위로 선택됩니다. 예를 들어 한 번 실행에서는 조건부 점프의 fallthrough 분기가 올바른 블록으로 이어집니다. 다른 실행에서는 조건부 점프의 대상 분기가 올바른 블록으로 이어집니다. 이로 인해 올바른 분기를 찾기가 더 어려워집니다.

불투명 조건식 표현 자체도 매우 중요합니다. 만약 평가하기 쉽다면 효과적이지 않을 것이기 때문입니다. 이러한 이유로, 페르마의 마지막 정리가 불투명 표현으로 선택되었습니다. 페르마의 마지막 정리는 "2보다 큰 정수 n 값에 대해 a^n + b^n = c^n을 만족하는 양의 정수 a, b, c는 존재하지 않는다"라고 말합니다(여기서 ^는 거듭제곱을 의미). 이 표현은 주어진 조건에서 항상 거짓임이 증명되었습니다. 세 개의 숫자 a, b, c가 선택되고, 3에서 7 사이에서 무작위로 선택된 지수로 거듭제곱됩니다. 이 표현은 새로 생성된 기본 블록에서 수행된 후, 조건부 분기가 올바른 블록으로 진행됩니다.

만약 매개변수 a, b, c, n이 알려져 있다면, 공격자는 표현식을 풀고 올바른 분기를 찾을 수 있습니다. 이를 방지하기 위해, 매개변수는 런타임에 결정되는 값들에서 선택됩니다: 스택 포인터(rsp)와 명령어 포인터(ASLR은 rip가 런타임에 재배치됨을 의미합니다). 예를 들어:```cpp
if ((rsp^n) + (rip^n) != (c^n))
{
… incorrect but similar basic block
}
else
{
… original basic block
}

root@kitploit:~
## 3.3. 제어 흐름 평탄화

제어 흐름은 프로그램이 취하는 실행 경로입니다(예: 'if 문'). 제어 흐름의 변경은 하나의 기본 블록이 다른 블록으로 점프할 때 발생합니다. 이러한 제어 흐름의 변경은 단일 디스패처 스텁으로 그룹화되고 이해하기 어렵도록 섞일 수 있습니다. 디스패처 스텁은 직접 수행되는 대신 다음 기본 블록으로 제어 흐름을 변경하는 역할을 담당합니다.

![](https://assets.kitploit.com/production/public/readmes/8091/6703f1d150f3bbc32338f8caf3eed71098a953b201000e35da94c3b7f3633a49.png)

그림 7. 제어 흐름 평탄화.

함수의 모든 기본 블록(프롤로그 제외)은 목록으로 그룹화되고, 모든 분기(조건부, 무조건부)가 수집됩니다. 각 기본 블록에는 고유한 ID/식별자가 부여됩니다. 디스패처 스텁은 모든 잠재적 분기를 가져와 모든 ID를 케이스로 하는 switch 문을 만듭니다. case 문은 대상 기본 블록으로 점프합니다. 원래 기본 블록/원래 제어 흐름으로의 모든 점프는 올바른 ID(대상 블록의 ID)를 가진 switch 문으로의 점프로 대체됩니다.

기본 블록의 물리적 레이아웃은 섞여서 메모리 위치가 원래 제어 흐름에 대한 힌트를 제공하지 않도록 합니다.

## 3.4. 선형 대체

이 기술은 명령어에 인코딩된 모든 숫자(메모리 피연산자 변위, 즉시 피연산자)를 가져와 런타임에 계산하여 실제 값을 숨깁니다.

난독화 시, 원래 숫자와 동일한 비트 폭의 임의 숫자 값이 생성됩니다. 이 값은 원래 숫자에 더해지고 사용되지 않는 레지스터에 즉시 피연산자로 로드됩니다.

런타임에, 임의 숫자가 레지스터에서 빼져서 원래 숫자가 됩니다.

R이 임의 숫자이고 N이 원래 숫자라면, 표현식은 효과적으로 ((R+N) - R)입니다.

메모리 피연산자를 다시 인코딩하기 위해, 레지스터 삽입이 기본 피연산자에 수행됩니다. 이미 기본 피연산자가 있는 경우, 그 값이 위에 추가됩니다.

대체되는 **스택** 메모리 피연산자의 경우, 푸시로 인한 스택 변위를 설명하기 위해 값에 변위가 추가됩니다. 푸시는 rflags 레지스터와 사용되지 않는 레지스터를 저장하는 데 사용됩니다.

다음은 'mov [rsp+24h], 0'에서 메모리 피연산자 대체의 예입니다:```asm  
push    r11  
pushfq ; save flags for now  
mov     r11, rsp  
add     r11, 1D4D9F71h  
sub     r11, 1D4D9F3Dh  
popfq ; restore flags, the original instruction will now execute and populate flags with the correct values  
mov     dword ptr [r11], 0 ; original instruction substituted (r11 == rsp+24h)  
pop     r11  

다음은 'add rbx, 8'에서 즉시 피연산자의 대체에 대한 또 다른 예입니다:```asm
push r10
pushfq
mov r10, 0FFFFFFFFE3F78112h
sub r10, 0FFFFFFFFE3F7810Ah
popfq
add rbx, r10
pop r10

root@kitploit:~
## 3.5. 혼합 부울 산술

이 기법은 일반적인 산술 표현식(예: x+y)을 가져와 동일한 결과를 생성하는 더 복잡한 표현식으로 변환합니다. 이는 표현식을 동등한 선형 항등식으로 대체함으로써 수행됩니다.

예를 들어, (x+y)는 ((x & y) + (x | y))와 같은 동등한 항등식을 가집니다. 난독화기가 (x+y)를 처리할 때, 리버스 엔지니어링하기 더 어려운 이러한 항등식 중 하나로 대체합니다.

다음 산술 명령어에 대한 항등식 목록이 있습니다: 'add', 'sub', 'and', 'or', 'xor'. 이는 각 명령어에 대해 더 복잡하고 무작위적인 대체가 선택되어 대체됨을 의미합니다. 목록에서 무작위로 항등식을 선택하면 각 난독화 출력이 다르게 됩니다.

디난독화를 더 어렵게 만들기 위해, 이는 재귀적으로 적용됩니다. 결과적으로 각 대체된 항등식이 여러 번 다시 대체되어 복잡성이 기하급수적으로 증가합니다. 아래는 이 기법을 2회 적용한 후 (x+y)의 변환 예시입니다.

![](https://assets.kitploit.com/production/public/readmes/8091/232db29a6146b7603612ab11656e106502901c0a1a5e3425b4126a8dbbd998b2.png) 

그림 8. 혼합 부울 산술.

고려해야 할 측면은 명령어의 플래그 계산 결과입니다. 원래 명령어는 플래그 레지스터에 특정 플래그 동작이 적용되었을 텐데, 표현식이 여러 다른 연산/명령어로 분할되면서 동일하지 않게 됩니다. 이를 해결하기 위해 난독화기는 올바른 플래그를 계산하고 적용하는 스텁을 삽입하여 플래그 동작을 에뮬레이션합니다.

'and', 'or', 'xor'의 경우 플래그 동작은 'test' 명령어와 동일합니다. 동일한 피연산자를 가진 test 명령어를 삽입하면 해당 3개의 대체된 명령어에 대해 플래그가 제대로 에뮬레이션됩니다.

'sub'의 경우 'cmp' 명령어가 동일한 플래그 동작을 가지며, 플래그 계산을 위해 동일한 피연산자로 삽입될 수 있습니다.

'add'의 경우 SF, ZF, PF는 'test'로 계산할 수 있지만, CF, AF, OF는 수동으로 계산해야 합니다. 그런 다음 add 명령어에 대해 CF, AF, OF를 수동으로 계산하는 스텁이 삽입됩니다.

이러한 플래그 에뮬레이션 스텁은 원래 명령어가 무엇인지 암시하므로, 꼭 필요한 경우에만 수행됩니다. 전체 기본 블록에서 플래그를 읽고 쓰는 명령어를 스캔합니다. 플래그 에뮬레이션을 추가하는 조건은 다음과 같습니다.

- MBA 명령어는 플래그 읽기 명령어 이전의 마지막 플래그 쓰기 명령어입니다.  
- MBA 명령어는 기본 블록 내의 마지막 플래그 쓰기 명령어입니다.

이를 통해 기본 블록의 끝에 도달하거나 플래그가 읽힐 때마다 플래그 에뮬레이션 스텁에 의해 최신 상태로 유지됩니다. 이는 조건문(예: if 문)이 잘못된 분기를 취하는 것을 방지합니다.

## 4. 빌드

다음은 CMake를 사용하여 프로젝트를 빌드하는 예제 명령어입니다. 프로젝트의 루트 디렉토리에서 이 명령어들을 실행하십시오.```  
cmake -B build
cmake --build build

Visual Studio가 설치된 Windows 시스템에서는 생성된 Visual Studio 솔루션 파일(.sln)을 통해 프로젝트를 빌드할 수 있으므로 마지막 명령어를 생략할 수 있습니다. Visual Studio 솔루션 파일은 'build/' 폴더에 있습니다.

5. 사용법

이 난독화 도구는 명령줄 기반 구성 시스템을 사용합니다. 다음 항목들을 명령줄 인수를 통해 구성할 수 있습니다:

  • 사용할 난독화 패스.
  • 입력 바이너리 파일 경로.
  • 입력 심볼 파일 경로 (선택 사항).
  • 출력 바이너리 파일 경로 (선택 사항).

아래는 명령줄 인수에 대한 개요입니다:

사용법: binprotect binary-path symbol-path [--out-binary-path VAR] [--control-flow-flattening VAR] [--virtual-machine VAR] [--opaque-predicates VAR] [--linear-substitution VAR] [--mixed-boolean-arithmetic VAR]

위치 인수:
binary-path 입력 바이너리 파일 경로 [필수]
symbol-path 입력 바이너리 심볼 파일 경로 [선택 사항]

--out, --out-path, --out-binary-path 출력 바이너리 파일 경로
--cff, --control-flow-flattening 제어 흐름 평탄화 패스 활성화 [기본값: 1]
--vm, --virtual-machine 가상 머신 패스 활성화 [기본값: 1]
--opa, --opaque, --opaque-predicate, --opaque-predicates 불투명 조건문 패스 활성화 [기본값: 1]
--lin, --linear-substitution 선형 치환 패스 활성화 [기본값: 1]
--mba, --mixed-boolean-arithmetic 혼합 부울 산술 패스 횟수 지정 [기본값: 2]

6. 약어

  • bin2bin - 이진에서 이진으로.
  • RVA - 상대 주소.
  • MBA - 혼합 부울 산술.
  • SEH - 구조적 예외 처리.
  • FH3 - FuncInfo3.
  • FH4 - FuncInfo4.
  • MSVC - Microsoft Visual C++.
  • LLVM - 저수준 가상 머신.
  • CLANG - LLVM을 위한 C/C++ 언어 프론트엔드.
  • GCC - GNU 컴파일러 모음.
  • SF - 부호 플래그.
  • ZF - 제로 플래그.
  • PF - 패리티 플래그.
  • CF - 캐리 플래그.
  • AF - 보조 캐리 플래그.
  • OF - 오버플로우 플래그.

7. 크레딧

프로젝트 개발 중 귀중한 조언을 제공한 다음 분들께 감사드립니다:

  • Aita.
  • Papstuc.
  • Eriktion.
  • IDontCode.
  • Abdulla.
  • Brit.
  • Phage.
도구 다운로드