이번에 할 건 계산기를 후킹하는 것이다.
타깃 : calc.exe
목적 : 숫자를 입력하는데 출력되는 화면엔 한글로 바뀌도록 함
( 1-> 일)
나는 아직 리버싱을 잘하지 못한다. 교재를 보고 따라하는 정도니까. 교재를 따라할 때마다 가끔씩 '실전에선 이걸 어떻게 알고 하라는거지' 싶을 때가 있는데 그게 바로 시스템에서 사용하는 api들을 척척 말할 때다.
후킹을 하고 싶다면, 내가 원하는 기능과 관련 있는 api가 뭔지 알아야 한다.
| 기능 | API |
|---|---|
| 파일 생성 | kernel32!CreateFile() |
| 레지스트리 생성 | advapi32!RegCreateKeyEx( ) |
| 네트워크 접속 | ws2_32!connect( ) |
| 텍스트 에디터에 글씨를 써줌 | user32.SetWindowText( ) |
등등을 알아야 한다. 내가 원하는 기능을 담당하는 api가 뭐지 검색해서 찾을 줄 알아야 한다. 때로는 공개된 API가 아니라 비공개된 undocumented API를 후킹할 때도 있다.
그리고 내가 원하는 기능을 정확하게 표현해낼 줄 아는 것도 중요하다.
테크맵에서 어떻게에 해당하는 테크닉으로는 dll인젝션을 고를 것이다.
장점. 동작원리와 구현이 간단하다.
원하는 api를 내가 DLL에 새로 수정하고 프로세스에 인젝션
단점. 프로세스가 임포트하는 API들만 후킹할 수 있다.
동적으로 dll로딩해서 사용하는 API는 이 방법이 불가능하다.
우리가 필요로 하는 기능은 텍스트를 화면에 출력해주는 것이다.
계산기가 사용하는 api들을 모두 보자.
PE View로 .text 섹션에서 Import Address Table에 가보면 참조되는 API 목록을 볼 수 있다.
이 중에서 뭔가 이름이 텍스트 출력에 관계 있어 보이는 걸 찾는다.
하나.
둘.
설명하자면 SetDlgItemTextW( )가 내부에서 호출하는게 SetWindowTextW( )이다.
SetWindowTextW( )는 이렇게 생겼다.
BOOL SetWindowText(
HWND hWnd,
LPCTSTR lpString
);
첫번째 인자로 텍스트창 핸들이랑 문자열의 포인터를 받는다. 저 문자열 파라미터인 lpString만 우리가 바꿔치기 하면 되지 않을까?
api 이름 뒤에 W가 붙는 것은 이 API의 여러 버전 중에서 Wide Character 버전이다.
뒤에 A가 붙는 것들은 어떤 API의 여러 버전 중에서 ASCII Character 버전이다.
참고로 윈도우 운영체제 내부에서 쓰이는 wide character는 유니코드이다.
x96dbg로 열어서 계산기를 실행시켜보고 호출되는 dll을 봤다.
여깄네. 여기에 브레이크 포인트를 걸어주고 실행시켜보자.
call 위에 파라미터가 쌓여있겠지. 두번째 파라미터를 먼저 push하니까 저 1002621 명령어가 문자열을 넣는 것일거야.

스택을 보니까 esp에서 0x10만큼 가니까 "0"에 해당하는 것이 적혀있다.
계산기 처음 실행하면 보여줘야 할 숫자가 0이니까 그런 것이다. 저 [esp+10]에 해당하는 오프셋 DFB10으로 가보자. DWORD 덤프 따라가기 하면 된다.
문자열 0 들어있는 것 맞네.
이제 실행시키면 계산기에 0이 뜬채로 나온다.
이 숫자를 7로 바꿔보자.
7 누른다고 화면에 바로 7나오지 않는다. 실행을 안 시켰으니까.
다만 스택에 들어간 파라미터의 문자열이 7로 바뀌어있다.
그리고 오프셋에서도 찾아볼 수 있지.
이걸 한글 '칠'로 바꾸자. '칠'의 유니코드는 CE60.
리틀 엔디언으로 60 CE라고 적어야 한다.
뭐 HxD 같은걸로 해도 되는데 여기서 바로 수정하자. 스페이스바 누르면 돼.


이제 실행해보자.

짠.
이렇게 타깃 API에 대한 검증을 마쳤다.
먼저 api가 호출되는 원리부터 보자
프로세스가 메모리에 로드되면 IAT 영역에 API 주소를 기록해둔다.
코드 영역에서 CALL [IAT 주소] 를 통해서 IAT 주소에 저장된 값을 호출한다.
[IAT 주소]엔 결국 API 주소가 있으니 CALL API 주소가 된다.
실행이 user32.SetWindowTextW( )로 갔다가 호출이 끝나면 다시 원래 코드 영역으로 돌아간다.
IAT가 후킹되면 어떤 과정이 되느냐, 결론부터 말하면 프로세스의 코드영역과 API 영역 사이에 공격자가 끼워넣은 DLL이 추가된다.
프로세스가 메모리에 로드되면 IAT 영역에는 API 주소가 적혀있다. 그런데 이때 이 API는 기존 API가 아닌 공격자가 심어둔 DLL의 API다.
프로세스 코드 영역에서 CALL [IAT 주소]를 하게 되면 공격자가 심어둔 Injected DLL의 API로 이동한다.
> 이 API가 공격자가 원하는 어떤 작업을 하고 원래 진짜 불러야 했던 API를 호출한다.
원래의 SetWindowTextW( )가 동작하고 여기서 제어가 끝나면 다시 제어가 Injected api로 돌아간다.
여기서의 흐름마저 끝나면 최종적으로 프로세스의 코드 영역으로 돌아간다.
우리가 해야할 건 맨 처음에 IAT 영역에 api주소를 원래 주소 4바이트에서 우리가 심어둘 dll.api의 주소 4바이트로 고치면 되는 것이다.

