[LINE CTF 2023] Fishing

hoon·2023년 5월 21일
0

CTF

목록 보기
1/1

당시에는 풀지 못한 문제를 https://www.ctfiot.com/107081.html를 보고 https://ling.re/hardware-breakpoints/ 내용을 참고해 풀어보았다.

문제


해당 파일에는 fishing.exe라는 파일이 존재하고 실행 시 flag 입력받는다.

또한 이 파일은 디버거를 탐지하는 안티 디버깅 기법이 걸려있다.

IDA 분석

우선, 실행 시 볼 수 있는 문자열을 확인해보았다.

하지만 어디서 해당되는 문자열이 참조되는지 알 순 없었다.

Failed decompile

분석을 하다보면 몇몇 decompile이 실패한 부분을 확인할 수 있다.

해당 부분을 보면 다음과 같이 해당 주소+1로 jmp를 해 제대로 decompile이 안되도록 막는다.

그 부분의 코드를 undefine(단일 byte로 쪼개는 기능)을 통해 고쳐보자

그 후 140001E93부터 다시 decompile을 해보면

inc ecx와 같이 실행흐름에 영향을 주지않는 명령어와 jmp address+1을 확인할 수 있었다.

다음 부분의 jmp address+1을 undefine후 decompile을 해보니

dec ecx와 같이 실행흐름에 영향을 주지않는 명령어를 확인 가능했다.

이로써 jmp address + 1인 EB FF ??를 없애는 바이너리 패치를 하면 아이다가 제대로 decompile을 해줄 수 있을 것이다.

#!/usr/bin/python3
# patch.py
f = open("fishing.exe", 'rb')

data = bytearray(f.read()) #byte type은 immutable 객체임

f.close()

for i in range(len(data)):
    if data[i:i+2] == b"\xeb\xff":
        print(f"Found: {data[i:i+3]}")
        for j in range(3):
            data[i+j] = 0x90
        print(f"Patch binary to NOP: {data[i:i+3]}\n")


f = open('fishing_patched.exe', 'wb')
f.write(data)

실행 결과

python patch.py
Found: bytearray(b'\xeb\xff\xc7')
Patch binary to NOP: bytearray(b'\x90\x90\x90')

Found: bytearray(b'\xeb\xff\xcf')
Patch binary to NOP: bytearray(b'\x90\x90\x90')

Found: bytearray(b'\xeb\xff\xc7')
Patch binary to NOP: bytearray(b'\x90\x90\x90')
...
...
...

Patched file 분석

IDA에서 문자열을 참조하는 부분을 찾을 수 있게 되었다.

해당 주소로 가보면 다음 코드를 확인할 수 있다.

해당 코드를 분석하기 전에 다른 Anti-Debugging 기법을 확인해보면 PEB의 BeingDebugged와 NtGlobalFlag를 이용해서 디버거를 탐지하는 것을 알 수 있다.


해당 부분이 디버거 탐지후 exit(1)를 하기에 해당부분을 NOP으로 패치해보겠다.

패치 전

패치 후

실행


nop으로 패치후 apply binary patch 해줄 것

안티 디버깅을 우회하는 패치를 진행했으니 아까 분석하기로 했던 코드를 마저 분석을 해보도록 하겠다.

lpThreadparameter가 뭔지 궁금해서 MSDN에서 찾아보았다.

실행을 시작한 코드에 대한 포인터로 나오는데 중간에 strlen()의 인자로 들어간걸 보아하니 입력한 문자열이 되는 것 같다.

sub_140001CDF


input와 0x21을 XOR 연산을 진행하는 것을 알 수 있다.

sub_140001D33


input에 -34를 하는 것을 확인할 수 있다.

sub_140001D87


key(v2)와 0x11을 XOR 연산을 진행하는 것을 알 수 있다.

sub_140001DDB


key(v2)에 18을 더해주는 것을 확인할 수 있다.

sub_140002310

