n0fate Forensic Lab any security stuff

Solaris(with Sun’s SPARC HW) wtmpx Analyzer

얼마 전 지인의 요청으로 Sun Sparc 장비(솔라리스)의 사용자 접속 로그를 분석할 일이 있었다. 리눅스/유닉스 시스템은 보통 현재 접속 중인 세션 정보는 utmp/utmpx에 저장하고 접속 누적 정보는 wtmp/wtmpx 에 저장하고 있어서 이 파일을 분석하면 된다. 솔라리스도 동일한 파일에 데이터를 기록하고 있다.

솔라리스는 ‘/var/adm/utmpx or wtmpx’에 기록하며, 헤더 정보는 Solaris Development Portal에서 확인할 수 있다.


struct futmpx {
    char ut_user[32]; /* user login name */
    char ut_id[4]; /* inittab id */
    char ut_line[32]; /* device name (console, lnxx) */
    pid32_t ut_pid; /* process id */
    int16_t ut_type; /* type of entry */
    struct {
        int16_t e_termination; /* process termination status */
        int16_t e_exit; /* process exit status */
    } ut_exit; /* exit status of a process */
    struct timeval32 ut_tv; /* time entry was made */
    int32_t ut_session; /* session ID, user for windowing */
    int32_t pad[5]; /* reserved for future use */
    int16_t ut_syslen; /* significant length of ut_host */
    char ut_host[257]; /* remote host name */
};

솔라리스의 wtmpx 로그 분석 스크립트는 용기백배(@yk100)님의 utmp parser를 기반으로 개발되었으며, 다음 경로에서 받을 수 있다.

Unix/Linux/OS X Login Session Parser by n0fate on Github

도구 수행 결과는 다음과 같다.


user    id session type   terminal  pid  start time(utc+0)   status  ip
...
xxxxxx ftp 0 USER_PROCESS ftp2xxx9 2xxx9 20xx 03 xx 02:51:17.000 x.xx.x.xx
xxxxxx ftp 0 DEAD_PROCESS ftp2xxx8 2xxx8 20xx 03 xx 03:00:00.000 x.xx.x.xx
xxxxxx ftp 0 DEAD_PROCESS ftp2xxx8 2xxx8 20xx 03 xx 03:00:00.000 x.xx.x.xx
xxxxxx ftp 0 DEAD_PROCESS ftp2xxx9 2xxx9 20xx 03 xx 03:00:00.000 x.xx.x.xx
           0 DOWN_TIME    system down 0  20xx 03 xx 03:12:59.000
reboot ~   0 BOOT_TIME    system boot 0  20xx 03 xx 03:31:14.000
           0 RUN_LVL      run-level S 0  20xx 03 xx 03:31:30.000

분석에 도움이 되길 바란다 ;-)

Mumblehard 악성코드의 몇가지 특징

mumblehard는 ESET에서 관리하는 블로그인 WeLiveSecurity를 통해 알려진 악성코드로 Linux와 FreeBSD를 둘 다 지원하는 멀티플랫폼 악성코드이다. ESET에 있는 보고서가 잘 정리되어 있으니 그 내용을 참조하기 바라며, 이 글에선 필자가 흥미롭게 본 부분만 정리한다. (사실 길게 쓰기 귀찮다..)

특징

  • 멀티 플랫폼 지원(Linux/BSD)
  • 인코딩 기법
  • 펄 스크립트 C&C

멀티 플랫폼 지원(Linux/BSD)

이 악성코드는 ELF 바이너리를 배포했음에도 여러 운영체제를 지원하도록 구성되어 있다. 일단 임포트하는 라이브러리가 하나도 없으며, 모든 코드가 직접적으로 시스템 콜을 호출하는 형태로 되어 있다. 리눅스 운영체제가 배포판 종류 및 버전에 따라 별도의 빌드가 필요한 것을 생각해봤을 때 다수의 리눅스 배포판에 효과적으로 배포가 가능하다고 볼 수 있다.
이 외에도 재미있는 점은 운영체제 환경이 리눅스인지 BSD인지 확인하여 OS에 따라 다른 행동을 하도록 구성했다는 점이다. 예를 들어, 악성코드 시작 코드를 보자.

LOAD:0804804C                                   public start
LOAD:0804804C                   start           proc near
LOAD:0804804C 89 25 0D 9A 04 08                 mov     MainStackPointer, esp
LOAD:08048052 B8 0D 00 00 00                    mov     eax, SYS_time   ; BSD : fchdir(stdin)
LOAD:08048052                                                           ; Linux : time(NULL)
LOAD:08048057 31 DB                             xor     ebx, ebx
LOAD:08048059 53                                push    ebx
LOAD:0804805A 50                                push    eax
LOAD:0804805B CD 80                             int     80h             ; syscall(SYS_time);
LOAD:0804805D 73 02                             jnb     short loc_8048061
LOAD:0804805F F7 D8                             neg     eax
LOAD:08048061
LOAD:08048061                   loc_8048061:                            ; CODE XREF: start+11j
LOAD:08048061 90                                nop
LOAD:08048062 90                                nop
LOAD:08048063 81 C4 08 00 00 00                 add     esp, 8
LOAD:08048069 3D 00 00 00 00                    cmp     eax, 0
LOAD:0804806E 7C 07                             jl      short isBSD
LOAD:08048070 B0 03                             mov     al, OS_LINUX
LOAD:08048072 E9 02 00 00 00                    jmp     loc_8048079
LOAD:08048077                   ; ---------------------------------------------------------------------------
LOAD:08048077
LOAD:08048077                   isBSD:                                  ; CODE XREF: start+22j
LOAD:08048077 B0 09                             mov     al, OS_BSD
LOAD:08048079
LOAD:08048079                   loc_8048079:                            ; CODE XREF: start+26j
LOAD:08048079 A2 11 9A 04 08                    mov     OSType, al

이 코드를 보면, 시스템 콜 13번(SYS_time)을 실행하는 것을 볼 수 있다. 이 콜은 운영체제에 따라 다른 기능을 수행하는데, Linux인 경우에는 시간 정보를 가져오는 time을 실행하고, BSD라면, fchdir 함수가 실행된다. 재미있는 점은 이 두 개의 호출이 다른 결과를 가져온다는 것이다. 예를 들어 위의 코드를 C로 구성해보면 다음과 같다.

int ret = syscall(13, 0);
if ret < 0:
    OSType = ISBSD;
else:
    OSType = ISLINUX;

BSD의 경우에 13번 fchdir은 두 번째 인자를 0을 받으면, fchdir(stdin)을 실행하고 그 결과를 준다. stdin을 입력으로 받으면 시스템 콜이 실패(-1)하고 디렉터리가 아니다(ENOTDIR, -20)이라는 에러 정보를 가진다. Linux라면, time(NULL)을 성공적으로 실행하여 현재 시간 정보를 int 형으로 돌려준다. 즉, 양수가 된다.
이런 결과를 토대로 운영체제를 판단한 후, 임무 수행 시 각 운영체제마다 다른 시스템 콜 호출을 수행하도록 구성하였다.

악성코드 디코더

Custom XOR을 사용했으며, 총 2개의 데이터를 복호화한다. 하나는 perl의 경로인 ‘/usr/bin/perl’이며, 다른 하나는 펄스크립트 코드이다. 복호화 코드는 다음과 같다.

import struct

def xorl(nbytes, buffer):
    base = 4
    size = 16
    offset = 0
    result = ''
    while nbytes:
        if base == size:
            if size == 128:
                size = 0
            size += 16
            base = 1

        result += chr(int(buffer[offset]) ^ base)
        base += 1
        nbytes -= 1
        offset += 1
    return result

# /usr/bin/perl
buf = [0x2B, 0x70, 0x75, 0x75, 0x27, 0x6B, 0x63, 0x65, 0x23, 0x7D, 0x6B, 0x7D, 0x6D, 0x00]
print xorl(0x0D, buf)

