IDA로 바이너리 코드를 패치하는 방법에 대해서 소개하겠습니다.
저는 target이라는 프로그램을 IDA로 패치해서 원하는 결과를 출력하는 것을 목적으로 진행할 것입니다.
실습하고 싶으신 분들은 다음 설명에서 C 코드 제공되니 직접 컴파일해서 따라해보시면 좋을 것 같습니다.
○ target.c
#include <stdio.h>
#include <stdlib.h>
int compare(const void *a, const void *b) {
int num1 = *(int *)a;
int num2 = *(int *)b;
if (num1 < num2) return -1;
if (num1 > num2) return 1;
return 0;
}
void solution(int arr[], int arr_size, int num[], int num_size) {
int N = arr[0];
int M = arr[1];
int K = arr[2];
qsort(num, num_size, sizeof(num[0]), compare);
int first = num[num_size-1];
int second = num[num_size-2];
int count = M / (K + 1) * K + M % (K + 1);
int sum = count * first;
sum += (M-count) * second;
printf("[+] result : %d\n", sum);
}
void number_detect(int arr[]){
if (arr[0] == 5){
printf("[+] I don't want arr[0] to be 5!\n");
exit(0);
}
}
int main() {
int arr[] = {5, 8, 3};
int arr_size = sizeof(arr) / sizeof(arr[0]);
int num[] = {2, 4, 5, 4, 6};
int num_size = sizeof(num) / sizeof(num[0]);
number_detect(arr);
solution(arr, arr_size, num, num_size);
return 0;
}
target 프로그램 이해를 위해서 원본 코드를 가져왔습니다.
우리의 목적은 solution()에서 출력되는 결과를 확인하는 것입니다.
코드는 solution()를 실행하고 연산 과정을 통해 생성된 결과를 printf()로 출력합니다. 하지만 solution()을 실행하기 전에 number_detect()를 실행하여 간단한 검증을 수행합니다.
void number_detect(int arr[]){
if (arr[0] == 5){
printf("[+] I don't want arr[0] to be 5!\n");
exit(0);
}
}
number_detect()는 "arr[0] == 5"인 경우 프로그램을 종료합니다.
main()에 정의된 arr[0]는 5이므로 프로그램을 실행하면 number_detect()에서 종료됩니다.
원하는 결과를 출력하기 위해서는 number_detect()에서 종료되지않도록 바이너리 패치를 수행해야합니다.
바이너리 패치를 해봅시다!
__int64 __fastcall number_detect(unsigned int *a1)
{
__int64 result; // rax
result = *a1;
if ( (_DWORD)result == 5 )
{
puts("[+] I don't want arr[0] to be 5!");
exit(0);
}
return result;
}
IDA로 solution 함수를 디컴파일하면 코드가 출력됩니다.
[TAB]을 누르면 asm 코드를 확인할 수 있습니다.
number_detect() 함수의 asm 코드를 패치해서 exit()이 실행되지않도록 해봅시다. 여러 방법이 있지만 저는 2가지 방법을 소개하겠습니다.
첫번째는 number_detect() 함수의 코드를 ret으로 바꾸는 방법입니다.
[edit] → [Patch Program] → [Assemble]
패치하고 싶은 asm 코드에 마우스 커서를 두고 IDA 상단 바에서 위와 같은 순서로 창을 열어봅시다.
asm 코드를 패치할 수 있는 화면이 나옵니다.
asm 코드를 ret으로 바꿔봅시다.
ret으로 변경된 것을 확인할 수 있습니다.
void number_detect()
{
;
}
number_detect()를 디컴파일하면 기존에 있던 코드가 사라집니다.
[edit] → [Patch Program] → [Apply patches to input file]
이제 위 순서로 창을 열어봅시다.
패치한 바이너리를 저장할 수 있는 창이 열립니다.
[OK]를 클릭하고 저장합니다.
저장한 target을 실행하면 원하는 결과가 출력됩니다.
코드가 원하는대로 패치된 것을 알 수 있습니다.
이번에는 다른 방법으로 우회해봅시다.
__int64 __fastcall number_detect(unsigned int *a1)
{
__int64 result; // rax
result = *a1;
if ( (_DWORD)result == 5 )
{
puts("[+] I don't want arr[0] to be 5!");
exit(0);
}
return result;
}
이전처럼 함수 코드를 ret으로 바꾸는 방법도 있지만, 하드코딩된 숫자 5를 바꾸는 방법도 있습니다.
asm 코드를 확인하면 cmp 명령이 if문 코드입니다.
[edit] → [Patch Program] → [Patch Bytes]
이번에는 위의 순서대로 창을 열어봅시다.
byte를 조작할 수 있는 창이 열립니다.
조작하고 싶은 값은 5이므로 바이트 값 중에 5를 찾아서 조작해봅시다.
5를 1로 조작했습니다.
[OK]를 누르고 asm 코드를 확인하면 1로 패치된 것을 확인할 수 있습니다.
__int64 __fastcall number_detect(unsigned int *a1)
{
__int64 result; // rax
result = *a1;
if ( (_DWORD)result == 1 )
{
puts("[+] I don't want arr[0] to be 5!");
exit(0);
}
return result;
}
디컴파일하면 코드가 원하는대로 패치된 것을 볼 수 있습니다.
arr[0]는 5이므로 프로그램이 종료되지않고 result를 출력할 것입니다.
패치한 바이너리를 저장하고 실행하면 결과를 얻을 수 있습니다!
지금까지 IDA를 이용해서 바이너리를 패치하는 방법에 대해 알아봤습니다.
다들 즐거운 리버싱하시길~!!~!