티스토리 뷰

서론

 

  kernel에서 프로세스가 실행된다고 생각하면 컴퓨터를 공부하면 한번 쯤은 보게 되는 운영체제 구조 그림과 매칭이 되지 않는데 뭔가 이상하다는 것을 확인할 수 있다. 그래서 보통 Local PC에서 kernel exploit을 하게 되면 특정 프로세스를 실행시킨 후에 token stealing과 같은 방법으로 해당 프로세스에 권한을 부여해주는 shellcode를 사용한다. 그럼 Remote PC에서의 shellcode는 어떻게 될까? 특정 프로세스를 실행하는 것부터 막막해진다. 그래서 궁금증을 풀기 위해 유명한 Remote kernel exploit인 EternalBlue 취약점에서 사용되었던 kernel shellcode를 분석하고자 한다.

 

 

요약

 

  해당 shellcode는 코드 전체가 한 번에 다 실행되어 User-land에서 User-land shellcode를 실행하는 형태가 아니고 shellcode의 각 부분이 호출되는 시점이 나눠져 있다. 크게 3개의 phase로 나뉜다.

  • sysenter hook
  • "lsass.exe" 또는 "spoolsv.exe" 프로세스에 APC 세팅
  • User-land shellcode 복사 및 실행

 

상세 분석

 

  x86x64 kernel shellcode가 있는데 x86을 기준으로 분석할 것이며, kernel shellcode에서 핵심이 되는 부분을 위주로 볼 것 이다. 우선 가장 상단의 comment를 읽어보면 핵심 내용에 대해서 설명하고 있다.

  여기서 핵심은 APC(Asyncroasynchronous Procedure Call)을 사용해 최종적으로 Ring 0에서 Ring 3로 shellcode의 실행 흐름을 전환하는 과정이라고 추측 가능하다.

  kernel shellcode가 가장 먼저 실행되면 우선 ring 3에서 ring 0로 전환되는 길목인 sysenter(windows xp 이상) 호출에 hooking을 건다. 여기서 핵심이 되는 부분은  65 ~ 90번째 줄이다.

  1. 65번째 줄 : MSR 레지스터가 가리키는 주소의 0x176번째 데이터를 EDX:EAX에 읽어들임.
    • MSR[0x176]은 sysenter가 호출되었을 때의 eip 주소인데, KiFastCallEntry의 주소(IA32_SYSENTER_EIP)
    • 64비트 호환을 위해 EDX는 상위 32비트 주소를, EAX는 하위 32비트 주소를 저장
    • 현재 32비트이기 때문에 EAX에 KiFastCallEntry 주소가 저장되고, EDX는 0
  2. 70번째 줄 : MSR[0x176](IA32_SYSENTER_EIP)의 주소가 후킹된 주소인지 확인
    • HAL heap 주소를 사용하여 syscall_hook 라벨의 주소 저장
    • 이미 후킹되었다면 eax의 값은 syscall_hook이랑 같으므로 _setup_syscall_hook_done으로 점프
  3. 75번째 줄 : eax 레지스터가 KiFastCallEntry 주소와 같은지 확인
  4. 79번째 줄 : 나중에 후킹된 주소를 원래 주소로 복구하기 위해 KiFastCallEntry의 주소를 저장
  5. 83번째 줄 : HAL heap의 KAPC 큐와 관련하여 백업하는 부분을 0으로 초기화(KAPC kqueue애 대한 flag 셋)
  6. 88번째 줄 : MSR[0x176]의 위치(IA32_SYSENTER_EIP)에 syscall_hook 라벨의 주소를 저장

  IA32_SYSENTER_EIP를 syscall_hook 라벨의 주소로 변경하였기 때문에 User-land에서 system call을 호출하면 syscall_hook 부분이 실행된다.

  syscall_hook이 실행되면 본래 KiFastCallEntry() 함수의 주소를 가져와 hooking된 이후에 본래 함수를 실행할 수 있도록 세팅해주고 IA32_SYSENTER_EIP의 값을 본래 값으로 복구해준다. 이후, APC를 설정할 수 있도록 넘어간다.

  1. 119 ~ 125번째 줄 : KiFastCallEntry() 함수와 동일한 프롤로그 과정 (Get segments for kernel)
  2. 127 ~ 129번째 줄 : KiFastCallEntry() 함수와 동일한 프롤로그 과정 + 범용 레지스터 백업
  3. 133 ~ 139번째 줄 : HAL 영역에 저장해둔 KiFastCallEntry() 함수의 실제 주소를 가져와 스택에 보관해둔 ecx 값(Return Address 역할) 위치에 +17 하여 저장 => +17은 segment register에 대한 프롤로그 과정을 넘기기 위해서
  4. 142 ~ 146번째 줄 : APC를 딱 1번만 실행하기 위해  HAL의 APC 관련 부분과 비교하고 교환
  5. 152 ~ 155번째 줄 : IA32_SYSENTER_EIP를 본래 KiFastCallEntry() 함수의 실제 주소로 복구
  6. 158 ~ 160번째 줄 : r3_to_r0_start 라벨을 실행하면서 도중 발생하는 인터럽트를 허용하도록 설정
  7. 163 ~ 165번째 줄 : stack에 저장하였던 KiFastCallEntry() 함수로 리턴하여 실제 FiFastCallEntry() 작업 수행

  단순하게 hooking을 1번한 이후에 복구하는 코드가 대부분이였기 때문에 r3_to_r0_start가 APC 작업까지 이어지는 것을 확인 가능하다.

  r3_to_r0_start가 시작되면 커널의 api 함수를 가져다 사용하기 위해 커널의 모듈 주소를 페이지 단위로 'MZ' 스트링을 확인하여 찾아낸다. 찾아낸 모듈의 주소는 HAL에 저장해둔다.

  1. 171 ~ 173번째 줄 : KiFastCallEntry() 함수의 하위 12비트를 0으로 바꿈으로써 페이지 주소를 얻는다.
  2. 176 ~ 178번째 줄 : KiFastCallEntry() 함수가 위치한 페이지 주소에서 0x1000(페이지 크기)만큼 감소시키면서 'MZ' 스트링을 찾는다. 
  3. 181번째 줄 : 찾아낸 nt 주소를 HAL에 저장한다.

  nt module의 주소는 win_api_direct 프로시져에서 사용된다. asm의 소스 파일 상단에 정의되었던 Hash를 사용하여 원하는 함수를 찾고 실행한다.

  1. 186 ~ 188번째 줄 : PsGetCurrentProcess() 함수를 호출하여 EPROCESS 구조체를 가져옴
  2. 193 ~ 197번째 줄 : PsGetProcessImageFileName() 함수를 호출하여 ImageFileName의 주소를 얻어와 offset 계산
  3. 204 ~ 214번째 줄 : ThreadListHead의 offset 계산
  4. 222 ~ 232번째 줄 : KPCR(Kernel Process Control Region)으로부터 현재 스레드에 대한 구조체(ETHREAD)를 가져와 ThreadListEntry offset 계산
    (단순히 offset만 계산하는데 현재 스레드에 대한 ETHREAD를 가져왔으므로 현재 스레드에 대해서 offset 계산)

  참고로 win_api_direct 프로시져는 다음과 같이 이루어진다.

  인자로 받아왔던 Hash는 함수의 이름에 대한 해시값인데 get_proc_addr 프로시져에서 함수의 이름을 해시로 비교하는데 사용한다. get_proc_addr 프로시져는 kernel의 nt 모듈 주소로부터 EAT를 참조해 함수의 주소를 얻어온다. 이 후, jmp eax를 실행하여 해당 Hash와 일치하는 함수를 직접 실행한다. 

 

  EPROCESS로부터 ActiveProcessLinks를 찾아 이를 통해 "lsass.exe" 또는 "spoolsv.exe"의 EPROCESS 주소를 획득한다.

  1. 238 ~ 241번째 줄 : PsGetProcessId() 함수의 주소를 구해 + 0xa에 위치한 4바이트 크기의 UniqueProcessId offset을 획득한다. UniqueProcessId 바로 뒤에 ActiveProcessLinks가 있기 때문에 +4를 하여 ActiveProcessLinks의 offset을 획득한다.
  2. 250 ~ 261번째 줄 : ActiveProcessLinks를 통해 link를 이동하면서 ImageFilename이 "lsass.exe" 또는 "spoolsv.exe"인 EPROCESS 주소를 찾는다.

  현재 프로세스의 EPROCESS 주소를 얻은 후, "lsass.exe" 또는 "spoolsv.exe"의 EPROCESS 주소를 찾는 과정을 pseudo code로 표현하면 아래와 같다.

  특정 프로세스의 EPROCESS 주소를 얻은 후에는 APC를 위한 루틴으로 넘어간다. 

  프로세스의 스레드를 탐색하면서 KeInsertQueueApc() 함수가 Queue에 APC를 삽입하는데 성공할 때 까지 loop를 한다. 이를 위해 사전 준비로 EPROCESS 주소로 ThreadListEntry의 주소를 얻는다.

  • 271번째 줄 : 나중에 CreateThread() 함수 주소를 찾을 때 사용하기 위해 따로 저장한다.
  • 280번째 줄 : EPROCESS.ThreadListHead 주소를 획득한다.
  • 281번째 줄 : HAL에서 KAPC 구조체에 저장될 데이터를 저장하기 위한 특정 주소를 가져온다.
  • 282번째 줄 : 232번째 줄에서 스택에 넣었던 값을 가져온다.

  1. 289번째 줄 : ThreadListEntry로부터 Backword node를 가져옴
  2. 292번째 줄 : 주석과 같은 형태로 KeInitializeApc() 함수 인자가 세팅된다.
  3. 300 ~ 307번째 줄 : 292번째 줄의 인자 형태로 인자를 세팅한다.
    • 305 ~ 307번째 줄 : call을 통해 현재 return address를 스택에 세팅하고 여기에 kernel_kapc_routine까지의 offset을 더해 apc에 맞춰 kernel_apc_routine이 세팅되도록 설정
    • userland shellcode를 일단 이걸로 지정하고, 쉘코드는 나중에 따로 복사해서 세팅한다.
    • context는 나중에 이 주소를 가져오기 위해서 세팅
  4. 308 ~ 313번째 줄 : 인자를 마저 세팅하여 KeInitializeApc() 함수를 실행한다.
  5. 316번째 줄 : 주석과 같은 형태로 KeInsertQueueApc() 함수 인자가 세팅된다. 
  6. 319 ~ 325번째 줄 : KeInsertQueueApc() 함수를 인자에 맞게 세팅하여 함수를 실행한다. shellcode를 실행하기 때문에 따로 인자가 없어 인자를 NULL로 채워서 실행한다.
  7. 327 ~ 328번째 줄 : 성공했는지 확인하고, 실패한 경우 다음 스레드에 대해 다시 Apc 세팅을 진행한다.
  8. 330 ~ 336번째 줄 : HAL에 마련해둔 KAPC 구조체에서 ApcListEntry의 Flink를 참조하고, 0xe 위치에 1이 세팅된 것으로 성공을 확인
  9. 339 ~ 341번째 줄 : 1이 세팅되지 않은 경우, ApcListEntry에서 node를 제거하고 다음 thread를 가져옴 
  10. 347번째 줄 : APC 세팅 완료

  APC까지 세팅을 완료했기 때문에 "lsass.exe" 또는 "spoolsv.exe"의 특정 스레드가 alertable 상태가 될 때, User-land에서는 shellcode가 Kernel-land에서는 kernel_kapc_routine이 실행된다.

  APC가 실행되면 먼저 사전에 세팅해두었던 kernel_kapc_routine이 실행된다. kernel_kapc_routine은 User-land의 shellcode를 실행하기 위해 실행 가능한 메모리를 할당한 후 shellcode를 세팅하여 User-land에서 shellcode를 실행시킬 수 있게 세팅해준다.

  1. 434번째 줄 : kernel_kapc_routine이 실행되면 주석과 같은 형태로 인자가 들어온다.
  2. 442 ~ 447번째 줄 : stack의 return address를 SystemArgument2로 세팅하고 그 외의 인자는 스택에서 정리한다.
  3. 449 ~ 451번째 줄 : 범용 레지스터의 내용을 stack에 백업하고 SystemArgument1과 NormalRoutine을 스택에 저장한다.
  4. 453번째 줄 : 301번째 줄에서 context를 HAL 영역의 주소로 세팅하였기 때문에 해당 주소를 ebp로 다시 세팅한다.
  5. 458 ~ 460번째 줄 : ZwAllocateVirtualMemory() 함수를 실행하기 위해서는 IRQL이 PASSIVE_LEVEL이어야 하기 때문에 이를 세팅
  6. 462 ~ 473번째 줄 : ZwAllocateVirtualMemory() 함수를 실행하기 위한 인자 세팅, 이 때 baseAddr은 NormalRoutine(HAL에 할당된 영역, KeInitializeAPC() 인자 확인)을 가리킨다.
  7. 475 ~ 476번째 줄 : 할당에 실패했는지 확인

  1. 482 ~ 484번째 줄 : 451번째 줄에서 스택에 저장된 &NormalRoutine을 가져와 edi에 세팅
  2. 486 ~ 489번째 줄 : esi에 userland_start의 주소를 세팅해 edi로 userland_start의 코드를 복사
  3. 494 ~ 497번째 줄 : HAL에 저장해뒀던 EPROCESS를 가져와 PsGetProcessPEB() 함수 호출
  4. 502 ~ 503번째 줄 : PEB로부터 메모리에 로드된 DLL들의 리스트 주소를 가져옴
  5. 510 ~ 518번째 줄 : DLL 리스트에서 BaseDllName.length가 0x18인 DLL을 찾을 때 까지 반복
  6. 520 ~ 523번째 줄 : BaseDllName.name의 0xc 부분이 유니코드로 "32"인지 확인(kernel32.dll의 32)
  7. 526 ~ 529번째 줄 : kernel32.dll의 DllBase를 가져와 CreateThread() 함수의 주소 획득
  8. 532 ~ 533번째 줄 : SystemArgument1이 가리키는 주소에 CreateThread() 함수 주소 저장
  9. 536 ~ 541번째 줄 : HAL의 Queueing KPAC를 0으로 세팅하고 IRQL을 APC_LEVEL로 복구
  10. 543 ~ 544번째 줄 : 정리하고 반환

  KeInitializeAPC의 인자에 따라서 kernel_kapc_start가 끝나고 userland_start가 실행되면 CreateThread를 호출해 userland_payload를 실행한다.(기존에 작업하던 thread 등에 문제가 발생하면 안 되기 때문에 추가로 Thread를 생성)

  1. 550번째 줄 : return address를 edx 레지스터에 백업
  2. 551 ~ 556번째 줄 : 기존 인자를 정리하고 CreateThread() 호출을 위한 인자로 변경
  3. 557 ~ 563번째 줄 : return address로 userland_payload의 주소를 획득해 인자를 마저 세팅하고 CreateThread() 함수 호출
  4. 565번째 줄 : 실제 User-land에서 실행될 shellcode가 위치(CreateThread의 인자인 lpStartAddr)

 

