유니버설 DLL 납치 기술 연구

8147 단어
유니버설 DLL 납치 기술 연구 by anhkgg 2018년 11월 29일

앞에 쓰다


Dll 납치는 모두가 낯설지 않다고 믿기 때문에 이론은 더 이상 말할 필요가 없다.Dll 납치의 목적은 일반적으로 자신의 dll 모듈이 다른 프로세스에서 실행될 수 있도록 하기 위해서이며, 묘사할 수 없는 일을 하기 위해서이다.
다른 사람의 프로그램이 정상적으로 실행될 수 있도록 하기 위해서는 일반적으로 자신의 dll에서 납치된 목표 dll와 같은 함수 인터페이스를 내보낸 다음에 자신의 인터페이스 함수에서 원시 dll의 함수를 호출하여 원시 dll의 기능을 정상적으로 사용할 수 있도록 해야 한다.내보내기 인터페이스는 직접 쓸 수도 있고 도구를 통해 자동으로 생성할 수도 있다. 예를 들어 유명한Aheadlib.이런 방법의 단점은 서로 다른 dll에 대해 서로 다른 인터페이스를 내보내야 한다는 것이다. 도구 도움말은 있지만 제한도 있다. 예를 들어 x64를 지원하지 않는다는 것이다.
이외에 일찍부터 일반적인 dll 납치 방법을 알고 있었다. 원리는 대체로 자신의 dll의 dllmian에 납치된 dll을 불러온 다음에 loadlibrary의 반환값을 납치된 dll이 불러온 후의 모듈 핸들로 수정했다.이런 방식은 바로 자신의 dll가 납치된 dll와 같은 함수 인터페이스를 내보내지 않고 사용이 더욱 편리하고 통용되는 것이다.
다음은 이런 통용되는 dll 납치 방법을 어떻게 실현하는지 분석해 보겠습니다.

원리 분석


테스트 코드를 마음대로 쓰십시오:
//mydll.dll        mydll.dll dll  
//mydll.dll.1  test.exe     dll       
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        __debugbreak();
        HMODULE hmod = LoadLibraryW("mydll.dll.1");
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
//test.exe
void main()
{
    LoadLibraryW(L"mydll.dll");
}

windbg로 창고를 불러옵니다. 아래와 같습니다.테스트에서 LoadLibraryW를 통해 mydll을 로드합니다.dll, 마지막으로 mydll에 들어갑니다!DllMain.현재 시스템 매핑 dll을 분석한 후에 기본 주소를 LoadLibraryW에 어떻게 되돌려 주어야만 이 값을 불러오는 mydll로 수정할 수 있습니다.dll.1의 값.
0:000> kvn
 # ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
00 0025eaf8 6e4112ec 6e410000 00000000 00000000 mydll+0x101d
01 0025eb38 6e4113c9 6e410000 00000001 00000000 mydll+0x12ec
02 0025eb4c 77d889d8 6e410000 00000001 00000000 mydll!DllMain+0x13
03 0025eb6c 77d95c41 6e4113ad 6e410000 00000001 ntdll!LdrpCallInitRoutine+0x14
04 0025ec60 77d9052e 00000000 74e92d11 77d77c9a ntdll!LdrpRunInitializeRoutines+0x26f (FPO: [Non-Fpo])
05 0025edcc 77d9232c 0025ee2c 0025edf8 00000000 ntdll!LdrpLoadDll+0x4d1 (FPO: [Non-Fpo])
06 0025ee00 75ee88ee 0037429c 0025ee40 0025ee2c ntdll!LdrLoadDll+0x92 (FPO: [Non-Fpo])
07 0025ee38 761b3c12 00000000 00000000 00000001 KERNELBASE!LoadLibraryExW+0x15a (FPO: [Non-Fpo])
08 0025ee4c 6848e3f5 0025ee58 003a0043 0055005c kernel32!LoadLibraryW+0x11 (FPO: [Non-Fpo])
09 0025f068 6848d1de d9131536 00000000 00000000 test!start+0x2b5
0a 0025f09c 6848e245 013a0000 761b3c26 76b3ea5f test!start+0x21e86e
0b 0025f328 013a1918 013a0000 0037187a 00000000 test!start+0x105
0c 0025fb44 013a30b9 013a0000 00000000 0037187a test+0x1918
0d 0025fb90 761b3c45 7ffd9000 0025fbdc 77d937f5 test+0x30b9
0e 0025fb9c 77d937f5 7ffd9000 74e93b01 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
0f 0025fbdc 77d937c8 013a312b 7ffd9000 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
10 0025fbf4 00000000 013a312b 7ffd9000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

