Tigress Virtualize Analysis - Switch Dispatch

안상준·2025년 11월 28일

Virtualize Deobfuscator

목록 보기
9/14

Introduction

Tigress 가상화 난독화를 적용한 코드에서 Feature를 생성하기 위하여 LLVM Pass를 사용해 보았다.
이전 실험에서 BERT 모델을 이용하여 Switch 구조 탐지와 적용된 난독화 기법을 탐지하는데 성공했다. 하여 후속 연구로 진행할 내용은 가상화 해제에 필요한 중요한 구조를 파악을 시도하려고 한다. 그러기 위해서 LLVM Pass 분석 도구를 이용하여 핵심적인 베이직 블록을 정적으로 태깅하고 이를 Feature로 하여 BERT 모델 학습에 사용하고자 한다.

LLVM Pass를 사용하기 위해서는 가상화 난독화된 코드가 어떻게 동작하며, 특징으로 삼을 수 있는 구조를 먼저 파악하여야 한다. 하여 본 글에서는 Tigress의 가상화 난독화 로직에 대해서 분석해 보았다.

Virtualize Structure

가상화 난독화 구조에 대해서 먼저 설명하자면, 가상 CPU 명령어를 이용하여 Fetch-Decode-Execute 로직으로 코드가 실행되도록 하는 난독화 기법으로 코드 역공학을 방어하기 위해 주로 사용한다.

위 사진은 가상화 난독화의 구조를 간단하게 표현한 사진이다. 먼저 VM(Virtual Machine) 영역 내부에는 Dispatch, 일종의 인터프리터가 존재한다. 내부에서는 Fetch를 VPC(Virtual Program Counter)를 계산한다. Decode를 통해 VPC가 가르키는 명령어 (Handler)를 구하고, Execute 해당 Handler를 실행하는 방식으로 동작한다.
Dispatch 내부는 loop-switch 구조를 통해 위의 동작을 반복적으로 수행한다.(고전적인 방식)
여기서 handler는 원본 코드를 파편화 한 코드 조각들이며, stack 기반 동작을 수행하게 된다.

간단한 사칙연산을 하는 코드에 가상화 난독화 적용한 결과의 CFG이다.
수많은 분기문들이 존재하는데 이는 switch문이며, 해당 분기문에 의해 실행되는 블록들은 handler가 된다. 각 handler에서 분기하여 모이는 블록이 존재하며 다시 switch의 시작 부분으로 분기하는 것을 볼 수 있다.

Dispatch Option

먼저 Tigress의 가상화 난독화 기법에는 dispatch option이 존재한다. dispatch option에 따라 dispath가 상이한 구조를 가지게 된다.

다양한 dispatch option이 존재하지만 크게 나누면, loop-switch 구조와 threaded 구조로 나눌 수 있다.
loop-switch 구조의 경우 switch 옵션을 사용할 경우의 구조이다.

threaded 구조는 위 사진과 같은 구조를 가지고 있다. 각 label은 goto문의 label을 의미하여 앞서 설명한 handler와 같다. handler에서 loop를 통해 다음 handler가 호출되는 방식이 아닌 handler에서 다른 handler로 직접 분기하는 구조를 가지고 있다. 이러한 방식은 반복적으로 호출되는 구조가 없어 분석하는데 어려움이 있다.

이번 연구에서는 3가지 disaptch option (switch, direct, indirect)에 대해서 살펴보았다.

switch

먼저 switch dispatch option을 적용한 코드를 먼저 살펴보자

tigress --Verbosity=0 --Environment=x86_64:Linux:Gcc:4.6 \
   --Transform=Virtualize \
      --Functions=main \
      --VirtualizeDispatch=switch \
   --out=test_switch.c test.c

Tigress 난독화는 위와 같이 하여 사용하였다.

가상화가 되면 위처럼 loop-switch 구조를 가지게 된다. switch의 조건 제어에 들어가는 값이 위에서 설명한 VPC가 된다. 그 다음 각 case는 핸들러가 된다. 각 핸들러에서는 스택기반 명령어를 실행한다. 위의 사진의 첫 번째 case문을 예시로 보면 맨 처음에 pc_inline_1이라는 배열에 있는 값을 1증가 시킨다. 이는 VPC 값을 증가시키는 것이고, 그 다음 명령어는 sp_inline_4[0] + -1 -> _int에 어떠한 값을 대입한다. 그 다음에 sp_inline_4[0]을 1감소 시킨다. 이는 stack top에 어떠한 정수 타입의 값을 넣고 stack point를 갱신하는 동작이다.
Tigress는 친절하게 변수명이 어떠한 것을 가르키는지와 어떠한 동작을 하는지 친절하게 나와 있어 해석이 비교적 쉬운 편이라 생각한다. 그럼 이번에는 앞에서 본 배열에 어떠한 값들이 들어가는지 확인해 보자.

VPC


앞에서 VPC라 설명한 배열은 어떠한 배열을 가리키는 포인터 배열이다.
정리하면 VPC (배열)은 어떠한 배열을 가리키고 있으며, VPC는 실행할 명령을 결정하는 역할을 하고 있다.

Instruction

이번에는 VPC가 가리키는 값에 대해서 자세하게 살펴보자.

이것이 해당 배열이다. 첫 번째 원소부터 보면 어떠한 문자열을 대입하고 그 뒤로 0을 4번 저장한다. 먼저 문자열이 어떤 것인지 살펴보면

이렇게 enum 타입으로 어떠한 정수를 저장하고 있다. 그런데 어떻게 이 값을 가지고 동작을 실행할 수 있는지 의문이 생길 수 있다.

case _TIG_VZ_NGJ6_1_main_Region_constant_int$result_STA_0$value_LIT_0: 
(_TIG_VZ_NGJ6_1_main_Region_$pc_inline_1[0]) ++;
(_TIG_VZ_NGJ6_1_main_Region_$sp_inline_4[0] + 1)->_int = *((int *)_TIG_VZ_NGJ6_1_main_Region_$pc_inline_1[0]);
(_TIG_VZ_NGJ6_1_main_Region_$sp_inline_4[0]) ++;
_TIG_VZ_NGJ6_1_main_Region_$pc_inline_1[0] += 4;
break;

한 case문을 예시로 가져왔다. 먼저 case문에 있는 값은 앞서 설명한 enum 타입으로 어떠한 정수를 가르키고 있다.

case문을 도달할 때의 VPC는 _TIG_VZ_NGJ6_1_main_Region_constant_int$result_STA_0$value_LIT_0 이 값을 가리키고 있을 것이다. 그 다음 증가를 하게 되면 434번째 원소 1을 가리키게 된다.
그 다음 (_TIG_VZ_NGJ6_1_main_Region_$sp_inline_4[0] + 1)->_int에 값을 대입하는데 해당 값은 union 타입으로 정수 4bytes가 된다. 그렇기 때문에 배열의 원소 (unsiged char = 1byte) 4개 0x00000001 (리틀엔디안)의 값이 들어가게 된다.

해당 case문을 정리하면 stack top에 정수 1을 저장하는 동작을 하는 핸들러다. 또한, 가상화 명령어는 VPC가 초기에 가르키는 값은 Opcode가 되며, 이후 4bytes 는 Operand 이거나 Operand가 존재하지 않을 수 있다.

0개의 댓글