반응형

nfofilemaker2.2.zip
0.31MB

 

die_win64_portable_3.06.zip
18.58MB
die_winxp_portable_3.06.zip
15.52MB

 

주제

- Stolen Byte


 

이번에는 Stolen Byte 라는 것을 메인으로 다루고, 바이너리를 언패킹 할 때 이용하는 Rebuild Import에 대해서 조금 더 다룬다.

 

Stolen Byte는 원래의 바이너리에서는 사라졌지만, 패커의 일부분처럼 동작하기 위해 패커 코드 내에 사용된 코드를 말한다.

 

OEP를 찾은 뒤 바이너리를 덤프하면 이런 byte들은 덤프된 바이너리에 존재하지 않거나 원래 있어야 하는 자리에 존재하지 않고 이상한 자리에 존재한다.

 

이렇게 되어 버리면 바이너리를 실행했을 때 Crash가 발생 되는데, 일종의 안티 디버깅 기법이라 할 수 있다.

 

그러나 패킹된 상태에서는 크래시가 발생하지 않는데, 이유는 이런 Stolen Byte들이 OEP에 도달하기 전에 패커 코드 내에서 사용되기 때문이다.

 

 

어떤 패커가 사용되었는지 알아보기 위해 Detect it Easy 툴을 사용한 결과 위와 같이 Borland Delphi라고 나왔다.

 

당연히 오탐이다.

 

 

디버거에서 해당 바이너리를 열면 패킹되어 있기 때문에 위와 같은 경고 창이 나타나는데, 아니오를 눌러 코드 조사를 하지 않는 것이 좋다.

 

 

패킹된 바이너리의 EP를 보면 위와 같다.

 

4B2278 주소에 PUSHFD 명령어가 있는데 4B2278 주소로 분기하고 있다.

 

PUSHFD 명령이 실행된 후 4B2279 주소에서 PUSHAD 명령어가 보이기 때문에 일반적으로 언패킹에 사용되는 ESP Trick을 사용한다.

(PUSHFD 명령어는 flag즉, 상태레지스터들의 값을 스택에 저장한다.)

 

 

4B2278 주소의 PUSHFD 명령어가 실행되면 ESP 값이 변경되는데 그 값은 12FFC0이다.

 

ESP 레지스터를 선택 후 우클릭 -> Follow in Dump -> 위와 같이 12FFC0 주소의 데이터 4byte 드래그 -> 우클릭 -> Breakpoint -> Hardware, on access -> Dword

 

위의 작업을 통해 하드웨어 BP를 설치하고 F9 키를 눌러 실행한다.

 

 

그러면 위와 같이 4C3550 주소에서 멈추게 된다.

 

"PUSH xxxxxxxx + Return" 형태의 명령어가 보이는데 이 형태는 "jmp xxxxxxxx" 형태와 동일한 의미로 해석될 수 있다.

 

그렇기 때문에 위 사진의 명령에서는 48ED30 주소로 분기하는 것이다.

 

 

F8 키를 눌러 48ED30 주소로 분기하면 위와 같다.

 

OEP처럼 보이긴 하지만 명령어 중간 중간에 NOP 명령어가 삽입되어 있어서 약간 이상한 형태이다.

 

어쨌든 코드를 분석해보면 ECX 레지스터에 7을 넣고, 2개의 0이 스택에 저장된 후 ECX 레지스터 값을 감소하고 48E9D5 주소로 점프한다.

 

 

48E9D5 주소는 위와 같은데 OEP 부분으로는 보이지 않는다.

 

하지만 수행되는 명령어를 보니 OEP로 추정되었던 이전 사진과 비슷한 명령어 PUSH 0과 DEC ECX 명령어들이 수행되는 것을 볼 수 있고

 

분기된 주소의 약간 위쪽을 보면 꽤 많은 NULL 바이트들이 존재하는 것을 볼 수 있다.

 

해당 NULL 바이트가 존재하는 이유는 그냥 우연이라고 생각할 수도 있는데, 도대체 OEP는 어디있길래 아직 보이지 않은 것일까

 

 

바로 이런 것들이 기본적인 Stolen Byte 예제이다.

 

패커나 프로텍터가 특정한 Byte를 선택한 뒤 바이너리 내 어딘가(빈 공간 또는 패커 코드 공간)에 위치시킨 것인데

 

위 사진의 경우 선택된 byte들이 암호화 되거나 패커 코드와 섞여 있지 않기 때문에 매우 기초적인 형태라고 할 수 있다.

 

이런 상황에서는 추가적인 코드(추가 섹션)를 덤프한 다음에 원래의 OEP와는 다른 새로운 OEP를 생성하는 것이 일반적인 해결 방법이다.

 

이번 경우는 원래 OEP를 올바르게 다시 만드는 부분만 주의해서 분석하면 된다.

 

 

일단 분기된 곳 주소의 명령어를 간단하게 해석하면 아래와 같다.

 

PUSH 0

PUSH 0

DEC ECX

