Ghidra - Crack Me 2

안상준·2026년 1월 24일

Reversing

목록 보기
12/16

Ghidra Stdudy

Strcpy

이전에 strcpy를 통해 입력값이 스택에 저장되는 방법을 활용하여 BOF를 발생시키는 실습을 진행해 보았다.
앞선 내용에 작성하지는 않았지만 이러한 함수는 보다시피 취약한 점을 가지고 있기 때문에 사용하지 않고, strncpy를 사용한다.
strncpystrncpy(dst, org, len)이렇게 복사할 길이를 인자로 받는데 이 때, lenorg보다 작거나 같아야 한다. 그렇기 때문에, 코드 작성자가 제한한 크기를 초과하여 입력할 수 없게 된다.

Script

먼저 살펴볼 것은 Ghidra의 Script를 이용하여 strcpy와 같은 취약한 함수를 찾아내는 실습을 진행해 보았다.

from ghidra.program.model.symbol import RefType

func_names = ['strcpy', 'strcat', 'gets', 'sprintf']
manager = currentProgram.getFunctionManager()

print("--- Function Call Analysis Start ---")

for func in manager.getFunctions(True):
    if func.getName() in func_names:
        for xref in getReferencesTo(func.getEntryPoint()):
            if xref.getReferenceType().toString() == 'UNCONDITIONAL_CALL':
                from_addr = xref.getFromAddress()
                caller_func = getFunctionContaining(from_addr)
                caller_name = caller_func.getName() if caller_func else "Unknown"
                
                print("[{}] {} is called from {} at {}".format(
                    func.getName(), 
                    func.getName(), 
                    caller_name, 
                    from_addr
                ))

print("--- Analysis Finished ---")

책에서 작성한 코드를 약간 변형하여 작성한 코드다.
1. FunctionManager 객체 생성
2. getFunctions 메서드를 통해 함수 객첵 목록 생성
3. getName 메서드로 func_names 목록에 해당하는 함수가 있는지 확인
4. getEntryPoint를 통해 함수의 주소를 가져옴
5. getReferencesTo를 통해 참조 객체의 목록을 가져옴
6. 참조 객체의 타입이 함수 호출인지 확인 후, 정보를 가져옴.

Stack Guard

위 방법 처럼 취약한 함수를 사용하는 방법이 있다면, 좀 더 low한 방법으로 OBF를 막을 수 있다.
Stack Canary를 기법을 활용할 수 있다. Canary라는 새를 활용하여 광부들은 일산화탄소의 여부를 확인하였다.
스택에 특정 값을 넣고 해당 값이 런타임에 지워지는지 확인하여 BOF를 탐지하는 방식으로 동작한다.

Prac

책에서 제공하는 예시 파일에 level-stackguard 파일이 있다. 이전에 실습한 파일에 stackgurad 기능이 추가된 것이라 생각하고 실제로 실행해 보면

이런식으로 stack smashing이 탐지됐다고 뜨며, BOF도 동작하지 않는 것을 확인할 수 있다.


undefined8 main(int param_1,undefined8 *param_2)

{
  int iVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  uint local_40;
  uint local_3c;
  uint local_38;
  uint local_34;
  FILE *local_30;
  char local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (param_1 == 2) {
    local_40 = 0;
    local_30 = fopen("/dev/urandom","r");
    fread(&local_40,4,1,local_30);
    fclose(local_30);
    strcpy(local_28,(char *)param_2[1]);
    printf("Welcome %s.\n",local_28);
    srand(local_40);
    puts("Guess the outcome of my rolled dice.");
    puts("Please clear this game without patching the binary...");
    for (local_38 = 1; (int)local_38 < 0xb; local_38 = local_38 + 1 ) {
      printf("My Dice rolled... Try your luck![%d/10]\n",(ulong)lo cal_38);
      puts("1? or 2? ... 6?");
      iVar1 = rand();
      local_34 = iVar1 % 6 + 1;
      iVar1 = getchar();
      local_3c = iVar1 % 0x30;
      if (local_3c == 10) {
        iVar1 = getchar();
        local_3c = iVar1 % 0x30;
      }
      printf("My dice: %d, Your guess: %d\n",(ulong)local_34,(ulo ng)local_3c);
      if (local_3c != local_34) {
        puts("You Lose!");
        uVar2 = 1;
        goto LAB_00100afa;
      }
      puts("You Win!\n");
    }
    printf("Congratz %s!\n",local_28);
    uVar2 = 0;
  }
  else {
    printf("%s your_name_is_here\n",*param_2);
    uVar2 = 0xffffffff;
  }
LAB_00100afa:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

코드를 확인해 보면 이전과 달리 추가된 부분이 있는 것을 확인할 수 있다.
local_10 = *(long *)(in_FS_OFFSET + 0x28);이 코드가 카나리값을 가져오는 부분이다.

어셈으로 보는 것이 더 정확한데, local_10이라 돼있어서 헷갈렸지만 opernad를 보면, f8=-8이다 즉 [rbp-8] 위치에 카나리값이 저장된 것이다. 이 부분은 아래 LAB_00100afa: 레이블을 확인하면 더욱 정확히 알 수 있다.

그러면 이제 카나리의 위치를 직접 코드 상에서 확인해보고 seed를 수정해 보도록 하자.

여기서 xor에 bp를 걸고 확인해 보면

요렇게 스택값을 확인하면 카나리의 위치를 알 수 있다.
이제 seed 값이 저장되는 곳을 찾아보면

8b 45 c8 MOV EAX,dword ptr [RBP + local_40] =[rbp-0x38] 으로 출력해 보면

아까랑 위치가 바뀐 것을 확인할 수 있다. GDB는 실행 타임에 레지스터값 변경이 가능하므로 fread 이후에 srand 호출 전 해당 위치에서 값을 변경하면 된다.

이 위치에 bp를 걸고

bp 걸린 위치에서 edi 레지스터 값을 변경한 뒤 seed가 0x61616161 일 때의 값을 입력하면

이렇게 잘 되는 것을 확인할 수 있다.

Conclusion

스택가드가 적용된 코드를 crack 해보았다. 카나리 값이 랜덤이고, 사용자의 입력은 코드 실행 시 인자로 들어가기 때문에 BOF로는 풀 수 없는 문제였다.
레지스터 값을 변경하여 간단하게 문제를 풀어 보았는데, 생각해 보니 이 방법은 stack guard가 적용되지 않은 곳에서도 사용이 가능하다고 한다.

우리는 seed 값 변경을 위해서 레지스터 변경만 하면 됬지만, 실제로 악성행위를 하기 위해서는 주로 ret 주소를 조작하여 악성코드가 실행되도록 한다고 한다.

분석하면서 느낀 점은 디컴파일 정보에 의존하여 분석하기 보다는 실제 Machine Code를 분석하는 연습이 필요하다는 생각이 들었다.

0개의 댓글