정리

 

  글 초반의 요약에서 해당 shellcode는 크게 3단계의 phase로 나뉜다고 하였는데 각 phase를 진행하기 위해 준비하는 과정의 코드를 추가로 확인할 수 있었다.

  • sysenter hook
  • "lsass.exe" 또는 "spoolsv.exe" 프로세스에 APC 세팅
    • 현재 프로세스의 EPROCESS로 "lsass.exe" 또는 "spoolsv.exe"의 EPROCESS 획득
    • 획득한 EPROCESS를 통해 스레드에 APC 등록
  • User-land shellcode 복사 및 실행
    • KeInitializeAPC()의 인자 설정으로 인해 kernel_kapc_start 루틴을 실행하여 user-land를 위한 normal routine 세팅
    • KeInitializeAPC()의 normal routine 설정으로 인해 kernel_kapc_start에서 세팅한 코드를 실행하여 shellcode를 실행하는 thread 생성

  위의 shellcode는 Exploit을 통해 HAL에 별도로 여분의 공간을 만든 후, 진행되었다. 만약 비슷한 방법으로 shellcode를 작성하는 경우에 HAL과 같이 별도의 여분 공간을 준비한 후, 여분 공간 등에 맞춰 코드를 수정할 필요가 있을 것 같다.

 

 

참고

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31