0. 프로필

1. 개요

2. Packed PE

3. 알고리즘

4. MUP




0. 프로필

라이선스 : 프리웨어

상태 : v1.03

주소 : https://sourceforge.net/projects/yodap/files/Yoda%20Protector/



1. 개요

  Yoda's Protector는 Ashkbiz Danehkar이 Visuaal C++ 환경에서 제작하었다. 이것은 기존의 Yoda's Crypter에 데이터와 코드 섹션을 압축하는 기능을 추가한 버전이다. 기본적으로 현재 프로그램이 디버깅되는지를 판단해 안티 디버깅을 수행하고 다형성 코드 방식의 암호화와 압축 기술(LZO라고 한다)을 사용한다.


  안티 디버깅 기법으로 "Jumping into the middle of an instruction" 방식과 IsDebuggerPresent(), BlockInput() 그리고 SoftICE의 드라이버를 검사하는 것 등이 있으며 부모 프로세스를 검사해서 Explorer.exe가 아닌 경우에는 중단시키는 기술도 존재한다. 또한 코드의 다형성을 위해 다형성 암호화와 압축 기술을 사용한다. 


옵션을 보면

- Protection

Anti-SoftICE Protection (default)

Checksum Protection (default)

API Redirection (default)

Anti-Dump Protection (default)

Clear Import Information (default)

Clear PE header


- Advanced

Remove .reloc section (default)

Remove debug information (default)

Eliminate MS-DOS header

Optimize MS-DOS header


- Other

Create backup copy (BAK-file) (default)

Auto run after loading

Exit when done

Section's Name


- Compress Option

1 - 10



2. Packed PE

  일반 섹션들은 이름이 지워지고 .rsrc 섹션은 그대로 있다. .yP 섹션이 추가되는데 여기에는 Import Table과 stub 코드가 있다. Import Table을 보면 기본적으로 LoadLibraryA()와 GetProcAddress()가 필요하다. EP는 .yP 섹션의 Import Table 바로 뒤에 존재한다.



