프로그래밍/리버스 엔지니어링

[중단됨] 배달의 민족 단골 관리 프로그램 개발기 - #1 구조 및 로그인 데이터 분석

Tetrahedrite 2022. 12. 9. 03:29

해당 프로젝트는 중단되었으므로 더 이상 후속 글이 작성되지 않을 예정입니다.

 

미리 알립니다: 해당 문서는 현재 작업과 동시에 작성되고 있으므로, 업데이트가 다소 느릴 수 있음을 양해 바랍니다.

미리 알립니다: 해당 문서는 리버스 엔지니어링에 익숙하지 않은 초보자를 기준으로 작성되었습니다. 다소 불필요한 내용과 주절거림이 많아도 양해 부탁드립니다. 익숙하지 않은 내용은 댓글 남겨주시면 도와드리겠습니다.

 

지인 중에 음식 장사를 시작한 분이 계시는데, 배달의 민족 주문 접수 프로그램을 사용해서 배달 및 포장 주문을 받고 계신다고 했다.

 

그런데 혹시 배달의 민족 주문이 들어오면, 그 주문자가 몇번이나 주문했는지 알 수 없냐고 물어봤다.

주문 횟수에 비례해서 단골 고객에게 어느 정도 서비스를 주려고 하는 목적인 것 같다.

 

우선 궁금한 건 못참는 성격이니까 바로 프로그램 분석을 시작한다.

 

배달의 민족 PC 주문 접수 클라이언트는 '배민외식업광장 -> 배달주문접수'에서 받을 수 있다. (링크)

 

우선 주문접수 프로그램을 설치하고, 해당 프로그램이 어떤 구조로 되어있는지 파악한다.

기본 설치 경로는 "C:\BaeminRelay"에 설치된다.

 

배민 주문접수 프로그램 설치 경로 (그림 1)

설치 위치는 파악이 됐고, 우선 주문 접수 프로그램을 실행시켜 본다.

 

배민 주문접수 프로그램 로그인 화면 (그림 2)

위 그림 2와 같이 표시되고, 당장 화면으로 봤을 때 얻을 수 있는 정보는 없는 것 같다.

 

우선 로그인을 시도해서 어떤 정보가 있는지 알아본다.

 

배민 주문접수 프로그램 메인 화면 (그림 3)

그림 3과 같이 표시되고, 여기서 주문이 들어오면 우리가 매장에서 흔히 들어 본 "배달의 민족 주문!" 이라는 사운드와 함께 배달 및 포장 정보가 표시되는 구조로 되어있다.

 

그러나 어떤 메뉴를 살펴봐도 주문자 정보에 대한 데이터를 외부로 연동할 수 있는 기능은 존재하지 않는 것으로 보인다.

 

그러면 다음과 같은 방식을 생각해 볼 수 있다.

 

1. 배달의 민족이 공개한 API가 있는지 확인해본다.

구글이나 네이버에 검색했을 때 관련된 자료를 전혀 찾을 수 없었다. 패스!

 

2. 배달의 민족 측에 기능 추가(단골 관리 기능, 혹은 데이터 외부 Export 기능)를 요청한다.

하지만 이 방법은 내가 요청한다고 되는 일이 아니다. 내가 해야하는 일이 아니라 회사에 직접 부탁을 하고 얼마나 걸릴지도 알 수 없다. 패스!

 

3. 배달의 민족 주문 접수 프로그램이 보내고 받는 통신 데이터를 확인해본다.

 

4. 배달의 민족 주문 접수 프로그램을 역분석한다.

 

우선 가장 쉬워보이는 3번 방법을 사용하도록 하자.

 

배달의 민족 주문 접수 프로그램을 실행한 상태로 Wireshark를 사용해서 패킷 정보를 보도록 하자.

 

Wireshark 패킷 분석 화면 (그림 4)

네트워크 인터페이스의 패킷을 보니, TCP, TLSv1.2 프로토콜의 패킷들이 많이 보인다.

이 말인 즉슨 TLS 통신, 즉 암호화 통신을 통해서 네트워크 패킷의 감청을 막고 있다는 사실을 알 수 있다.

 

