앵하니의 더 나은 보안

(reversing.kr) Easy CrackMe 본문

보안 기술/ETC

(reversing.kr) Easy CrackMe

앵한 2023. 11. 22. 22:48

reversing.kr의 제일 첫문제 Easy_CrackMe를 한번 풀이해보자.

목적

Easy_CrackMe.exe 실행 시 텍스트박스와 확인 버튼이 있는 다이얼로그가 나타난다.

 

임의의 값 입력 후 확인 클릭 시 “Incorrect Password” 메시지가 표시된다.

 

적절한 값을 찾아 넣어 Incorrect Password가 아니라 다른 메시지를 출력시켜야 하는것으로 보이니,
정적분석/동적분석을 통해 적절한 값을 획득해보자.

정적분석

정적분석을 위해 ghidra로 Easy_CrackMe.exe 파일 분석을 수행한다.

 

 

우선 텍스트필드에 입력한 값에 대한 분기점을 찾기 위해 임의의 값을 입력했을 때 표시된 “Incorrect Password” 문자열을 검색, 접근한다.

 

그리고 해당 문자열을 사용하는 FUN_00401080함수에 접근한다.

 

 

접근해보면, FUN_00401080함수 내에서 어떤 분기에도 포함되지 않는 경우 마지막에 “Incorrect Password”가 출력됨을 알 수 있다.

해당 함수의 디컴파일 결과는 아래와 같다.

void __cdecl FUN_00401080(HWND param_1)

{
  byte bVar1;
  byte *pbVar2;
  int iVar3;
  char *pcVar4;
  undefined4 *puVar5;
  bool bVar6;
  char local_64;
  char local_63;
  char cStack_62;
  byte abStack_61 [97];
  
  local_64 = '\0';
  puVar5 = (undefined4 *)&local_63;
  for (iVar3 = 0x18; iVar3 != 0; iVar3 = iVar3 + -1) {
    *puVar5 = 0;
    puVar5 = puVar5 + 1;
  }
  *(undefined2 *)puVar5 = 0;
  *(undefined *)((int)puVar5 + 2) = 0;
  GetDlgItemTextA(param_1,1000,&local_64,100);
  if (local_63 == 'a') {
    iVar3 = _strncmp(&local_63 + 1,&DAT_00406078,2);
    if (iVar3 == 0) {
      pcVar4 = s_AGR3versing_0040606a;
      pbVar2 = (byte *)(&local_63 + 3);
      do {
        pcVar4 = (char *)((byte *)pcVar4 + 2);
        bVar1 = *pbVar2;
        bVar6 = bVar1 < (byte)*pcVar4;
        if (bVar1 != *pcVar4) {
LAB_00401102:
          iVar3 = (1 - (uint)bVar6) - (uint)(bVar6 != 0);
          goto LAB_00401107;
        }
        if (bVar1 == 0) break;
        bVar1 = pbVar2[1];
        bVar6 = bVar1 < ((byte *)pcVar4)[1];
        if (bVar1 != ((byte *)pcVar4)[1]) goto LAB_00401102;
        pbVar2 = pbVar2 + 2;
      } while (bVar1 != 0);
      iVar3 = 0;
LAB_00401107:
      if ((iVar3 == 0) && (local_64 == 'E')) {
        MessageBoxA(param_1,s_Congratulation_!!_00406044,s_EasyCrackMe_00406058,0x40);
        EndDialog(param_1,0);
        return;
      }
    }
  }
  MessageBoxA(param_1,s_Incorrect_Password_00406030,s_EasyCrackMe_00406058,0x10);
  return;
}

디컴파일 결과를 근거로 적절한 입력값을 한번 찾아보자.

 

우선 적절한 입력 값을 찾아 넣었을 때 표시되는 메시지는 “Congratulation !!"로 추정되는데, 해당 문구의 메시지박스를 유도하기 위해선 총 4개의 분기에 진입해야 한다.

 

첫번째 분기 if (local_63 == 'a')

변수 local_63을 'a’로 만들어 줘야하는데, 그럴려면 먼저 local_63이 뭔지 알아야겠다.

  char local_64;
  char local_63;
  char cStack_62;
  byte abStack_61 [97];

최초 변수 선언 시 char형 local_64가 선언되고 이후 char local_63…이 선언된다.
이때 스택메모리에서는 아래와 같은 모양으로 변수가 할당된다.
※아래 주소값은 임의로 설정한 주소값이며, 스택메모리는 큰 주소값에서 작은 주소값 순서(0xFFFF > 0x0000)로 값이 할당 된다.

