본문 바로가기
IT/DreamHack

[DreamHack] Reversing Basic Challenge #0, #1, #2, #3 (#14, #15, #16, #17) / 어셈블리어 / 악성코드 분석 방법

by 고간디 2024. 4. 4.

[동아리] 팀프로젝트 과제 #1

[어셈블리어 (Assembly Language)]

1. 기계어와 어셈블리어

CPU는 어떠한 프로세서에 명령을 내리기 위해서 고유의 명령어 세트들을 가지고 있다.

이 명령어 세트를 기계어라고 하는데 숫자들의 규칙 조합이기 때문에 이대로 그냥 사용하기에는 어려움이 많다.

때문에 이를 쉬운 기호 코드로 나타낸 것이 어셈블리어이다.

 

어셈블리어는 저급 언어에 속하는 기계어보다 위의 단계의 프로그래밍 언어이다.

통일되어 있는 규격이 없어 컴퓨터 구조와 어셈블러에 따라 문법이나 표현 등이 다르다.

본 글은 Intel 문법과 64비트 운영 체제를 기준으로 설명한다.

 

어셈블리어는 기계어와 일대일대응이기 때문에 어셈블리어를 공부하면 컴퓨터 시스템과 메모리 구조 등에 대해서 더 깊게 이해할 수 있다.

반대로 컴퓨터 시스템과 메모리 구조에 대해서 잘 알아야 어셈블리어를 잘 이해하고 공부할 수도 있다.

 

 

 

2. 기본 하드웨어

CPU (Central Processing Unit, 중앙 처리 장치)

메모리에 있는 내용을 읽고 쓰고 데이터를 메모리와 레지스터로 보내는 역할을 수행한다.

하나의 프로세서는 약 12 ~ 14개의 레지스터를 가지고 있다.

한 번에 처리할 수 있는 비트수에 따라서 운영 체제가 나뉜다. (예: 32비트, 64비트 운영 체제)

 

RAM (Random-Access Memory)

프로세서가 프로그램을 실행시키고 작동하기 위해서 필요한 데이터들을 저장한다.

여러 개의 셀들이 집합해 있는데, 각 셀들은 숫자값을 포함하고 주소가 정해진다.

 

 

 

3. 데이터 타입

메모리 할당 메모리 접근 바이트 레지스터 C 자료형
DB byte 1byte AL char
DW word 2bytes AX short
DD dword 4bytes EAZ int
DQ qword 8bytes RAX long

부호 없는 정수의 데이터 타입인데 부호 있는 정수의 경우 s를 앞에 붙인다.

예를 들어, sqword, sdword 등..

DT로 10바이트를 저장할 수도 있다.

 

 

 

4. 레지스터 (Register)

CPU의 요청을 처리하는 데에 사용될 데이터가 임시로 저장되는 공간이다.

32비트 아키텍쳐의 경우 레지스터의 처음 이름이 E, 64비트의 경우 R이다.

예를 들어, 32비트에서는 EAX, 64비트에서는 RAX가 되는 것이다.

 

RAX 64비트에서 32비트의 절반 영역을 EAX라고 한다.

EAX는 또 다시 EAL 과 EAH로 나뉜다.

 

범용 레지스터 (General Purpose Register)

RAX (Accumulator) 산술 연산에 자동으로 사용되며 함수의 반환 값을 처리할 때 사용된다.

RBX (Base) 간접 번지 지정에 사용되며 상수, 변수를 저장한다.

RCX (Counter) 반복문에서 반복 횟수를 저장한다.

RDX (Data) RAX를 보조한다. 예를 들어, 나눗셈 연산 중에 몫은 RAX, 나머지는 RDX에 저장한다.

 

인덱스 레지스터 (Index Register)

RSI (Source) 복사나 비교를 할 때 출발지 주소를 저장한다.

RDI (Destination) 복사나 비교를 할 때 목적지 주소를 저장한다.

 

포인터 레지스터 (Pointer Register)

RIP (Intstruction) 다음에 실행될 명령어의 주소를 가진다. 

RSP (Stack) 가장 최근에 저장된 공간의 주소를 저장한다.

