Check kernel state for OS X/BSD rootkit analysis

프로세스가 커널 모듈(e. 윈도는 디바이스 드라이버)와 통신할 때, IO Control / Kernel Event API를 이용하여 약속된 메시지를 전달/이벤트를 설정하고, 드라이버는 그 메시지를 핸들러에서 받아 그에 맞는 임무를 수행하는 구조를 가진다. 범용 운영체제로 알려진 시스템은 대부분 이러한 구조로 프로세스와 커널 모듈이 통신한다.

기존 BSD는 커널 모듈에서 커널의 특정 값을 가져오고 싶거나 커널 설정을 변경하여 커널 내의 서비스 설정을 변경/참조하거나, 커널 튜닝을 할 때, 기존 모듈을 수정하여 그에 맞는 인터페이스를 만들어야하며, 커널 튜닝의 경우 커널을 리빌드해야 할 수도 있다. (물론 다른 방법이 있을 수도 있다.) 이는 운영체제의 확장성, 유연성, 가용성을 낮추는 문제로 이어진다. 이러한 문제를 해결하기 위해 BSD는 커널 상태를 관리하는 구조를 만들어서 사용자가 필요한 변수를 확인하여 해당 변수를 커널/커널 모듈이 참조할 수 있게 하였다.

커널 상태는 리눅스와 BSD에 있으며, 커널 상태가 변경되면, 각 커널 상태 변수와 맵핑된 핸들러로 제어를 넘겨 동적으로 변경된 사항을 처리할 수 있도록 구성되어 있다.

커널 상태 정보를 확인하거나 설정하기 위한 도구로 sysctl(system control)이 있다. 이 도구는 몇몇 유닉스 스타일 운영체제에서 인자 값을 동적으로 변경하고 가져오기 위한 인터페이스 역할을 한다. 여기에서는 OS X의 커널 상태 관리 구조를 알아보고 OS X 루트킷이 이를 어떻게 활용했으며, 메모리에서 추적하는 방법에 대해 알아본다.

OS X에서의 커널 상태 관리 구조

커널 상태 정보에는 다양한 정보가 있을 수 있기 때문에 이 정보를 카테고리화 하여 관리한다. 예를 들어, 커널 관련된 상태 정보는 kern 카테고리에 변수로 저장한다.

$ sysctl -a | grep "kern."
kern.ostype: Darwin
kern.osrelease: 14.5.0
kern.osrevision: 199506
kern.version: Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64
kern.maxvnodes: 132096
kern.maxproc: 1064
kern.maxfiles: 12288
kern.argmax: 262144
kern.securelevel: 0
kern.hostname: Macbook
kern.hostid: 0

주 카테고리는 다음과 같이 나눠진다.

카테고리 설명
kern 일반적인 커널 인자
vm 가상 메모리 옵션
fs 파일 시스템 옵션
machdep 기기 독립적인 설정
net 네트워크 관련 설정
debug 디버깅 설정
hw 하드웨어 정보(보통 읽기 전용)
user 사용자 프로그램에 영향을 미치는 인자
ddb 커널 디버거

필요 시엔 서브 카테고리를 여러개 둘 수도 있다. 단순하게 생각하면 여러 자식을 가질 수 있는 트리 구조라고 생각하면 된다. 이 커널 상태 정보는 KEXT에 의해 추가될 수 있으며, 프로세스는 시스템 제어(system control, sysctl) API를 통하여 해당 값을 읽고 쓸 수 있다. KEXT에는 각 커널 상태 변경 값을 SYSCTL_PROC 매크로로 등록할 수 있다.

SYSCTL_PROC(parent, nbr, name, access, ptr, arg, handler, fmt, descr);

각 커널 상태마다 접근권한과 MIB, 이름, 상태를 처리할 핸들러를 등록할 수 있다. 접근 권한은 “읽기 전용, 읽기/쓰기 가능, 시스템 튜닝에 의해 설정 가능”을 각각 R,W,L으로 표현한다. 예를 들어 하드웨어 정보는 시스템이 부팅되어 있는 동안에 변경되긴 힘들기 때문에 대부분 읽기 전용으로 설정된다.

MIB(Management Information Base)는 숫자로 커널 상태를 관리하는 코드라고 보면된다. 이 값은 sysctl에서는 나타나지 않으며, 내부적으로 관리 목적으로 활용된다. 보통 자동(OID_AUTO)으로 설정한다.

OS X 루트킷의 커널 상태 활용

커널 상태에는 INT, FLOAT, STRING과 같은 다양한 정보를 저장할 수 있으므로, 프로세스가 KEXT에 정보를 전달할 수 있다. 루트킷도 이 방법으로 프로세스 명이나 사용자 명과 같은 정보를 전달할 수 있다. 예를 들어 특정 프로세스를 은닉하고 싶다면, 커널 상태에 프로세스의 ID만 설정하면 커널이 변경된 커널 상태를 확인하여 루트킷의 핸들러로 제어를 넘긴다다. 루트킷은 핸들러를 통해 커널 상태 변수를 확인하여 프로세스 ID에 해당하는 프로세스를 은닉하고 커널 상태 값을 초기화한다.

