【逆向專題】【危!!!刑】(一)使用c#+Win32Api實現程式注入到wechat

四處觀察發表於2023-09-19

引言

    自從上篇使用Flaui實現微信自動化之後,這段時間便一直在瞎研究微信這方面,目前破解了Window微信的本地的Sqlite資料庫,使用Openssl,以及Win32Api來獲取解密金鑰,今天作為第一張,先簡單寫一下,獲取微信的一些靜態資料,以及將自己寫的c語言dll透過Api注入到微信程式裡面去,最後呼叫我們的dll的方法。話不多說,讓我們開始吧。

逆向

    靜態資料的話,需要用到的軟體 CE,全稱是Cheat Engine,圖示如下所示。接下來我們開啟CE,可以看到左上角有一塊綠色的按鈕,我們點選按鈕是附加程式到CE,然後我們點選附加微信到CE,在下面的圖中,我們看到已經把微信程式載入到了CE裡面去,然後我們要開始獲取靜態資料了。

 

    在獲取靜態資料之前,我們先開始講幾個概念,就是記憶體的概念,我們都知道,在程式啟動的時候,作業系統會給我們的程式分配虛擬記憶體,預設應該是4g,具體是和作業系統位數也有關係,然後在執行時也會動態的分配記憶體空間,我們學過計算機原理的肯定知道,我們的記憶體儲存結構就像是一個連結串列或者陣列,我們在給這個程式分配記憶體空間的時候,他的樣子也是是類似陣列的這種結構,首先假如我們的程式現在有一個主模組,主模組裡面又有自己的方法,自己的類,屬性等資訊,那分配的這個主模組的記憶體就是一個陣列,然後我們主模組有一個基礎地址,你可以將這個基礎地址看作是這塊記憶體陣列的索引0,而我們主模組的其他的方法,類,變數資訊,都是在這個0的索引進行移動到指定的地址,這個地址指向我們的記憶體,這個記憶體儲存著我們要的資訊。簡而言之,就是主模組是的地址就是索引0,而其他變數資訊可能在5,7,9等等,我們就需要判斷從0到5有多少間隔,這個就叫偏移量,我們透過屬性或者方法的記憶體地址減去主模組的地址,這個就是我們的偏移量,借這個例子就是5-0就是5,偏移量是5。

    然後我們回來,我們載入微信程式到了我們的CE之後,在wechat有一個模組叫Wechatwin,這個是window作業系統下的微信用到的主要模組,我們的和微信相關的基本都在這裡,當然不包括一些resource,這個有一個專門的模組,我們在此不多贅述,所以我們假如要找我們的靜態資料,例如微信暱稱,微訊號,或者手機號,所在地區,就需要找到我們的wechatwin的地址,這個就是這個模組的基址,然後我們需要在CE中,檢索字串找到我們要的資料,例如暱稱,手機號等資訊。然後用他的地址減去基址,得到偏移量。從而我們就可以在程式碼中獲取到這些資訊,接下來,我先帶大家在CE中找到我們想要找的資料。

    在CE上方右側,有一個輸入框,我們在這裡輸入我們需要檢索的資訊,支援的格式有byte,string,以及array,double等資料型別,我們需要找到是string,所以在ValueType那裡,我們選擇string。我的微信暱稱是雲淡風輕,所以在這裡搜尋雲淡風輕,可以看到,就檢索出來我們的暱稱資訊了,找到了這麼多,這裡我們往最下面拉,有一個綠色開頭的,Address是WechatWin的,就是我們要找的地址了,其他的也有的是綠色基於Wechatwin有的不是,有的就需要一個一個測試修改資料從而得到驗證了。

    我們雙擊那條綠色記錄,Wechatwin 將他加入到下面的列表去,代表我們選中的檢測的記憶體,接下來我們驗證一下,是否是找的正確的,雙擊Value,雲淡風輕,我們修改我們的Value,將雲淡風輕,改為good man 點選ok,可以在下面看到,我們的微信暱稱已經同步改為了good Man,說明我們找到的是對的,接下來,我們雙擊Address,

 

    彈出Change address姐妹,我們複製WechatWin.dll,需要我們找到我們這個模組的基址。然後在右邊有一個Add Address Manually,手動新增地址,,我們把複製的WeChatWin.dll複製過去,然後點選ok,在下面的列表我們就看到了這個模組的基址,接下來,我們需要判斷這個基址和暱稱之間的偏移量,按照我們剛才所說的方式計算,轉換16進位制就是0x7ffd3d668308-0x7ffd39b40000,隨便找一個16進位制計算器,算下來的結果就是3B28308,也就是Address裡面顯示的那個,實際上CE已經給我們把偏移算出來了,接下來按照同樣的方式,去搜尋我們的所在地區,以及手機號,如果有的資訊找不到的話,我們選擇我們的暱稱哪一行資料,右鍵,選擇Browse this Memory region,在記憶體頁顯示這個記憶體記錄,然後我們在旁邊就可以看到我們的國家,以及省份地區資訊了,如果有檢視地址,在右側,選擇我們要複製的記錄,右鍵,有一個goto Address,然後就導航到了我們的記憶體,然後複製地址即可。

 