JNZ 분기문에 의해 ECX의 값이 0이 아닌 동안 48E9D0 주소로 점프하여 PUSH 0 명령어 수행

 

ECX 레지스터는 루프 내에서 몇 번 돌아야 하는지 알려주는 카운트 변수로 사용된다.

 

ECX 값이 0이 아닌 이상 JNZ 분기문은 계속 루프 내에서 실행될 것이다.

 

이전에 ECX 레지스터의 값이 7이였기 때문에 7번의 루프가 진행될 것이고, 이는 PUSH 0 명령어가 14번 실행된다.

 

이런 정보를 기반으로 추측하면 위 사진의 부분이 원래 OEP를 재건하는 부분일 수도 있다.

 

키보드에서 ' - ' 대쉬 키를 눌러서 Stolen Bytes 부분으로 이동한다.

 

 

위 사진이 OEP 부분으로 추정했던 부분이다.

 

몇 개의 byte가 stolen 되었지만 새로운 OEP를 생성하기 위해서는 위 사진의 코드에서 몇몇 명령어를 이용해야 한다.

 

몇 번의 삽질을 해보니 위 사진의 주소에 새로운 OEP를 생성하는 것이 제일 좋은 방법이라 판단되었고, 사용하고자 하는 주소에 잉여 byte가 있다는 것은 꽤 편리한 상황이다.

 

 

원래의 OEP를 재건하는 방법 말고도 48ED30 주소에 새로운 OEP를 생성하는 방법도 있다.

이를 위해서는 바이너리를 덤프 뜨고, 덤프된 바이너리를 부분적으로 수정한 뒤 해당 바이너리를 재빌드하면 된다.

 

 

하지만 원래의 바이너리와 최대한 동일하게 만들기 위해 OEP를 재건하는게 오래 걸리지 않을 때는 원래의 OEP를 재건하는 것을 선호한다.

 

어쨌든 생성하고자 하는 새로운(가짜) OEP와 "Stolen Byte"는 바이너리가 실행될 때 생성 될 것이다.

 

사실은 이런 새로운(가짜) OEP를 생성하기 위해 언패킹 루틴이 실행되는 동안 원래의 Byte를 훔치는(Stolen Byte를 생성하는) 것도 언패킹 코드이다.

 

 

위와 같이 48ED30 주소 선택 -> 우클릭 -> Binary -> Binary copy

 

 

48ED60 주소 선택 -> 우클릭 -> Binary -> Binary paste

 

 

위의 방법을 통해 48ED32 주소와 48ED35 주소의 명령어를 복사해 위와 같이 만들어 준다.

 

 

그리고 나서 위와 같이 48ED60 주소부터 48ED63 주소까지 드래그 한 후 -> 우클릭 -> Binary -> Binary copy 를 선택한다.

 

Binary copy를 수행하고자 하는 부분이 바로 Stolen Byte여야 한다.

 

왜 그런지 이해가 되지 않는다면 맨 위로 올라가 다시 설명을 읽어본다.

 

 

OEP로 추정됐던 코드 루틴의 마지막을 보면 위 사진과 같이 48E9D5 주소로 분기된다.

 

 

48E9D5 주소를 보면 위와 같은데 해당 주소 위쪽을 보면 사용할 수 있는 NULL byte가 존재하는 것을 볼 수 있다.

 

이 NULL 바이트가 존재하는 것은 우연의 일치지만 어쨌든 꽤 유용하게 이용될 수 있다.

 

해당 NULL 바이트에 새로운 OEP를 생성한다.

 

 

48E9C8 주소를 선택 후 -> 오른쪽 마우스 -> Binary -> Binary paste를 선택하여 위와 같이 만든다.

 

위 사진이 바로 특정 바이트가 stolen(훔쳐진 상태) 되기 전에 있었던 원래 OEP의 형태이다.

 

새로운 OEP를 만들었으니 해당 주소를 이제 OEP로 인식시켜야 한다.

 

 

48E9C8 주소를 선택 후  -> 우클릭 -> New origin here 선택

 

new origin here 기능은 EIP 레지스터를 변경한다.

 

이번 경우에는 해당 기능을 이용함으로써 프로그램이 Stolen byte 부분 말고 OEP를 기준으로 실행되게 만들어 준다.

 

 

변조된 이전의 OEP를 재건하지 않고, 새로운 OEP를 생성하고 인식 시켰으니 이제 덤프를 뜬다.

 

이번에는 ollydump 플러그인을 사용하지 않고 PE Tools를 이용할 것인데, 플러그인이 덤프에 실패하더라도 PE Tools는 덤프가 성공적으로 잘 될 때가 종종 있다.

 

 

PE Tools를 실행한 후 위와 같이 대상 프로세스명을 마우스 우측 클릭 -> Dump Full 클릭한 후 파일 이름을 정한 후 저장한다.

 

PE Tools를 이용해 덤프를 완료했다면, 이제 Import 테이블을 수정해야 하는데, 덤프를 뜰 때 "Full dump : paste import table from disk" 옵션이 해제된 상태에서 덤프를 떠서 변조된 Import 테이블이 같이 덤프되었기 때문이다.

 

 