먼저reactos에 가서 다음 함수 호출 구조를 찾아보세요.LdrLoadDll 매개 변수에서BaseAddress는 LoadLibraryW에 마지막으로 되돌아오는 값이기 때문에BaseAddress가 어떻게 값을 부여하는지 계속 보십시오.BaseAddress는 LdrpLoadDll에 계속 전달됩니다. LdrpLoadDll에서 먼저 LdrpMapDll을 통해 dll 모듈을 비추고 LdrEntry의 LDR 를 되돌려줍니다.DATA_TABLE_ENTRY 구조로 dll이 로드하는 기본 주소, 크기, 이름 등의 정보를 저장합니다.이어서 LdrEntry는peb->ldr 체인 테이블 구조에 삽입된 다음에 LdrpRunInitializeRoutines를 호출하고 LdrpRunInitializeRoutines에서 최종적으로 DllMain을 호출합니다. 여기서 더 이상 깊이 분석하지 않습니다.마지막으로 LdrEntry->DllBase는BaseAddress에 값을 부여합니다.이 절차에 대해 명확하게 분석한 다음에 이 값을 어떻게 수정하는지 고려하겠습니다.
NTSTATUS
NTAPI
LdrLoadDll(IN PWSTR SearchPath OPTIONAL,
           IN PULONG DllCharacteristics OPTIONAL,
           IN PUNICODE_STRING DllName,
           OUT PVOID *BaseAddress) {
               Status = LdrpLoadDll(RedirectedDll,
                         SearchPath,
                         DllCharacteristics,
                         DllName,
                         BaseAddress,
                         TRUE);
           }

NTSTATUS
NTAPI
LdrpLoadDll(IN BOOLEAN Redirected,
            IN PWSTR DllPath OPTIONAL,
            IN PULONG DllCharacteristics OPTIONAL,
            IN PUNICODE_STRING DllName,
            OUT PVOID *BaseAddress,
            IN BOOLEAN CallInit)
            {
                Status = LdrpMapDll(DllPath,
                            DllPath,
                            NameBuffer,
                            DllCharacteristics,
                            FALSE,
                            Redirected,
                            &LdrEntry);

                 //  peb->ldr  

                Status = LdrpRunInitializeRoutines(NULL);

                if (NT_SUCCESS(Status))
                {
                    /* Return the base address */
                    *BaseAddress = LdrEntry->DllBase;
                }
            }    

LdrpRunInitializeRoutines-> LdrpCallInitRoutine -> DllMain