c#程式碼獲取資料以及遠端注入     

    在上面我們講了,如何使用CE,去獲取我們微信的一些靜態資料,接下來,我們就需要使用c#程式碼,去實現我們獲取靜態資料,以及最後寫的一個遠端注入,來呼叫我們寫的一個庫。首先我們需要用到的有幾個Api函式,

    WaitForSingleObject,等待某個控制程式碼多長時間,在我們建立遠端執行緒的時候需要使用這個函式來等待執行緒執行結束。引數是等待的控制程式碼,我們填寫我們的執行緒控制程式碼。

    GetProcAddress,需要使用這個函式來呼叫kernel32.dll的LoadLibraryA方法,來載入我們的自己寫的dll,因為在每個程式啟動的時候,都會去呼叫這個方法來載入程式所依賴的dll,還有一個方法是LoadLibraryW,和這個方法區別在於不同針對不同的編碼來進行呼叫,W結尾主要是針對UNICODE的編碼,A結尾對應Ascii編碼,所以各位在呼叫的時候根據自己的編碼去呼叫,如果一個找不到就試試另一個。

    GetModuleHandle,這個函式是用來獲取kernel32.dll,結合上面的GetProdAddress來使用。

    OpenProcess,這個方法是根據指定的PID,對應就是Process類的Id,開啟指定的程式,同時指定以什麼許可權開啟這個程式,引數是三個,第一個是許可權,第二個是返回值是否可以被繼承,返回的程式控制程式碼是否可以被繼承,第三個引數就是我們的PID。

    VirtualAllocEx,給指定的程式分配虛擬記憶體,第一個引數是程式的控制程式碼,OpenProcess返回值,第二個引數指定程式內那個記憶體地址分配的記憶體,此處我們只是載入dll呼叫方法,並不注入到某個方法或者哪裡所以是Intptr.Zero,第三個引數是,分配的記憶體長度,我們載入dll需要dll的路徑,這裡就選擇路徑.Length就行,字串的長度就可以,第三個引數是記憶體分配的一些配置,可選值在後面會有,此處我們選擇Memory_Commit,第四個引數是記憶體許可權相關,記憶體是隻讀還是可以讀寫,以及用來執行程式碼或者怎麼樣,這裡我們選擇可以讀寫。

    ReadProcessMemory,讀指定程式的記憶體,第一個引數程式控制程式碼,OpenProcess返回值,第二個引數是這個程式某個記憶體的地址,第三個是資料緩衝區,讀取之後的內容就在這個緩衝區,我們讀取這個緩衝區就可以拿到資料,第四就是緩衝區的長度,第五個就是讀取的位元組數量。

    GetLastError,用來獲取Win32api呼叫的時候的errorcode,錯誤編碼,

    CloseHandle,關閉某一個控制程式碼,關閉基礎,關閉執行緒。

    WriteProcessMemory,寫入記憶體,我們需要將我們的dll地址寫入到指定記憶體中去,第一個引數程式控制程式碼,OpenProcess返回值,第二個引數,要寫入的記憶體地址基址,例如我們後期需要在某個方法進行注入,這塊就需要寫入這個方法的記憶體地址,第三個引數,寫入的byte資料,第四個引數是第三個引數的長度,最後一個引數是寫入的資料數量。

    CreateRemoteThread,在指定的程式中建立遠端執行緒,第一個引數 OpenProcess返回值,第二個引數是執行緒安全的一些特性描述,按網上所說,一般null或者 IntPtr.Zero,第三個引數設定執行緒堆疊大小,預設是0,即使用預設的大小,第四個引數是執行緒函式的地址,我們要透過這個方法去呼叫Kernel32的LoadLibrary方法載入我們的dll,那這個引數就填寫我們的GetProcAddress返回值,第四個引數就是建立這個執行緒的引數,就是分配的遠端記憶體的地址VirtualAllocEx返回值,就是說透過建立遠端執行緒來呼叫LoadLibrary方法載入我們寫入指定記憶體地址的dll庫,來實現注入,是這樣一個邏輯,第五個引數是執行緒建立的一些引數,是建立後掛起還是直接執行等,最後一個引數是輸出引數,記錄建立的遠端執行緒的ID。

    以上是我們所需要用到的所有的Win32Api函式,接下來我們進入程式碼階段。

    在下面的窗體,窗體會在載入的時候就去呼叫注入我們的dll,同時介面在載入的時候就獲取獲取我們的靜態資訊。我們的dll地址是E盤下面的一個dll,這個Dll使用c語言編寫。在啟動的時候我們去獲取我們的微信程式,拿到的ID,然後去注入我們的Dll,在下面的程式碼裡,我們判斷是否模組是WechatWin.dll,如果是,就定義了phone,NickName,Provice,Area等int值,這個其實就是我們在CE拿到的靜態資料的記憶體地址,減去我們的Wechatwin.Dll的出來的偏移量,然後定義了我們各個靜態資料的緩衝區,用來讀取從微信程式讀取的記憶體資料。然後我們呼叫了ReadProcessMemory函式讀取記憶體,獲取我們需要的靜態資料。然後使用Utf8轉為字串,顯示到介面上。這就是獲取靜態資料的原始碼,然後關閉我們的程式控制程式碼,並不是關閉微信,而是關閉我們獲取的這個程式控制程式碼。

 string dllpath = @"E:\CoreRepos\ConsoleApplication2\x64\Debug\Inject.dll";