스택메모리
주소값 할당된 변수
...
0x0019FA6B abStack_61[96]
...
0x0019FACB abStack_61[0]
0x0019FACC local_62
0x0019FACD local_63
0x0019FACE local_64
...

 

그리고 이 local_64와 local_63, abStack_61[0], …abStack_61[96]은 아래 함수를 통해 채워지게 된다.
GetDlgItemTextA(param_1,1000,&local_64,100);

 

해석하자면 local_64 변수의 주소에 텍스트 박스에 입력한 값을 100字만큼 복사한다라는 뜻이다.
예를 들어 “test”라고 입력한 후 확인 버튼을 클릭했다면 스택메모리는 아래와 같은 구조가 된다.

스택메모리
주소값 할당된 변수
...
0x0019FA6B abStack_61[96]
...
0x0019FACB abStack_61[0]
0x0019FACC local_62 = s
0x0019FACD local_63 = e
0x0019FACE local_64 = t
...

 

그러므로 local_63의 값을 a로 만들려면 입력한 값의 두번째 값이 “a”면 된다.

두번째 분기 if (iVar3 == 0)

분기문 내에 존재하는 변수 iVar3은 분기문 직전 아래와 같이 값을 할당 받는다.
iVar3 = _strncmp(&local_63 + 1,&DAT_00406078,2);

이는 &local_63+1값과 &DAT_00406078을 2字 비교해서, 같을 경우 0을, 다른경우 1을 할당한다는 뜻이된다.

&DAT_00406078은 “5y”임을 확인할 수 있다.

 

 

그러니까 local_62, abStack_61[0]와 “5y”를 비교, 같을 경우 iVar3는 0이 되고, 해당 분기문에 진입할 수 있게된다.

스택메모리
주소값 할당된 변수
...
0x0019FA6B abStack_61[96]
...
0x0019FACB abStack_61[0] = y
0x0019FACC local_62 = 5
0x0019FACD local_63 = a
0x0019FACE local_64
...

 

그러니 첫번째/두번째 분기문을 통과하기 위해선 “?a5y???…”를 입력해야되겠다.

 

세번째 분기 if (bVar1 != *pcVar4)

 

여기부터 반복문에, 포인터에 머리가 살짝 꼬일 수 있는데 하나하나 찬찬히 뜯어보자

  pcVar4 = s_AGR3versing_0040606a;
  pbVar2 = (byte *)(&local_63 + 3);
  do {
    pcVar4 = (char *)((byte *)pcVar4 + 2);
    bVar1 = *pbVar2;
    bVar6 = bVar1 < (byte)*pcVar4;
    if (bVar1 != *pcVar4) {
LAB_00401102:
      iVar3 = (1 - (uint)bVar6) - (uint)(bVar6 != 0);
      goto LAB_00401107;
    }
    if (bVar1 == 0) break;
    bVar1 = pbVar2[1];
    bVar6 = bVar1 < ((byte *)pcVar4)[1];
    if (bVar1 != ((byte *)pcVar4)[1]) goto LAB_00401102;
    pbVar2 = pbVar2 + 2;
  } while (bVar1 != 0);
  iVar3 = 0;

 

line 1: pcVar4 = s_AGR3versing_0040606a;

우선 pcVar4에 할당되는 s_AGR3versing_0040606a는 문자열 “AGR3versing”를 의미한다.

 

line 2: pbVar2 = (byte *)(&local_63 + 3);

그리고 pbVar2는 &local_63+3의 포인터, 그러니까 abStack_61[1]의 포인터가 된다.

스택메모리
주소값 할당된 변수
...
0x0019FA6B abStack_61[96]
...
0x0019FACA = pbVar2 abStack_61[1]
0x0019FACB abStack_61[0] = y
0x0019FACC local_62 = 5
0x0019FACD local_63 = a
0x0019FACE local_64
...

 

