여러분의 DllMain이 예외를 발생시키면 어떻게 될까요?

@codemaru · December 21, 2011 · 8 min read

#0.

이 내용은 최문혁 MVP 햄님께서 친히 제보해 주신 xfav 프로그램 버그를 찾다가 알게 된 사실입니다. 이 자리를 빌어서 다시한번 감사의 마음을 전합니다. 여러차례 덤프를 떠서 전달해 주시는 수고를 마다하지 않으셨습니다. 흙~ 눈물날라 그러는군요. 좀 오래된 이야긴데 바빠서 그간 정리를 못하고 있다가 인제서야 올리게 되는군요. 수정된 프로그램은 다음에 시간날 때 빌드해서 올리도록 하겠습니다. ㅋㅋㅋ~

증상은 간단했습니다. xfav.exe에서 인젝션하는 DLL이 explorer.exe에 주입되면서 explorer.exe가 크래시가 발생하는 현상이었습니다. 덤프는 explorer.exe가 종료되려고 하는데 엉뚱하게도 언로드된 xfav.exe의 _freefls를 가리키고 있더군요. 뭐 그런 상황이었습니다. 원인이 무엇이었는지는 제목이 암시하고 있는 거겠죠. 애니웨이, 시작해 봅시다!!!

#1.

제일 먼저 DllMain에 대해서 운영체제가 어떤 정책을 취하고 있는지 알아보아야 합니다. 우리가 아무리 잘 만들어본들 우리는 그저 O/S 위에서 돌아가는 일개 프로그램일 뿐이잖아요. ㅋ~ MSDN을 찾아보면 아래와 같이 설명이 나와있습니다.

If the DLL’s entry-point function returns FALSE following a DLL_PROCESS_ATTACH notification, it receives a DLL_PROCESS_DETACH notification and the DLL is unloaded immediately. However, if the DLL_PROCESS_ATTACH code throws an exception, the entry-point function will not receive the DLL_PROCESS_DETACH notification.

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682583(v=vs.85).aspx

영어를 잘하는 제가(ㅋㅋ) 짧게 요약을 해보면 이렇습니다. DLL_PROCESS_ATTACH에서 FALSE를 리턴하면 DLL_PROCESS_DETACH 호출을 받을 것이고, DLL_PROCESS_ATTACH에서 예외가 발생했다면 DLL_PROCESS_DETACH를 받을 생각 따위는 아예 하지도 말아라. 헉. 그렇습니다. 기본적으로 예외가 발생하면 DLL_PROCESS_DETACH 호출을 받지 못합니다. 우리가 운영체제를 만든다고 하더라도 그러겠지요? 생성자에서 예외가 발생하면 소멸자가 호출되지 않는 것과 동일한 원리 아니겠습니까? ㅋㅋ~

#2.

다음으로 우리는 그 지긋지긋한 CRT에 대해서 다시 살펴보는 시간을 가져야 겠습니다. CRT는 멀티스레드 환경을 지원하기 위해서 FLS(Fiber Local Storage)라는 것을 사용합니다. CRT 초기화 루틴에서 FlsAlloc(_freefls)를 호출해서 등록하죠. 이는 다시 CRT가 종료될 때 회수됩니다. 일반적인 DLL을 만들면 우리의 DLlMain이 호출되기 전에 FlsALloc으로 flsindex를 할당 받고, DLL_PROCESS_DETACH가 호출되면 CRT가 그것을 정리하는 원리로 돌아갑니다.

즉, 코드로 살펴보면 다음과 같은 구조가 되는 겁니다. OurDllMain이 우리가 작성한 것이고, 실제 DLL의 엔트리는 CrtDllMain이 되는 구조입니다. 머 심플하죠.

BOOL CALLBACK CrtDllMain(HINSTANCE instance, ULONG reason, PVOID reserved)
{
    if(reason == DLL_PROCESS_ATTACH)
    {
        flsindex = FlsAlloc(_freefls);
        return OurDllMain(instance, reason, reserved);
    }
    else if(reason == DLL_PROCESS_DETACH)
    {
        BOOL r = OurDllMain(instance, reason, reserved);
        FlsFree(flsindex);
        return r;
    }

    return TRUE;
}

#3.

그런데 이 CRT가 싸지른 FlsCallback이 문제입니다. 앞선 코드에서 _freefls가 되겠죠. 이 FlsCallback은 운영체제에서 필요한 경우, 스레드 종료, 파이버 종료, fls 해제와 같은 상황이 되면 호출을 합니다. 이런 복잡한 처리를 하기 위해서 운영체제는 이놈을 운영체제의 테이블, 리스트같은 곳에 등록을 해놓습니다. 뭐 PEB같은 곳에 말이죠. 그리고는 필요한 때에 그 루틴을 불러서 호출한답니다.

#4.

자 이제 다왔습니다. 그렇다면 우리가 만든 DllMain의 DLL_PROCESS_ATTACH에서 예외가 발생하면 어떻게 될까요? DLL_PROCESS_ATTACH가 실패했으니 일단 DLL은 로드되지 않겠죠. 여기까지는 문제가 없습니다. 그냥 DLL이 로드되지 않고 기능이 동작하지 않을 뿐이죠. 문제는 프로세스가 종료할 때 발생합니다. FLS를 정리하기 위해서 운영체제는 우리 DLL의 CRT 코드가 싸지른 FlsCallback을 호출하려고 시도합니다. 비극은 여기서 시작됩니다. DLL은 로드에 실패했기 때문에 메모리에 존재하지 않고, 운영체제는 그 바운더리에 있는 FlsCallback을 호출하려고 하는 겁니다. 결과는 머 당연하죠. 크래시가 발생합니다.

이유는 우리가 예외를 발생시켰고, 예외가 발생한 경우에 운영체제는 DLL_PROCESS_DETACH를 호출하지 않고, 그러니 CRT는 자신이 싸지른 FlsCallback을 회수할 기회가 없었던 겁니다.

#5.

결론을 요약하면 이렇습니다. DllMain에서는 절대로 예외를 발생시켜서는 안됩니다. 왜냐하면 그 예외가 CRT의 코드에 영향을 미칠 수 있기 때문입니다. 모든 예외를 봉쇄하고 발생한 경우에는 그냥 FALSE를 리턴하는 것이 현명합니다. 그러면 적어도 다른 프로그램에 민폐는 안끼칠 수 있겠죠. 그렇지 않다면 민폐를 끼치고 엉뚱한 프로그램이 크래시가 발생하는 문제가 생길 수 있습니다.

#0.

다시 처음으로 돌아와서 그렇다면 xfav.exe에서 주입했던 DLL의 DllMain은 왜 크래시가 발생했던 걸까요? 궁금하시죠? 이유는 제가 예전에 썼던 글들에 답이 있습니다. DllMain과 관련된 글을 두 개를 썼더랬습니다. 하나는 DllMain에서는 절대로 하지 말아야 할 것이고, 다른 하나는  DLL_PROCESS_DETACH 시점에서 주의해야 할 점이었습니다. 그런데 제가 그 DllMain에서는 절대로 하지 말아야 할 것을 해버린 것이었죠. user32.dll의 함수를 DLL_PROCESS_ATTACH에서 호출한 겁니다. user32.dll이 아직 초기화가 되어있지 않아서 예외가 발생해버린 겁니다. 하지 말라는데에는 다 이유가 있는 거겠죠. 흙~~

그래서 어떻게 했냐구요? 초기화를 지연 시켰습니다. DLL_PROCESS_ATTACH에서 하지 않고 말이죠.

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중