var process = Process.GetProcessesByName("wechat").FirstOrDefault(); InjectDll(process.Id, dllpath); var pid = OpenProcess(ProcessAccessFlags.PROCESS_ALL_ACCESS, false, process.Id); int bytesRead; int bytesWritten; foreach (ProcessModule item in process.Modules) { if (item.ModuleName.ToLower() == "WechatWin.dll".ToLower()) { int phone = 0x3B28248; int NickName = 0x3b28308; int provice = 0x3B282A8; int Area = 0x3B282C8; var Nickbuffer = new byte[12]; var Phonebuffer = new byte[11]; var proviceBuffer= new byte[12]; var areaBuffer=new byte[12]; ReadProcessMemory(process.Handle, item.BaseAddress + NickName, Nickbuffer, Nickbuffer.Length, out bytesRead); ReadProcessMemory(process.Handle, item.BaseAddress + phone, Phonebuffer, Phonebuffer.Length, out bytesRead); ReadProcessMemory(process.Handle, item.BaseAddress + provice, proviceBuffer, proviceBuffer.Length, out bytesRead); ReadProcessMemory(process.Handle, item.BaseAddress + Area, areaBuffer, areaBuffer.Length, out bytesRead); var Nickvalue = Encoding.UTF8.GetString(Nickbuffer); var Phonevalue = Encoding.UTF8.GetString(Phonebuffer); var Provicevalue = Encoding.UTF8.GetString(proviceBuffer); var Areavalue = Encoding.UTF8.GetString(areaBuffer); label1.Text = Nickvalue; label2.Text = Phonevalue; label3.Text = Provicevalue; label4.Text = Areavalue; var buf = Encoding.UTF8.GetBytes("我是你爹"); CloseHandle(process.Handle); } }

 

    

     然後我們開始看看注入DLL的程式碼,我們先引入了諸多函式,然後定義了OpenProcess第一個引數許可權的列舉,定義了INFINITE 用來WaitForSingleObject等待指定的控制程式碼進行某些操作的執行結束,當然有一些我沒有定義完整,只定義我們此處需要的,完整的可以參考官網api去進行看。在剛進入這段程式碼,我們呼叫OpenProcess指定最高許可權開啟這個程式,然後獲取我們的dll地址的byte陣列,並將分配記憶體VirtualAllocEx到我們這個程式裡面,同時最後兩個引數代表分配記憶體的一些操作,例如記憶體是Memory_Commit,0x1000,以及記憶體是可以讀寫的0x04,分配好記憶體之後,我們去往我們分配好的記憶體寫入我們的dll路徑,呼叫WriteProcessMemory方法,傳入程式控制程式碼,記憶體地址,寫入的資料等,在下面GetProcAddress和GetModuleHandle用來載入kernel32的LoadraryA方法控制程式碼,最後我們呼叫了CreateRemoteThread函式將我們的dll注入到遠端程式中去。

 #region 32 api
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, int flAllocationType, int flProtect);
        [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool ReadProcessMemory(
       IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead
   );
        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(
       ProcessAccessFlags dwDesiredAccess,
       bool bInheritHandle,
       int dwProcessId
   );
        [DllImport("kernel32.dll")]
        static extern uint GetLastError();
        [DllImport("kernel32.dll")]
        public static extern bool CloseHandle(IntPtr hObject);

        [DllImport("kernel32.dll")]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
        [DllImport("kernel32.dll")]
        public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        // 程式訪問許可權標誌位
        [Flags]
        public enum ProcessAccessFlags : uint
        {
            PROCESS_ALL_ACCESS = 0x1F0FFF,
            PROCESS_CREATE_PROCESS = 0x0080,
            PROCESS_QUERY_INFORMATION = 0x0400,
        }
        const uint INFINITE = 0xFFFFFFFF;
        #endregionpublic  bool InjectDll(int processId, string dllPath)
        {
            IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_ALL_ACCESS, false, processId);

            if (hProcess == IntPtr.Zero)
            {
                Console.WriteLine("開啟失敗");
                return false;
            }

            byte[] dllBytes = Encoding.UTF8.GetBytes(dllPath); ;
            IntPtr remoteMemory = VirtualAllocEx(hProcess, IntPtr.Zero, dllBytes.Length, 0x1000, 0x04);


            int bytesWritten;

            if (!WriteProcessMemory(hProcess, remoteMemory, dllBytes, dllBytes.Length, out bytesWritten))
            {
                var ooo = GetLastError();
                Console.WriteLine("寫入失敗");
                return false;
            }
            IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
            var ooaa = GetLastError();
            if (loadLibraryAddr == IntPtr.Zero)
            {
                Console.WriteLine("獲取LoadraryA失敗");
            }

            // 建立遠端執行緒,在目標程式中呼叫 LoadLibraryA 載入 DLL
            var hRemoteThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLibraryAddr, remoteMemory, 0, IntPtr.Zero);
            var ooaa1 = GetLastError();
            if (hRemoteThread == IntPtr.Zero)
            {
                Console.WriteLine("目標程式建立遠端執行緒失敗");
            }

            // 等待遠端執行緒執行完畢
            WaitForSingleObject(hRemoteThread, 0xFFFFFFFF);


            WaitForSingleObject(hRemoteThread, INFINITE);

            CloseHandle(hRemoteThread);
            CloseHandle(hProcess);

            Console.WriteLine("注入成功");

            return true;
        }

     我們看看我寫的dll裡面是包括了什麼內容,我們的dll內容很簡單,就是建立一個txt檔案,然後寫入一個資料就行,這裡需要注意的是,在使用vs建立dll的時候 選項必須是選擇的是動態連結庫,這樣才有DLLMain方法,這樣在呼叫LoadraryA方法的時候才會呼叫我們的dll,自動呼叫DLLMain方法,同時裡面還有一個switch case語句是程式載入執行緒載入,以及執行緒解除安裝,程式解除安裝的判斷 我們可以在這裡去去一些我們的邏輯判斷,此處我並沒有寫,只是在外層建立了一個資料夾,接下來執行一下我們的winform,看看有沒有獲取到靜態資料,以及將我們的dll注入進去。馬賽克手機號。

 

 

 

    可以看到我們啟動了介面之後,檢視我們的Process.Modules,可以看到我們注入的Inject.dll,那我們看看有沒有建立txt呢。在下面可以看到,我們已經成功注入到微信程式並且建立了一個example.txt,並且寫入的內容和上圖定義的內容是一致的,到此,我們將我們dll注入到了微信程式中去了。

結語

    在上面我們講了一些如何找到靜態資料,以及根據基址,偏移量在程式啟動的時候找到我們想要的資料,並且將我們的dll成功注入到程式裡面去,在後面,我可能還會在深入研究一下逆向,到時候會繼續發文,感興趣的朋友可以關注一波,同時,近期,還破解了微信Sqlite本地資料庫獲取了一些內容,下面是獲取的資料內容,這個我應該不會開源,但是會有一個c語言的寫的解密demo開源,同時可能會分享一部分c#獲取解密金鑰的程式碼,同時也需要一些逆向的知識,win32api,這個東西由於涉及個人隱私,所以我尚不確定是否開源,因為存在有的人如果掛馬,可以竊取他人的隱私,所以後續再說,同時在寫的,講的不對的地方,歡迎各位大佬指正。

 

相關文章