안의 sub_140002230에서 key와 key_len을 가지고 추가적인 작업(PSK)을 거쳐 암호화 키를 만든다. buf2에 input와 암호화 키로 RC4 암호화를 한다. (RC4: https://en.wikipedia.org/wiki/RC4)

모든 과정을 거치면 길이가 41인 미리 설정된 BUF1과 비교를 한다.

이제 동적으로 분석을 해보도록 하겠다. XOR_0x11과 ADD_0x12를 거친 key는 다음과 같다.

하지만 해당 key는 x64dbg를 사용한 결과와 달랐다. (x64dbg에는 처음 분석을 위해 scylla 플러그인을 사용해 모든 안티 디버깅 우회 기능을 키고 분석을 함)
XOR_11과 ADD_0x12을 거친 키가 "m4g1KaRp_ON_7H3_Hook"로 나왔다.

각 XOR_0x21, SUB_0x22, XOR_0x11, ADD_0x12에 breakpoint를 걸고 진행한 결과
SUB_0x22실행 시 push rbp를 하면서 kiUserExceptionDispatcher가 실행되는 것을 확인할 수 있었다.

또 XOR_0x11을 실행하면 push rbp를 하면서 kiUserExceptionDispatcher가 실행되는 것을 확인할 수 있었다.

Debug Register

문자열을 XOR, SUB, XOR, ADD하고 RC4로 암호화하기 전의 실행되는 흐름이 뭐가 있을까?를 보면
시작지점에서 Code = sub_1400020FA();에서 문자열을 입력받는 것을 알 수 있다.

sub_1400020FA()


중간의 sub_140002010를 보면 다음 동작을 하는 것을 알 수 있다.

GetThreadContext는 ContextFlags 멤버 값에 따라 선택적으로 Context를 검색하고 hThread 매개 변수로 식별된 스레드는 일반적으로 debug되지만 스레드가 debug되지 않을 때 함수가 작동할 수도 있다고 한다.

Context.Dr0 ~ Dr3 레지스터에는 4개의 hardware breakpoint 조건 중 하나와 관련된 linear address가 포함된다고 한다.
AddVectoredExceptionHandler로 Exception handler가 등록된다.

__int64 __fastcall sub_1400017D0(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
  int Rdx; // [rsp+Ch] [rbp-54h]
  DWORD64 Rcx; // [rsp+10h] [rbp-50h]
  int v4; // [rsp+1Ch] [rbp-44h]
  DWORD64 v5; // [rsp+20h] [rbp-40h]
  int v6; // [rsp+2Ch] [rbp-34h]
  DWORD64 v7; // [rsp+30h] [rbp-30h]
  int v8; // [rsp+3Ch] [rbp-24h]
  DWORD64 v9; // [rsp+40h] [rbp-20h]
  int m; // [rsp+4Ch] [rbp-14h]
  int k; // [rsp+50h] [rbp-10h]
  char v12; // [rsp+57h] [rbp-9h]
  int j; // [rsp+58h] [rbp-8h]
  int i; // [rsp+5Ch] [rbp-4h]

  if ( ExceptionInfo->ExceptionRecord->ExceptionCode != -2147483644 )
    return 0i64;
  if ( (ExceptionInfo->ContextRecord->Dr6 & 1) != 0 )
  {
    Rcx = ExceptionInfo->ContextRecord->Rcx;
    Rdx = ExceptionInfo->ContextRecord->Rdx;
    for ( i = 0; i < Rdx; ++i )
      *(_BYTE *)(i + Rcx) ^= i;
  }
  else if ( (ExceptionInfo->ContextRecord->Dr6 & 2) != 0 )
  {
    v5 = ExceptionInfo->ContextRecord->Rcx;
    v4 = ExceptionInfo->ContextRecord->Rdx;
    for ( j = 0; j < v4; ++j )
      *(_BYTE *)(j + v5) = (*(char *)(j + v5) >> 5) | (8 * *(_BYTE *)(j + v5));
  }
  else if ( (ExceptionInfo->ContextRecord->Dr6 & 4) != 0 )
  {
    v12 = 105;
    v7 = ExceptionInfo->ContextRecord->Rcx;
    v6 = ExceptionInfo->ContextRecord->Rdx;
    for ( k = 0; k < v6; ++k )
    {
      *(_BYTE *)(k + v7) ^= v12;
      v12 *= v12;
    }
  }
  else if ( (ExceptionInfo->ContextRecord->Dr6 & 8) != 0 )
  {
    v9 = ExceptionInfo->ContextRecord->Rcx;
    v8 = ExceptionInfo->ContextRecord->Rdx;
    for ( m = 0; m < v8; ++m )
    {
      *(_BYTE *)(m + v9) = 2 * *(_BYTE *)(m + v9) + 3 * m;
      *(_BYTE *)(m + v9) ^= (_BYTE)m + 5;
    }
  }
  ExceptionInfo->ContextRecord->Dr6 = 0i64;
  ExceptionInfo->ContextRecord->EFlags |= 0x10000u;
  return 0xFFFFFFFFi64;
}

DR6의 0~3까지의 비트는 어떤 hardware breakpoint가 유발되는지에 따라 설정된다.

Context.Dr6 & 1 -> Dr0 -> string[i] ^= i
Context.Dr6 & 2 -> Dr1 -> string[j] = (string[j] >>5) | (8*string[j])
Context.Dr6 & 4 -> Dr2 -> v12 = 105 
						  for k in range(len(string)): 
                          		string[k] ^= v12
                                v12 *= v12
Context.Dr6 & 8 -> Dr3 -> string[m] = 2*string[m] + 3*m
						  string[m] ^= m+5

X64dbg에서 디버깅을 시도할 때는 SUB_0x22부분에서 kiUserExceptionDispatcher가 발생해 예외 handler가 실행되었다.

RC4 암호화를 복호화하는 방법은 암호문과 키를 가지고 역순으로 진행시켜 주면 된다.

Encryption

input ^= 0x21
input -> (string[j] >>5) | (string[j] << 3) # ror 5
input -= 0x22
customRC4(input, key) // key = “m4g1KaRp_ON_7H3_Hook”
// customRC4에는 암호화 키를 만드는 과정이 있다. sub_140002230

Decryption

output = customRC4(flag, key)
output += 0x22
output -> (string[j] << 5) | (string[j] >> 3) # rol 5
output ^= 0x21

EncryptedFlag

0000004FB09FF9A0 D0 BE 9F 5A BD F0 34 B5 D0 6F FB E2 99 BA AE D7
0000004FB09FF9B0 36 D5 2D C2 22 45 B0 03 9D 63 66 53 C7 28 CC 2A
0000004FB09FF9C0 2B 14 BB 09 9B E3 60 46 3A 00 00 00 00 00 00 00
....

key = b'm4g1KaRp_ON_7H3_Hook'
encrypted_flag_list = "D0 BE 9F 5A BD F0 34 B5 D0 6F FB E2 99 BA AE D7 36 D5 2D C2 22 45 B0 03 9D 63 66 53 C7 28 CC 2A 2B 14 BB 09 9B E3 60 46 3A".split()
flag = list(map(lambda x: int(x,16), encrypted_flag_list))

# RC4 Encryption

def custom_rc4(input, key, output):
    v7 = [0] * 256
    psk(v7,key)
    v12 = 0
    v11 = 0
    for i in range(0,len(input)):
        v12 += 1
        v11 = (v7[v12] + v11) & 0xff
        v9 = v7[v12]
        v7[v12] = v7[v11]
        v7[v11] = v9
        v8 = v7[(v7[v12]+v7[v11]) & 0xff]
        output[i] = ((v11-24) ^ v8 ^ input[i]) & 0xff

def psk(v7, key):
    for i in range(256):
        v7[i] = i
    v6 = 0
    for i in range(256):
        v6 = (v7[i] + v6 + key[i%20]) & 0xff
        v4 = v7[i]
        v7[i] = v7[v6]
        v7[v6] = v4

def exception_handler2(data):
    for i in range(0,len(data)):
        # rol
        data[i] = ((data[i] << 5) & 0xff) | (data[i] >> 3)

output = [0] * len(flag) 
custom_rc4(flag, key, output)
output = list(map(lambda x: x+0x22, output))
exception_handler2(output)
output = list(map(lambda x:x^0x21, output))

print(bytes(output))

# b'LINECTF{e255cda25f1a8a634b31458d2ec405b6}'

0개의 댓글