1. 프로그램 분석
우선 프로그램이 어떻게 작동하는지 알기 위해, 실행시켜보자
$ ./simple_crack_me_2
12341234123412342134123412341234123412342134
your input length is wrong x(
$ ./simple_crack_me_2
12342134asdfasdfadsf
your input length is wrong x(
$ ./simple_crack_me_2
asdfasfasd123
your input length is wrong x(
$ ./simple_crack_me_2
123123asdasd
your input length is wrong x(
$
우선, 입력 값을 입력했을 때, 입력 값의 길이가 틀리다는 문자열이 출력된다.
입력 값의 일치와 불일치를 따지기 전에 길이부터 판별한다고 짐작할 수 있다.
그렇다면 본격적으로 Ghidra를 이용하여 정적 분석을 해보자
2. 파일 임포트하기
simple_crack_me라는 폴더에 파일을 임포트하기 위해 폴더를 누르고 용 버튼을 누른다.
그러면 빈 화면에 아무것도 뜨지 않게 되는데, 왼쪽 파일에서 임포트 파일을 클릭하여 파일을 임포트 할 수 있다. 그러면 파일을 선택해주고, 파일 포맷이 자동적으로 분석된 것을 볼 수 있다. 파일 포맷이 맞기 때문에 확인을 눌러준다.
임포트 결과 요약 창이 뜨는데, 분석하기를 눌러준다.
그러면 드디어 simple_crack_me_2가 임포트된 것을 확인할 수 있다.
3. 문자열 검색하기
아까 떴던 출력문이 20바이트가 넘기 때문에 String의 최소 길이를 20으로 하여 검색을 해본다.
이렇게 "your input length is wrong x(" 라는 똑같은 문자열을 찾을 수 있다. 클릭해주고 나가주면 해당 문자열이 저장된 위치를 찾을 수 있다. 그러면 참조 기능을 사용해서 해당 문자열이 사용된 함수의 위치로 이동해주자.
4. 참조된 위치 찾기
이동하면 문자열이 사용된 함수의 어셈블리어와 디컴파일 버전이 나타나게 된다.
그러면 이제 함수의 위치를 ctrl+D를 눌러 찾기 쉽게 북마크 해주고 분석을 시작해보자
5. 함수 분석
undefined8 FUN_00401390(void)
{
int iVar1;
long lVar2;
undefined8 uVar3;
long in_FS_OFFSET;
undefined local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__isoc99_scanf(&DAT_00402045,local_118);
lVar2 = FUN_004011b6(local_118);
if (lVar2 == 0x20) {
FUN_004011ef(local_118,&DAT_00402068);
FUN_00401263(local_118,0x1f);
FUN_004012b0(local_118,0x5a);
FUN_004011ef(local_118,&DAT_0040206d);
FUN_004012b0(local_118,0x4d);
FUN_00401263(local_118,0xf3);
FUN_004011ef(local_118,&DAT_00402072);
iVar1 = memcmp(local_118,PTR_DAT_00404050,0x20);
if (iVar1 == 0) {
puts("Correct!");
uVar3 = 0;
}
else {
puts("your input is wrong x(");
uVar3 = 1;
}
}
else {
puts("your input length is wrong x(");
uVar3 = 1;
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar3;
}
디컴파일된 코드를 자세히 들여다보자.
1. 함수는 local_118에 문자를 입력받는다.
2. FUN_004011b6 함수에 해당 문자를 매개변수로 넣어 나온 값을 lVar2에 저장한다.
3-1. 만약 lVar2의 값이 0x20과 같다면
아래의 함수들을 실행하고, local_118의 값을 PTR_DAT_00404050 값과 비교한다.
4-1. iVar1 값이 0이라면 "Correct!"를 출력하고 uVar3을 0으로 초기화한다.
4-1. iVar1 값이 0이 아니라면 "your input is wrong x("을 출력하고 uVar3을 1로 초기화한다.
3-2. 만약 lVar2의 값이 0x20과 다르다면
"your input length is wrong x("을 출력하고 uVar3를 1로 초기화한다.
우리는 입력 값의 변수가 다를 때 해당 문자열을 출력하는 것을 알고 있으니,
lVar2는 입력된 문자열의 길이라는 것을 알 수 있다. 또한 그 길이 값은 32여야한다.
그럼 우선 FUN_004011b6 함수를 분석해보자
문자열을 포인터로 받아 이 문자열이 끝날 때까지 변수에 1씩 더하는 것으로 보아 문자열의 길이를 계산하는 함수임을 알 수 있다. 원래의 함수를 왼쪽과 같이 수정해주면 훨씬 이해하기 쉽다.
그러면 북마크를 눌러 원래의 함수로 돌아가보자. 그 다음에 확인해야 할 함수들은 다음과 같다.
FUN_004011ef // FUN_00401263 // FUN_004012b0
- FUN_004011ef
- FUN_00401263
- FUN_004012b0
각각의 함수들은 input에 param_2로 지정된 연산 수행한다는 것을 알 수 있다.
이렇게 해서 변형된 입력 문자열의 32바이트가 PTR_DAT_00404050과 같은지를 판단하기 때문에,
프로그램이 원하는 PTR_DAT_00404050 값에다가 앞에서 수행했던 연산들을 거꾸로 수행해주면
우리가 어떤 값을 입력해야 하는지 알 수 있다. 그러면 해당 문자열에 저장된 값을 찾아보자.
DAT_00402008 XREF[3]: FUN_00401390:004014a0 (*) ,
FUN_00401390:004014b3 (*) ,
00404050 (*)
00402008 f8 ?? F8h
00402009 e0 ?? E0h
0040200a e6 ?? E6h
0040200b 9e ?? 9Eh
0040200c 7f ?? 7Fh
0040200d 32 ?? 32h 2
0040200e 68 ?? 68h h
0040200f 31 ?? 31h 1
00402010 05 ?? 05h
00402011 dc ?? DCh
00402012 a1 ?? A1h
00402013 aa ?? AAh
00402014 aa ?? AAh
00402015 09 ?? 09h
00402016 b3 ?? B3h
00402017 d8 ?? D8h
00402018 41 ?? 41h A
00402019 f0 ?? F0h
0040201a 36 ?? 36h 6
0040201b 8c ?? 8Ch
0040201c ce ?? CEh
0040201d c7 ?? C7h
0040201e ac ?? ACh
0040201f 66 ?? 66h f
00402020 91 ?? 91h
00402021 4c ?? 4Ch L
00402022 32 ?? 32h 2
00402023 ff ?? FFh
00402024 05 ?? 05h
00402025 e0 ?? E0h
00402026 d9 ?? D9h
00402027 91 ?? 91h
00402028 00 ?? 00h
해당 문자열은 각 인덱스마다 이러한 값을 가진다. 그러면 이제 flag를 알아내기 위해 해독 프로그램을 코딩해보자
6. flag 찾기
// Name: decode.c
// Compile: gcc decode.c -o decode
#include <stdio.h>
size_t GetStrLen(char *input) {
size_t local_18;
char *local_10;
local_18 = 0;
for (local_10 = input; *local_10 != '\0'; local_10 = local_10 + 1) {
local_18 = local_18 + 1;
}
return local_18;
}
void XORWithParam2(char *input,char *param_2) {
size_t sVar1;
int local_14;
sVar1 = GetStrLen(param_2);
for (local_14 = 0; local_14 < 0x20; local_14 = local_14 + 1) {
input[local_14] = param_2[(unsigned long)(long)local_14 % sVar1] ^ input[local_14];
}
return;
}
void IncWithParam2(char *input,char param_2) {
int local_c;
for (local_c = 0; local_c < 0x20; local_c = local_c + 1) {
input[local_c] = param_2 + input[local_c];
}
return;
}
void DecWithParam2(char *input,char param_2) {
int local_c;
for (local_c = 0; local_c < 0x20; local_c = local_c + 1) {
input[local_c] = input[local_c] - param_2;
}
return;
}
void Decode(char *encoded) {
XORWithParam2(encoded, "\x11\x33\x55\x77\x99\xbb\xdd");
DecWithParam2(encoded, -13);
IncWithParam2(encoded, 77);
XORWithParam2(encoded, "\xef\xbe\xad\xde");
IncWithParam2(encoded, 90);
DecWithParam2(encoded, 31);
XORWithParam2(encoded, "\xde\xad\xbe\xef");
}
int main(void) {
char data[] = "\xf8\xe0\xe6\x9e\x7f\x32\x68\x31\x05\xdc\xa1\xaa\xaa\x09"
"\xb3\xd8\x41\xf0\x36\x8c\xce\xc7\xac\x66\x91\x4c\x32\xff\x05"
"\xe0\xd9\x91";
Decode(data);
printf("Decoding result: %s\n", data);
return 0;
}
XOR 연산을 두번하게 되면 다시 원래의 값을 얻을 수 있기 때문에 다음과 같이 코딩할 수 있다. 아래의 코드는 원래의 코드이다.
XOR(input,&DAT_00402068);
ADD(input,31);
SUB(input,90);
XOR(input,&DAT_0040206d);
SUB(input,77);
ADD(input,-13);
XOR(input,&DAT_00402072);
iVar1 = memcmp(input,PTR_DAT_00404050,32);
그러면 코드를 실행했을 때
Decoding result: 9ce745c0d5faaf29b7aecd1a4a72bc86
이러한 결과가 나오고 우리는 flag를 구할 수 있게된다.