만약 이게 웹 브라우저라면 서명되지 않은 인증서를 허용하는 방식으로 Fiddler나 Burp Suite 같은 프로그램을 통해서 가짜 인증서를 설치하고 프록시를 통해 지나다니는 패킷을 감청할 수 있다. 하지만 이런 식으로 별도의 앱으로 만들어져 있는 경우는 그 과정이 조금 어렵다.

 

그렇다면 방법은 단 하나밖에 없다. 프로그램을 역분석하는 것 뿐이다.

 

프로그램 역분석에 앞서 어떤 컴파일러, 어떤 언어를 사용하여 제작된 프로그램인지 먼저 파악한다.

 

언어나 컴파일러에 따라 효율적인 역분석 방법이 다르다.

 

예를 들어서 .NET, Java처럼 바이트코드로 컴파일된 언어라면 ILSpy, Java Decompiler 등을 사용하여 바이트코드 디컴파일하여 보는 편이 분석이 더 빠르고, Python, Lua처럼 인터프리터가 읽는 스크립트로 제작된 언어라면 그 스크립트 파일을 찾아내서 읽어보는 편이 가장 빠르다.

만약 C, C++, Delphi처럼 네이티브 머신 코드로 컴파일된 언어라면 디버거를 사용 해 머신 코드를 한 줄씩 분석하는 방법밖에는 존재하지 않는다.

 

우선 Detect It Easy라는 프로그램을 사용해 프로그램의 정보를 취득한다.

 

Detect It Easy 화면 (그림 5)

위 그림 5를 보면 bmrelay.exe 파일에 대한 정보를 취득할 수 있다.

프로텍터는 Themida, 컴파일러는 Borland Delphi임을 알 수 있다.

 

역시 큰 기업이라 그런지 보안 의식 하나는 확실하게 갖추고 있다는 사실을 확인할 수 있다.

TLS 통신 + Themida + '우아한 형제들' 인증서로 서명된 PE 파일...

 

만약 다른 분석 프로그램을 사용하고 있다면 PE 헤더 섹션에서 확실한 증거를 찾을 수 있다.

 

Detect It Easy PE 헤더 분석 화면 (그림 6)

대부분의 프로그램은 UPX 혹은 Themida라는 패커를 사용하여 패킹 된다.

 

Themida는 대부분의 상용 프로그램 패킹에 사용되며, 언패킹에 가장 큰 어려움을 가져다 주는 상용 패커이다.

각종 방법을 총 동원해 프로그램의 역분석을 방해할 목적으로 많이 사용된다.

그리고 또한 섹션의 이름이 랜덤한 문자열로 변경됨을 확인할 수 있다. (eltqddug, iqetwkoq)

 

UPX는 단순히 실행 파일의 크기를 줄이기 위한 공개 패커이며, 실행 파일의 헤더를 UPX0, UPX1과 같은 형태로 변경하기에 쉽게 유추할 수 있고, 동일한 UPX 버전을 찾아 쉽게 언패킹할 수 있다는 특징이 있다.

 

중요한 내용은 아니고, 어쨌거나 우리가 분석해야 할 대상이 Borland Delphi로 컴파일 되어있다는 내용만 확인하면 된다.

 

델파이 언어의 특징은 순수한 Win32 응용 프로그램의 구조를 그대로 가져간다는 의미이다.

 

바이트코드가 아니기 때문에 일반적인 디버거를 붙여서 작업을 시작한다.

 

여기서 우리의 목적은 프로그램을 완전히 언패킹하는 것이 아니라 단순히 프로그램이 주고 받는 데이터를 분석하려고 하므로, 언패킹을 위한 준비는 하지 않아도 된다. 별도의 클라이언트를 구현하는 것이 언패킹하고 프로그램에 DLL을 주입해서 수정하는 것보다 더 싸게 먹힐테니까.

 

해당 작업에 있어서 필요한 디버거를 다운로드한다. 해당 글에서는 x64dbg라는 오픈 소스 디버거를 사용할 예정이다.

기본적인 한글이 지원되며, TitanEngine을 기반으로 한 막강한 디버깅 능력 및 플러그인을 제공한다. 또한 이름과 달리 x64뿐만 아니라 32비트 응용 프로그램의 디버깅 또한 지원한다.

 

x32dbg 경로 (그림 7)

다운로드 받은 압축 파일의 압축을 풀고 release\x32\x32dbg.exe를 실행한다.

 