위와 같이 Import REC 툴에서 대상 프로세스를 선택한 후 

 

 

위와 같이 OEP 값을 8E9C8로 수정 후 AutoSearch 버튼을 누르면 RVA와 Size 값이 자동으로 수정된다.

 

그런데 Size 값이 4(bytes)이다.

 

딱 한개의 DLL만 Import 된다는 의미인데, 말이 안되므로 Import Rect에서 어떤 걸 찾았는지 알아내야 한다.

 

 

디버거의 dump창에서 492018 주소로 이동하여 어떤 값들이 존재하는지 보면 위와 같다.

 

모든 값이 00으로 되어 있다.

 

아마도 패커에서 Import 테이블을 조작한 것으로 보이는데, 다른 방법을 통해 Import 테이블을 찾아야 한다.

 

바이너리 안에 jump 테이블이 있는데, 이 테이블은 opcode가 FF 25로 시작한다.

 

그렇기 때문에 바이너리 검색 기능에서 FF 25를 검색하는 방법으로 진행한다.

 

 

디스어셈블리 창에서 우클릭 -> Search for -> Binary string을 선택하거나 아니면 Ctrl + B 키를 누른다.

 

 

그리고 위와 같이 입력 후 OK 버튼을 누른다.

 

 

그러면 위와 같이 JMP 명령어들이 모여있는 jump 테이블로 이동되는데

 

검색 결과를 보니 정상적인 jump 테이블을 찾은 것을 알 수 있다.

 

가장 위쪽에 위치하는 CloseHandle 함수의 주소를 덤프창에서 이동한다.

 

 

이동된 곳을 보니 위와 같은데, IAT 부분이 맞는 것 같다.

 

덤프 윈도우에서 IAT를 알아보는 것은 Windows XP 기준으로 대부분의 IAT 내의 주소들은 "7x xx xx xx" 값을 가진다.

 

Import REC에서 값을 수정하기 위해서는 IAT의 시작과 끝 주소를 알아야 한다.

 

 

IAT의 끝 주소를 알아내기 위해 dump 창에서 스크롤을 쭉 내리다 보면 위와 같이 49384C 주소 부분에 "7x xx xx xx" 형태의 주소가 끝나는 지점과 우측의 ASCII 창에서 "kernel32.dll" 문자열이 보여지기 전 부분을 적당히 보면 끝 주소를 찾을 수 있게 된다.

(위 사진에서 드래그 된 00 00 00 00 부분의 주소는 493854이다.)

 

위 사진에서 IAT 끝 주소 바로 직전에서 보여지는 6E170010 값으로 인해 왜 493854 주소 부분이 IAT의 끝 부분인지 이해가 안 될 수도 있는데

 

해당 주소는 Updsystem.dll(어플리케이션에서 제공하는 dll)로부터 import 되는 ShowUpdateDialog 함수의 주소이다.

 

Import REC에서 해당 DLL을 Import 하지 않아도 프로그램이 실행되면서 자동으로 Import 될 것이다.

 

자세한 이해를 위해 디스어셈블리 창에서 493850 주소의 코드를 봐본다.

 

 

493850 주소의 명령어는 위와 같다.

 

이전 덤프 창에서 보았던 주소 값과 동일한 OPCODE인 6E170010을 가지고 있다.

 

해당 UpdSystem.dll은 Import 하지 않아도 자동으로 Import 된다.

 

이제 IAT의 시작 주소를 찾는다.

 

 

IAT의 시작 주소를 찾는 법은 더 간단한데 그냥 dump 창에서 스크롤을 쭉 올리다가 "00 00 00 00 + XX XX XX 7X" 형태를 찾으면 된다.

(위 사진에서 드래그 된 부분의 주소는 493168이다.)

 

이제 시작과 끝 주소를 알아냈으니 Import REC에서 사용할 IAT 사이즈를 구한다.

 

0x3854 - 0x3164 = 0x6F0이다.

 

 

위와 같이 RVA와 Size의 값을 변경하고 Get Imports 버튼을 눌러 DLL 파일이 제대로 Import 되는지 본다.

 

 

위와 같이 Import 되는 모든 DLL 파일의 상태가 Valid:YES 상태이다.

 

이제 이전에 덤프 뜬 바이너리를 대상으로 IAT를 변경한다.

 

 

Fix Dump 버튼을 눌러 위와 같이 덤프 떴던 파일을 선택 후 적용한다.

 

 

그러면 위와 같이 성공 로그가 나오고

 

 

PE Tools에서 위와 같이 Rebuild PE 기능을 이용해 IAT를 수정한 대상 바이너리를 선택하여 PE 헤더를 재빌드하면

 

 

위와 같이 뜨는데 9%나 파일 사이즈가 줄어들었다고 한다.

 

 

 

IAT가 수정 후 PE 헤더를 재빌드 한 바이너리를 디버거에 열면 위와 같이 정상적인 EP에서 프로그램이 성공적으로 실행된다.

 

 

반응형

+ Recent posts