이미지의 그런 방법은 스택을 통해 Ldrp LoadDll로 거슬러 올라가서 Ldr Entry를 찾아서 수정하는 것입니다. (준비 여부가 확실하지 않고 시간이 오래 지났습니다.) 그러나 Ldr Entry는 국부 변수이기 때문에 시스템에 따라 다를 수 있고 호환성이 떨어집니다.그러나 이 호출 절차를 보고 나면 다른 방법이 있다.LdrEntry->DllBase가BaseAddress에 값을 부여하면 값을 부여하기 전에 이 LdrEntry->DllBase를 수정하면 됩니다. DllMain은 바로 수정할 시기입니다. 그러나 스택을 거슬러 올라갈 필요가 없습니다.LdrEntry가peb->ldr에 삽입되었기 때문에, DllMain에서peb->ldr를 직접 가져와 체인 테이블을 훑어보고 목표 dll 창고를 찾을 수 있는 LdrEntry는 수정이 필요한 LdrEntry입니다. 그리고 수정하면 됩니다.
그러나 이 분석은 모두reactos를 바탕으로 한 것이므로 윈도우즈 시스템의 ntdll이 어떻게 우선적인지 확인해야 한다.
win7 x64 시스템에서 ntdll의 키 코드는 다음과 같습니다.차이점은 LdrpLoadDll이 BaseAddress가 아니라 직접 되돌아오는 ldrentry입니다. LdrpLoadDll 내부 프로세스는 기본적으로reactos와 일치합니다.그래서 방안은 반드시 실행 가능해야 하며, 후속 검증은 확실히 실행 가능하다는 것을 증명해야 한다.
int __fastcall LdrLoadDll()
{
v11 = LdrpLoadDll(v5, v9, v10, 1, 0i64, &dataentry);
  v12 = v11;
  if ( v11 >= 0 )
    *dllbase = dataentry->DllBase;

}

실현을 시도하다


실현은 사실 매우 간단하다. 핵심 코드는 다음과 같다.두 부분의 코드, 하나는 원시 dll 모듈(mydll.dll.1)을 불러와서 진짜 모듈 핸들 hMod(기본 주소)를 가져오는 것이고, 두 번째는peb->ldr를 옮겨다니며 mydll을 찾는 것이다.dll의ldrentry를 수정한 다음 dllbase를 hMod로 수정합니다.
void* NtCurrentPeb()
{
    __asm {
        mov eax, fs:[0x30];
    }
}
PEB_LDR_DATA* NtGetPebLdr(void* peb)
{
    __asm {
        mov eax, peb;
        mov eax, [eax + 0xc];
    }
}
VOID SuperDllHijack(LPCWSTR dllname, HMODULE hMod)
{
    WCHAR wszDllName[100] = { 0 };
    void* peb = NtCurrentPeb();
    PEB_LDR_DATA* ldr = NtGetPebLdr(peb);

    for (LIST_ENTRY* entry = ldr->InLoadOrderModuleList.Blink;
        entry != (LIST_ENTRY*)(&ldr->InLoadOrderModuleList);
        entry = entry->Blink) {
        PLDR_DATA_TABLE_ENTRY data = (PLDR_DATA_TABLE_ENTRY)entry;

        memset(wszDllName, 0, 100 * 2);
        memcpy(wszDllName, data->BaseDllName.Buffer, data->BaseDllName.Length);

        if (!_wcsicmp(wszDllName, dllname)) {
            data->DllBase = hMod;
            break;
        }
    }
}
VOID DllHijack(HMODULE hMod)
{
    TCHAR tszDllPath[MAX_PATH] = { 0 };

    GetModuleFileName(hMod, tszDllPath, MAX_PATH);
    PathRemoveFileSpec(tszDllPath);
    PathAppend(tszDllPath, TEXT("mydll.dll.1"));

    HMODULE hMod1 = LoadLibrary(tszDllPath);

    SuperDllHijack(L"mydll.dll", hMod1);
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DllHijack(hModule);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

총결산


테스트를 거치면 win7 x84와 win10 x64에서 유효하며 다른 시스템은 테스트하지 않았습니다. 문제가 있으면 메시지를 남기거나 스스로 해결하십시오.
이런 방안이 안 될까 봐 또 다른 생각을 했다. dllmain에서 hook Ldrp LoadDll의 반환 호출 주소에서 데이터entry의 값을 수정했다. Ldr LoadDll 함수 인터페이스가 고정되어 있기 때문에 이런 방식도 통용되어야 한다. 그러나 실현은 사실 지금보다 좀 번거롭다. 그래서 이런 사고방식을 보류했을 뿐이다. 검증을 실현하지 않고 괴롭히는 친구에게 남겨두자.
마지막으로 코드가github에 업로드되었는데,https://github.com/anhkgg/SuperDllHijack

좋은 웹페이지 즐겨찾기