리버싱 핵심원리 1부 2장 정리
Note
본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리하였습니다.
책의 내용과 일부 상이할 수 있고, 이해를 돕기 위해 강좌 형식의 말투와 적절한(?) 예시를 추가하였습니다.
본 게시글에서 사용되는 소스 코드와 파일, 프로그램은 리버싱 핵심원리에서 제공하는 파일과 언급되는 것들을 기반으로 하며, 일부 상이할 수 있습니다.
Hello World! 프로그램
HelloWorld.c | |
---|---|
"Hello World!"
를 출력하는 프로그램은 모든 개발자가 프로그래밍 언어를 처음 배울 때 작성해보는 프로그램이 아닐까 싶습니다.
Quote
책의 내용과는 별개로 저는 첫 프로그래밍 언어로 C 언어를 독학으로 배웠습니다. 검은색 바탕에 흰색 텍스트만 출력되는 터미널 창에서 실행 결과를 보면서 배웠는데요... 정말 하기 싫었습니다. 당시엔 게임처럼 눈에 바로 보이는 그런 걸 만들고 싶었거든요. C#을 시작했어야 했네요... 그래서 프로그래밍 언어를 공부하다가 바로 그만두는 계기가 되기도 했답니다.
디버거와 어셈블리어
통합개발환경이나 컴파일러를 통해 소스 코드를 빌드하면 실행 파일이 생성됩니다. 이러한 과정을 거치는 이유는 C 언어는 사람이 이해하기 쉬운 고급 언어이고 이를 기계가 이해하기 쉬운 언어인 기계어로 번역해야하기 때문입니다. 기계어는 2진수나 16진수 등으로 표현되어 사람이 이해하기 어렵기 때문에 조금 더 이해하기 쉽도록 디버거(DebuggeR)라는 도구를 사용합니다. 디버거의 디스어셈블러(Disassembler)를 통해 기계어를 어셈블리어(Assembly Language)로 번역해 그나마 읽기 쉽고 이해하기 쉬운 언어로 번역해줍니다.
기계어 | 어셈블리어 |
---|---|
0xB8 0x04 0x00 0x00 0x00 |
mov eax, 4 |
어셈블리어 기준으로 EAX 레지스터에 4라는 값을 할당하라는 간단한 코드입니다. 이를 기계어로 보면 이해가 가시나요? 패턴을 외우면 보이겠지만... 쉬운 일은 아닐겁니다. (폰 노이만 당신은 도대체...)
HelloWorld! 디버깅
목표
HelloWorld.exe 실행 파일을 디버깅하여 어셈블리어로 변환된 main()
함수를 찾아볼 것입니다. 찾는 과정을 거치면서 디버거의 기본적인 사용법과 어샘블리 명령에 대해 배워봅시다.
디버깅 시작
OllyDbg는 더 이상 사용 안 합니다
올리디버거라 부르는 OllyDbg는 더 이상 사용하지 않습니다. 책의 내용이 오래된 탓도 있지만 OllyDbg의 개발자가 더 이상 지원을 안 해주는 이유가 더 큽니다. 대신 더 발전된 프로그램인 x64dbg를 사용합니다. 더 나은 인터페이스와 기능을 지원해주고 있고, 오픈 소스이자 무료로 공개 및 배포되고 있습니다.
ㅈ고수가 되었을 땐 IDA를...
일반적으로 실행 파일을 분석할 때 소스 코드가 없는 경우가 대부분이기 때문에 x64dbg와 같은 디버거를 통해 분석합니다.
x64dbg는 OllyDbg의 뒤를 잇는 디버거로(개인적인 생각), 오픈 소스이자 무료로 공개되고 있고 꾸준히 업데이트하여 지원해주고 있습니다. 더 나은 인터페이스와 기능 그리고 플러그인 기능을 제공해주고 있습니다.
본인의 리버싱 실력이 ㅈ고수에 버금 갈 정도로 올라갔다면 Hex-Rays의 IDA를 사용해보세요. 현존하는 디버거 중 매우 강력하다고 평가 받고 있고 디컴파일의 성능이 매우 좋습니다. 무료 버전과 유료 버전이 있는데 무료 버전으로도 충분합니다.
실행
x96dbg.exe 실행 파일을 실행하시면 이미지 1-1과 같이 런청 창이 나타납니다.
오늘 우리가 다루게 될 HelloWorld.exe 실행 파일은 32Bit의 환경에서 개발된 프로그램이기 때문에 런처에서 x32dbg 버튼을 클릭해주세요.
여러분의 모니터만큼이나(?) 자주 보게 될 x64dbg의 기본 화면입니다.
이미지 1-2에 보이는 기본 화면 구성에 대한 설명은 아래 표와 같습니다:
코드 창 | 레지스터 창 | 덤프 창 | 스택 창 |
---|---|---|---|
어셈블리 코드와 주석, 라벨 등을 표시하고 코드를 분석하여 루프 및 점프되는 영역을 시각화하여 표시합니다. | CPU의 레지스터 값을 실시간으로 표시하며, 일부 레지스터는 수정이 가능합니다. | 프로세서의 메모리 주소와 값을 16진수 또는 기타 인코딩 방식으로 표시 및 수정이 가능합니다. | ESP 레지스터가 가르키는 프로세서의 스택 메모리를 실시간으로 표시하고 수정이 가능합니다. |
파일 열기
HelloWorld.exe 실행 파일을 x32dbg가 실행된 프로그램에 드래그 앤 드롭()하거나 F3 키를 눌러 실행 파일을 직접 열어주세요.
실행 파일을 불러오면 책과는 다른 주소에서 멈춘 게 이미지 1-3에서 확인됩니다. 시스템 중단점에서 멈춘 것이기 때문에 책에 나오는 주소에서 멈추려면 설정에서 중단점 위치를 바꿔야 합니다.
설정 메뉴에서 환경설정을 클릭하신 후, 이벤트 탭에서 시스템 중단점 체크박스의 체크를 해제해주세요. 그리고 저장 버튼을 눌러주세요.
Ctrl+F2를 누르면 실행 파일을 다시 불러올 수 있습니다. 다시 불러오시면 책에서 언급하는 0x4011A0
주소에서 멈추는 것을 확인할 수 있습니다.
디버거에서 멈춘 해당 주소는 EP(EntryPoint)로(1), HelloWorld.exe 실행 파일의 시작 주소입니다. 해당 EP 코드에서 가장 눈에 띄눈 코드는 call
과 jmp
명령입니다.
- EP(EntryPoint)는 Windows 실행 파일(exe, dll, sys 등)의 코드 시작점을 의미합니다. 프로그램이 실행될 때 CPU에 의해 가장 먼저 실행되는 코드 시작 위치라 생각하시면 됩니다.
EP는 보통 프로그램의 메인 함수가 위치하는 곳이기 때문에 매우 중요한 역할을 합니다. EP의 위치는 이후 배우게 될 PE 헤더의 AddressOfEntryPoint에서 확인할 수 있습니다.
Address | Instruction | Disassembled Code |
---|---|---|
004011A0 | E8 67150000 | call 40270C |
004011A5 | E9 A5FEFFFF | jmp 40104F |
위 두 줄의 어셈블리 명령은 의미가 매우 명확합니다.
40270C
주소의 함수를 호출한 후40104F
주소로 점프(이동)하라!
계속 디버깅을 진행해서 main()
함수에서 MessageBox()
함수의 호출을 찾는 것을 목표로 해봅시다.
40270C
함수 따라가기
EP 코드인 0x4011A0
주소에서 F7 키를 누르면 0x40270C
의 함수 내부로 진입할 수 있습니다. F7 키는 Step Into이라 하여 하나의 명령을 실행하는데 call
명령을 만나면 해당 함수의 내부로 진입하도록 합니다.
갑자기 모든 걸 그만두고 싶어지는 코드가 나타났습니다. 우리는 아직 어셈블리어에 익숙하지 않기 때문에 모든 코드를 반드시 해석해서 알아야 할 필요는 없습니다.
xAnalyzer
과거 올리디비거에 경우 기본적으로 호출되는 API의 매개변수와 같은 정보를 보여준 것으로 보입니다. 하지만, 우리가 사용하는 x64dbg의 경우 별도 플러그인이 없으면 부가적인 정보를 확인하기 힘듭니다.
API의 호출 정보를 확인하려면 xAnalyzer의 설치가 필요합니다. 설치는 간단하기 때문에 생략합니다.
0x40270C
주소 라인에서 마우스 우클릭하시면 컨텍스트 메뉴가 나타납니다.
좌측 하단에 있는 xAnalyzer 메뉴에서 Analyze function 메뉴 아이템을 클릭해주세요. 해당 기능한 선택한 함수를 분석하여 호출되는 API의 매개변수 등의 정보를 표시해줍니다.
이미지 1-8처럼 사용되고 있는 API의 매개변수 정보가 표시되는 걸 확인할 수 있습니다.
그런데... 호출되는 API의 이름을 보면 우리가 작성한 소스 코드에는 없는 API입니다. 그렇다면 지금 우리가 보고 있는 0x40270C
함수는 우리가 찾고 있던 메인 함수가 아닌 걸로 보입니다.
사실 이 부분은 컴파일러가 프로그램의 실행을 위해 추가한 Stub Code입니다. 이 Stub Code는 사용하는 컴파일러와 버전에 따라 생성되는 모습이 다릅니다.
F8 키를 눌러 Step Over하여 0x4027A1
까지 진행합니다. Step Over는 Step Into와 다르게 call
명령을 만나면 함수 내부로 진입하지 않고 해당 함수를 실행합니다.
0x4027A1
주소에 ret
명령이 존재합니다. 이 명령은 함수의 끝에서 사용되는데 함수가 호출되었던 원래의 주소로 되돌아가도록 합니다. 해당 명령의 경우 리턴 주소는 0x4011A5
입니다.
0x4027A1
에서 Step Into 또는 Step Over하시면 이미지 1-4에서 보았던 0x4011A5
주소로 오게됩니다.
리턴 전까지 실행
0x4027A1
까지 Step Over하거나 Ctrl+F9 키를 누르면 ret
명령을 수행하기 전까지 명령을 실행합니다.
40104F
점프문 따라가기
0x4011A5
주소의 jmp
명령을 실행하면 0x40104F
주소로 이동하게 됩니다.
역시... 이번에도 프로그램을 꺼버리고 싶은 욕구가 드는 코드들이 나타났지만, 이 역시 컴파일러가 생성한 Stub Code입니다. 이 코드들을 따라가다 보면 우리가 찾는 메인 함수가 나옵니다.
메인 함수 찾기
단순 무식한 방법이지만 0x40104F
주소에서 부터 명령어를 하나하나 실행하다 보면 우리가 원하는 메인 함수를 찾을 수 있습니다. 단순하면서 무식한 방법이지만 이러한 과정은 리버싱을 처음 배우는 분들에겐 디버깅하는 데 있어 큰 도움이 됩니다.
0x40104F
주소에서부터 Step Over를 하여 call
명령을 만나면 Step Into하여 해당 함수의 내부로 진입해주세요. 그리고 해당 함수의 내부에서 MessageBox()
API가 호출되고 있는 지 확인해주세요. 왜냐하면 우리는 메인 함수에서 MessageBox()
API를 호출하는 걸 작성했기 때문입니다. 없으면 Execute till Return을 통해 함수를 바로 나오도록 합시다. 같은 방식으로 진행하다보면 0x4010E4
주소에 오게됩니다.
0x4010E4
주소에 있는 call <JMP.&GetCommandLineW>
명령은 Win32 API의 호출 코드입니다. 지금 우리가 배우고 있는 단계에서는 굳이 Step Into 할 필요 없습니다. 그리고 0x4010EE
의 call
부분을 Step Into해서 진입하면 내부의 반복문이 존재하기 때문에 탈출하는 데 시간이 꽤 소요됩니다.
별 무리 없이 디버깅하셨다면 이미지 1-12처럼 0x401144
주소를 만나게 됩니다. 0x401144
주소에 명령을 보면 call 401000
이 있습니다. Step Into로 해당 함수의 내부로 진입해봅시다.
MessageBoxW()
를 호출하는 명령이 있고 해당 API의 넘겨지는 매개변수의 값으로 문자열 "www.reversecore.com"
와 "Hello World!"
가 보입니다. 우리가 소스코드에서 작성한 내용과 일치하는 곳을 발견하였습니다. 맞습니다. 바로 이곳이 메인 함수입니다.
Hello World! 문자열 패치
Hello World!
의 문자열을 Hello Reversing
문자열로 출력되도록 패치해봅시다. 우리는 이 패치 기술을 이용해 기존 응용 프로그램의 버그를 수정하거나 기능을 추가하는 등 다양한 작업을 수행할 수 있습니다.
앞에서 메인 함수를 찾았기 때문에 메인 함수 주소(0x401000
)에 F2 키를 눌러 BP(BreakPoint)를 설정하고 F9 키를 눌러 실행해주세요. 그러면 메인 함수의 내부까지 진입하게 됩니다.
문자열 버퍼 수정
문자열을 수정하는 간단한 두 가지 방법이 있습니다. 문자열 버퍼를 직접 수정하는 것과 다른 메모리 영역에 문자열을 작성한 후 이를 불러오는 것입니다. 하나씩 살펴봅시다.
이미지 1-14를 보시면 "Hello World!"
의 문자열이 저장된 곳은 0x4092A0
주소입니다.
0x401007
주소 라인에서 마우스 우클릭하시면 컨텍스트 메뉴가 나타나는데, 덤프에서 따라가기(Follow in Dump) 메뉴에서 helloworld.004092A0 메뉴 아이템을 클릭해주세요. 그러면 좌측 하단에 있는 덤프 창에서 이동된 주소를 확인할 수 있습니다.
이미지 1-16을 보시면 Hello World!
문자열이 저장된 공간을 확인할 수 있습니다. 여기서, 문자열 버퍼의 공간은 0x4092A0
~ 0x4092B9
입니다. 유니코드 문자열로 저장되었기 때문에 한 문자 당 2Bytes씩 차지합니다.
이미지 1-17과 같이 0x4092A0
~ 0x4092BF
까지 마우스로 드래그하여 선택한 후 Ctrl+E를 눌러주세요.
이미지 1-18/span>과 같이 데이터 수정 창이 나타나는데, UNICODE에 작성된 Hello World!
문자열을 Hello Reversing
으로 변경하신 후 OK 버튼을 눌러주세요.
덤프 창에서 문자열 데이터가 변경되었는 지 확인하신 후, 마지막 2Bytes가 NULL 데이터로 되어 있는 지 확인해주세요. 유니코드 문자열은 마지막에 2Bytes 크기의 NULL 데이터로 끝나야합니다 .(0x00의 값 두 번)
코드 창으로 돌아와 xAnalyer의 기능을 다시 사용하면 Hello Reversing 문자열로 패치된 걸 확인할 수 있습니다. F9 키를 눌러 실행해봅시다.
실행 결과를 보시면 우리가 패치한 문자열이 출력되는 걸 확인할 수 있습니다.
문자열 공간
원본 Hello World!
문자열보다 Hello Reversing
문자열의 길이가 훨씬 더 깁니다. 보통 원본 문자열 길이 뒤쪽에 의미있는 데이터가 존재할 수 있기 때문에 원본 문자열의 길이를 넘어가는 문자열 데이터로 덮어쓰지 않습니다. 어디까지나 실습을 위해 그렇게 했을 뿐입니다.
파일 생성
위에서 우리가 수행한 패치 작업은 임시적으로 메모리의 내용을 수정한 것이라 디버거가 종료되면(HelloWorld.exe 프로세서가 종료되면) 패치했던 내용은 그대로 사라집니다. 따라서 우리가 변경한 내용을 그대로 저장하려면 x64dbg의 패치 기능을 이용해야 합니다.
마우스 우클릭하시면 Patches(패치)라는 메뉴를 찾을 수 있습니다. 단축키는 Ctrl+P입니다. 패치 기능을 통해 우리가 수정한 내역(패치 내역)을 파일에 적용할 수 있습니다.
패치 기능을 실행하시면 패치 창이 나타나는데, 우리가 수정한 내역이 나타납니다. 하단 우측에 있는 파일 패치(Patch File) 버튼을 클릭해주세요.
HelloWorld.exe 실행 파일의 사본을 만드신 후(백업) 사본 파일에 저장하시면 우리가 작업한 내역이 해당 파일에 그대로 적용됩니다. 패치된 실행 파일을 실행하여 Hello Reversing
문자열이 출력되는 지 확인해보세요.
다른 메모리 영역에 문자열 데이터 생성하여 전달하기
만약 원본 문자열보다 훨씬 긴 문자열 내용으로 패치해야 한다면 앞에 방법은 적절하지 않습니다. HelloWorld.exe를 다시 열어주신 후 메인 함수로 이동해주세요.
0x401007
주소를 보시면 PUSH
명령을 통해 MessageBoxW
함수에 문자열 매개변수를 전달하고 있습니다. MessageBoxW
함수는 매개변수로 넘겨진 주소의 문자열을 출력하기 때문에 이 주소를 변경해서 전달하면 변경된 문자열이 출력될 수 있습니다.
아이디어는 좋지만... 그 공간은 어디에?
어느 메모리 영역에 문자열 데이터를 작성해야 할까요? 이는 추후 배우게 될 PE 파일 구조와 가상 메모리에 대한 개념을 알고 있어야 하기 때문에 잠시 궁금증은 접어두기로하고 이번 실습에선 임의로 적절한 영역을 선택합니다.
덤프 창에서 원본 문자열 주소(0x4092A0
)로 이동하신 후 아래로 스크롤하다 보면 0x00(NULL)로 채워진 영역을 발견할 수 있습니다. 이 영역은 프로그램에서 사용되지 않는 NULL 패딩(Padding)입니다.
이곳애 문자열 데이터를 작성하여 주소를 넘겨주면 될 것 같습니다. 적당한 위치(0x409F50
)에 Hello Reversing!!!
문자열을 작성해봅시다.
문자열 데이터를 작성하였으니 MessageBoxW
함수에 넘겨지는 문자열 주소를 우리가 임의로 새롭게 작성한 주소(0x409F50
)으로 변경해봅시다.
0x401007
주소 라인을 선택하신 후 Space Bar 키를 누르시면 명령어 수정 창이 나타납니다. 아래의 명령어를 입력한 후 OK를 눌러주세요.
F9 키를 눌러 실행하면 Hello Reversing!!!
문자열이 출력되는 걸 확인할 수 있습니다.
패치하면 아무 것도 출력되지 않아요...
위 방식대로 작업을 수행 후 패치 파일을 만들어 실행하면 문자열이 출력되지 않습니다. 이유는 0x409F50
주소 때문입니다. 실행 파일이 메모리에 로딩되어 프로세스로 실행될 때 어떠한 규칙으로 인해 그대로 메모리 적재되지 않고 올라가게 되기 때문에 그렇습니다. 정확히 이해하려면 PE 파일 구조에 대해 알아야하기 때문에 자세한 설명은 생략합니다.