문제
이 프로그램은 디버거 프로그램을 탐지하는 기능을 갖고 있다. 디버거를 탐지하는 함수의 이름은 무엇인가
다운로드
실행
프로그램을 실행해보면 이렇게 정상이라는 메세지가 계속 출력됩니다.
그런데 여기서 올리디버거 같은 프로그램으로 디버깅을 시도하면?
올리디버거는 *.exe 파일을 열어서 분석도 되지만, Attach 기능을 이용하면 실행중인 프로그램을 잡아 내어 분석도 가능합니다. Attach로 04.exe 프로세스를 잡았서 디버깅을 시도할려했더니 바로 디버깅 당함이 떴네요.
이 프로그램은 디버깅 감지 여부를 친절하게 알려준다만, 일반적으로 상용 프로그램이 디버깅을 감지한다면 이런 메세지 없이 얄짤 없이 프로그램이 튕겨버리겠죠? ㅎㅎ
04.exe 프로그램을 우선 IDA로 뜯어서 이 프로그램이 어떤 로직으로 디버깅을 탐지하는지 알아봅시다.
이게 바로 오늘의 문제 토픽이기도 하구요. 디버거를 탐지하는 함수의 이름은 무엇일까??
분석
일단 프로그램을 열면 IDA 형님께서 exe 파일을 디스어셈블리 한다음에 DB를 생성해 쫘악~ 분석해줍니다.
일단은 우리가 궁금한건 저 디버깅을 탐지하는 부분인데요. 저 정상 / 디버깅 당함 메세지를 띄우는 부분에서 디버깅 당한지 검사가 이루어 질겁니다.
저 텍스트들을 한번 찾아볼까요?
IDA Strings 탭으로 왔는데요. "정상" 이나 "디버깅 당함에" 대한 문자가 안보입니다..
당연히 IDA를 만든 곳에서 기본적으로 한글을 지원하는건 아니기 때문에 바이너리 파일에 영어가 아닌 다른 언어가 입력되어 있으면 그냥 바이트의 묶음으로 보이게 됩니다. ex)40484546...
IDA에서 한글 표시 시키기
해당 링크에서 방법을 친절하게 설명해주고 있습니다.
글을 따라가서 잘 설정해주면 됩니다.
위 설정을 하고 다시 돌아와서.. 보니깐 "당함 \n" 이라는 문자열이 보입니다. 저게 "디버깅 당함\n" 의 일부분 이겠죠?
일단 설정대로 하니깐 한글이 보이긴 하네요 좀 개떡같이 보이긴합니다만
저기로 가면 디버깅 하는 부분에 대한 코드를 찾을 수 있을겁니다.
디버깅을 탐지하는 부분은 바로 이곳입니다.
보면 sleep으로 3E8(16진수) 만큼 기다리고, IsDebuggerPresent 를 이용해 디버깅을 탐지하고 있습니다.
일단 문제에 대한 답은 찾았는데 디버거를 탐지하는 함수의 이름은 바로 IsDebuggerPresent() 입니당.
코드도 간단해보이고 하니 뭔가 IDA의 디컴파일 기능이 잘 동작할 거 같은데요.
F5를 눌러서 이 코드를 Pesudo-C언어 형태로 복구해봅시다.
그럼 이렇게 깔끔한 코드로 변환됩니다.
이 정도면 충분히 읽을 수 있죠.
다만.. 불편한점이 있는데 저기 대기하는 Sleep 부분에 들어간 숫자가 16진수라 정확히 몇 초 기다리는지 잘 모르겠네요.
저 16진수를 오른쪽 클릭 - Decimal 을 클릭하면 저 숫자를 10진수로 표현해줍니다.
그러면 요렇게 변합니다. 아하 ㅎㅎ 0x3E8은 1000, 즉 1초 였군요.
1000 끝에 붙은 u의 경우 unsigned 를 나타냅니다. 애초에 시간 자체가 0초 이상이지, 음수는 없잖아요? 그래서 unsinged int를 썼다는 뜻에서 1000u 라고 IDA에서 표기해줍니다. (올리디버거에선 이런 분석 없이 걍 1000으로 뜹니다.)
그러니깐 이 코드는 한마디로 while(1) 로 무한루프를 돌리면서 1초마다 IsDebuggerPresent() 함수의 값이 참이면, "디버깅 당함" 을 표시하고, 반환값이 거짓이면 "정상" 을 출력하는 코드라고 이해하시면 됩니다.
실제로 IsDebuggerPresent() 함수는 호출할 때마다 해당 프로세스가 디버깅을 당하고 있는지 여부를 체크하고, 디버깅을 당하고 있지 않으면 0을 반환(False)하고, 아니면 1 (True)을 반환합니다. (정확히는 0이 아닌 값)
C언어에서는 0이 아닌값은 전부 참으로 간주합니다. 1뿐만 아니라 2도 참이고, 3도 참입니다.
사실 그래서 햇갈리면 IsDebuggerPresent() 는 디버깅 감지(True) 시 그냥 1을 반환한다고 생각해도 무방합니다.
#include <stdio.h>
#include <windows.h>
int main()
{
while (1)
{
Sleep(1000);
if (IsDebuggerPresent())
printf("디버깅 당함\n");
else
printf("정상\n");
}
}
위 IDA가 디컴파일 해준 코드를 기반으로 C언어로 직접 복구해본 코드는 위와 같습니다.
참고로 IsDebuggerPresent() 나 Sleep() 함수는 전부 windows.h 에 선언되어 있는, 윈도우 API 함수들입니다.
IsDebuggerPresent() 의 원리
아까도 말씀드렸듯이 IsDebuggerPresent() 함수는 해당 프로세스가 디버깅을 당하면 True를 반환하고, 아니면 False를 반환합니다. 만약에 저 디버깅 감지를 우회하려면 IsDebuggerPresent() 함수의 반환값이 EAX 레지스터에 저장될 것인데, EAX 레지스터의 값을 mov eax, 0 과 같이 0(false)으로 변조해서, 디버깅 당하지 않는 상태로 리턴값을 속이면 될 것입니다.
일단은 그건 그렇다 치고, IsDebuggerPresent() 함수는 어떤 원리로 디버깅을 감지하는 걸까요? 좀 더 깊게 알아봅시다.
디버깅을 막는걸 Anti-Debugging 이라고 하는데, 이 안티디버깅 기법중에 가장 기초적인 방법이 바로 IsDebuggerPresent() 함수입니다. 윈도우 API 에서 제공합니다.
일단은 너무 깊게 깊게 설명하면 끝이 없으므로 제가 간단히 이해한 바로 아주 간단히만 설명드리면, 기본적으로 윈도우는 프로세스가 수행되면 PEB(Process Environment Block) 이란 구조체를 만들어서, 이 곳에 프로세스의 정보를 관리하게 됩니다. 이 PEB라는 구조체는 관리중인 프로세스의 다양한 정보를 담고 있는데, 이 구조체의 멤버 변수 중에 BeingDebugged 라는 멤버 변수가 있는데, 여기에 값을 읽으면 그 프로세스가 디버깅 당하고 있는지 안하는지 알 수 있습니다.
IsDebuggerPresent() 함수는 PEB의 BeingDebugged 멤버 변수 값을 읽어서 디버깅 여부를 판단하게 됩니다.
패치(수정)
ESC를 누르고 디컴파일 창에서 일단 어셈블리 코드 창으로 다시 돌아옵니다.
사실 코드엔진 해당 문제에서 물어보는건 그냥 디버깅 함수 이름인데 한번 이 디버깅 탐지를 우회해보겠습니다.
중간 중간 chkesp 라고 esp를 체크하는 루틴이 있는데 정확히 무슨 의도로 삽입됐는진 모르겠네요. 일단 디컴파일 시 해당 정보를 날려보이는 것으로 보아 중요한 함수는 아니고 그냥 esp 값을 체크하는 함수 같습니다.
결론적으로 test eax, eax 를 통해 eax 값이 0인지 체크 하고 jz를 통해 0인 경우 점프하는 저 어셈블리 코드가 핵심입니다.
IsDebuggerPresent() 가 0을 반환해야 EAX 값으로 0이 반환되고 test eax, eax 와 jz 검사에 의해 디버깅 당하지 않는 코드 위치로 점프될 거니깐요.
사실 이번에는 특별히 IDA로 패치하는걸 보여드리려 했는데 아무래도 IDA는 정적 분석 도구고 올리디버거는 동적 분석 도구다 보니 동적으로 디버깅 하는건 올리디버거가 훨씬 편하더라구요 -.- 일단 IsDebuggerPresent 함수 위치로 올리디버거로 왔구요.
저 IsDebuggerPresent 함수가 항상 0을 반환하는 바보 함수로 만들어보겠습니다.
보통은 F8로 함수 안으로 들어가지 않고 리턴값만 보는 Step Over모드로 실행되고 있었을건데요.
이번엔 F7 (Step into) 모드를 통해 저 IsDebuggerPresent 함수가 실행하는 코드 위치로 들어가봅시다.
우선 함수 안으로 들어오자마자 어디론가 점프시키고 있습니다. 이제부턴 함수로 들어가진 않을꺼니깐 일단 F8만 계속 누를겁니다. F8을 눌러서 점프하란 곳으로 가봅니다.
충격적이게도 사실 IsDebuggerPresent 함수의 정체는 위 처럼 어셈블리 코드 3줄입니다.
mov 를 통해서 eax값으로 무언가 2번 불러오고 리턴해서 돌아오는게 끝이에요.
지금이야 정직하게 IsDebuggerPresent() 라는 함수로 호출해서 분석할 수 있었던 거지 실제로 리버서들을 엿먹일려면 저 2줄짜리 코드를 함수 호출 없이 사용해서 중간에 끼워넣으면 찾기가 매우 힘들겠죠? 패턴을 알면 찾을수야 있을건데 변형된 형태로 저 코드를 끼워 넣는다면..?
어쨌던 실제로 저 [30] 을 이용한 부분이 PEB 데이터 구조를 찾는 부분이고, 두번째 명령 EAX+2 는 PEB 구조 시작 후 2byte로 저장된 값을 가져오는 부분입니다. 그 저장된 값이 바로 BeingDebugged 입니다. [BeingDebugged 는 BYTE 타입으로 정의되어 있고, PEB 구조체의 2번째 멤버 변수임]
사실 좀 더 자세히 설명을 드리고 싶었는데, 나중에 정돈하여 쉽게 쉽게 설명드릴 수 있는 실력이 되면 글을 따로 작성해보도록 하겠습니다.
결론적으로 movzx eax... 저 부분이 BeingDebugged 값을 읽어서 eax 레지스터로 가져오는 부분인데 저 값을 mov eax, 0 과 같은 형태로 바꿔주면 IsDebuggerPresent 함수는 항상 0이 반환되는 바보 함수가 되는거죠.
더블클릭해서 Assemble 기능을 통해 이렇게 바꿔줍니다.
그리고 F8로 계속 실행해줍니다.
RETN 코드 실행 이후 ISDebuggerPresent 함수가 리턴되면서 원래 위치로 돌아와 호출이 끝났구요. 끝 코드에 BeingDebugged 값을 가져오는게 아니라 항상 eax에 0을 세팅하도록 바꿨으니깐 이제 저 함수를 호출해도 0이 반환되는 바보 함수가 된것이죠 ㅋㅋ
이렇게 패치했으므로 무조건 디버깅 코드가 정상이 나옵니다.
물론 저처럼 IsDebuggerPresent 실행 코드를 직접 패치해버리는거 보다는 IsDebuggerPresent 호출 이후 바로 아랫 라인에 mov eax, 0을 추가하는게 더 좋습니다 ㅎㅎ. 아니면 아까 jz 부분을 jmp로 바꿔서 무조건 성공 위치로 점프 시키는 방법도 있구요.
출처
'보안 강좌 > CodeEngn' 카테고리의 다른 글
[CodeEngn] Basic RCE L06 풀이 (0) | 2023.01.12 |
---|---|
[CodeEngn] Basic RCE L05 풀이 (0) | 2023.01.11 |
[CodeEngn] Basic RCE L03 풀이 (0) | 2023.01.06 |
[CodeEngn] Basic RCE L02 풀이 (0) | 2023.01.05 |
[CodeEngn] Basic RCE L01 풀이 (0) | 2023.01.05 |