주의해야 할 점은 무조건 x32dbg를 배민 주문접수 프로그램 이후에 실행해야 한다는 점이다.

 

배민 주문접수 프로그램은 Themida로 패킹되어 있기 때문에 실행 시점에 프로세스를 검색하여 디버거가 있다고 인식하면 프로그램을 종료해버린다.

 

x32dbg 실행 화면 (그림 8)

상단 탭에서 '파일 > 부착'을 선택하여 '부착' 창을 연다.

 

x32dbg 부착 화면 (그림 9)

부착 화면에서 bmrelay를 선택하고 '부착' 버튼을 누른다.

 

x32dbg 부착 성공 화면 (그림 10)

위 그림 10과 같이 꺼지지 않고 로드되었다면 성공이다. 만약 배민 주문접수 프로그램이 꺼진 경우, 배민 주문접수 프로그램을 먼저 실행하고 잠시 후에 x32dbg를 실행하여 다시 부착을 시도해보도록 한다.

 

여기서 우리가 이전 단계에서 Wireshark로 TLS 통신(HTTPS) 방식을 사용한다는 사실을 알고 있다.

그리고 또한 Borland Delphi는 거의 대부분 Wrapper 없이 윈도우 DLL을 직접적으로 호출하므로 해당 함수에 중단점을 걸면 정보를 파악할 수 있을 것이다.

 

따라서 우리는 해당 통신의 내용을 얻고 싶기 때문에 먼저 '기호' 탭으로 가서 불러온 모듈에 대한 기호를 파악하도록 한다.

 

x32dbg 기호 화면 (그림 11)

위 그림 11에서 기호 탭으로 들어오면 수많은 모듈에 대한 정보를 확인할 수 있다.

 

그 중에서 통신과 관련된 모듈을 먼저 파악하는 것이 중요하다. 만약 여기서 통신과 관련된 모듈이 뭔지 안다면 이 글을 보고 있지 않을 것이 분명하다. 따라서 약간의 부가 설명을 더 드리자면 아래 사진과 같은 모듈들을 위주로 보면 된다.

 

x32dbg 모듈 설명 (그림 12)

위 그림 12에서 빨간 원이 쳐진 부분이 통신을 담당하는 모듈들이다.

 

보통 윈도우의 대부분 통신은 Windows Socket(winsock)을 통해서 이루어지며, mswsock, ws2_32 등은 그에 관한 모듈들이다.

 

그런데 우리는 아까 TLS 통신이라는 증거를 얻지 않았는가? 그러면 99% 이상 HTTPS일 확률이 높다. 그렇다면 어디를 주목해야할까? 바로 wininet과 winhttp이다. 문서에 의하면 wininet은 winhttp의 슈퍼셋으로 어쩌구 저쩌구... IE의 설정을 그대로 따른다고 되어있는데, 별로 중요한 내용은 아닌 것 같다.

 

winhttp 모듈을 조금 더 자세히 살펴보자.

 

x32dbg winhttp 모듈 화면 (그림 13)

HTTP 관련 프로그램을 작성해보거나, 구조를 이해하고 있다면 위 그림 13에서 빨간 원이 표시된 부분이 HTTP 통신이 시작되는 부분이라는 점을 직감할 수 있을 것이다.

 

보이지 않는 위치에 WinHttpConnect라는 함수도 있었는데, 그걸로 말미암아 대충 봤을 때, Connect로 어떤 주소에 대한 커넥션을 열고 (아마 초기화도 하겠지?), Open이나 OpenRequest, SendRequest를 통해서 어떤 목표 주소로 연결한 뒤에, 필요에 의해 ReadData, WriteData 따위로 데이터를 추가적으로 읽거나 쓰는 작업을 수행할 것으로 보인다.

 

이 구조를 파악하기 위해서 먼저 Microsoft의 공식 문서를 참조한다.

우선 제일 시작 부분이 되어보이는 WinHttpConnect의 공식 문서를 먼저 참조한다.

 

WinHttpConnect의 공식 문서 (그림 14)

위 그림 14를 보면 함수의 원형에 관한 기술, 함수 파라메터에 대한 설명이 나와있음을 알 수 있다.

 

어? 그런데 파라메터에 HINTERNET 형식의 hSession이라는 파라메터를 요구한다. 옆에 있는 [in]의 표시로 보아 이는 세션이 하나 필요하다는 걸로 들린다.

 