# Perl script for command and control server
f = open('dump_perl_code', 'rb')
buf = f.read()

buflist = []
for i in buf:
    buflist.append(int(struct.unpack('=b', i)[0]))

f.close()

print xorl(0x1706, buflist)

디코딩 된 펄 코드는 C&C 통신을 수행하며, HTTP 헤더에 있는 ‘Set-Cookie’ 필드에 PHPSESSIONID 뒤의 값을 이용하여 데이터 통신을 수행한다. 내부 정보는 Predefine된 구조체 형태로 되어 있으며, 인코딩되어 전송한다. 디코더는 펄 스크립트에 있으며, 코드는 다음과 같다.

sub xorl {
    my ( $line, $code, $xor, $lim ) = ( shift, "", 1, 16 );
    foreach my $chr ( split( //, $line ) ) {
        if ( $xor == $lim ) { $lim = 0 if $lim == 256; $lim += 16; $xor = 1; }
        $code .= pack( "C", unpack( "C", $chr ) ^ $xor );
        $xor++;
    }
    return $code;
}

펄 스크립트 C&C

앞서 잠깐 언급한 것처럼 펄 스크립트는 C&C와 통신하여 실제 명령을 수행하는 역할을 한다. 재밌는 점은 해당 스크립트에는 윈도 환경도 고려되어 있는 것 같다.

if ( $^O eq "linux" )   { $ewblock = 11;    $eiprogr = 115; }
if ( $^O eq "freebsd" ) { $ewblock = 35;    $eiprogr = 36; }
if ( $^O eq "MSWin32" ) { $ewblock = 10035; $eiprogr = 10036; }

통신은 HTTP 패킷의 헤더에 정보를 은닉하여 수발신하도록 구성되어 있다. 코드 자체가 난독화되어 있진 않기 때문에 쉽게 분석할 수 있다. C&C 접속에 사용된 URL/IP는 다음과 같다.

my $url = [
    "184.106.208.157",     "194.54.81.163",
    "advseedpromoan.com",  "50.28.24.79",
    "67.221.183.105",      "seoratingonlyup.net",
    "advertise.com",       "195.242.70.4",
    "pratioupstudios.org", "behance.net"
];

HTTP Request 패킷은 기본적인 구조를 가지고 있다.

    my $buffer  = join( "x0Dx0A",
        "GET $path HTTP/1.1",
        "Host: $host",
"User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:7.0.1) Gecko/$gecko Firefox/7.0.1",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.8,*/*;q=0.9",
        "Accept-Language: en-us,en;q=0.5",
        "Accept-Encoding: gzip, deflate",
        "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7",
        "Connection: close",
        "x0Dx0A" );

할 일은 PHPSESSID 뒤에 붙은 인코딩된 문자열을 디코딩하여 수행한다.

    if ( open( F, "<", $header ) ) {
        flock F, 1;
        my ( $test, $task ) = ( 0, "" );
        while (<F>) {
            s/^s*([^s]?.*)$/$1/;
            s/^(.*[^s])s*$/$1/;
            next unless length $_;
            $test++ if $_ eq "HTTP/1.0 200 OK" || $_ eq "Connection: close";
            $task = $1 if /^Set-Cookie: PHPSESSID=([^;]+)/;    // 값 추출
        }
        close F;
        ( $link, $file, $id, $command, $timeout ) = &decd($task)    // 디코딩
          if $test == 2 && length $task;
    }

Featured Image Source : https://www.flickr.com/photos/barmala/2561602478/in/photostream/

악성코드 동적 분석 시스템 탐지 기법(Bypass dynamic malware analysis system)

1. 서론

동적 분석 시스템은 시그니처 기반 보안 제품만 사용할 때 간지러운 부분을 긁어주는 역할을 한다. 동적 분석 시스템은 악성 유무가 판단되지 않는 파일을 샌드박싱된 공간에 넣어 구동시키고 그 행위를 모니터링하고 로깅한다. 상용 시스템의 경우에는 각 과정에서 발생하는 행위를 분석하여 의심가는 행동에 대해 점수를 매긴다. 점수는 각 시스템마다 별개의 스코어링 기법(Scoring Scheme)을 사용한다. 동적 분석 시스템은 별도의 시스템으로 존재하기도 하지만, 몇몇 안티 바이러스에 내장되어 애플리케이션 실행 시점에 바이너리를 검증하기도 한다.
이러한 동적 분석 시스템이 기업이나 공공기간에 도입되면서, 공격자들은 동적 분석 시스템을 탐지하여, 실제 시스템과 다른 행동을 하는 기능을 악성코드에 넣기 시작하였다. 본 포스팅에서는 분석가가 알아두면 좋을만한 악성코드의 탐지 방법을 논하겠다. 여기에 작성한 기법은 고도의 기술을 적용한건 아니지만 상당히 효과적인 방법이며 실제로 마지막 탐지 방법을 제외한 모든 기능은 'Operation SNOWGLOBE'에 사용되었다.
참고로 기존에 잘 알려진 가상머신(Virtual Machine) 탐지 기법(SIDT, IO Instruction)에 대한 내용은 인터넷에 많이 정리되어 있으니 본 글에서는 다루지 않는다.

2. 시스템 탐지 기법

2.1 자기자신의 이름으로 판단 (AV 동적 분석 탐지)

동적 분석 기능이 내장된 안티바이러스 제품군은 신뢰되지 않은 실행 파일이 실행되면, 샌드박싱 시스템에서 테스트를 거친 다음에 문제 없는 바이너리로 판단되면, 다시 정상적으로 실행한다. 여기서 재미있는 점은 각 안티바이러스 제품은 샌드박싱에 해당 실행 파일을 로드할 때 실행 파일의 이름을 변경한다는 점이다. 예를 들어, 비트디펜더(BitDefender) 같은 경우는 “TESTAPP”이라는 이름을 가지며, 카스퍼스키의 경우엔 “afyjevmv.exe”, “lstcvix.exe”, “tudib.exe”, “izmdmv.exe” 등을 사용한다 [1]. 악성코드는 이 점을 이용하여 자신이 구동될 때 자기자신의 파일 이름을 확인함으로 동적 분석 상태인지 확인할 수 있다. 다음과 같은 코드로 작성된다.

// pseudo code
Path = GetModuleFileName();
Filename = _splitpath(Path);
if strstr(Filename, "klaveme") is not NULL:
    exit(1);
elif strstr(Filename, "myapp") is not NULL:
    exit(1);
elif strstr(Filename, "TESTAPP") is not NULL:
    exit(1);
elif strstr(Filename, "afyjevmv.exe") is not NULL:
    exit(1);
else:    // is NULL
    // 작업 진행

2.2 프로세스 갯수 기반 탐지 (가상화 기반 동적 분석 탐지)

프로세스 갯수 기반 탐지는 가장 쉽게 시스템을 판단할 수 있는 방법이다. 보통 사용자가 운영체제를 설치하면, 단순히 운영체제만 구동하는 것이 아니라 안티바이러스, 클라우드 스토리지 프로그램, 클라우드 노트 프로그램, 메신저 등 다양한 프로그램이 운영체제에서 돌아가게 된다. 이 점을 악성코드가 이용하면 동적 분석 환경임을 쉽게 인지할 수 있다. 예를 들어 ‘SNOWGLOBE 작전’에 사용된 악성코드는 현재 시스템의 프로세스가 15개 미만이라면, 아무런 행위를 하지 않고 종료한다.

EnumProcesses(ProcessIDs, cbsize, cbsizereturned);
ProcessCount = cbsizereturned/4;
if ProcessCount < 0x0F:
    exit(1);
else:
    // 작업 진행

2.3 페이로드를 자동 실행에만 등록

동적 분석 시스템은 바이너리 샘플을 로드하면, 로드한 샘플이 실행하는 프로세스에 대해서도 분석을 진행한다. 최근 DBD을 통한 드롭퍼를 다운로드하여 페이로드를 실행하는 경우도 많다보니 동적 분석 시스템의 기본 기능이 되었다. 드롭퍼 형태의 악성코드를 보면 드롭퍼 자체는 페이로드를 다운로드/익스포트하고 페이로드가 실행되도록 환경을 구성하는 기능만을 가지고 있으며, 대부분의 악성 행위는 페이로드에서 실행된다. 즉 연관되는 프로세스까지 분석해야지 해당 샘플의 악성유무를 판단할 수 있다. 악성코드는 이점을 역 이용하여 드롭퍼가 페이로드를 자동 실행에 등록’만’ 하는 방법을 사용한다. 보통은 드롭퍼가 페이로드를 자동 실행되도록하고 페이로드가 실행 후 자기자신을 지우는 것이 일반적이나, 페이로드를 재부팅 시 실행되도록하면 분석 시스템은 드롭퍼만 분석하고 끝내게 된다. 단 이 방법은 공공기관과 같이 하루 한번 시스템을 껐다가 키는 환경에 적합하다.

2.4 Time distortion (시간 왜곡)(AV 동적 분석 탐지)

안티바이러스 제품군이 악성코드를 동적 분석 기능은 실행 이벤트를 받았을 때 수행되므로, 최대한 적은 시간을 소모하는 것이 좋다. 그러다보니 제한시간을 두고 분석을 할 수 밖에 없게되는데, 이 경우 악성코드가 길게(라고 해봤자 3분?) 슬립한다면 제대로 분석할 수 없게 된다. 이러한 문제를 해결하기 위해 몇 가지 트릭을 사용하는데 가장 좋은 방법이 Sleep 이벤트에 대해서 에뮬레이션(emulation)을 통해 예정 시간보다 빠르게 다음 명령어를 실행하는 것이다. 악성코드는 이러한 특징을 역이용하여 동적 분석 환경인지를 식별할 수 있다. 방법은 생각보다 간단하다. 클럭/시간 정보를 가져오는 함수를 두 번 호출하고 그 사이에 일정시간 Sleep하게 만들어 두 번 호출한 함수간의 차이 값을 비교하는 방법이다. 말로 설명하면 이해가 안갈 듯하여 그림을 하나 첨부한다.

timedistortion

위 그림의 경우, GetTickCount API를 호출하고 값을 저장한다. 그리고 Sleep API에 1000(ms)을 주어 1초 동안 대기하고, 다시 GetTickCount API를 호출하여 값을 저장한다. 그리고 두 CPU Count(TickCount)의 차이를 구한다. 정상적인 시스템이라면 1000(ms)이상 차이가 발생한다. 하지만, AV 동적 분석 환경이라면, 에뮬레이션에 의해 Sleep이 정해진 CPU Count에 비해 따르게 리턴되어 다음 명령어가 실행되므로, 1000보다 낮은 TickCount을 가질 수 있게 된다. 악성코드는 이 방법으로 간단하게 동적 분석을 탐지하고 의도치 않은 행동을 할 수 있게 된다.

2.5 후킹 탐지(Cuckoo Sandbox)

후킹 탐지 방법은 가상화 기반 동적 분석 시스템에 유용할 수 있는 방법으로, 루트킷 탐지 소프트웨어처럼 시스템의 특정 API가 후킹되어있는지 확인하는 방법이다. 오픈소스 동적 분석 시스템으로 잘 알려진 ‘Cuckoo Sandbox(이하 쿠쿠샌드박스)’는 특정 함수의 프롤로그(prologue) 영역에 JMP 코드를 삽입하여 자신이 원하는 행동을 수행하는 ‘detour’ 기법을 사용한다. 악성코드 개발자는 몇몇 함수의 프롤로그를 검증하므로 ‘Cuckoo Sandbox’ 환경인지 유추할 수 있다.

(FARPROC &)pIsDebuggerPresent = (FARPROC)GetProcAddress(hwnd, "IsDebuggerPresent");
printf("CODE : %x %x %x %xn", *((PBYTE)pIsDebuggerPresent), *((PBYTE)pIsDebuggerPresent+1), *((PBYTE)pIsDebuggerPresent+2), *((PBYTE)pIsDebuggerPresent+3));
if(*(PBYTE)pIsDebuggerPresent == 0xE9)    // JMP Opcode
{
    printf("Function is hookedn");
}

예를 들어, 위와 같은 코드를 구동하면, Cuckoo는 다음과 같은 결과를 보여준다.

CuckooHookingDetection

3. 결론

동적 분석 환경을 탐지하기 위한 몇 가지 트릭을 알아보았다. 읽어보면 알겠지만 악성코드는 동적 분석 시스템을 역공학 또는 테스트함으로 단순한 트릭으로 시스템을 감지/우회하고 있다. 하지만 동적 분석 시스템이 의미가 없다는 것은 아니다. 동적 분석 시스템은 시그니처 기반의 기존 탐지체계의 가려운 부분을 긁어주는 훌륭한 시스템이다. 단지 기업 내 보안 담당자분들에게는 동적 시스템에 너무 많은 신뢰를 줌으로써 큰 실수를 범하지 않길 바랐으며, 분석가에게는 이러한 트릭도 있으니 앞으로 발생하는 침해사고에서 참고하길 바랄 뿐이다.

4. Reference

[1] Cyphort, SyScan 15, Shooting Elephants, 2015.

REGIN의 은닉 기법 및 대응 방법

I. 서론

2014년 말에는 차세대 Stuxnet이라 불리는 Regin가 전세계 정보 보안 분야를 핫하게 만들었다. (이 글을 쓰는 시점에 소니 픽처스와 대한민국의 한수원 사태 덕에 묻혀버렸지만..) Regin은 사이버 첩보(Cyber espioinage) 임무를 수행하는 악성코드로 기존 사이버전 악성코드와 다르게 프레임워크 구조를 가지고 있었다. 프레임워크 구조라는 것은 다른게 아니라, 각 기능을 파일(플러그인 모듈)단위로 쪼개두고 이를 자동 또는 수동으로 동적 로딩할 수 있는 형태를 말한다.

이러한 프레임워크 기반의 악성코드는 여러 모듈이 시스템에 흔적으로 남겨질 수 있다보니, 다양한 방법으로 모듈을 은닉하여 시스템 상태 모니터링 도구를 우회하거나, 포렌식을 통한 역추적을 최소화하도록 한다. 본 포스팅에서는 Regin의 다양한 은닉 기법을 알아보고 침해 대응 관점에서 어떻게 이를 탐지해야하는지 알아보도록 한다. 여기서 설명하는 Regin의 대응 방법은 명확하지 않으며, '추 후 침해사고 시스템을 분석 시 이러이러한 경우이렇게 한번 해봐라' 정도의 대안만 제시할 수 있음을 염두해 두길 바란다. (사실 명확하게 나올 수가 없다.)

또한, 본 글은 Regin에 대한 상세한 내용을 기술하진 않았으므로, 기본적인 행위에 대한 이해가 이루어진 후에 읽어보길 추천한다. 이미 시만텍카스퍼스키엔 훌륭한 보고서가 나와있다.

II. 은닉 기능

1. 기존 스테이지의 악성 행위 제거

Regin은 매 단계를 수행할 때마다 이전 단계에 남겨둔 흔적을 제거하는 기능을 가지고 있다. 이러한 기능으로 분석가가 악성코드를 분석하더라도, 악성코드의 기능 파악을 쉽게할 수 없게 된다. 이 경우 분석가는 디스크 이미징을 수행하고, 이미징에서 파일 카빙을 통해 악성코드의 후보군을 추출, 관련있는 악성코드를 추려내는 작업이 필요하다. 이려한 일련의 작업은 분석가 입장에서는 많은 노동 시간으로 낮은 효율을 보여주지만 공격자 입장에선 최고의 효율을 보여주는 방법이라 볼 수 있다. 안티포렌식을 고려한 악성코드는 기존 스테이지에서 남긴 악성 행위를 제거하는 방법과 다른 은닉 기법을 같이 결합하여 사용하기도 하며, 데이터를 와이핑(Wiping)하여 복구 자체를 불가능하게 만드는 방법도 사용된다.

A. Regin의 악성 행위 제거

Regin은 바이너리 내에 저장된 인코딩된 설정 파일을 디코딩하여, 관련 정보를 추출하고 행위를 제거한다. 예를 들어 스테이지1의 설정 정보에 대한 디코딩 스크립트는 다음과 같다.

xorkey = [0x33, 0x10, 0x15, 0xEA, 0x26, 0x1D, 0x38, 0xA7]
SourceString = [0x6F, 0x43, 0x52, 0xae, 0x6b, 0x4b, 0x6a, 0xf2, 0x62, 0x45, 0x52, 0x80, 0x49, 0x78, 0x5F, 0xC6] # 0x15009, Example
decoded = []

count = 0

while count < 750:
    decoded = (SourceString[count] ^ xorkey[count%8]) ^ count
    count += 1

    print '%c'%decoded

위 스크립트를 통해 데이터 영역을 디코딩하면, 파일이나 레지스트리 경로 정보를 볼 수 있다.

decodestr

스테이지1은 위 스크립트에 표시된 디렉터리 경로나 레지스트리 경로에서 스테이지2를 덤프하여 실행한다. 그리고 스테이지2가 실행되면, 스테이지1가 생성한 스테이지2의 데이터를 제거함으로 탐지를 최소화하고 역추적을 방해한다.

 

B. 대응 방법

이전 악성 행위를 대응하려면, 앞서 잠깐 언급한 디스크 카빙 기법이 필요하게 된다.
만약 사내에 여러 시스템이 감염되어 있다면, 몇몇 시스템에서는 알수 없는 이유로 이전 스테이지의 악성 코드가 남아있을 수 있으므로 해당 자원을 체크하는 것도 도움이 된다.

 

2. NTFS 확장 속성(Extended Attributes)에 악성코드 저장

스트림은 바이트 배열로, NTFS 파일 시스템에서는 파일에 대한 더 많은 정보를 제공하기 위한 속성(attributes)와 정보(properties)가 저장된다. 예를 들면, 키워드 검색이나 파일을 생성한 사용자 계정 식별자를 스트림으로 만들 수 있다.
NTFS에는 여러개의 스트림 타입이 있는데, Regin이 사용한 ‘$EA’ 는 확장 속성 데이터를 정의하는 영역이다. 이 스트림은 HPFS(High Performance File System)의 확장 속성을 따서 만든 것이며, 스트림의 크기를 조정할 수 있기 떄문에 non-resident 속성을 가진다. 다음은 스트림 구조체를 표현한 것이다 (NTFS $EA Format).

위치 크기 설명
0x00 4 다음 확장 속성의 위치
0x04 1 플래그
0x05 1 이름 길이(N)
0x06 2 값 길이(V)
0x08 N 이름
N+0x08 V

$EA는 위에서 설명한 것처럼 non-resident 속성을 가지며, 구조체를 단일 연결 리스트(Single Linked List)로 관리한다. 각 구조체의 Name 정보를 다르게하여 다른 속성 정보를 여러개 생성할 수 있다. 이 방법은 ZeroAccess 악성코드에서 최초 발견된 방법(Detecting Extended Attributes and other Frankenstein's Monters with HMFT)이며, Regin 또한 동일한 방법을 사용하였다.

A. Regin의 은닉기법

Regin은 이 방법을 이용하여 악성코드를 은닉하였다. $EA 영역에 드롭퍼가 다음 스테이지의 인코딩된 바이너리를 저장하고, 실행 시점에 ntoskrnl.exe의 Undocumented API 인 NtQueryEAFile를 이용하여 해당 파일 스트림을 추출한다.

regin_006

대상 경로는 다음과 같다.

  • WINDOWS
  • WINDOWSfonts
  • WINDOWSCursors

Undocumented API 사용으로 인한 탐지를 피하기 위해 API 이름은 XOR로 인코딩하여 보관하며, 호출이 필요한 시점에 동적으로 로드하도록 구성하였다. 32비트인 경우 대략적인 코드는 다음과 같다.

Stream = ''    # Stream is Next Encoded Stage (Binary)
Handle = ZwCreateFile(FILEPATH, FILE_READ_EA|FILE_READ_DATA, ...);
DecodeStr();    # Decode a byte array for extracting extended attributes
EncNtQueryEaFile = GetNtQueryEaFileAddr('ntoskrnl.exe');
EncNtQueryEaFile(Handle, Buf, IoStatusBlock, NULL, NULL, NULL, TRUE); # EncNtQueryEaFile is decoded by DecodeStr()
do:
    NextPtr = Buf->Offset;
    NameLen = Buf->NameLength;
    if NameLen != 1:
        return
    if Buf->Name[0] != '_':
        return
    ValueLength = Buf->ValueLen
    Stream += Buf->Value[:ValueLength]
while NexPtr != NULL

Decodestr()의 수행결과는 다음과 같다.

$ python decodestr.py
ntoskrnl.exe
KeServiceDescriptorTable
NtQueryEaFile
NtSetEaFile

Regin은 앞서 설명한 디렉터리를 ZwCreateFile로 오픈하고, 심볼 이름을 기반으로 ntoskrnl.exe 바이너리 내에 있는 NtQueryEaFile API의 시작 주소를 찾는다. 만약 윈도우 2000(NT 4.0)이라면, KeServiceDescriptorTable 주소를 기준으로 해당 함수의 주소를 가져온다. 커널과 드라이버가 동일한 메모리 영역에 있기 때문에 이러한 행위가 가능하다.

regin_007

획득한 함수 주소를 이용하여 해당 디렉터리에 저장된 EA를 추출한다. 추출한 EA 구조체의 이름 필드 값이 ‘‘가 아니라면, 프로그램을 종료한다. ‘‘ 이라면, 파일 스트림을 추출하고 추출한 스트림을 디코딩하여 메모리에 로드한다.

B. 대응 방법

과거 악성코드는 NTFS의 파일 스트림 중 하나인 ADS(Alternate Data Stream)을 이용하여 자신을 은닉하였다. 이러한 기법이 잘 알려져있다보니 분석가들이 디스크 이미지를 분석할 때 ADS를 꼭 확인하고 있으며 포렌식 도구에서도 추가 데이터 스트림을 별도의 파일로 표현해주고 있다. $EA도 동일한 방법으로 분석이 가능하다. 실제로 윈도우는 $EA를 사용하지 않기 때문에 이 스트림이 발견된다면 악성코드 후보군으로 삼아도 될 정도이다. 단, 포렌식 도구에서 이 영역을 따로 보여주지 않으므로, 분석 시 이 점을 고려해야한다.

 

3. 암호화된 레지스트리 데이터로 악성코드 저장

레지스트리는 윈도우 관리를 위한 핵심 요소로, 파일 연결, 서비스 등의 정보를 관리한다. 트리 구조로 이루어져있으며, 여러 정보를 관리할 수 있도록 다양한 포맷의 데이터를 저장(문자열, 정수, 바이너리)할 수 있다. 레지스트리에 악성코드를 저장하는 방법은 TrendMicro에 작성한 POWELIKS: Malware Hides In Windows Registry 를 통해 널리 알려졌다.

A. Regin의 기법

Regin 32비트 스테이지1은 파일시스템이 NTFS라면, EA에서 스테이지2를 추출하고 아닐 경우, 레지스트리에서 스테이지2를 추출한다. 스테이지2는 레지스트리의 특정 값(Class)에 저장된다.

  • REGISTRYMachineSystemCurrentControlSetControlClass{3939744-44FC-AD65-474B-E4DDF8C7FB91} <- 스테이지2에서 로드
  • REGISTRYMachineSystemCurrentControlSetControlClass{3F90B1B4-58E2-251E-6FFE-4D38C5631A04} <- 스테이지2에서 로드
  • REGISTRYMachineSystemCurrentControlSetControlClass{4F20E605-9452-4787-B793-D0204917CA58} <- 스테이지1에서 로드

이 경로에는 각 클래스에 대한 설명과 관련 파일, 버전 정보를 기록한다. 악성코드는 여기에 스테이지2의 인코딩된 바이너리터를 저장한다. 인코딩 알고리즘은 EA와 같이 Customized XOR을 수행한다.

B. 대응 방법

Stuxnet이나 Duqu가 레지스트리를 설정 정보를 저장하는 용도로 사용한 것처럼 Regin도 보통 포렌식 분석가가 접근하지 않는 Class 키에 악성코드 바이너리를 저장하여 탐지를 우회하고 있다. 레지스트리에 저장된 바이너리를 찾는 일은 파일시스템에서 악성행위를 하는 파일을 샘플링하는 것과 같은 난이도를 가질 수 있다. 하지만 다행이도 레지스트리에는 바이너리 형태로 데이터를 저장하는 경우가 많지 않기 때문에, 레지스트리 값에서 바이너리 타입인 ‘REG_BINARY’만 추출하면 후보군을 많이 줄일 수 있다.

또한, Regin이 저장한 값인 Class는 클래스 이름을 정의하는 값이기 때문에, 문자열인 ‘REG_SZ’ 타입으로 저장된다. 분석가는 Class값을 해석하여 ‘REG_BINARY’ 인 값의 데이터만 추출하는 방법을 적용해볼 수도 있다.

 

4. 하드디스크 비할당 영역에 악성파일 보관

하드디스크의 마지막 섹터 영역은 보통 파티션이 사용하지 않는 영역이 있다. 이 영역을 보통 비사용 영역(Unused Area) 또는 비할당 영역(Unallocated Area)로 부르고 있다. 윈도우7의 경우에는 파티션을 생성하면서 디스크의 끝부분의 일정 섹터 크기를 파티션 변경을 원할히 하기 위해 남겨 두는데, 이 영역이 된다.

A. Regin의 은닉 기법

Regin은 악성파일이 파일 시스템에서 드러나지 않도록 핵심 기능을 가진 플러그인을 비할당 영역에 악성코드를 위치하여 필요 시 동적으로 로드하는 방법을 사용한다. 이 방법은 과거에 TDL3나 TDL4에서 사용했던 방법으로 Regin은 이에 더해 비할당 영역에 있는 바이너리를 커스터마이징된 RC5 알고리즘으로 암호화하여 필요할 때 메모리에 맵핑한 후, 복호화하는 방법을 사용하였다.

regin_005

이 방법의 가장 큰 장점은 라이브포렌식으로는 악성 파일의 핵심 기능을 추적할 수 없다는 것이다. 운영체제에서 접근 가능한 윈도우 파티션에는 아무런 파일이 존재하지 않으므로, 악성코드를 직접 분석해서 EVFS의 존재를 알아야만 분석이 가능하다.

B. 대응 방법

이전에 이 기법을 사용한 TDL 패밀리의 경우에는 MBR 루트킷 기법을 이용하여 MBR 코드 실행 시점에 가상 파티션의 코드를 로드하도록 조치하다보니 안티바이러스 제품들이 MBR의 코드 영역의 변조 여부를 판단하여 조기에 차단할 수 있었다. 하지만 Regin의 경우에는 스테이지 로딩 시점에 복호화되어 로드하므로 악성코드가 먼저 발견되어야 차단이 가능하게 된다. 악성코드에서 비할당 영역 접근 여부를 확인하고, 복호화 알고리즘을 해석하여 암호화된 악성코드를 복호화하는 과정이 필요하다.

 

5. 암호화된 가상 파일시스템 구성

가상 파일 시스템 구성은 필자가 TDL에서 처음으로 확인한 기법으로 프레임워크 구조를 가지는 악성코드가 다수의 플러그인을 효과적으로 은닉하기 위한 방법이다. 피해자 시스템에서 사용되지 않는 디스크 영역에 공격자가 정의한 파일시스템을 구성하여 이 곳에 파일을 저장한다. 그리고 악성코드는 C&C 서버의 명령에 따라 파일시스템을 해석하여 관련 파일을 메모리에 로드한다.

A. Regin의 EVFS(Encrypted Virtual File System)

Regin은 스테이지3,4의 커널/사용자 프레임워크에서 사용할 플러그인을 EVFS에 저장한다. Regin은 앞서 설명한 하드디스크 비할당 영역에 악성코드를 은닉했기 때문에 EVFS를 로컬 디스크 영역에 파일 형태로 생성한다. 예를 들어 스테이지3인 커널 프레임워크는 다음 EVFS에 접근한다.

  • %System32%configSecurityAudit.evt
  • %System32%configSystemAudit.evt

각 파일이 파일시스템 구조를 가지고 있으며, 내부에는 여러 파일을 컨테이너(Container)라는 이름으로 보관한다. 각 파일은 RC5 암호화 알고리즘 + NV2e 압축 알고리즘으로 저장되며, 각각의 파일의 메타 정보는 FAT 파일시스템의 FAT 테이블과 유사한 형태로 관리한다.

regin_004

또한 각 컨테이너는 별도의 파일 이름을 가지고 있지 않으며, 메이저/마이너 ID를 이용하여 파일을 식별한다. 메이저 ID는 각 파일의 카테고리 형태로 나누고 있으며, 마이너 ID 가 각 세부 기능을 정의한다.

B. 대응 방법

Regin의 EVFS는 일반적인 파일 포맷의 구조와 상이하기 때문에, 탐지 자체는 용이하다. 특히 Regin은 evt라는 이벤트로그 확장자를 사용하기 때문에, Encase와 같은 도구를 이용한다면, 확장자와 파일포맷이 상이한 파일로 추출할 수 있다. 단, EVFS는 말 그대로 파일 시스템의 각 컨테이너가 커널 프레임워크 바이너리 내의 키로 암호화되어 있고 커스터마이징된 암호화 알고리즘을 사용할 수 있으므로, 바이너리 분석 과정이 추가적으로 필요하다.

 

6. 메모리에 로드한 바이너리의 PE 헤더 제거

최근에 고도화된 악성코드는 시스템에 자신의 흔적을 남기지 않기 위해 메모리에 악성코드를 로드하고 파일시스템에 있는 데이터를 제거하였다. 악성코드 메모리 상주 기법은 포렌식 분석을 까다롭게 만드는 훌륭한 안티 포렌식 기법이다.

A. Regin의 메모리 상주 기법

Regin은 NTFS의 EA 영역이나 레지스티리의 특정 값에 인코딩된 악성코드를 보관하고 있다가, 메모리에서 코드를 로드하고 디코딩한 후, 파일시스템에 있는 악성코드를 삭제한다. 그것만하면 최근 메모리 상주 악성코드와 동일한 방법이다. 하지만 분석가가 메모리에서 MZ 시그니처와 같이 PE 파일 헤더 패턴을 이용하여 악성코드를 탐지하는 방법이 존재했다. Regin은 이러한 탐지를 피하기 위해 꼭 필요한 정보만 메모리에 보관하고 로드한 PE파일의 헤더 전체를 제거하였다. 재미있는 점은 자기자신도 이미 로드한 악성코드를 찾을 수 없기 때문에, 파일의 맨 처음 4바이트를 특정 시그니처(0xfedcfedd)를 기록하였다.

B. 대응 방법

Regin과 같은 메모리 상주 악성코드를 탐지하기 위한 가장 좋은 방법은 메모리 포렌식이다. 메모리 포렌식 도구는 운영체제가 로더를 통해 로드하지 않은 PE 파일을 시그니처 기반으로 찾는 기능을 제공한다. 하지만 Regin을 개발한 사람도 이 기능을 알고있다보니, 탐지 우회를 위해 PE파일의 헤더를 NULL로 제거한다.이러한 기능으로 시그니처 기반으로 PE파일을 찾는 포렌식 도구를 우회할 수 있다.

regin_002
이 악성코드를 추적하려면, 정상적으로 로드된 PE 파일의 코드섹션을 제외한 메모리 페이지의 권한을 확인해야한다. 그 후, 제외한 메모리 페이지의 권한이 R*X(PAGE_READ/WRITE_EXECUTE)라면, 메모리 페이지를 의심영역으로 식별하여 추출한다.

regin_003

추출한 메모리 영역을 디스어셈블하여 함수 프롤로그가 나오는지 확인한다면, 오류를 줄일 수 있다. Volatility에서는 이렇게 의심되는 메모리 페이지를 코드 인젝션 탐지 플러그인(malfind)으로 추출할 수 있다. 단, 이 플러그인은 오탐이 존재할 수 있으므로 덤프한 영역의 악성 여부는 분석가의 판단이 요구된다.

 

7. 코드 보호(암호화, 인코딩, 압축)

사이버전으로 분류된 악성코드의 가장 눈에 띄는 특징은 상용 패커나 프로텍터를 사용하지 않는다는 점이다. 악성 샘플로 발견된 시점에는 언젠가는 분석이 가능하기도하고, 공격자 입장에서는 프로텍터로 인한 탐지의 위험도를 올릴 필요가 없다는 이유에서 사이버전 악성코드는 프로텍터를 잘 사용하지 않는다. 패커도 유사한 이유로 잘 사용되지 읺는데, 상용 패커는 아무리 잘 만들더라도 시그니처가 생성될 수 밖에 없기 때문이다. 그러더라도 악성코드가 자기자신을 대놓고 드러내는 것도 매우 위험한 행위가 될 수 있다.

A. Regin의 코드 압축 기법

Regin은 일반적인 프로그램도 사용하는 정상적인 기법으로 자기자신을 은폐한다. 크게 다음과 같다.

  • 암호화 : 알려진 암호화 기법인 RC5를 이용하여 암호화하였다.
  • 인코딩 : Regin의 경우에는 커스텀 XOR 인코딩으로 의심을 받을 수 있는 문자열을 인코딩하였다. 정적 분석을 까다롭게 만드는 요소 중 하나이다.
  • 압축 : Regin의 경우에는 UCL 라이브러리의 NRV2e 압축 알고리즘을 사용하였다. 악의적이지 않은 압축 목적의 라이브러리를 사용함으로 탐지되지 않는다.

B. 대응 방법

사실 이러한 기법을 적용한 악성코드는 일반적인(General) 실행 파일과 큰 차이를 보이지 않기 때문에, 탐지 시그니처나 패턴을 찾기가 쉽지 않다. 보통은 임포트 함수를 추출하거나, 문자열 추출 과정에서 추출된 정보가 너무 적은 경우 문자열 암호화/인코딩을 의심하여 접근한다. 각 섹션의 엔트로피를 계산하여 일정 수치 이상이 나타나거나, 코드 섹션의 크기가 유난히 작은 경우엔 암호화/압축되었음을 의심해볼 수 있다.

 

7. 다중 채널을 이용한 통신

악성코드에게 통신은 공격자와 연결하기 위한 가장 중요한 기능이다. 보통 악성코드는 단일 프로토콜을 이용하여 공격자와 통신하거나 악성코드간의 통신을 수행하는데, 이러다보니 분석가가 특정 프로토콜을 시그니처로 잡아 네트워크 보안장비에 추가하면 손쉽게(?) 네트워크 레벨의 통신을 차단할 수 있다. 이에 최근 악성코드는 알려진 프로토콜과 알려진 포트를 이용하여 마치 정상적인 것처럼 보이는 네트워크 통신을 사용하기 시작하였다. Regin은 이러한 악성코드보다 좀 더 진보된 다중 채널로 정상적인 것처럼 보이는 통신을 수행하였다.

A. Regin의 통신 방법

Regin은 탐지를 피하기 위해 중앙 서버를 통한 제어와 시스템간의 P2P 통신을 둘 다 지원하며, 여러 통신 프로토콜을 이용하였다. 이러한 통신은 분석가의 네트워크 분석을 더 까다롭게 만들었다. 통신 프로토콜도 일반적으로 많이 사용하는 HTTP, HTTPS, SMB, NamedPipe, ICMP를 이용하였는데, ICMP의 경우에는 핑 패킷의 뒷부분에 자신이 전송할 데이터를 추가하여 전송하는 은닉 채널(Covert Channel) 기법을 이용하였으며, HTTP나 HTTPS의 경우에는 잘 알려진 포트(80, 443)으로 특정 값(SMSWRAP 등)을 생성하여 데이터를 베이스64로 인코딩하여 전송하는 방법을 사용하였다.

Screen Shot 2014-12-28 at 10.52.34 PM

또한, 위 그림과 같이 기관 단위 통신은 유관기관끼리 연결되도록 구성함으로 네트워크 보안 장비로부터 의심을 최소화하도록 노력한 흔적을 확인할 수 있다.

B. 대응 방법

이건 사실 정해진 방법이 없다. 가장 좋은 방법은 악성코드 분석을 통해 사건 시간에 생성된 패킷만 덤프한 후, 정상 상태일 때의 패킷 덩어리와 덤프한 패킷 덩어리를 비교하여 비정상 상태의 패킷만 따로 추출 후 분석하는 것 정도를 생각해볼 수 있다. 하지만 이 방법은 기업 내에서 정상 패킷 데이터를 제대로 보관안한다면 적용할 수 없다. 사실 이전에 로깅한 패킷이 정상 패킷이라는 보장도 없다.

 

8. 디지털 서명

디지털 서명은 바이너리에 대한 신뢰성을 입증하는 매체로 PE 파일의 디지털 서명은 PKCS7 공개키 암호화 표준과  X.509 인증서를 기반으로 한다. 해당 서명은 exe, dll, sys 파일에 적용할 수 있으며, 최상위 서명자인 마이크로소프트가 서명한 기관만 유효한 디지털 서명을 할 수 있다. 디지털 서명은 마이크로소프트가 인증한 기업에서 배포한 바이너리임을 입증하는 하나의 수단이기 때문에 처음에는 유효한 디지털 서명을 가진 바이너리는 제외하고 분석하는 절차를 가지고 있었다. 하지만 Stuxnet나 Duqu와 같은 악성코드가 가짜 기업을 통해 유효한 디지털 서명을 하게되면서, 더이상 디지털 서명된 바이너리도 신뢰할 수 없게 되었다. MS가 비스타 이상의 윈도우 64비트 운영체제부터는 디지털 서명된 드라이버만 로드되도록 KMCS라는 기능을 추가하다보니, 이러한 초정밀 악성코드는 KMCS의 취약점을 찾거나, 어떻게든 유효한 디지털 서명으로 임무를 안전(?)하게 수행하려 할 것이다.

A. Regin의 디지털 서명

Regin은 감염 운영체제가 64비트 환경일 경우, 로더(Loader)로 디지털 서명이 적용된 DLL을 사용한다. 하지만, 해당 DLL을 확인해보면, Issuer가 올바르지 않다는 메시지를 확인할 수 있다. 즉, 마이크로소프트에서 공식적으로 서명된 인증서가 아니라는 의미이다. 실제로 Issuer를 확인하면, 'Windows Root Authority'라는 정상적인 명칭을 가지고 있으나, 비정상적인 KeyID를 가지고 있었다. 이러한 부분으로 오히려 악성 파일로 의심을 살 수 있음에도 Regin은 왜 유효하지 않은 인증서로 위장하려고 했던 것인가?

카스퍼스키의 보고서에 따르면 Regin의 목적은 단지 자신이 따라하고자하는 바이너리와 동일한 것처럼 보이게 하기 위한 것이였다. Regin은 네트워크 통신을 수행하는 윈도의 정상적인 DLL 파일인 것처럼 보이도록 파일의 속성정보를 그대로 가져왔을 뿐만 아니라 디지털 서명처리도 해두어서, 디지털 서명의 존재여부만 확인한다면 외형적으론 문제없는 DLL인 것처럼 구성하였다. 이게 Regin이 유효하지 않은 서명을 이용해서라도 디지털 서명을 한 이유이다.

B. 대응 기법

위에 Regin의 기법에 써있다시피, 각 PE 파일 내 디지털 서명의 유효성(validation)만 확인하여 validation하지 않은 파일을 추출한다면, 샘플 파일을 찾아낼 수 있다. 단, Regin은 로더(Loader)에만 이러한 행위를 수행하다보니 다음 스테이지가 이전 스테이지를 제거하는 특징으로 인해 삭제된 파일을 복구하여 디지털 서명을 검증해야하므로, 분석이 좀 더 까다롭게 된다.

 

9. 악성코드 동적 분석 회피

안티 바이러스의 주 기능 중 하나인 시그니처 기반의 악성코드 탐지 기법은 시그니처만 우회하면 손쉽게 공격을 파악할 수 있다는 문제점을 가지고 있다. 이러한 문제를 해결하고자, 악성코드를 샌드박스 환경에서 동작시키고 그 행동을 모니터링하는 동적 분석 시스템이 등장하기 시작하였다. 이러한 동적 분석 기술은 악성코드의 패커 및 안티 가상화를 제외한 프로텍터 기능을 무력화시킬 수 있게 되었다. 현재의 동적 분석 기술은 자동화로 발전되었으며, 동적 분석의 여러 단점을 해소하기 위해 스코어링 기법과 안티-안티 가상머신 기술을 적용하고 있다. Regin은 이러한 기법이 적용된 솔루션을 우회하는 기술을 내장하고 있다.

A. Regin의 동적 분석 회피

동적 분석기가 악성코드를 모니터링하려면, 실행 프로세스가 어떠한 함수를 호출하며, 호출한 함수의 리턴 값의 정보를 확인할 수 있어야 한다. 이를 위해 주요 커널 함수를 호출하는 시점을 미리 후킹해두고, 함수가 호출될 때 리턴 주소를 확인하여, 호출한 프로세스를 파악한다. 다음은 Regin의 우회 방법을 그림으로 표현한 것이다.

regin_001

Regin은 자동화된 동적 분석을 방해하기 위해 시스템 함수의 리턴 주소를 변경하는 방법을 사용하였다. 악성코드의 IAT(Import Address Table)을 후킹하여, Pre API 단계로 이동(JMP)하도록 수정한다. Pre API는 CALL로 인해 스택에 추가된 악성코드의 다음 명령어 주소를 신뢰성을 가진 안전한 모듈의 JMP 코드의 주소로 변경한다. Regin은 신뢰도를 가진 몇 개의 모듈(ntoskrnl.exe, disk.sys, HAL.dll, atapi.sys)에서 다음과 같은 방법으로 JMP 코드를 스캐닝한다.

for offset in lenofbin:
    if bin[offset] == 0xFF:
        if bin[offset+1] == 0x26:    # jmp dword ptr [esi]
            return true    # find
        if bin[offset+1] == 0x27:    # jmp dword ptr [edi]
            return true    # find!
        if bin[offset+1] == 0x66:    # jmp dword ptr [esi+word]
            return true    # find!
        if bin[offset+1] == 0x67:    # jmp dword ptr [edi+word]
            return true    # find!
        if bin[offset+1] == 0xA6:    # jmp dword ptr [esi+dword]
            return true    # find!
        if bin[offset+1] == 0xA7:    # jmp dword ptr [edi+dword]
            return true    # find!
        if bin[offset+1] == 0xE6:    # jmp esi
            return true    # find!
        if bin[offset+1] == 0xE7:    # jmp edi
            return true    # find!

return false

JMP 코드로 리턴 주소를 변경하면, 시스템 함수를 실행한다. 시스템 함수가 코드를 실행하고 리턴 명령어가 실행되면, 앞서 설명한 JMP 코드의 주소로 복귀하게 되고, 이 JMP 코드는 바로 Post API로 제어를 이전한다. 이런식의 코드 수행으로 시스템 함수의 리턴 주소를 검증하더라도, 신뢰도를 가진 모듈이 호출자로 식별되어 동적 분석 시스템이 악성 행위로 판단하지 않는다.

B. 대응 방법

현재 이 방법을 탐지하려면, 악성 실행 파일의 호출 흐름을 실행파일에서부터 추적해야한다. 실행 파일이 로드되는 시점에 해당 PID에서 호출하는 흐름을 추적하는 방법을 사용할 수 있다. 단, 사용자 레벨 후킹을 위해 IAT를 수정했다면, Regin이 덮어씌우지 않도록 해당 영역을 보호해야한다. 단, 이 방법은 시스템 성능이 매우 저하될 수 있기 때문에, 단일 샘플에 대한 분석을 수행할 경우 적용해볼 수 있다.(사실 이 방법도 명확한 해결 방법이라곤 볼 수 없다..)
또 다른 방법으로는 주요 시스템 모듈의 호출 흐름을 데이터베이스화 해두고, 이와 다른 흐름이 발생할 경우를 추적해볼 수 있다. 단, 이 방법은 운영체제가 업데이트 될 때마다 데이터베이스를 갱신해두어야하며, 백신과 같이 보호 목적으로 흐름 제어를 변경하는 경우에 오탐이 발생하는 문제를 가지고 있다.

결론

Regin의 은닉 기능은 일반적인 탐지 방법을 적용하기가 쉽지 않다. 공격자 입장에서는 다른 악성코드에서 몇 가지 요소만 수정하여 기법을 적용하면, 악성코드 탐지가 힘들도록 만들 수 있다. 기술적인 부분으로 탐지가 여의치 않다면, 정책적인 부분으로 사전에 차단할 수 있어야 한다. 기업 내 보안 인력에 대한 지속적인 보안 의식 고취와 물리적인 망 분리 뿐만이 아니라, 직원들의 예상치 못한 행동으로 발생하는 피해도 논리적인 망 분리도 완벽하게 이루어져야 한다. 또한 침해 사고 대응을 위한 분석 증거를 안전한 장소에 보관하여 역추적이 용이한 사후 분석 프로세스를 정립해야 할 것이다.

이렇게 새로운 기법이 적용된 악성코드는 지속적으로 등장할 것이며, 보안 의식이 낮은 기업에서는 몇겹의 보안 솔루션이 진을 치고 있더라도 무조건적인 피해를 당하게 될 것임을 알아두어야 할 것이다.

 

 사족) 앞서 설명한 침해 대응 방법을 보면 명확한 방법을 제시한 것은 몇 개 없이 대부분 추상적인 내용으로 작성되어 있는 것을 볼 수 있을 것이다. 침해사고대응 업무를 하는 사람들은 항상 이렇게 많은 변수를 안고 일을 시작할 수 밖에 없게 된다. 이러한 변수를 최소화하는 방법 중 하나가 공격자의 관점에서 사건을 바라보는 것이다.

하지만 포렌식 업무를 하는 사람들 중엔 공격자가 사용하는 기술은 크게 관심없고, 알려진 포렌식 분석 기법만 알아도 충분하다고 생각하는 사람이 가끔있다. 공격자의 의도를 파악하지 못하면 분석 시간의 지연으로 이어질 수 있으며, 분석된 증거 조각간의 관계 파악도 힘들기 때문에 공격자의 숨겨진 의도를 파악하지 못할 수 있다. 손자병법에도 나와있는 지피지기 백전백승은 괜히 있는 말이 아니다. 그래서 필자는 포렌식 공부를 하는 학생들에게 포렌식 뿐만 아니라 해킹 기술에 대한 내용도 알아두길 권고하고 있다.

메모리에서 덤프한 KEXT 분석 시 심볼 정보 맞춰주기

1. 서론

메모리에서 덤프한 파일을 IDA와 같은 디스어셈블러로 분석할 때 가장 곤란한 점은 제거된 임포트 함수 정보이다. 임포트 함수 정보는 심볼 테이블에 정의되어 있으며, 이 테이블에는 프로그램이 임포트할 라이브러리와 함수 이름이 정의되어 있다. 프로그램은 메모리에 로드되어 프로세스화 되는 과정에서 임포트 함수의 메모리 주소를 찾는데 이 테이블을 사용한다.

메모리에서 추출한 KEXT를 분석할 때는 해당 파일을 직접 분석할 때와 다르게 다음 사항을 고려해야 한다.

  1. 심볼 테이블 제거
  2. Call opcode의 오퍼랜드를 임포트 함수의 주소로 대치
  3. KASLR로 인한 커널 함수의 주소 변경

2. 이슈

1. 심볼 테이블 제거

KEXT와 같은 커널 영역 데이터는 함수 주소를 맞춰준 후, 심볼 정보를 제거한다. 정확하게는 불필요한 커맨드(섹션)을 제거한다. 그러다보니 덤프한 바이너리 파일에는 코드 영역에 작성된 호출한 커널 주소만 남게 된다. 심볼 정보가 모두 유실되므로, 바이너리만 보고서는 분석에 한계를 가지게 된다.

2. call 오퍼랜드 값 변경

바이너리 내 코드 영역에도 변화가 생기는데, CALL opcode 뒤의 오퍼랜드가 0x00에서 임포트 주소로 변경된다. 예를 들어 커널 함수인 OSMalloc을 호출한다면 IDA에서는 다음과 같이 보인다.

00000624 E8 48 23 00 00 CALL _OSMalloc

이 정보는 디스어셈블러에서 호출하는 함수 주소를 바이너리에 있는 심볼 주소와 매핑한 것으로 실제 헥사코드로 보면 다음과 같다.

00000620 89 C3 XX XX E8 00 00 00 00
...
00002340 .... _OSMalloc

KEXT가 메모리에 로드되는 시점에 오퍼랜드 정보가 변경된다. IOKit은 KEXT 로드 시점에 심볼 테이블을 확인하여 임포트하는 커널 함수 이름을 식별하고, 그 이름을 토대로 메모리 상의 커널 함수의 위치를 알아낸다. 그리고 이 위치를 call opcode의 오퍼랜드로 적용한다. 메모리에서 덤프한 커널 코드를 보면 다음과 같다.

00000620 89 C3 XX XX E8 28 64 CC 7C

이 주소에 해당하는 커널 함수를 찾기
위한 방법으로 커널의 심볼 테이블을 이용한다. 커널 이미지의 심볼에는 각 커널 함수의 이름과 가상 주소 정보를 가진다. 덤프한 KEXT에서 호출하는 주소와 커널 심볼 주소를 매칭함으로 올바른 커널 함수명을 알아낼 수있다. 이 작업은 OS X Lion까지는 정상적으로 적용할 수 있으나, 최신 버전에서는 적용할 수 없다. 그 이유는 KASLR 때문이다.

3. KASLR(Kernel address space layout randomization)

마운틴 라이언 이상(10.8)의 운영체제에는 KASLR 기술이 적용되어, 부팅할 때마다 커널의 베이스 주소가 변경된다. 베이스 주소의 변경은 주요 함수의 엔트리 포인트 변경으로 이어져서 재부팅할 때마다 KEXT가 임포트하는 커널 함수의 주소가 변경된다. 결국 커널의 베이스 주소에 맞춰 심볼 주소를 재정의하지 않으면, 덤프한 KEXT 분석 시 함수 식별이 힘들어진다.

3. 해결방안

이 문제를 해결하기 위해 커널 이미지에 있는 심볼 테이블 정보를 획득하고, 심볼 테이블에 있는 각 심볼 레코드의 주소 값에 KASLR로 변경된 커널 베이스 주소를 적용하여 실질적인 커널 가상 주소를 산출한다.

for symrec in symtable:
    print '0x%.8x'%(symrec->vaddr + kernelbaseaddr)

그리고, 디스어셈블러의 스크립트를 이용하여 call opcode의 operand 값에 맞는 심볼 정도를 코멘트 형태로 기입할 수 있다. volafox에 플러그인으로 개발한 모듈은 dumpsym 명령어
로, KASLR로 변경된 커널 심볼 주소를 정리하여 json의 덤프 기능으로 파일로 떨구는 기능을 제공한다. 이 때 키를 address로 하고, 값을 심볼 명으로 하여, 주소 기반의 매칭을 수행할 수 있도록 재정의하였다.

IDA에서는 idapython 스크립트를 구현하였다. makecommsyscallref.py 스크립트는 바이너리의 함수 영역을 검색하여 call opcode 뒤에 "near ptr"이 붙어 있을 경우 그 뒤의 주소 값을 파싱하여 덤프한 심볼 주소 정보와 일치하는 심볼을 찾는다.

import json
import idc

'''
License : GPLv2
Author : n0fate ([email protected]), forensic.n0fate.com
Name : makecommsyscallref.py
Requirements : This script need to open symbol file. The symbol file is output of volafox 'dumpsym' option. volafox : code.google.com/p/volafox
Site : github.com/n0fate/idascript
It can comment a kernel symbol name considered as KASLR to callee address if symbol isn't identified for analyzing the KEXT dumped in memory
Dumped symbol template of volafox : dictionary {address(hex):name(string), address(hex):name(string), ...}
'''

FILENAME = AskFile(0, '*.*', 'open symbol file')

d2 = json.load(open(FILENAME))

for f in Functions():
    func = idaapi.get_func(f)
    for head in Heads(func.startEA,func.endEA):
        if GetMnem(head) == "call":
            if(GetOpnd(head,0).startswith('near ptr')):
                try:
                    funcname = str(d2[hex(GetOperandValue(head,0))])[1:]    # remove a prefix('_')
                    print '%s: 0x%.8X (click here:%x)'%(funcname, GetOperandValue(head,0), head)
                    idc.MakeComm(head, funcname)
                except KeyError:
                    print 'Could not find function: 0x%.8X (click here:%x)'%(GetOperandValue(head,0), head)

함수 코멘트 시에는 심볼의 prefix인 "_"를 제거하도록 하였다. IDA에서 이 스크립트를 실행하고 덤프한 심볼 정보를 적용하면 된다.

4. 한계점

앞의 해결방안을 통해 덤프한 KEXT를 효과적으로 분석이 가능하다. 단, 이 방법의 경우, 커널이 아닌 다른 KEXT에 있는 함수를 호출하는 경우에는 추적할 수 없는 문제점을 가지고 있다. 이 문제를 해결하려면 모든 KEXT의 익스포트 심볼을 분석해야하는데, 시도는 가능하지만, 도구의 분석 시간이 많이 소모될 수 있다.