[Cheat Engine] 상용프로그램 후킹하기1

들어가며

앞서 직접 윈도우 소켓 프로그램을 제작하고 후킹해 보았다. 상용 프로그램 또한 복잡도만 높아졌을 뿐 동일한 원리로 동작하기 때문에 분석 및 후킹이 가능하다. 그 과정을 아래와 같이 설명한다.

생각하기

채팅 프로그램을 제작할 때 생각해야 할 것들이 많다. 단순히 데이터를 주고 받는 것에서 끝나는 것이 아니다.

안전한 데이터 전송을 구현하려면 어떻게 해야 할까? 데이터가 전송 중에 변조되지 않았음을 검증하려면 어떻게 해야할까? 송신 측에서 수신 측이 데이터를 제대로 수신했는지 확인하려면 어떻게 해야 할까?

개발자들이 생각해서 구현해놓은 프로그램을 분석하기 위해서는 반대로 접근할 필요가 있다.

간단하게 생각했을 때 프로그램의 로직은 이럴 것이다라고 생각해볼 수 있다.

(송신측) 사용자가 채팅 입력 > (통신구간)전송 > (수신측) 수신자가 메시지 읽기

가장 간단하게 생각할 수 있는 로직이지만, 보안 측면에서 생각했을 때, 공격자가 통신구간에서 가로채기(스니핑) 공격을 할 경우 전송한 데이터를 읽을 수 있다는 취약성이 발생한다.

취약성을 해결하기 위해 암호화 로직을 추가한다!

(송신측) 사용자가 채팅 입력 > (송신측) 암호화 > 전송 > (수신측) 복호화 > 수신자가 메시지 읽기

이런 로직을 구성했을 때 공격자가 전송구간에서 암호화된 패킷을 저장해 두었다가 나중에 그대로 다시 전송시킬 수 있는 공격(재전송 공격)에는 여전히 취약하다. 이를 방지하기 위해 메시지의 시간값을 추가하거나 전체 메시지를 일방향 암호화 함수를 이용(hash)하여 메시지 끝에 이어 붙여 보내는 방법을 사용할 수 있다.

(송신측) 사용자가 채팅 입력 > (송신측) 암호화 > (송신측) 메시지 무결성 검증 값 추가 > 전송 > (수신측) 메시지 무결성 검증 > (수신측) 복호화 > 수신자가 메시지 읽기

이외에도 다양한 채팅방이 존재하는 경우 채팅방 Id 식별자를 추가해서 전송해야 하며, 전송하는 데이터가 일반 텍스트가 아닌 그림이나 파일을 전송하는 경우 이를 구분하는 방법도 추가해야한다.

사용자 입장에서는 단순히 메시지 하나를 보내는 것이지만 기밀성, 무결성, 가용성을 보장한 안전한 통신을 위해서 많은 과정이 따라야 한다.

분석

상용앱의 경우 회사의 자산인 소스코드가 노출되지 않도록 많은 보안 기법을 적용한다. Windows 환경에서 동작하는 PE파일의 경우 바이너리의 역공학(reversing)이 불가능하도록 패커(Packer)를 사용하여 난독화, 역공학 프로그램 탐지 등의 기법을 사용한다. 때문에 정적 분석과정에서는 이러한 로직들의 우회가 필요하기 때문에 오랜 시간이 필요하다.

분석의 대상인 카카*톡의 경우 더미다(themida) 패커를 이용한 패킹이 적용되어 있어 디버거 프로그램에서의 실행이 어렵다.

Themida 에러 메시지

앞선 포스트에서 사용했던 Cheat Engine을 사용한다. 바이너리 파일은 실행을 위해 메모리에 Mapping 되어야 한다. 이때 메모리영역에서의 함수 호출 및 인자 분석을 통해서 후킹할 함수의 pointer를 찾는다.

WSASend()

WSASend()와 Send()는 데이터를 전송한다는 기능상 차이는 없으나 통신 중 Blocking, Non-Blocking 에서 차이가 있다. 하지만 주제와는 조금 떨어지는 이야기이므로 넘어가도록 한다.