3. 알고리즘

  처음에는 CALL과 예외를 이용한 안티 디스어셈블리 방식이 존재한다. 일반적으로 "Jumping into the middle of an instruction"라고도 불리는데 CALL 명령어를 사용해 디스어셈블러의 분석이 실패하도록 주소를 호출한다. 물론 Ollydbg의 경우에는 Ctrl+A 단축키 또는 "Analyse Code"를 통해 깔끔하게 정리된 디스어셈블을 볼 수 있다. 정확히 표현하자면 명령어 중간에 "DB E9" 즉 무의미한 바이트를 써 넣고 call이나 jmp 명령어로 이 주소 다음 주소로 분기하도록 하여 실제 흐름에는 영향이 없지만 디스어셈블러가 명령어를 분석하는데 실패하도록 하는 방식이다. 이것도 여기서는 간단하게 사용되서 감당할만 한데 과하게 사용되는 경우에는 Ollydbg를 사용해도 분석하기 매우 까다롭다. Ollydbg의 경우 분석 이후 화면이 전환되기도 하고 그 부분만 분석하는 기능도 한계가 있기 때문이다. 더 좋은 디스어셈블러들의 경우 깔끔하게 정리된 것을 보여주기도 한다. 또한 동시에 다음 EIP 주소를 스택에 PUSH하는 CALL의 특징을 사용하는데 예외 핸들러를 설치할 때 이 PUSH된 주소가 예외 핸들러의 주소로 가게 된다. 그리고 INT 3을 이용해 예외를 발생시킨다. 이러한 부분이 5번 존재한다. 이 부분은 Step Into를 사용해 진행하다가 SEH가 설치되면 그 핸들러의 주소에 BP를 걸고 INT 3 명령어로 인한 예외를 디버거가 받으면 Shift+F9 키를 사용해 예외를 프로그램에게 돌려준다. 물론 옵션을 사용해 이 예외를 받지 않게할 수도 있다.


  사실 특이한 점으로는 예외 발생 시에 예외 핸들러가 호출되고 이것이 반환된 후에 다음 코드가 실행되는 것이 아니고 예외 핸들러 실행 도중에 다른 예외가 발생하여 다른 예외 핸들러가 호출되는 방식이 5번이나 존재한다는 것이다.


  어쨌든 이후에 복호화 루틴이 존재한다. 진행하다 보면 LOOP로 만들어진(즉 LOOP로 끝나는 프로시저) 복호화 루틴이 있는데 이 부분에 주의해야 할 것이 이 부분도 CALL과 JMP를 이용한 "Jumping into the middle of an instruction" 기법이 사용되기 때문에 코드가 상태가 좋지 않아서 조심스럽게 Step Into로 진행해야 한다. 이곳은 LOOP 명령어에 Conditional BP를 걸면 되는데, "ECX == 2"를 입력하고 실행(F9)시켜야 한다. 그러면 쉽게 LOOP 부분을 빠져나올 수 있다. 참고로 이런 루프가 하나 더 존재한다. 여기서 LOOP 명령어 뒤의 명령어에 F4를 눌러서 하는 방식은 통하지 않기 때문에(왜냐하면 이 LOOP 명령어 바로 뒤의 명령어가 현재 루프의 복호화 과정으로 인해 수정되기 때문에) 조건부 BP를 걸어야 하는 것이다.


  자세히 살펴보면 이 복호화 루프는 복호화해서 넣는 주소가 바로 다음 주소부터 0x3031만큼이라는 것을 알 수 있다. 이후 복호화가 끝나면 다음 명령어를 수행하게 된다. 그리고 앞에서 언급하였듯이 다음에도 또 다른 복호화 루프가 존재한다. 이 루프가 하는 행위를 보면 파일에 존재했던 Import Table 말고 스텁 루틴에서 사용할 Import Table을 복구한다. 복구되는 위치는 이미 존재하는 Import Table의 바로 뒤이다. 


  이 루프가 끝나면 예외를 이용한 안티디버깅이 하나 더 존재한다. 평소와 다른 차이점은 예외 관련 지식이 있으면 알겠지만 앞 부분과 달리 일반적인 방식처럼 호출한 예외 핸들러가 종료하여 반환을 하게 되는데 그렇기 때문에 KiUserExceptionDispatcher() 내부에서 NtContinue()까지 실행된다. 여기서 저장된 Context를 통해 복귀할 주소를 알아내는 방법을 보겠다. NtContinue()에서 받는 pContext 인자의 주소를 보면 스택에 존재하는 것을 볼 수 있다. 이 주소가 0x0019E41C라고 하자. 여기에서 0xB8만큼 더한 값 즉 0x0019E4D4 주소에 들어있는 값인 0x0041A7C1이 예외 핸들러에서 복귀한 후에 갈 주소이다. 


  사실 일반적인 경우라면 예외 핸들러가 호출된 후에 다음 명령어로 복귀될 것이다. 그래서 이렇게 복잡한 방식 대신 그냥 다음 명령어에 BP를 걸면 될 것이다. 하지만 이 부분도 복호화되서 변하는 부분이므로 미리 BP를 걸어놓고 실행하면 종료되어 버리기 때문에 이렇게 복잡한 방식을 사용하였다.


  이 부분을 지나면 API들의 주소를 찾는다. 참고로 원본 프로그램에서 사용할 API들이 아니라 Yoda가 압축 해제와 보호용으로서 사용할 API들이다. 전형적인 방식으로서 LoadLibraryA()와 GetProcAddress()를 통해 처리한다. 아까 구했던 이름을 가지고 주소를 얻어서 이름 다음 부분에 차례로 집어넣는다. 이후 스텁에서는 이 주소를 가지고 API들을 호출하게 된다. 패커가 아닌 프로텍터여서 그런지 사용하는 API들이 상당히 많다. 다음은 그 목록이다.


- Kernel32.dll

GetModuleHandleA(), VirtualProtect(), GetModuleFileNameA(), CreateFileA(), GlobalAlloc(), GlobalFree(), ReadFile(), GetFileSize(), CloseHandle(), IsDebuggerPresent(), CreateToolhelp32Snapshot(), GetCurrentProcess(), GetCurrentProcessId(), Process32First(), Process32Next(), Module32First(), Module32Next(), Thread32First(), Thread32Next(), OpenThread(), OpenProcess(), TerminateProcess(), SetPriorityClass(), GetPriorityClass(), ExitThread(), GetWindowsDirectoryA(), CreatWindowA(), GetCurrentThread(), SetThreadPriority(), SuspendThread(), Resumethread(), GetLastError(), GetSystemTime(), GetTickCount(), GetVersion(), DebugActiveProcess(), DebugActiveProcessStop()


- User32.dll

MessageBox(), SendMessageA(), WaitForInputIdle(), BlockInput(), GetWindowLongA(), SetWindowLongA(), GetForegroundWindow(), FindWindowA(), GetTopWindow()


- Advapi32.dll