RBP (Base) 기준점 (바닥)을 저장한다.

 

 

 

5. 명령어 (Opcode)

데이터 통신 (Data Transfer)

MOV (Move) 소스의 데이터를 복사해서 목적지에 값을 넣는다.

PUSH (Push) 오퍼랜드에 스택을 쌓는다.

POP (Pop) 스택으로부터 값을 추출한다.

XCHG (Exchange Register/Memory with register) 첫 번째와 두 번째 오퍼랜드를 서로 교환한다.

LEA (Load Effective Address to Register) 메모리의 주소 값을 레지스터로 로드한다.

 

산술 (Arithmetic)

ADD (Add) 캐리를 포함하지 않은 덧셈

ADC (Add with Carry) 캐리를 포함한 덧셈

SUB (Subtract) 캐리를 포함하지 않은 뺄셈

SBB (Subtract with Borrow) 캐리를 포함한 뺄셈

INC (Increment) 오퍼랜드의 값을 1 증가시킨다.

DEC (Decrement) 오퍼랜드의 값을 1 감소시킨다.

MUL (Multiply) AX와 오퍼랜드를 곱한 결과를 AX 또는 DX:AX에 저장한다.

IMUL (Integer Multiply) 부호화된 곱셈

DIV (Divide)  AX 또는 DX:AX 내용을 오퍼랜드로 나누고 몫은 AL, AX에, 나머지는 AH, DX에 저장한다.

IDIV (Integer Divide) 부호화된 나눗셈

NEG (Change Sign) 오퍼랜드 내용의 부호 반전 (2의 보수)

CMP (Compare) 두 개의 오퍼랜드 비교 (뺄셈 연산 후 플래그 설정)

* 캐리는 연산 수행 시 자리 넘김을 말한다.

 

논리 (Logic)

NOT (Invert) 오퍼랜드 비트 반전 (1의 보수)

AND (And) 논리 AND 연산 후 첫 오퍼랜드에 저장한다.

OR (Or) 논리 OR 연산 후 첫 오퍼랜드에 저장한다.

XOR (Exclusive Or) 배타 논리 합 연산 후 첫 오퍼랜드에 저장한다.

SHL (Shift Logical) 왼쪽으로 오퍼랜드만큼 자리를 이동한다. (LSB는 0)

SHR (Shift Logical Right) 오른쪽으로 오퍼랜드만큼 자리를 이동한다. (MSB는 0)

SAL (Arithmetic Left) SHL과 동일

SAR (Shift Arithmetic Right) SHR과 비슷하되, MSB는 유지한다

ROL, ROR (Rotate) 왼쪽, 오른쪽으로 오퍼랜드만큼 회전 이동한다.

TEST (And function to Flags, no result) 첫 번째 AND 두 번째 오퍼랜드 연산 후 플래그를 설정한다.

 

문자열 조작 (String Manipulation)

REP (Repeat) 뒤의 스트링 명령을 CX가 0이 될 때까지 반복

 

제어 이전 (Control Transfer)

CALL (Call) 프로시저를 호출한다.

JMP (Unconditional Jump) 무조건 분기한다. (외에도 다양한 조건분기 명령어 존재한다.)

RET (Return from CALL) CALL로 스택에 PUSH된 주소로 복귀한다.

JE/JZ (Jump on Equal/Zero) 결과가 0이면 분기한다.

JNE/JNZ (Jump on not Equal/Zero) 결과가 0이 아니면 분기한다.

LOOP (Loop CX times) CX를 1씩 감소시키면서 0이 될 때까지 지정된 라벨로 분기한다.

INT (Interrupt) 인터럽트를 실행한다. (DOS 내부 함수를 실행한다.)

 

BND 128비트 바운드 레지스터, BND0 부터 BND3 까지 가능하고, 가상 주소 공간의 크기를 결정한다.

 

 

 

6. 문법

Opcode + Operand1 + Operand2 형식으로 구성된다.

Intel 문법에서는 Operand1과 Operand2는 모두 피연산자 또는 레지스터인데 Operand1 은 주로 Destination, Operand2는 주로 Source를 의미한다.

다만, AT&T 문법에서는 반대이다.

 

 


 