그렇다면 위에 내가 얘기한 내용중에 Connect가 먼저일 것이라고 유추한 내용이 틀렸음을 알 수 있다.

그럴 수 있다. 원래 리버스 엔지니어링이란 무수한 예측과 시간과의 싸움이니까...

 

아래의 파라메터 부분을 보면 hSession에 대한 설명이 또 친절하게 표시되어 있다. 이전에 호출한 WinHttpOpen이 반환한 올바른 WinHTTP 세션을 달라고 되어있다.

 

아하! 그 말인 즉슨 WinHttpOpen이 먼저고 WinHttpConnect가 그 다음임을 짐작할 수 있겠다.

 

대부분의 Microsoft 공식 문서에는 친절하게 함수의 사용법에 대한 예제까지 포함되어 있는 경우가 많다.

 

따라서 아래로 조금 더 스크롤 하다보면 예제에 관한 내용이 나온다.

 

WinHttpOpen의 예제 (그림 15)

위 그림 15를 보면 어떤 식으로 사용해야할지에 대한 예제가 나와있다.

해당 내용으로 순서를 유추해보면...

1. WinHttpOpen을 호출하여 hSession을 얻는다.

2. WinHttp 세션과 주소, 포트를 인자로 넘겨 WinHttpConnect를 호출하고 hConnect을 초기화한다.

3. WinHttp 연결과 HTTP 메소드(예제는 "GET"), 기타 플래그를 인자로 넘겨 WinHttpOpenRequest를 호출하고 hRequest를 얻는다.

4. WinHttp Request와 기타 정보를 넘겨 WinHttpSendRequest를 호출하고 WinHttpReceiveResponse를 호출하여 응답 정보를 얻는다.

5. WinHttpQueryDataAvailable을 호출해서 버퍼에 데이터가 있는지 확인하고, WinHttpReadData을 호출하여 데이터를 읽는다.

 

정말 복잡한 내용이 아닐 수 없다.

 

어차피 우리의 목적은 정해져 있다. 데이터를 어떻게 주고 받는지만 알면 되는 것 아닌가?

 

그렇기 위해 얻어야 할 데이터와 해야할 일은 아래와 같다

 

1. 접속하는 주소와 포트 - WinHttpConnect에 들어오는 인자를 분석

2. HTTP 메소드 - WinHttpOpenRequest에 들어오는 인자를 분석

3. 요청 데이터 (Request Body) - WinHttpSendRequest에 들어오는 인자를 분석

4. 응답 데이터 (Response Body) - WinHttpReadData로 받아온 데이터를 분석

 

이 데이터만 있으면 외부 클라이언트를 사용 해 내가 원하는 기능을 구현할 수 있게 된다!

 

우선 로그인부터 시작해보자. 일단 로그인 이전에 WinHttpConnect, WinHttpOpenRequest, WinHttpSendRequest, WinHttpReadData에 모두 중단점을 걸어본다. 클릭하고 F2, 혹은 우클릭하여 '중단점 설정/해제' 버튼을 누르면 된다.

 

WinHttp 주요 함수에 중단점 설정 (그림 16)

그러면 위 그림 16과 같이 주소 부분이 빨간색으로 표시됨을 알 수 있다.

이제 로그인을 시도해보자!

 

배민 주문접수 프로그램 로그인 대기 화면 (그림 17)

위 그림과 같이 입력하고 로그인 버튼을 클릭해본다.

 

그러면 프로그램이 멈출 것이고, 디비거를 보면 아래 그림과 같이 되어있을 것이다.

 

WinHttpConnect에 중단점 적중 (그림 18)

 

위 그림 18과 같이 디버거엔 왼쪽 아래에 '일시 중지됨' 이라는 메시지와 함께 CPU 탭을 클릭해보면 중단점이 걸린 부분을 확인할 수 있다. 우리가 걸었던 WinHttpConnect에 대한 중단점이 현재 적중한 상태이다.

 

WinHttpConnect의 함수 원형을 기억하는가? WINHTTPAPI를 호출 규약으로 사용한다.

조금의 구글링을 통해 WINHTTPAPI는 WINAPI고, WINAPI는 stdcall을 호출 규약으로 사용하는 것을 알 수 있다.

 