라이브 분석의 한계

이 정보를 라이브로 분석하려면 sysctl 도구에 의존해야 한다. sysctl에서는 각 커널 상태 문자열(이름)과 설정 값을 보여준다. 즉, 라이브로 분석하려면 명령어 결과를 개별적으로 확인하여 의심가는 설정 정보를 확인해야 한다. 분석은 가능하나 효과적인 방법은 아니다. 또한 루트킷은 시스템 콜 후킹을 통해 명령어 결과 값을 간단히 변조할 수 있는 문제점도 가지고 있다.

메모리 분석을 이용한 루트킷 탐지

메모리 포렌식에서는 라이브 포렌식보다 더 많은 정보를 획득할 수 있다. 커널 변수 중 sysctl__children 은 각 커널 상태 리스트인 oid_list 구조체의 시작 주소를 가리키고 있으므로 이를 통해 커널 상태 구조체인 sytsctl_oid 를 분석할 수 있다. volafox를 이용해 요세미티 10.3 메모리 이미지의 커널 상태 정보를 분석한 결과는 다음과 같다.

$ python vol.py -i ~/Desktop/external/dumped.bin -o sysctl
Name                                                                   MIB PERMISSION                                                         Handler Value
sysctl.debug                                                           0.0        R-L                                    __kernel__(ffffff80045e0a70) None
sysctl.name2oid                                                        0.3        RWL                                    __kernel__(ffffff80045e0500) None
sysctl.proc_native                                                   0.101        R-L                                    __kernel__(ffffff80045dccd0) None
sysctl.proc_cputype                                                  0.102        R-L                                    __kernel__(ffffff80045dcc20) None
kern.ostype                                                            1.1        R-L                                    __kernel__(ffffff80045df370) Darwin
...

구조체를 분석하면 커널 상태 명, MIB, 권한 등의 정보 뿐만 아니라 커널 상태를 처리하는 핸들러의 주소를 획득할 수 있다. 이 핸들러와 KEXT 주소를 비교하면 어떤 KEXT가 커널 상태를 제어하는지 알 수 있다. KEXT 로 필터링하여 루트킷을 식별할 수 있는 것이다.

케이스 스터디

케이스는 이전 Virus Bulletin 2013에 투고한 논문의 샘플 루트킷인 rubilyn으로 하였다. 루트킷에 대한 자세한 정보는 Hunting Mac OS X rootkit with memory forensics를 참조하기 바란다. 이 루트킷은 디바이스에 명령을 내리고 명령에 필요한 정보를 커널 상태로 전달하도록 구성되어 있다. 예를 들어 KEXT의 권한 상승처리는 다음과 같다.

static int getroot(int pid)
{
    struct proc *rootpid;
    kauth_cred_t creds;
    rootpid = proc_find(pid);
    if(!rootpid)
        return 0;
    lck_mtx_lock((lck_mtx_t*)&rootpid->p_mlock);
    creds = rootpid->p_ucred;
    creds = my_kauth_cred_setuidgid(rootpid->p_ucred,0,0);
    rootpid->p_ucred = creds;
    lck_mtx_unlock((lck_mtx_t*)&rootpid->p_mlock);
    return 0;
}
...[SNIP]..
/* prototypes for read/write handling functions for our sysctl nodes. */
static int sysctl_rubilyn_pid SYSCTL_HANDLER_ARGS;
...[SNIP]..
SYSCTL_PROC(_debug_rubilyn,OID_AUTO,pid,
            (CTLTYPE_INT|CTLFLAG_RW|CTLFLAG_ANYBODY),
            &k_pid,0,sysctl_rubilyn_pid,"IU","");
...[SNIP]..
static int sysctl_rubilyn_pid SYSCTL_HANDLER_ARGS
{
    int ret = sysctl_handle_int(oidp, oidp->oid_arg1, oidp->oid_arg2, req);
    getroot(k_pid);
    k_pid = 0;
    return ret;
}

루트킷이 로드된 메모리를 덤프하여 volafox의 플러그인인 sysctl을 구동하면 다음과 같은 결과를 볼 수 있다.