[Reverse Engineering Practice with x64dbg]

Reversing Basic Challenge #0 (Dreamhack 14, rev-basic-0)

그냥 IDA에서 Strings 보거나 디스어셈블만 해도 플래그일 것 같은 문자열을 바로 확인할 수 있지만 어셈블리어와 친해지기 위해 x64dbg라는 툴을 사용하기로 한다.

 


일단 주어진 바이너리를 실행해보면 Input을 입력받는다.

문제 설명에도 쓰여 있듯이 입력값을 검증하여 correct 또는 wrong을 출력하는 것 같다.

 


실행 버튼 누르고 맨 위로 스크롤해봤더니 문자열을 하나 발견했다.

그냥 봐도 플래그 같다.

 


문자열 참조를 통해서도 수상한 문자열과 함께 Correct와 Wrong 그리고 바이너리 실행 시 볼 수 있는 묹자열인 "Input : "도 보인다.

 


int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[256]; // [rsp+20h] [rbp-118h] BYREF

  memset(v4, 0, sizeof(v4));
  sub_140001190("Input : ", argv, envp);
  sub_1400011F0("%256s", v4);
  if ( (unsigned int)sub_140001000(v4) )
    puts("Correct");
  else
    puts("Wrong");
  return 0;
}

아직 어셈블리어만 보고 프로그램이 어떻게 동작하는지 전혀 알 수가 없어서 일단 IDA를 통해서 C언어로 디스어셈블을 먼저 해보았다.

프로그램을 실행시켜서 어떻게 동작할지 예측해봤기 때문에 sub_140001190 함수와 sub_140011F0 함수가 무엇인지 유추해볼 수 있다.

 

두 함수는 입출력 함수이고, sub_140001000 함수는 문자열을 비교할 것으로 보이니 strcmp 함수일 것으로 예상된다.

 


__int64 sub_140001190(__int64 a1, ...)
{
  FILE *v1; // rax
  va_list va; // [rsp+48h] [rbp+10h] BYREF

  va_start(va, a1);
  v1 = _acrt_iob_func(1u);
  return (unsigned int)sub_140001060(v1, a1, 0i64, (__int64 *)va);
}

 

int __fastcall sub_140001060(FILE *a1, const char *a2, __crt_locale_pointers *a3, va_list a4)
{
  unsigned __int64 *v4; // rax

  v4 = (unsigned __int64 *)sub_140001040();
  return _stdio_common_vfprintf(*v4, a1, a2, a3, a4);
}

 

__int64 sub_1400011F0(const char *a1, ...)
{
  FILE *v1; // rax
  va_list va; // [rsp+48h] [rbp+10h] BYREF

  va_start(va, a1);
  v1 = _acrt_iob_func(0);
  return (unsigned int)sub_1400010B0(v1, a1, 0i64, (__int64 *)va);
}

 

int __fastcall sub_1400010B0(FILE *a1, const char *a2, __crt_locale_pointers *a3, va_list a4)
{
  unsigned __int64 *v4; // rax

  v4 = (unsigned __int64 *)sub_140001050();
  return _stdio_common_vfscanf(*v4, a1, a2, a3, a4);
}

함수를 클릭해서 해당 함수를 확인할 수 있다.

일단 각 함수가 pintf와 scanf 입출력 함수인 것을 확실하게 확인했다. 

 


_BOOL8 __fastcall sub_140001000(const char *a1)
{
  return strcmp(a1, "Compar3_the_str1ng") == 0;
}

strcmp 함수로 예상했던 함수인데 맞았다.

여기에서도 플래그로 보이는 문자열을 확인할 수 있다.

 


x64dbg로 프로그램을 실행해봤다.

문자열 참조에서 찾은 Input: 을 클릭해서 메인 함수로 찾아온 것 같다. 

 

문자열 "Input : "을 메모리에 로드했는데 아마 그 아래에 call 하는 chall0.7FF7ABC31190이 printf 함수일 것으로 보인다.

 


chall0.7FFABC31060
<__stdio_common_vfprintf>

호출되는 프로시저를 클릭하면서 따라가다 보면 printf 함수인 것을 확인할 수 있다. 

 


