20.1 인라인 패치
인라인 코드 패치 (혹은 인라인 패치) 기법이란?
원하는 코드를 직접 수정하기 어려울 때 간단히 코드 케이브라고 하는 패치코드를 삽입한 후 실행해 프로그램을 패치시키는 기법이다. 주로 대상 프로그램이 실행 압축(혹은 암호화)되어 있어서 파일을 직접 수정하기 어려운 경우 많이 사용된다.
ㄴ그림 20.1 인라인 코드 패치
위 그림처럼 EP 코드의 복호화 과정 이후 JMP 명령어를 수정하여 패치코드로 가서 실행시킨 후 OEP로 가는 식이다.
그래서 인라인 패치는 코드 패치와는 사뭇 다르다. 인라인 패치는 프로그램이 실행될 때마다 패치된다. 그리고 코드 패치와 다르게 파일과 메모리 모두를 패치하고, 원하는 위치에 직접 패치하는 것이 아닌 Indirect(패치 코드를 미리 설치한 후 메모리에서 원하는 영역이 복호화되었을 때 패치)로 패치한다.
20.2 실습 - Patchme
ap0x라는 리버서가 제작한 patchme 예제이다. (리버싱 연습을 위해 완전 공개된 프로그램이다.)
먼저 실행파일을 실행시켜 어떤 파일인지 확인한다.
ㄴ 그림 20.2 메시지 박스
실행하면 위와 같이 메시지 박스가 나타난다. 메시지를 읽어보니 문자열을 패치하라고 한다. 확인 버튼을 누르면 아래와 같은 다이얼로그가 나타난다.
ㄴ 그림 20.3 메인 다이얼로그
다이얼로그를 읽어보니 자신을 unpack하라고 한다.
우리의 목적은 위 두 군데의 문자열을 바꾸는 것이다. (가령, You must patch this NAG !!!는 ReverseCore로, You must unpack me !!!는 unpacked로 바꾼다.)
20. 3 디버깅 - 코드 흐름 살펴보기
OllyDbg로 해당 실행 파일을 열어본다.
ㄴ 그림 20.4 EP 코드
실행하면 바로 EP 코드가 보인다. 401007 주소부터는 코드가 암호화되어있다. 모든 문자열이 암호화되어 있기 때문에 'Search for All referenced text strings'를 눌러도 위에서 본 두 문자열은 나오지 않는다.
ㄴ 그림 20.5 복호화1 코드
401001 주소의 CALL 함수에 들어가 또 CALL함수를 따라 들어가다 보면 위와 같은 복호화1 루프가 나온다. (총 154번 돈다.)
4010A3 주소에 있는 XOR 명령어를 통해 (4010F5~401248)의 복호화를 진행한다.
154번의 복호화가 끝나고 4010B0 주소의 CALL을 따라 들어가면 다음과 같은 코드가 나온다.
ㄴ 그림 20.6 복호화2,3 코드
(4010C8~4010D2) 주소가 복호화2 루프이고, (4010DB~4010E5) 주소가 복호화3 루프이다.
복호화2에서는 (401007~401085) 영역이 복호화되고, 복호화3에서는 (4010F5~401248) 영역이 복호화된다.
이 때, 복호화1과 복호화3에서 복호화하는 영역이 겹치는 것으로 보아 이 영역은 "이중으로 인코딩"되는 영역이다.
복호화2,3을 다 하고 나서 4010B6 주소의 CALL 함수로 들어가면 다음과 같은 코드가 나온다.
ㄴ 그림 20.7 Checksum 계산 영역
이 코드는 Checksum 계산 코드이다. (401041~40104F)
CHecksum 계산 방법은 특정 영역의 코드/데이터가 변조되지 않았음을 검증하는 용도로 많이 사용된다. (그렇기 때문에 해당 영역의 코드/데이터를 변경할거면 관련 Checksum 비교 부분도 수정해야 한다.)
EDX를 0으로 초기화하고, ADD 명령으로 특정 주소 영역의 값을 읽어들여 EDX 레지스터에 덧셈 연산으로 누적시킨다. 루프를 종료하면 EDX에는 특정한 값이 저장될 것이고, 이 값이 바로 Checksum 값이 된다.
401062 주소에서 CMP 명령을 통해 Checksum 값이 31E8DB0과 같다면 코드가 변조되지 않은 것으로 알고 OEP로 JMP한다. 값이 다르면 에러 메시지(CrC of this file has been modified !!!)를 띄우고 프로그램이 종료된다.
ㄴ 그림 20.8 OEP 코드 - 다이얼로그 실행
OEP 코드가 나타나있다. 만약 이 주소로 왔는데도 이 코드가 나타나지 않는다면, Ctrl+A를 누르면 나타난다. (단, 복호화를 다 하고 나서 눌러야 OEP 코드가 나온다.)
40123E 주소를 보면 다이얼로그를 실행시키는 코드가 함수 명령이 있다. 그리고 40122C 주소를 보면 DIgProc 주소가 4010F5라는 것을 알 수 있다.
ㄴ 그림 20.9 문자열 코드
4010F5 주소로 오면 익숙한 문자열들이 보인다. 바로 파일을 실행시켰을 때 나오는 메시지들이다. 좀 더 아래로 내려보면 우리가 패치해야 할 문자열들이 보인다.
ㄴ 그림 20.10 패치해야 할 문자열들
이렇게 우리가 패치해야 할 문자열들을 모두 찾았다. 이 프로그램은 곳곳이 암호화되어 있으며, 특히 패치하려는 문자열 부분은 이중으로 암호화되어 있으며, 심지어 Checksum값 비교를 통해 문자열의 변경 여부까지 검증하기 때문에 쉽게 수정하기 어렵다. 이런 식의 프로그램은 일반적인 패치보다는 인라인 패치 방법을 이용하는 것이 더 쉽다.
20.4 코드 구조
ㄴ 그림 20.11 코드 구조
이 프로그램의 대략적인 코드 구조는 이렇다. [A], [B], [C] 영역은 암호화되어 있는 곳이고 [EP Code]와 [Decoding Code]부분 영역에는 암호화를 해제하는 복호화 코드가 존재한다. [EP Code]는 단순히 [Decoding Code]를 호출하는 역할만 하며, 실제 디코딩은 [Decoding Code]에서 이루어진다.
[B] -> [A] -> [B] 순서로 디코딩(XOR)되고, 암호가 해제된 [A] 영역의 코드를 실행한다.
20.5 인라인 패치 실습
인라인 패치의 작업 순서를 간단히 설명하자면,
- 파일의 적절한 위치에 문자열을 패치시키는 코드를 삽입한다.
- 그림 20.11의 [A]영역에 있는 JMP OEP 명령을 JMP Patch Code로 수정한다.
2번을 하는 이유는 그림 20.1의 오른쪽 그림처럼 패치하기 위해서다. OEP를 가기 전에 패치코드로 가서 패치코드를 실행한 후, OEP로 JMP한다.
프로그램이 실행되어 [A] 영역에 있는 JMP Patch Code를 만났다는 것은, 모든 코드가 복호화되어있고 Checksum을 통과했다는 말이기 때문에 패치코드에서 문자열을 변경한 후 OEP로 JMP하면 인라인 패치가 완성된다.
20.5.1 패치 코드를 어디에 설치할까?
20.5에서 작업 순서를 설명했다. 1번이 파일의 '적절한 위치'에 패치 코드를 삽입하는 것이라고 했는데, 그 적절한 위치는 어디일까? 인라인 패치에 있어서 중요한 질문이다. 총 세 가지 정도의 방법이 있다.
- 파일의 빈 영역에 설치
- 마지막 섹션을 확장한 후 설치
- 새로운 섹션을 추가한 후 설치
패치코드의 크기가 작은 경우 1번을, 나머지의 경우 2번 또는 3번의 방법을 사용한다.
책에서는 1번 방법을 시도했으므로 여기서는 2번 방법을 시도할 것이다. 섹션은 두배로 확장하기로 한다.
일단 PEView로 파일의 .rsrc 섹션 헤더를 살펴본다.
ㄴ 그림 20.12 .rsrc 섹션의 헤더
보아하니 Size of Raw Data값은 800이고, 248에 위치한다. (이는 모두 16진수값이다.)
두 배로 확장해야 하니 800 -> 1000으로 늘리면 된다.
다음은 NT 헤더에 있는 OPTIONAL 헤더를 살펴본다.
ㄴ 그림 20.13 OPTIONAL_HEADER
Size of Image값은 4630이며 118에 위치한다. 4630에 800을 더하면 4E30이 나온다. (다시 말하지만 16진수이다.)
그럼 PEView에서 중요한 내용을 모두 찾았으니 HxD를 열어 마지막 섹션을 확장한다.
ㄴ 그림 20.14 Size of Image값 수정
ㄴ 그림 20.15 Size of Raw Data값 수정
마지막 섹션(.rsrc)를 두 배로 확장하려면 HxD에서 편집-바이트 삽입-800 입력 후 확인을 누르면 된다.
ㄴ 그림 20.16 마지막 섹션을 확장해 1BFF까지 늘어난 모습
이렇게 하면 마지막 섹션(.rsrc)을 두 배로 확장하는 작업이 모두 끝난다. 다른 이름으로 저장해 파일을 저장한다.
20.5.2 패치 코드 만들기
다시 OllDbg를 이용해서 실습 예제를 디버깅하고 빈 영역으로 간다. 우리는 마지막 섹션을 확장시켰으니 마지막 섹션의 빈 영역으로 가서 그 곳에 패치 코드를 써넣으면 된다.
빈 영역 주소는 그림 20.12의 RVA와 Virtual Size값을 보면 된다. RVA는 메모리의 시작 주소값이다. (파일이 메모리에 로딩되었을 때 주소값이 다르다. 파일은 offset을 쓰고, 메모리는 Virtual Adress를 쓴다.) 그리고 Virtual Size는 섹션에 내용이 얼마나 있는지를 나타낸다. 그러니까 RVA와 Virtual Size를 합하고(4000+630) 그 뒤의 영역은 NULL-Padding 영역이다.
이렇게 NULL 영역의 주소를 구했으니 OllDbg에서 404630으로 가서 확인해본다.
ㄴ 그림 20.17 404630 주소
예상대로 NULL 영역이 맞았다. 이 곳에 패치 코드를 아래와 같이 삽입한다.
ㄴ 그림 20.18 패치 코드
패치 코드는 매우 간단하다. 40128F 주소와 4012A0 주소의 REP 명령에 의해 문자열이 패치된다.
이 때 패치할 문자열은 덤프창에서 40465A와 404668로 가서 수정하면 된다.
(40463A) "You must patch this NAG !!!" -> (404635) "esReverseCore"
(404646) "You must unpack me !!!" -> (40464B) "esUnpacked"
(그림 20.18은 복호화하기 전 코드를 캡쳐해온 것이기 때문에 "You must..."의 문자열이 암호화되어 보인다. ReverseCore와 Unpacked 앞에 붙인 es는 이름 이니셜이다.)
주소 (404658~404671) 부분은 문자열 작업이므로 크게 신경쓰지 않아도 된다.
20.5.3 패치 코드 실행하기
인라인 패치의 마지막 작업으로 앞에서 만든 패치 코드가 실행되도록 파일을 직접 수정해야 한다. 인라인 패치의 작업 순서를 소개하면서 말했듯이, JMP OEP를 JMP Patch Code로 바꿔준다. JMP OEP 코드는 401083 주소에 있다.
ㄴ 그림 20.19 수정 전
ㄴ 그림 20.20 수정 후
여기서 주의할 것은 이 영역(401083)은 원래 암호화되어 있는 영역이라는 것이다. 앞서 설명한 복호화 과정에 따르면 이 영역은 [A]영역에 속하며 XOR 7로 암호화되어 있는 영역이다. (코드 흐름 참조)
그러니까 그림 20.20의 E9 A8 35는 복호화된 값으로, XOR 7을 하기 전의 값이다. HxD로 이 파일을 열었을 때는 암호화된 값이 나와야 한다.(HxD로 파일을 열면 실행을 하기 전 상태이므로 복호화를 하지 않았기 때문이다.)
E9 A8 35를 XOR 7 연산을 하면 EE AF 32가 나온다.
ㄴ 그림 20.21
다음과 같이 HxD에서 E9 A8 35를 EE AF 32로 수정한다.
이 모든 작업을 마치고 파일을 저장하면 인라인 기법을 이용한 패치 파일이 완성된다.
20.5.4 결과 확인
ㄴ 그림 20.22 결과 확인
ㄴ 그림 20.23 결과 확인
우리가 원한대로 문자열이 제대로 패치된 것을 볼 수 있다.
인라인 패치 기법은 향후 API 후킹에도 활용된다.
👉 참고한 내용
책 "리버싱 핵심 원리(악성 코드 분석가의 리버싱 이야기)" - 저자 이승원
"리버싱 핵심 원리" 소스코드와 실습 예제 파일