RegCreateKeyExA(), RegOpenKeyExA(), RegCloseKey(), RegSetValueExA(), RegQueryValueExA(), CryptAcquireContextA(), CryptReleaseContext(), CryptCreateHash(), CryptDestroyHash(), CryptHashData(), CryptDeriveKey(), CryptDestroyKey(), CryptEncrypt(), CryptDecrypt()


  참고로 CALL을 통한 "Jumping into the middle of an instruction" 기법으로 인해 스택은 계속 쌓여 가지만 큰 의미는 없어보인다. 그리고 언패킹을 하면서 상황에 맞게 Code Analysis를 수행했다 말았다를 해야할 것이다. 


  어쨌든 이 부분이 끝나면 여러 안티 디버깅 기법들이 나온다. 먼저 GetVersion(), GetForegroundWindow(), FindWindowA(), GetTopWindow()를 사용하는 프로시저가 나온다. 이후 프로시저에서는 GetCurrentProcess(), GetPriorityClass(), SetPriorityClass()를 사용하고 BlockInput()을 사용한다. 참고로 이 API는 안티디버깅에 사용되는데 해결하는 방법은 Step Into로 따라 들어가서 RETN 4를 제외하고 모든 부분을 NOP으로 바꾸면 된다. 그리고 암호화와 관련한 루프문이 나온다. CryptAcquireContextA(), CryptCreateHash(), CryptHashData(), CryptDeriveKey(), CryptDestroyHash() 등의 API들을 사용한다.


  다음 프로시저에서는 SoftICE와 관련한 것이 나온다. 먼저 CreateFileA()로 "\\.\SICE"를 오픈하고 다음으로 "\\.\NTICE"를 오픈한다. 만약 이 파일들이 존재한다면 즉, 반환 값이 -1이 되면 정상적으로 진행되는데, 아닌 경우에는 CloseHandle()과 RtlExitUserThread()를 수행하여 종료한다.


  다음 프로시저에서는 GetCurrentProcessId()를 호출하는데 이것도 안티디버깅에 사용되므로 처리가 필요하다. Step Into로 들어가서 FS:[18]을 통해 PID를 구하는 부분을 직접 올리디버거의 PID를 구해서 이 값을 넣는다. (작업관리자에서 올리디버거의 PID를 찾은 후 이 깂은 10진수이므로 계산기에서 이 값을 16진수로 변환해서 얻는다) 즉 다음과 같다.


  MOV EAX, DWORD PTR FS:[18}

  MOV EAX, DWORD PTR DS:[EAX+20]

  RETN


이것을


  MOV EAX,14AC ; 참고로 16진수로.

  NOP

  NOP

  NOP

  NOP

  RETN


으로 바꾼다. 이후에 CreateToolhelp32Snapshot(), Process32First(), Process32Next() 등의 여러 API들의 결합을 이용해서 모든 실행중인 프로세스들의 PID 번호를 구하고 현재 실행된 프로세스를 검색한다. 타겟 프로세스의 PID와 스스로의 PID를 비교한 다음 서로의 PID가 다르면 그 프로세스를 종료시킨다.


  지겨울 정도로 진행하다 보면 PE 헤더부터 복구화 루틴이 진행되는 것을 볼 수 있다. 마지막 즈음에 IsDebuggerPresent()를 통한 안티디버깅이 존재하는데 이것은 그냥 이 API 호출 후에 EAX를 0으로 바꿔주면 된다. 이후에도 PID 관련한 부분이 반복되고 CryptDestroyKey()와 CryptReleaseContext() 등을 호출한다. 다음으로는 실제 Import Table을 복구한다. 즉 원본 프로그램에서 사용하는 API들을 위한 Import Table이 복구된다.


  이제 GetTickCount()를 통한 안티 디버깅 기법이 나온다. 결과 값이 들어올 EAX의 값을 아래의 비교문에서 비교할 값보다는 적게 맞추어야 한다.


  이제 끝이 다가오면서 BlockInput()을 또 수행하고(이미 이 API 내부를 수정하였기 때문에 추가적으로 할 일은 없다) GetCurrentProcess(), SetPriorityClass(), SetWindowLongA()를 수행한다. 이후에도 이전에 나왔던 IsDebuggerPresent() 외에도 GetCurrentProcessId(), SoftIce 관련 안티디버깅 등이 나오고 어느 정도 진행되다가 핸들러의 주소로 OEP를 넣고 예외를 발생시킨다. 이제 핸들러로 가게 되면 그곳이 OEP가 된다.



4. MUP

  특이점으로는 MUP가 끝난 바이너리는 원본 파일과 같다는 점이다. 다른 상용 프로텍터들과는 달리 Stub을 지난 후에도 안티 디버깅 기법들이 삽입되어 있거나 수정되어 있지 않다는 점에서 일반적인 패커와 비슷한 느낌이 든다.

'악성코드 분석' 카테고리의 다른 글

윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
패커들 분석  (5) 2017.04.23
윈도우의 자료형 정리  (1) 2017.04.23
Ollydbg [올리디버거] 2.01 매뉴얼  (1) 2017.04.23
Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.4
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

최근에 올라온 글

최근에 달린 댓글

글 보관함