call chall0.7FF7ABC31080
<__stdio_common_vfscanf>

scanf도 같은 방식으로 확인할 수 있다.

 


아까 IDA에서 알아낼 수 있었던 것을 토대로 하면 chall0.7FF7ABC31000 이 strcmp 함수를 호출하는 역할을 할 것이라고 추정할 수 있다.

실제로 해당 프로시저의 진입점으로 이동했을 경우 strcmp 함수를 호출하는 것을 확인할 수 있다.

더불어, 비교할 문자열이 로드된 메모리 공간에 저장된 문자열 또한 오른쪽에서 확인할 수 있다.

 


덤프를 보니 여기에서도 지정된 주소에 플래그로 추정되는 문자열을 헥스와 함꼐 확인할 수 있다.

 


플래그로 추정되었던 문자열을 바이너리를 실행하고 입력해보면 Correct가 출력되므로 해당 문자열이 플래그인 것을 확인할 수 있다.

 

 

 

Reversing Basic Challenge #1 (Dreamhack 15, rev-basic-1)

문제 설명을 보거나 바이너리를 직접 실행했을 때 앞선 문제와 동작 방식 자체로서는 다를 것이 없어보인다.

 


다만, 이전 문제와는 다르게 단순히 IDA와 x64dbg의 문자열 참조를 통해서 플래그로 의심되는 문자열을 찾을 수 없다.

 


이전 문제와 비슷한 것 같다.

같은 방법으로 printf와 scanf 함수를 확인할 수 있는데 이번에는 strcmp 함수를 호출하지 않는다.

 


이전에는 저장된 문자열 자체와 비교했다면 이번 문제는 문자 하나하나씩 비교를 수행한다.

오른쪽에 변환된 문자들을 합치면 플래그로 추정되는 문자열을 알아낼 수 있다.

 


합친 문자열이 플래그임을 확인할 수 있다.

 


IDA에서도 디스어셈블을 해보면 문자의 아스키 코드와 비교하는 것 같다.

이제 보니 x64dbg에서도 문자만 표시한 것이 아니라 문자 옆에 아스키 코드를 함께 표시해놨다.

 

 

 

Reversing Basic Challenge #2 (Dreamhack 16, rev-basic-2)

아까와 같은 방식으로 동작하며 이 문제도 바로 이전 문제와 마찬가지로 문자열 참조를 통해 플래그로 의심되는 문자열을 찾을 수 없다.

 


입력값을 검증하는 프로시저로 진입해봤는데 뭔 말인지 모르겠다.

계속 뭔가 반복되는 느낌이다.

eax를 계속 1씩 증가시키는데 cmp rax,12 이므로 비교할 문자열 길이는 18글자(0x12)일 것으로 추정해볼 수 있겠다.

문자열의 끝을 나타내는 헥스인 00도 포함해서 판단할 것이기 때문에 실제로는 17글자일 것이다.

 

7FF7516F3000 주소를 참조하는데 메모리 덤프에서 저 주소에 어떤 값이 저장되어 있는지 살펴봤다.

 


플래그로 추정되는 문자들이 저장되어 있는 것을 확인해볼 수 있었다.

플래그 문자열 내용으로 봐서는 배열 형태로 저장되어 있는 것 같은데 배열을 계속해서 참조하느라 반복되는 느낌이 들었던 것 같다.

 


__int64 __fastcall sub_140001000(__int64 a1)
{
  int i; // [rsp+0h] [rbp-18h]

  for ( i = 0; (unsigned __int64)i < 0x12; ++i )
  {
    if ( *(_DWORD *)&aC[4 * i] != *(unsigned __int8 *)(a1 + i) )
      return 0i64;
  }
  return 1i64;
}

IDA에서 디스어셈블을 했을 때에도 배열을 참조하면서 입력값을 비교하는 것처럼 보인다.

 


바이너리를 실행하고 저장되어 있던 배열 요소들을 조합해 만든 문자열을 입력하니 Correct로 통과되었다.

 

 

 

Reversing Basic Challenge #3 (Dreamhack 17, rev-basic-3)

