Ghidra Study
이번 시간에 공부한 내용은 버퍼오버플로우를 활용하여 크랙미 문제를 풀어보았다.
예제 크랙미 파일은 "리버스엔지니어링 기드라 실전 가이드" 라는 책에서 제공하는 실습 파일을 이용하였다.
먼저 받은 크랙미 파일을 실행하여 어떠한 동작을 하는 코드인지 분석을 진행하였다.

파일 실행시 argument로 이름을 넣으면 게임이 실행된다.
게임은 1부터 6까지 숫자를 골라 연속으로 10번 맞추면 정답인 것으로 보인다.
실행할 때 마다 숫자가 달라지는 것을 보면 랜덤 함수를 이용하여 1부터 6 숫자를 구하고, 사용자의 입력과 같은지 비교를 10번 수행하는 것으로 추측한다.
정확한 분석을 위하여 디버깅 도구 Ghidra를 활용하였다.
Ghidra에서는 Decompile을 제공하기 때문에 실행파일을 보기 편한 C 코드로 분석할 수 있다.
undefined8 main(int param_1,undefined8 *param_2)
{
int iVar1;
undefined8 uVar2;
char local_38 [24];
uint local_20;
uint local_1c;
FILE *local_18;
uint local_10;
uint local_c;
if (param_1 == 2) {
local_20 = 0;
local_18 = fopen("/dev/urandom","r");
fread(&local_20,4,1,local_18);
fclose(local_18);
strcpy(local_38,(char *)param_2[1]);
printf("Welcome %s.\n",local_38);
srand(local_20);
puts("Guess the outcome of my rolled dice.");
puts("Please clear this game without patching the binary...");
for (local_10 = 1; (int)local_10 < 0xb; local_10 = local_10 + 1 ) {
printf("My Dice rolled... Try your luck![%d/10]\n",(ulong)lo cal_10);
puts("1? or 2? ... 6?");
iVar1 = rand();
local_1c = iVar1 % 6 + 1;
iVar1 = getchar();
local_c = iVar1 % 0x30;
if (local_c == 10) {
iVar1 = getchar();
local_c = iVar1 % 0x30;
}
printf("My dice: %d, Your guess: %d\n",(ulong)local_1c,(ulo ng)local_c);
if (local_c != local_1c) {
puts("You Lose!");
return 1;
}
puts("You Win!\n");
}
printf("Congratz %s!\n",local_38);
uVar2 = 0;
}
else {
printf("%s your_name_is_here\n",*param_2);
uVar2 = 0xffffffff;
}
return uVar2;
}
예상한 대로 동작을 수행하는 것을 볼 수 있다.
main 부분은 우리가 흔히 사용하는 int main(int argc, char **argv) 형태이며, 기본적으로 인자는 파일의 경로가 들어가고 추가로 입력(이름)하였으므로, 첫 번째 if 문에 들어갈 것이다.
/dev/urandom: Ubuntu 내에 위치한 난수가 저장돼있는 파일fread: 파일 내에 있는 값 읽어오기srand: 랜덤함수의 seed 값 설정Buffer Overflow(OBF)는 buffer의 크기를 초과시키는 것을 의미하며, 보통 악성행위에서 사용되는 기술이다.
최근 있었던 MongoDB 취약점의 경우 BOF를 활용한 것이다.
Buffer의 크기를 초과시켜 메모리에 원하는 값을 쓰고, 이를 통해 예상치 못한 동작을 수행시킬 수 있다.
이번 CrackMe에서는 이 BOF를 이용하여 문제를 풀어보았다.
앞서 코드의 동작 방식에서 설명한 부분이 srand에 들어가는 값의 data flow를 분석하였다. BOF를 이용해 원하는 값으로 seed를 설정해 보았다.
동적 분석이 필요할 것 같아 GDB를 사용하였다.

이렇게 실행하면 된다.

main에서 strcpy를 통해 사용자의 입력을 stack에 복사를 하는 동작을 볼 수 있다. 사용자 입력을 하고 스택의 값이 어떻게 돼있는지 확인해 보면

입력은 aaaaaa를 입력하였다. 보면 61이라는 숫자가 6번 반복된 것을 볼 수 있는데, 이 부분이 입력 값이 저장되는 부분이라 볼 수 있다. 만약 a를 더 많이 입력한다면,

61이 다른 공간까지 침범한 것을 볼 수 있다.
그러면 여기서 srand에 들어가는 변수는 어떻게 찾을 수 있을까?

Ghidra를 확인해보면 알 수 있는데 우측에 C 코드에 보면 fread를 통해 local_20에 seed 값을 저장하는데 왼쪽 어셈코드를 보면 해당 위치가 RBP-0x18인 것을 볼 수 있다.

GDB로 확인해보면 0x72e3c1f8인 것을 확인할 수 있고 위 사진(stack)에서 확인해 보면 어디에 위치한지 알 수 있다. 이제 저 부분까지 a의 개수를 늘려 덮어쓰기 하면 된다.
a를 28번 입력하면 덮어쓰기가 가능하며 직접 입력하면 힘들기 때문에
$(python3 -c 'print("a"*28)') 이렇게 인자로 실행해 주면

이렇게 잘 덮어쓰기가 되었다. 이제 61616161이 seed일 때의 난수값만 알면 정답을 맞출 수 있다.
#include <stdio.h>
#include <stdlib.h>
int main() { unsigned int seed = 0x61616161;
srand(seed);
for (int i = 1; i <= 10; i++) {
int r = rand();
int dice = (r % 6) + 1;
printf("%d\n", dice);
}
return 0;
}



위에서 찾은 숫자 10개를 차례대로 입력하면 최종적으로 위 사진과 같이 나오게 된다. (입력한 이름이 출력됨)
책에 있는 내용을 참고하여 문제를 풀어보았는데, 왜 a를 28번 입력해야 seed 값을 덮어쓰기 할 수 있는지에 대한 설명이 없어 이 부분을 GDB로 동적 분석으로 찾아 보았다.
스택값 읽는 방법과 스택 구조에 대해서 다시 공부가 필요하다고 느겼으며, 상세하게 분석하는 과정이 재미있었다.
추가적으로 해당 문제는 BOF를 이용해 풀도록 하였는데, 코드 구조를 확인해 보면 랜덤값이 결정된 다음 사용자의 입력을 받는 순서대로 동작한다.

그렇기 때문에 getchar를 통해 문자열을 입력받는 곳에

사진과 같이 bp를 걸고 레지스터 eax의 값을 확인하여 입력한다면

이런식으로 BOF를 이용하지 않고 간단하게 풀 수 있다.