호출 규약에 관한 내용은 알아서 구글링해서 찾아보길 바라고, stdcall은 결국 인자 전달 순서가 오른쪽에서 왼쪽, 모두 스택으로 전달된다는 사실이 가장 중요하다.

 

덤프 화면 표현식 이동 (그림 19)

위 그림처럼 왼쪽 아래에 덤프 탭의 화면을 클릭하고 Ctrl + G, 혹은 우클릭하여 '이동 > 표현식'을 눌러 이동할 주소를 입력한다.

우리가 확인해야 할 내용은 스택이므로 스택 포인터인 esp(Extended Stack Pointer)를 입력하고 확인을 누른다.

 

덤프 이동 표현식 입력 화면 (그림 20)

위 그림 20처럼 입력하고 확인을 누르면 아래에 적혀진 표현식 부분으로 덤프가 이동함을 알 수 있다.

 

덤프 이동 후 화면 (그림 21)

위 화면에서 덤프 부분을 자세히 살펴보자. 스택에 있는 0x9C3E0B는 WinHttpConnect가 함수가 끝나고 return 하면 돌아갈 주소가 기록되어있다. 해당 위치는 역시 bmrelay 프로그램의 일부분이다.

 

그 다음이 인자인데, 인자는 오른쪽에서 왼쪽으로 전달되고 x86 CPU에서 스택은 거꾸로 증가한다.

다시 위로 올라가서 WinHttpConnect의 함수 원형을 자세히 살펴보자.

 

첫번째 인자인 HINTERNET은 void*, 즉 4바이트 포인터 형식이다. 따라서 4바이트의 크기를 가진다.

두번째 인자인 LPCWSTR은 또한 4바이트 포인터 형식이다. 따라서 4바이트의 크기를 가진다.

세번째 인자인 INTERNET_PORT는 int, 4바이트의 크기를 가지는 형식이다.

마지막 인자인 DWORD는 이름에서부터 눈치 채듯 int, 역시 4바이트의 크기를 가지는 형식이다.

 

나중에 리버스 엔지니어링을 하다보면 알겠지만 윈도우는 대부분의 인자를 4바이트로 전달한다.

 

따라서 현재 스택에서 가장 가까운대로 확인해보면...

 

핸들은 0x198D7A28, 주소는 0x17E6386C, 포트는 0x1BB (10진수로 443, 즉 HTTPS 기본 포트)임을 확인할 수 있다.

 

덤프에서 DWORD 따라가기 (그림 22)

해당 부분의 시작 부분을 우클릭하고 '현재 덤프에서 DWORD 따라가기'를 클릭하거나, 아까처럼 다시 덤프 표현식 이동을 클릭해 0x17E6386C를 입력하고 해당 위치로 이동한다.

 

덤프 화면에서 보이는 접속 주소 (그림 23)

그러면 advance-relay.baemin.com에 443 포트, 즉 https://advance-relay.baemin.com으로 접속을 시도한다는 사실을 알 수 있다.

 

사실 이미 위에서 x32dbg가 다양한 정보를 표시하고 있기에 누구나 그 뒤의 주소인...

https://advance-relay.baemin.com/v3/auth/login

에서 로그인을 시도한다는 사실을 알 수 있다.

 

좀 치는(?) 개발자라면 주소에서 알 수 있듯이 REST API를 사용하여 로그인을 진행하는 것이 틀림없을 것도 눈치챘을 것이다.

 

하지만 여기서는 그 뒤에 있는 과정까지 계속 진행하도록 하겠다. F9 키를 눌러 계속 진행하도록 하자.

 

WinHttpOpenRequest에 중단점 적중 (그림 24)

이번엔 WinHttpOpenRequest에서 한번 더 중단점이 적중한다.

 

어차피 인자 분석이 목표이기 때문에 위 과정을 한번 더 반복한다. 덤프에서 esp로 이동하면 될까?

굳이 그렇지 않아도 된다. x32dbg는 기능이 아주 훌륭하기 때문에 스택에 있는 것을 4바이트 단위로 표시해준다.

 

덤프 화면 오른쪽을 보자. 눈치챘는가? 스택의 모습이 그대로 표시되어있다.

 