이 문제도 앞선 문제들과 동작 방식은 똑같다.

이제 x64dbg 사용법도 좀 익숙해졌으니 어셈블리어만 보고 분석해서 플래그를 찾아내고 IDA로 이중 검증을 수행해보려 한다.

 

printf, scanf 함수 확인했고, 문자열 참조로도 플래그로 추정할 만한 문자열은 찾지 못했다.

입력값을 검증하는 함수로 이동해봤다.

 


바로 전 문제와 비슷한 것 같으면서도 아래에 몇 가지 명령어들이 추가되어 있다.
inc eax에 cmp rax,18인데 헥스값으로 저장되기 때문에 실제로 플래그는 24글자일 것이다.

 


참조하는 7FF6CF953000 메모리 주소로 이동해보면 알 수 없는 값들이 저장되어 있는데 저장된 헥스값을 어떻게 계산해서 문자열을 만들어야 하는 것 같다.

 


F2를 눌러 엔드포인트를 설정할 수 있다.

입력값을 검증하는 함수의 진입점에 엔드포인트를 설정하고 바이너리를 실행 후 아무 문자열을 입력하고 디버깅을 해본다.

메모리에서 발견했던 헥스값들의 첫 번째 글자가 'I' 였기 때문에 'I'로 시작하는 문자열을 입력했다.

 

첫 번째 글자가 'I'면 루프를 탈출하고 Wrong 문자열을 출력하는 함수 주소로 점프하지 않고 다시 루프의 시작으로 돌아온다.

이로써 첫 번째 글자는 I인 것을 알아낼 수 있다.

해보지는 않았지만 이렇게 쌩노가다를 통해서 전체 문자열을 획득할 수도 있을 것 같긴 하다.

 


F7을 눌러서 함수로 진입하면서 어셈블리 명령어들을 한줄 한줄 실행하면서 디버깅할 수 있다.

다시 한 번 입력값으로 "I_test"를 주고 동작 과정을 분석해봤다.

 


\\\\\\\\\

더보기

RCX 레지스터의 값 "I_test"을 RSP 레지스터의 주소(FD08)로부터 8바이트 떨어져 있는 곳(FD00)으로 복사한다.

 


RSP (FCF0)에 0x8을 뺀 값을 저장한다.

FD08 -> FCF0

 

cmp rax,18에 jae로 탈출하는 것으로 봐서 문자열 비교를 0x18글자를 하기 때문에 실제 문자열의 길이는 23글자일 것이다.

0x18 = 24, 문자열의 끝 00 헥스도 포함한다.

 


RSP 레지스터 (FCF0)를 0으로 초기화한다.

 


jmp를 통해 101A로 강제 분기가 되는데 그 사이에 있는 mov, inc, mov는 다음 루프에서 진행될 것이다.

 


RAX 값이 아까 0으로 초기화 했던 RSP 레지스터 값이 복사되기 때문에 0으로 복사된 것 같다.

 


RAX와 18을 비교하는 연산이 수행되었지만 서로 다른 값이기 때문에 JAE로 분기되지 않고 다음 명령어로 진행된다.

 


RAX에 RSP 주소에 있는 값을 복사한다.

 

몇 번째 글자인지 나타내기 위한 것 같다.

(첫 번째 글자라면 0, 두 번째 글자라면 1 ...)

 


RCX에 3000주소에 저장된 값을 저장한다.

아까 저장되어 있던 헥스값들이다.

 


RCX + RAX를 한 주소의 값을 EAX에 저장한다.

 

3000 + 0 = 3000, EAX에 'I' 저장됨

 


RSP 레지스터 값을 RCX에 저장해 0으로 초기화한다.

 


RSP에 20을 더한 주소의 값을 RDX에 저장한다.

 

0xFCF0 + 0x20 = 0xFD30

 


RDX에 RCX를 더한 값의 주소의 값을 ECX에 저장한다.

 

FD30 + 0 = FD30

ECX에 'I' 저장됨 (아스키 헥스 49)

 


RSP 주소의 값과 ECX의 값끼리 XOR 연산을 수행하고 결과값을 ECX에 다시 저장한다.

 