$ python vol.py -i ~/Desktop/rubilyn.mem -o sysctl
Name                                                          MIB PERMISSION                                                         Handler Value
sysctl.debug                                                  0.0        R-L                                    __kernel__(ffffff8000556650) None
sysctl.name2oid                                               0.3        RWL                                    __kernel__(ffffff8000556120) None
..[SNIP]..
debug.SerialATAPI                                           5.118        RW-             com.apple.iokit.IOAHCISerialATAPI(ffffff7f809eef0f) None
debug.USB                                                   5.119        RW-                   com.apple.iokit.IOUSBFamily(ffffff7f809fa7cb) None
debug.rubilyn.pid                                       5.120.101        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc70dd) 0
debug.rubilyn.pid2                                      5.120.102        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc714b) 0
debug.rubilyn.pid3                                      5.120.103        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc71ed) 0
debug.rubilyn.dir                                       5.120.104        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc72aa)
debug.rubilyn.cmd                                       5.120.105        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc72bb) /tmp
debug.rubilyn.user                                      5.120.106        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc72cc) victim
debug.rubilyn.port                                      5.120.107        RW-                   com.hackerfantastic.rubilyn(ffffff7f80bc72dd) 88
..[SNIP]..

앞서 설명한 것처럼 메모리를 분석하면 각 커널 상태의 핸들러를 등록한 KEXT와 어떤 함수가 처리하는지 확인할 수 있다. 예를 들어 debug.rubilyn.pid(핸들러 0xffffff7f80bc70dd) 핸들러를 분석한다면 다음과 같이 코드를 볼 수 있다. KEXT는 로드되는 베이스 주소가 매번 달라지기 때문에 메모리에서 추출해서 비교하는 것이 함수 식별이 좀 더 빠르다.
또한 메모리에서 추출한 KEXT를 분석할 때는 KASLR을 고려해야하기 때문에 volafox에서 dumpsym으로 현재 KASLR에 맞는 심볼 정보를 추출한 후, IDAPython 스크립트인 makecommsyscallref로 변경된 커널 함수를 매핑할 수 있다.

$ python vol.py -i ~/Desktop/rubilyn.mem -o kextstat | grep rubilyn
0x37E387C8    1  85                                 com.hackerfantastic.rubilyn                1           0 0xFFFFFF8004969BE0 0xFFFFFF7F80BC6000   20480       0 0xFFFFFF7F80BC7E6A 0xFFFFFF7F80BC7EB6
$ python vol.py -i ~/Desktop/rubilyn.mem -o kextstat -x 85
[+] Find KEXT: com.hackerfantastic.rubilyn, Virtual Address : 0xFFFFFF7F80BC6000, Size: 20480
[DUMP] FILENAME: com.hackerfantastic.rubilyn-ffffff7f80bc6000-ffffff7f80bcb000
[DUMP] Complete.

[IN IDA PRO]
__text:FFFFFF7F80BC70DD sub_FFFFFF7F80BC70DD proc near
__text:FFFFFF7F80BC70DD                 push    rbp
__text:FFFFFF7F80BC70DE                 mov     rbp, rsp
__text:FFFFFF7F80BC70E1                 push    r15
__text:FFFFFF7F80BC70E3                 push    r14
__text:FFFFFF7F80BC70E5                 push    rbx
__text:FFFFFF7F80BC70E6                 push    rax
__text:FFFFFF7F80BC70E7                 mov     edx, [rdi+20h]
__text:FFFFFF7F80BC70EA                 mov     rsi, [rdi+18h]
__text:FFFFFF7F80BC70EE                 call    near ptr 0FFFFFF8000555D10h ; sysctl_handle_int
__text:FFFFFF7F80BC70F3                 mov     ebx, eax
__text:FFFFFF7F80BC70F5                 mov     edi, cs:g_pid
__text:FFFFFF7F80BC70FB                 call    near ptr 0FFFFFF8000546DC0h ; proc_find
__text:FFFFFF7F80BC7100                 mov     r14, rax
__text:FFFFFF7F80BC7103                 test    r14, r14
__text:FFFFFF7F80BC7106                 jz      short loc_FFFFFF7F80BC7134
__text:FFFFFF7F80BC7108                 lea     r15, [r14+50h]
__text:FFFFFF7F80BC710C                 mov     rdi, r15

결론

메모리 분석 시 커널 상태 정보를 확인(sysctl 플러그인)하면, 루트킷 분석 속도를 높일 수 있는 다양한 정보를 얻을 수 있다.
여러 컨퍼런스에서 자주 언급하지만 메모리 포렌식은 고도의 악성코드/루트킷 분석에 소요되는 많은 시간을 절약해주는 훌륭한 기술이다. 메모리 분석의 특성상 분석을 통해 악성 행위임을 유추한 결과를 전달하다보니 분석가 자신도 커널에 대한 이해가 많이 필요하다. 그로인해 진입 장벽이 높아 시도하지 않는 분들이 종종 있다. 한번 익혀두면 업그레이드된 자기자신을 볼 수 있을 것이다.

참조

[1] sysctl, wikipedia, https://en.wikipedia.org/wiki/Sysctl.
[2] sysctl(9) - The FreeBSD Project, https://www.freebsd.org/cgi/man.cgi?query=sysctl&sektion=9.
[3] BSD sysctl API, Kernel Programming Guide, http://docs.huihoo.com/darwin/kernel-programming-guide/boundaries/chapter_14_section_7.html.