Send() 함수를 찾았던 방법으로 WSASend() 함수를 dll에서 찾는다. Windows 소켓 통신을 위한 dll이므로 기본적으로 로드되어 있기 때문에 찾을 수 있다.

Break Point(F5)를 걸고 메시지를 보내거나, 프로그램을 사용하면 WSASend()함수를 호출하는 순간 프로그램이 멈추면서 호출 시점의 인자를 확인할 수 있다.

WSASend()의 명세는 다음과 같다.

int WSAAPI WSASend(
  [in]  SOCKET                             s,
  [in]  LPWSABUF                           lpBuffers,
  [in]  DWORD                              dwBufferCount,
  [out] LPDWORD                            lpNumberOfBytesSent,
  [in]  DWORD                              dwFlags,
  [in]  LPWSAOVERLAPPED                    lpOverlapped,
  [in]  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

Send() 함수와 마찬가지로 첫 번째 인자에 Socket 식별자를 담고, 두 번째 인자로 전송할 데이터 버퍼의 포인터를 전달한다. 세 번째 인자로 전송할 데이터의 크기임을 확인할 수 있다.

그러면 메시지를 전송해서 어떤 데이터가 전송되는지 확인해 보자.

두 번째 인자(ESP+8)의 메모리 주소를 참조한 결과 알 수 없는 데이터가 전송되는 것을 확인할 수있다.

메신저 프로그램에서 메시지를 입력해서 전송으로 보냈는데 왜 이런 데이터가 소켓을 통해 전송되는 걸까

앞서 설명했던 추가 로직들(암호화, 무결성 검증 등)이 동작한 이후 소켓을 통해 전송되기 때문이다.


간단한 의사코드를 통해 구조를 추측해보자.

*msg = getmessage(); // 입력폼으로부터 메시지 입력받아 msg에 저장
crypted_msg = crypt(msg); //msg 암호화
integrity(crypted_msg);
...
WSASend(s, final_data, lenof_final_data, ...);

따라서 WSASend 이전에 문자열을 처리, 가공하는 다른 함수들의 실행 이전의 값을 분석해야 원래 메시지 데이터를 확인할 수 있다.

!주의 암호화, 무결성 검증 등의 로직은 실제 순서가 아니다. 메모리에 저장되는 메시지 평문을 찾는 과정을 설명하기 위함이므로 이 로직에 대한 자세한 분석은 진행하지 않았다.

BP가 걸려있는 WSASend() 함수 지점에서 Step Out - Execute till return을 통해서 반환하는 지점까지 실행(execute)한다. 단축키는 Shift+F8이다.

실행하면 WSASend 함수가 반환된 이후 다음 라인으로 EIP가 이동하는 것을 확인할 수 있다.

함수의 호출 시점에서 스택 영역의 값을 확인하면서 호출 과정을 역순으로 분석한다.

사진의 화살표 방향으로 거꾸로 올라가서 이 함수가 호출된 지점으로 가서 파라미터를 분석하고 이 과정을 반복한다.

 

4~5번 정도 반복하다 보면 입력한 메시지를 평문으로 확인할 수 있는 함수가 나타난다. 첫 번째 인자가 전송할 데이터, 두 번째 인자가 길이인 것을 추측할 수 있다. Windows DLL에서 로드하여 실행한 함수가 아니기 때문에 심볼 등을 확인할 수는 없지만 함수의 동작과정과 인자를 통해서 추측가능하다.

메모리의 값들은 알려진 프로토콜이 아닌 자체 프로토콜을 사용하고 있으나 간략하게 확인해 보면 메시지를 보낼 때 ‘WRITE’ 키워드를 사용하는 것을 알 수 있으며 chatId, msg, msgId 등의 키(key)에 값(value)으로 하여 전송하는 것을 확인할 수 있다. 각각의 필드는 null(0x00)으로 구분한다.

전송한 메시지 또한 msg 의 값으로 “ABCDEFG”를 확인한다.

Review

상용프로그램의 CheatEngine을 이용한 동적 분석을 통해 데이터를 전송하는 함수를 찾았다.

다음 글을 통해 이 함수를 후킹하는 방법을 다룬다.

반응형