RSP 주소 값을 EDX로 복사했기 때문에 RDX는 0으로 초기화된다.

 


RCX + RDX * 2

 

루프가 반복될 때마다 EAX가 1씩 증가하기 때문에 RDX도 1씩 증가하게 된다.

현재 사진 속에서는 첫 번째 글자를 비교 중이기 때문에 RCX에 0이 더해진 채로 ECX에 로드된다.

RCX는 입력받은 문자열의 글자인데 이도 마찬가지로 루프가 반복될 때마다 그 다음 글자를 비교하게 된다.

 

I (아스키 헥스 49)

49^0 + 0*2 = 49 (I)

 

_ (아스키 헥스 95)

95^1 + 1*2 = 96 (`)

 


EAX와 ECX를 비교하는데 같은 값을 가지므로 JE 조건 분기를 통해서 1051로 분기한다.

후에 1012로 강제 분기되면서 루프가 다시 시행된다.

 

만약, EAX와 ECX의 값이 다르다면 XOR 연산을 통해서 EAX는 0으로 초기화된 후 1058로 분기하여 RSP에 다시 0x18만큼 더해져 초기화되고, 함수를 빠져나간다.

후에 Wrong을 출력하며 프로그램이 종료된다.

 

RAX가 18이 될 때까지 루프가 시행되면서 cmp eax, ecx가 0이 되도록 하는 문자열이 플래그일 것이다.

 


재시행되는 루프에서는 EAX가 1만큼 증가한다.

후에 RSP 주소의 값은 00000에서 EAX가 복사되어 00001이 된다.

/////////

 


결과적으로 역연산을 통해서 플래그를 구할 수 있을 것 같다.

 

한 글자 한 글자 비교하면서 프로그램이 동작하는데 x번째 글자의 아스키 헥스 코드를 XOR 연산 후 x * 2 를 더해서 얻은 문자열이 7FF6CF953000 주소에 저장된 문자열과 일치하면 Correct를 출력하는 것을 알 수 있다.

 


byte=[
    '49', '60', '67', '74', '63', '67', '42', '66', '80', '78', '69', '69',
    '7B', '99', '6D', '88', '68', '94', '9F', '8D', '4D', 'A5', '9D', '45',
      ]

print("FLAG: ", end='')
for i in range(24):
    print(chr((int(byte[i], 16)- i*2)^i), end='')
    
# I_am_X0_xo_Xor_eXcit1ng

파이썬을 통해서 쉽게 XOR 연산을 해볼 수 있다.

역연산 식으로 7FF6CF953000 주소에 저장된 값들을 계산하여 플래그를 출력하는 소스 코드이다.

XOR 연산의 경우 역연산도 XOR이기 때문에 쉽게 작성할 수 있다.

 


추출된 문자열을 입력해보면 플래그인 것을 확인할 수 있다.

 


__int64 __fastcall sub_140001000(__int64 a1)
{
  int i; // [rsp+0h] [rbp-18h]

  for ( i = 0; (unsigned __int64)i < 0x18; ++i )
  {
    if ( byte_140003000[i] != (i ^ *(unsigned __int8 *)(a1 + i)) + 2 * i )
      return 0i64;
  }
  return 1i64;
}

IDA에 돌려봐도 포인터가 사용된 수식으로 XOR 연산과 곱셈 연산이 있는 것을 알 수 있다.

 

 


 

[악성 코드]

1. 악성 코드 구조 및 기능

세상에는 수많은 악성 코드가 있고 각각 다른 목적으로 댜양한 사람들에 의해 만들어졌기 때문에 수많은 구조와 기능을 가지고 있다.

대부분의 악성 코드가 주로 가지는 기능에 대해서 알아보겠다.

 

악성 페이로드

악성 코드의 주된 기능이라고 볼 수 있다.

사용자 시스템에 허용된 접근 권한 이상의 권한을 획득하여 침투할 수도 있고, 시스템에 저장된 데이터에 피해를 준다.

 

최근 전세계적으로 논란이 되고 있는 XZ Utils에서 발견된 백도어나, 랜섬웨어의 암호화 기능들이 악성 페이로드가 주로 수행하는 작업이라고 할 수 있다.

사용자가 마우스나 키보드로 입력한 값들을 훔치거나, 개인 정보를 수집하는 등의 기능을 수행하는 악성 코드도 존재한다.

이처럼 악성 코드들은 다양한 악성 페이로드를 가진다.

 

암호화

일반인보다는 기업체 등에서 많이 발견되는 악성 코드 중 하나이다.

시스템의 파일을 암호화하는데 랜섬웨어라는 이름으로도 많이 알려져있다.

기업의 시스템을 마비시키거나 파일을 암호화하여 돈을 요구하기도 한다.

 

C&C 서버

보안 뉴스 기사나 악성 코드 분석 보고서를 보면 거의 항상 등장하는 단어가 C&C서버이다.

Command & Control Server를 의미하는 것으로, 악성 코드가 외부 서버와 통신하면서 공격자의 지시를 받아 작업을 수행하거나 공격자에게 수집한 정보를 전송한다.

 

공격자는 C&C 서버를 통해서 여러 시스템에 퍼진 악성 코드와 통신하면서 명령을 내리고 감염된 시스템을 제어할 수 있게 된다.

역추적을 피하기 위해 다양한 암호화가 적용되어 있다.

 

자가 복제

시스템이 악성 코드에 감염되었을 때 그 시스템만의 감염에서 끝나는 것이 아니라 감염된 시스템과 연결된 다른 시스템들로 악성 코드가 퍼져나갈 수 있도록 하는 기능이다.

연결된 네트워크를 사용하여 다양한 단말기로 퍼져나갈 수 있으며, 네트워크를 손상시키거나 백도어를 심기도 한다.

주로 이메일이나 USB 드라이브 등을 통해서 감염된다.

 

요즘에는 보통 하나의 시스템을 감염시킨 후 다른 시스템들로 악성 코드를 복제하다가 적절한 시점에 한꺼번에 피해를 주는 경우가 많다.

 

스텔스

감염된 시스템의 사용자나 관리자 또는 백신 프로그램에 의해서 감지가 되지 않도록 하는 기능이다.

시스템 프로그램으로 위장하여 숨어 있기도 하며 백신 프로그램 우회를 위해 대부분 난독화와 패킹 작업이 되어 있다.

 

트로이 목마라는 이름으로도 유명하다.

다른 프로그램으로 위장하여 시스템의 정보를 빼돌린다.

쉽게 발견되지 않지만 쉽게 감염될 수 있기 때문에 조심해야 하는 유형의 악성 코드이다.

 

어떠한 프로그램을 다운 받을 경우 확장자명이 jpg, pdf 등으로 정상적인 파일처럼 보이는 경우가 있지만 익스플로잇, 이중 확장자 또는 스크립트 실행이 가능한 파일을 통해서 위장한 악성 코드일 수 있다.

북한 APT 해킹 그룹이 우리나라 공무원들을 대상으로 이중 확장자를 자주 이용한다.

 

피싱 웹사이트를 통해 개인 정보를 수집하거나 악성 코드를 다운로드하도록 할 경우 웹사이트의 주소를 유명한 웹사이트의 주소에서 몇 글자를 바꾸는 등으로 사용자를 속이기도 한다.

 

영구 보존

시스템이 재부팅되거나 초기화되어도 남아있도록 하는 기능이다.

이를 통해서 악성 코드는 제거되지 않고 오랜 기간 지속적으로 감염된 시스템에 피해를 줄 수 있다.

 

정보 탈취

다양한 악성 코드들의 주된 목적을 수행하는 기능이다.

키로깅, 스크린샷, 클립보드 복사, 브라우저 기록, 쿠키/비밀번호 수집 등의 기능들이 있다.

 

변종 및 업데이트

악성 코드가 한번 발견되어 분석되면 백신 프로그램 등에 등록된다.

이 때문에 계속해서 업데이트를 통해 악성 코드는 진화하고 정밀해진다.

더불어, 누군가 유포한 바이러스를 다른 공격자가 수정하거나 기능을 추가해서 사용하는 경우도 많다.

 

자기 파괴

시스템 사용자에 의해 감지될 경우 자기 스스로 시스템에서 삭제되어 분석해내지 못하도록 한다.

또한, 정보 탈취 등의 목적을 수행한 경우 정보가 탈취되었다는 사실을 알지 못하도록 하기 위해 흔적을 모두 지우기도 한다.

 

 

 

2. 악성 코드 분석 방법

악성 코드 분석은 크게 3가지 단계로 나뉜다.

기초 분석, 정적 분석, 동적 분석인데 3가지 방법을 적절하게 사용하는 것이 중요하다.

 

기초 분석

파일 확장자릁 통해서 어떤 형식의 파일이며 어떤 식으로 동작할 지 미리 예측해볼 수 있다.

백신 프로그램 등을 우회하기 위해 패킹되어 있는지 확인한다.

ZIP 파일의 경우 암호 설정이 되어 있기도 한다.

 

VirusTotal 등 파일을 업로드하면 다양한 분석 프로그램을 통하여 자동으로 파일 분석 결과를 알려주는 서비스도 존재한다.

이를 통해 업로드된 파일이 악성 코드인지 정상적인 파일인지 확인해볼 수 있다.

때문에 자동화 분석이라고도 부른다.

 

정적 분석

악성 코드를 실행하지 않고 분석하는 과정이다.

실행 파일을 IDA 등의 프로그램으로 디스어셈블하여 분석하거나 PE 구조를 확인하여 다양한 정보들을 얻을 수 있다.

 

동적 분석

악성 코드를 직접 동작시키면서 어떤 기능을 수행하는지 분석하는 과정이다.

악성 코드이기 때문에 주로 가상 환경에서 실행하며 동적 분석 도구를 통해 분석한다.

 

악성 코드가 동작하면서 일어나는 메모리의 변화나 시스템에서 실행되는 프로세스를 관찰하면서 분석하기도 한다.

네트워크 트래픽을 분석해서 악성 코드와 통신하는 C&C 서버를 찾아내기도 한다.

 

상세 분석

세 가지 분석 방법들을 유기적으로 활용하여 악성 코드의 작동 방식을 이해할 수 있다.

암호화/난독화나 안티 디버깅 수법 등으로 분석이 어렵도록 하는 악성 코드가 대다수이기 때문에 쉽지 않은 작업이다.

 

 

 

cs.virginia.edu, x86 Assembly Guide, 2022/03/08

velog.io @hey-chocopie, [Libasm] 2. 어셈블리어란?, 개념 및 특징 정리, 명령어 정리, 이호용, 2021/03/22

tistory.com @aistories, 어셈블리어(Assembly) 기초, 풀나방, 2017/01/29

tistory.com @coding-factory, [Assembly] 어셈블리어 기초 사용법 & 예제 총정리, 코딩팩토리, 2021/01/26

tistory.com @coding-factory, [Assembly] 어셈블리어 명령어 총정리, 코딩팩토리, 2021/01/26

varonis.com, How to Analyze Malware with x64dbg, Neil Fox, 2021/11/03

tistory.com @securitymax, 어셈블리 조건분기 명령어, Great king, 2019/06/09

tistory.com @mumumi, 어셈블리어 15. 조건분기명령어 JNE JA JB 상태 레지스터 CF, Sorrel, 2018/08/13

blog.naver, [Special Report] 해커들이 사용하는 C&C 서버의 보안장비 우회 기술, 인포섹, 2017/10/25

보안뉴스, [시큐리티 Q&A] 은밀한 APT 공격, C&C 서버와의 통신 차단방법은?, 2016/11/21

velog.io @lmlabs, 악성코드 분석 절차, LahmedLabs, 2022/11/29

crowdstrike.com, MALWARE ANALYSIS, Kurt Baker, 2023/04/17

tistory.com @min-12, [악성코드 분석] 악성코드 분석 방법, IT 공부방, 2019/12/20

728x90
반응형

'IT > DreamHack' 카테고리의 다른 글

[Dreamhack] ROT128 #852  (0) 2024.04.03
[DreamHack] addition-quiz #1114  (0) 2024.03.30
[DreamHack] Path Finder #702  (0) 2024.03.30

댓글