계산기 pid는 1576이다.

이렇게 후커를 실행해주면

계산기에 hookiat.dll이 설치된걸 볼 수 있다.

이제 계산기에는 어떤 숫자를 입력해도 한글만 나온다. 물론 연산도 다 잘 작동한다. 출력만 건드린거니까.
언훅은 아까 커맨드에서 i 옵션을 e로 바꾸면 된다.
그럼 다시 숫자로 잘 나온다.

injectDll은 dll injection에서 사용한 것과 비슷하다. 이따가 다시 보고 와야지.
더 자세히 볼 건 hookiat.dll의 코드인 hookiat.cpp이다.
전에 dll이 로드되면 제일 먼저 하는건 dllmain이라 했지.

간단하다.
SetWindowTextW의 주소를 전역변수인 g_~~~에다가 잘 보관해준다. 마지막에 오리지널 api 부를때도 쓰고 언훅할 때도 써야하니까.
이때 GetProcAddress를 쓴다는 건 해당 프로세스에 내가 보관하려는 dll이 제대로 로드되어 있는게 맞는지 확실해야 한다.
hook_iat 함수로 iat를 후킹하네. 원래의 api 주소를 MySetWindowTextW 주소로 바꿔치기 한다.
여기까지가 DLL이 로드되는 DLL_PROCESS_ATTACH 이벤트에서 벌어지는 작업이다.
언훅 함수를 새로 쓰는게 아니라 그냥 아까 hook_iat 함수에서 파라미터 인과를 바꿔주면 되겠지.
우리가 원하는 작업인 숫자를 한글로 바꿔쳐내는 이 작업은 어떻게 이루어지고 있을까

생각보다 별 거 없고 심플하지
입력된 숫자의 길이만큼 반복문을 돌면서 숫자를 한글로 바꿔주면 된다. 이건 숫자랑 한글이 각각 2바이트로 딱딱 매칭시킬 수 있기 때문에 가능하다.

반복문이 끝나면 원래 불러야 할 api를 불러야 하니까 아까잘 보관해둔 오리지널 api 주소를 호출한다.
이제 실제 iat 후킹을 해주는 hook_iat 함수를 보자.
꽤 긴데 내가 주석을 좀 짧게 한줄로 바꿈

여기는 ImageBase -> PE 시그니처 -> Import Directory Table까지 따라가는 코드다.
IDT는 IID(IMAGE_IMPORT_DESCRIPTOR) 구조체들의 배열이라고 했지.
우리가 원하는건 저기 마지막 pImportDesc 변수에 계산기 프로세스의 IDT의 첫번째 IID 구조체의 시작주소가 저장되도록 하는 것이다.

그 PE View로 보면 더 잘 보이는데 .text 섹션에 IMPORT Directory Table에 가면 IID들 나열된거 보이지. 그 첫번째 구조체의 시작 주소가 RVA 12B80이잖아. 이걸 pImportDesc 변수에 집어넣겠다는 거야.
우리가 찾는 타깃은 user32.dll의 SetWindowTextW( )였지.
우선 그럼 user32.dll로 가야하니까 user 32.dll의 IAT를 보자.

IID 시작 주소는 12BE4, IAT주소가는 12BF4에 적혀있네.
이제부터 hook_iat 함수 코드를 다시 보자.

for문을 돌면서 szDllName()이랑 pImportDesc->Name를 비교하면서 szDllName("user32.dll")이랑 매치될 때까지 돌면서 user32.dll의 IID 구조체 시작주소를 찾아낸다.
그렇게 되면 pImportDesc가 00012BE4가 되는 것이다.
그 다음 pImportDesc->FirstThunk 를 통해서 IAT를 가리키게 된다.
** 사실 이거 어떻게 이렇게 동작하게 되는지 모르겠어. 말로 풀어쓴 문장은 이해되는데 "USER32.DLL"이 szDllName에 어떻게 들어가게 됐을까.
마지막 저 pThunk에 user32.dll의 IAT의 RVA 주소인 10A4가 들어간다.

RVA 10A4 가보면 여기가 USER32.DLL이 임포트하는 API들 주소가 담겨있다. 우리가 찾는 API를 찾자.

미츠케타(찾았다)
다시 그다음 코드를 볼게.

아까 큰 for문 안에 있던 작은 for문이 있다.
큰 for문에서 pThunk -> u1.Function이랑 pfnOrg에 있던 77CF61C9 SetWindowTextW 시작주소랑 비교해내서 정확하게 SetWindowTextW의 iat주소인 1001110을 구해낸다.
** 뭐 어케 그게 되는거야 ;;

pThunk를 pfnNew로 바궈친다. 그럼 계산기가 user32.SetWindowTextW( )를 호출하면 실제로는 hook_iat.MySetWindowTextW( )가 호출되는 것이다.
아오 길어