line 3: do {
이후 do while 반복문에 진입한다.


line 4: pcVar4 = (char *)((byte *)pcVar4 + 2);
pcVar4는 기존 “AGR3versing”의 포인터에서 2바이트 만큼 뒤로 간 주소 값, 그러니까 “R3versing”의 포인터로 재정의 된다.


line 5: bVar1 = *pbVar2;
그리고 pbVar2(abStack_61[1]~\0)의 값을 bVar1에 할당한다.


line 6: bVar6 = bVar1 < (byte)*pcVar4;
pbVar2(abStack_61[1]~\0)의 첫번째 바이트 값 abStack_61[1]과 *pcVar4(“R3versing”)의 최초 값 “R”의 바이트값을 비교, 그 결과 값을 bVar6에 저장한다.


line 7: if (bVar1 != *pcVar4) {
만약 pbVar2(abStack_61[1]~\0) 값과 *pcVar4(“R3versing”)의 값이 다르다면,


line 8: LAB_00401102:
line 9: iVar3 = (1 - (uint)bVar6) - (uint)(bVar6 != 0);
iVar3의 값은 bVar6 값에 의해 값이 재할당 된다.
만약 abStack_61[1]의 ASCII 값이 “R”의 ASCII 값보다 작으면, bVar6은 1이되고 그에 의해 iVar3값은 -1이 된다.
iVar3 = (1 - 1) - (1 != 0);
iVar3 = 0 - 1;
iVar3 = - 1;

반대로 abStack_61[1]의 ASCII 값이 “R”의 ASCII 값보다 크면, bVar6은 0이되고 그에 의해 iVar3값은 1이 된다.
iVar3 = (1 - 0) - (0 != 0);
iVar3 = 1 - 0;
iVar3 = 1;


line10: goto LAB_00401107;
그리고 LAB_00401107로 점프 시키는데, LAB_00401107은 마지막 분기점을 의미한다.

LAB_00401107:
      if ((iVar3 == 0) && (local_64 == 'E')) {
        MessageBoxA(param_1,s_Congratulation_!!_00406044,s_EasyCrackMe_00406058,0x40);
        EndDialog(param_1,0);
        return;
      }

마지막 분기문으로 점프했을때 iVar3의 값은 절대 0이 될 수 없으므로, 해당 분기문에 진입할 수 없다.
우선 여기까지 정리하자면, 마지막 분기점에 진입하기 위해서라도, ?a5y이후에 입력되는 문자들은 전부 R3versing의 각 ASCII 값과 같아야한다.

 

line11: if (bVar1 == 0) break;
만약 bVar1의 값이 0 그러니까 널 값이 되면, do while문을 탈출한다.

 

line13: bVar1 = pbVar2[1];
bVar1가 널 값이 아니라면 pbVar2[1] 값, 최초엔 abStack_61[2]의 값을 할당한다.

 

line14: bVar6 = bVar1 < ((byte *)pcVar4)[1];
abStack_61[2] 문자와 pcVar4[1]=”R3versing”의 [1] = ”3”의 문자의 각 아스키 값을 비교하여, 비교한 결과를 bVar6에 저장한다.

 

line15: if (bVar1 != ((byte *)pcVar4)[1]) goto LAB_00401102;
abStack_61[2]과 “3”이 다른경우, 전의 LAB_00401102로 점프시킨다.

 

line16: pbVar2 = pbVar2 + 2;
abStack_61[2]과 “3”이 같은 경우 pbVar2 주소 값을 2증가 시킨다.
이렇게 유저가 입력한 값 전부와 “R3versing”에 대한 모든 문자열 비교를 수행한다.

 

line17: } while (bVar1 != 0);
이 과정을 bVar1 값이 0이 될 때까지 그러니까 유저가 입력한 문자열의 끝, null 값을 만날때까지 반복한다.

 

line18: iVar3 = 0;
반복문을 정상적으로 완료했다면 iVar3 값을 0으로 할당한다.

스택메모리 pcVar4
주소값 할당된 변수
...
0x0019FA6B abStack_61[96] 인덱스
...
0x0019FAC1
(다섯번째 반복문 수행시
pbVar2[1])
abStack_61[10] [11] \0
0x0019FAC2
(다섯번째 반복문 수행시
pbVar2[0])
abStack_61[9] [10] 다섯번째 반복문 수행 시 [0]이 됨 g
0x0019FAC3
(네번째 반복문 수행시
pbVar2[1])
abStack_61[8] [9] n
0x0019FAC4
(네번째 반복문 수행시
pbVar2[0])
abStack_61[7] [8] 네번째 반복문 수행 시
[0]이 됨
i
0x0019FAC5
(세번째 반복문 수행시
pbVar2[1])
abStack_61[6] [7] s
0x0019FAC6
(세번째 반복문 수행시
pbVar2[0])
abStack_61[5] [6] 세번째 반복문 수행 시
[0]이 됨
r
0x0019FAC7
(두번째 반복문 수행시
pbVar2[1])
abStack_61[4] [5] e
0x0019FAC8
(두번째 반복문 수행시
pbVar2[0])
abStack_61[3] [4] 두번째 반복문 수행 시
[0]이 됨
v
0x0019FAC9
(첫번째 반복문 수행시
pbVar2[1])
abStack_61[2] [3] 3
0x0019FACA
(첫번째 반복문 수행시
pbVar2[0])
abStack_61[1] [2] 첫번째 반복문 수행 시
[0]이 됨
R
0x0019FACB
abStack_61[0] = y [1] G
0x0019FACC
local_62 = 5 [0] A
0x0019FACD
local_63 = a  
0x0019FACE
local_64
...

 

 

그러니까 결국 이 과정은 iVar3 값을 0으로 만들기위한 과정이며, iVar3을 0으로 만들기위해서는
최초 pcVar4의 세번째 인덱스 이후로 같은 문자열이여야만 한다.
그러니까 “?a5yR3versing”을 입력해야 되는거다.

네번째 분기 if ((iVar3 == 0) && (local_64 == 'E'))

만약 반복문을 중간에 빠져나오지 않고 제대로 완수 했다면, iVar3에는 자연스레 0값이 할당된다.
그리고 local_64는 입력 값의 제일 첫번째 값이니, 제일 첫 문자로 E를 입력하면 되겠다.

스택메모리
주소값
할당된 변수
0x0019FAC1
abStack_61[10] = \0
0x0019FAC2
abStack_61[9] = g
0x0019FAC3
abStack_61[8] = n
0x0019FAC4
abStack_61[7] = i
0x0019FAC5
abStack_61[6] = s
0x0019FAC6
abStack_61[5] = r
0x0019FAC7
abStack_61[4] = e
0x0019FAC8
abStack_61[3] = v
0x0019FAC9
abStack_61[2] = 3
0x0019FACA
abStack_61[1] = R
0x0019FACB
abStack_61[0] = y 
0x0019FACC
local_62 = 5
0x0019FACD
local_63 = a
0x0019FACE
local_64 = E

 

그래서 “Congratulation !!”을 출력하기 위해선 “Ea5yR3versing”을 입력해야된다고 추측된다.

확인해보자.

 

동적분석

마찬가지로 Incorrect Password 검색으로 시작한다.

 

Incorrect Password 문자열을 사용하는 함수를 확인한다.

 

해당 문자열을 사용하는 구간으로 점프하는 분기들을 전부 확인한다.

 

“Congratulation !!” 메시지박스를 확인하려면 저 4개의 분기문을 전부 통과해 도달해야하겠다.

그럼 분기문 4개를 통과하기 위해 동적분석을 해보도록하자.

 

첫번째 분기 cmp byte ptr ss:[esp+5],61

함수 프롤로그 과정을 확인해 “Incorrect Password“ 문자열을 사용하는 함수 도입부 추측 후 GetDlgItemTextA 그러니까 텍스트필드에서 값을 가져오는 부분에 Break Point를 설정한다.

 

이후 동작 과정 확인을 위해 텍스트필드에 “test” 입력 후 확인을 클릭한다.

 

이후 텍스트필드에서 가져온 값은 스택메모리에 저장되고, 분기점에서는 0x61과 esp+5 값을 비교한다.

비교시의 ESP는 0x0019F7EC이며, +5 값이라면 0x0019F7F1가 된다.
그럼 0x61과 비교하는 0x0019F7F1는 뭐냐? 바로 우리가 입력했던 “test”가 저장되는 영역인데, 0x0019F7F0 주소에 입력한 4바이트가 할당돼 있다. 1바이트씩 확인해보자면 아래와 같다.

스택메모리
주소 값
0x0019F7EC(ESP)
00
0x0019F7ED
00
0x0019F7EE
01
0x0019F7EF
11
0x0019F7F0
t
ESP+5 = 0x0019F7F1
e
0x0019F7F2
s
0x0019F7F3
t

그러니까 입력한 두번째 문자 값이 0x61(ASCII 'a')와 같은지 비교하는거다.
cmp로 비교해 같을경우 Zero Flag가 set되고, 다를경우 set되지 않는다.
그 후 JNE를 통해 서로 같지 않으면, 그러니까 Zero Flag가 set되지 않은경우 0x00401135로 jmp하게 된다.

 

결국 0x00401135로 jmp하지 않으려면 Zero Flag를 set해야하고, 그럴려면 입력 값의 두번째 값을 0x61와 같은 “a”로 입력해야 하겠다.

“test”로 입력했을때 Zero Flag가 0으로 설정돼 0x401135로 jmp수행

password : ?a???…

두번째 분기 test eax, eax

두번째분기 jne 401135 직전에 test eax, eax를 수행하는데 이는 EAX가 1로 설정돼있는지 0으로 설정돼있는지 확인하는 용도로 사용되며 이때 0으로 설정돼있다면 Zero Flag가 Set 된다.
그래서 첫번째 분기와 마찬가지로 Zero Flag를 Set해주어 해당 분기문을 통과해야한다.
그럼 어떻게해야 EAX를 0으로 설정할 수 있는지 디버깅을 통해 확인해보자.

 

첫번째 분기와 두번째 분기 사이에 eax 값을 설정해줄듯한 애는 call 401150밖에 없으니 해당 주소에 Break Point를 설정한 후 테스트해보자.

 

첫번째 분기를 통과하기 위해 “tast” 입력 후 확인을 클릭한다.

 

그럼 Break Point로 설정해둔 4010C3에서 실행이 멈추게 되는데, 이때 어떤 일이 발생하는지 확인하기 위해 Step Into로 해당 주소로 이동해보자.

 

그럼 아래와 같은 어셈블리어 코드들을 볼 수 있는데, 핵심은 ‘EAX가 어디서 세팅되는가’다.
그래서 거기에 집중해 코드를 보면 return 직전 EAX가 한번 세팅되는데 이때 ECX가 관여한다.

 

그럼 ECX는 어떻게 세팅되는지를 보면, xor ecx, ecx로 한번 0으로 설정한 후
mov al, byte ptr ds:[esi-1]
cmp al, byte ptr ds:[edi-1]
로 ds:[esi-1]와 ds:[edi-1] 값을 비교하는데 각 값은 “5y”와 입력했던 문자열의 세/네번째 자리에 존재하는 값이다.

 

그래서 만약 입력했던 세/네번째 값이 “5y”라면, Zero Flag가 set되고 그에따라 je 401181가 수행되면서 0으로설정됐던 ecx의 값이 그대로 eax로 전달돼 eax도 마찬가지로 0으로 설정된다.

그러니 eax로 설정하려면 세/네번째 값을 “5y”로 입력하면 되겠다.

password : ?a5y???…

세번째 분기 test eax, eax

두번째 분기와 마찬가지로 세번째 분기도 eax의 값을 확인하는데, 여기서도 마찬가지로 Zero Flag를 set해주어 jne를 통과하도록 해야한다.
그럼 eax를 0으로 설정하려면 어떻게해야하는지 확인해보자.


두번째 분기 직후 Break Point를 설정한다.

 

그리고 두번째 분기를 통과하기 위해 ta5y 입력 후 확인을 클릭한다.

 

그러고 Step Over로 동작 확인 시, 문자를 서로 비교하는데 ta5y 이후의 값을 비교한다.

[esp+10]은 19F7F4인데, ta5y 입력 이후의 텍스트를 참조한다.
 
 
 

그러니 이후의 값을 추가한 후 다시 확인해봐야하는데, 우선 바로 노출되는 미심쩍은 R3versing을 추가하여 다시 테스트해본다.

 

그럼 무난히 다음 분기문을 통과하게 되는데 이를토대로 각 어셈블리어 구문이 뜻하는바를 해석하자면, 다음과 같다.

  1. 사용자가 입력한 문자열에서 4번째 이후의 문자열을 가져와 “R3versing”문자열과 비교하는데, bl/dl을 이용해 한글자씩 비교, 같은 값이 아닐 경우 eax 값을 0으로 만드는 xor eax, eax 구문을 지날 수 없음
  2. 그 다음 문자를 비교하기 위해 eax+1, esi+1 그러니까 다음 글자 비교
    마찬가지로 같은 값이 아닐 경우 xor eax, eax 구문을 지날 수 없음
  3. 5번째, 6번째 이후의 문자를 비교하기 위해 각 주소 값을 2씩 증가 시킨 후 1,2과정 반복하되 cl 그러니까 입력한 값이 0, 널 값인 경우 반복 종료
  4. xor eax, eax를 수행해 eax를 0으로 세팅
  5. 이후 test eax, eax를 통해 Zero Flag set 후 세번째 분기문 통과

Password : ?a5yR3versing

네번째 분기 cmp byte ptr ss:[esp+4],45

마지막으로 esp+4와 0x45 그러니까 “E”와 비교해 같은 값일 경우 분기문을 통과할 수 있다.
그럼 esp+4는 뭐냐? 19F7EC+4 = 19F7F0는 사용자가 입력한 제일 첫번째 문자다.

 

이 분기문만 통과하면 드디어 “Congratulation !!”을 확인할 수 있다.
한번 확인해보자.
Password : Ea5yR3versing

 

 

'보안 기술 > ETC' 카테고리의 다른 글

XZ Utils Backdoor(CVE-2024-3094)  (0) 2024.06.23
Log4shell(CVE-2021-44228)  (0) 2024.06.23
(reversing.kr) Easy Keygen  (2) 2023.11.22
AWS Pentesting tool PACU  (0) 2022.11.10
flAWS cloud level 1  (0) 2022.08.03
Comments