처음은 반환하면 이동 할 주소, 그 다음은 HINTERNET 핸들, 그 다음은 pwszVerb (HTTP 메소드), 그 다음은 pwszObjectName (HTTP 요청 주소)가 순서대로 기록되어있다. 굳이 덤프에서 따라가지 않아도, 그 내용까지 모두 분석해서 보여준다. 정말 친절하지 않은가?

 

따라서 https://advance-relay.baemin.com/v3/auth/login에 POST를 통해서 어떠한 데이터를 보냄으로써 로그인을 진행한다는 정보를 얻었다.

 

그러면 다음 함수의 진행으로 진행하자. F9 버튼을 한번 더 눌러준다.

 

WinHttpSendRequest에 중단점 적중 (그림 25)

위 그림처럼 예상대로 WinHttpSendRequest를 호출했고 중단점이 적중했다. 그런데 스택 창을 보니까 뭔가 이상하다?

 

WinHttpSendRequest에는 총 7개의 인자가 포함된다. 그런데 스택 화면을 보면 hRequest 핸들과 dwTotalLength를 제외하고는 모두 0으로 가득하다는 사실을 알 수 있다.

 

WinHttpSendRequest 공식 문서를 더 자세히 읽어보자.

 

WinHttpSendRequest 공식 문서 (그림 26)

정말 이상하다, POST나 PUT을 사용하여 추가적인 데이터를 보낼 때에는 lpOptional을 사용하여 보낸다고 되어있는데?

 

분명 어딘가에 더 자세한 내용이 있을 것이다. 더 자세히 읽어보자.

 

그러면 lpOptional에 뭔가 마지막에 힌트가 될 만한 문장이 있다.

 

"이 버퍼는 WinHttpReceiveResponse가 완료되거나 요청 핸들이 닫히기 전까지는 유효하다" 라는 문장이다.

 

아하! 그러면 그 함수 전에 무언가 더 추가할 수 있는 방법이 분명 존재하지 않을까?

 

그러면 WinHttpSendRequest가 끝나고 반환 하는 주소의 디스어셈블리를 확인해보자.

 

이 함수 이후에 WinHttp 관련 어떤 함수를 호출하지는 않을까?

 

WinHttpSendRequest 호출 함수로 돌아가기 (그림 27)

위 그림 27처럼 스택 화면에서 반환 주소를 우클릭하고 '디스어셈블리에서 DWORD 따라가기'를 클릭한다.

 

그러면 디스어셈블리에서 해당 부분으로 이동되는데, call이 적힌 부분에 마우스를 한번씩 갖다대면서 아래로 계속 진행해본다.

 

WinHttpWriteData를 호출하는 부분 (그림 28)

그러면 5번째 call 부분에서 WinHttpWriteData를 호출하는 코드가 있음을 확인할 수 있다.

 

WinHttpWriteData에 중단점 설정 (그림 29)

다시 기호 탭으로 이동하여 WinHttpWriteData에 중단점을 설정한다. 그리고 F9 키를 눌러 코드를 계속 진행시킨다.

 

WinHttpWriteData에 중단점 적중 (그림 30)

예상과 같이 WinHttpWriteData에 중단점이 적중했다.

 

이제 무엇을 해야할지 예상이 되는가? 함수 원형을 다시 보러가지 않아도 알 수 있다.

 

스택 창에 표시된 두번째 인자가 너무나도 확고하게 우리가 원하던 데이터였음을 알 수 있다.

 

이제 저 값을 우클릭하고 '현재 덤프에서 DWORD 따라가기'를 클릭하면 덤프에서 우리가 원하던 데이터를 확인할 수 있다.

 

덤프에 표시된 JSON 데이터 (그림 31)

예상한 것 처럼 JSON 데이터로 REST API를 통해 로그인을 진행함을 확인할 수 있다.

 

JSON 데이터를 파일로 저장 (그림 32)

이제 이 데이터를 다른 곳에서 사용할 수 있도록 그대로 '파일로 저장'을 선택하여 저장한다.

 

저장된 JSON 데이터 (그림 33)

열어보면 상당히 잘 저장되었다는 사실을 알 수 있다.

 

저장된 JSON 데이터는 나중에 다른 클라이언트에서 인증 절차를 받을 때 사용 될 예정이다.

 

다음 글로 이어질 예정입니다.

해당 프로젝트는 중단되었으므로 더 이상 후속 글이 작성되지 않을 예정입니다.