search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

DLL 注入技術的 N 種姿勢

本文中我將介紹DLL注入的相關知識。不算太糟的是,DLL注入技術可以被正常軟體用來添加/擴展其他程序,調試或逆向工程的功能性;該技術也常被惡意軟體以多種方式利用。這意味著從安全形度來說,了解DLL注入的工作原理是十分必要的。

不久前在為攻擊方測試(目的是為了模擬不同類型的攻擊行為)開發定製工具的時候,我編寫了這個名為「injectAllTheThings」小工程的大部分代碼。如果你想看一下利用DLL注入實施的攻擊行為的若干示例,請參閱網址:。如果你想學習DLL注入的相關知識,你會發現該工程也是有用的。當你想要查詢這類信息/代碼時,你會發現網上充斥著垃圾;我的代碼可能也屬於垃圾。我並不是程序員,我只是在需要時對代碼進行修改。無論如何,我以一種便於閱讀和理解的方式,將多種能在32位和64位環境下生效的DLL注入技術(事實上一共7種不同的技術),整合到了一個單獨的Visual Studio工程之中。有些朋友對這些代碼感興趣,所以它也可能會吸引你。為了區分,每種技術有其獨有的源文件。

下圖為工具的輸出信息,其中顯示了所有的選項和實現的技術。

網友@SubTee認為,DLL注入是多餘的(如下圖所示);我傾向於同意TA的觀點,然而DLL注入並不僅僅是載入DLL那麼簡單

你確實可以利用簽名認證的微軟二進位文件來載入DLL,但你無法附加到一個特定的進程來干預其內存內容。

為什麼大部分滲透測試師實際上不知道DLL注入是什麼,或者它是如何工作的?因為Metasploit平台替他們包辦的太多了,他們一直盲目地使用它。我認為,學習這種「奇特的」內存操作技術的最好地點,實際上是遊戲黑客論壇。如果你正在進行攻擊方測試,那麼你就必須干這些「臟」活兒,同時研究這些技術;除非你樂意僅僅使用別人隨意編寫的工具。

大部分時間,我們使用很複雜的技術開始一次攻擊方測試;如果我們未被發現,則開始降低複雜度。基本上這就是我們開始向磁碟投放二進位文件和應用DLL注入技術的時間點。

本文試圖以一種簡單而高階的方式縱覽DLL注入技術,同時為GitHub中的項目(網址為:)提供「文檔」支持。

簡介

DLL注入技術,一般來講是向一個正在運行的進程插入/注入代碼的過程。我們注入的代碼以動態鏈接庫(DLL)的形式存在。DLL文件在運行時將按需載入(類似於UNIX系統中的共享庫)。本工程中,我將僅使用DLL文件,然而實際上,我們可以以其他的多種形式「注入「代碼(正如惡意軟體中所常見的,任意PE文件,shellcode代碼/程序集等)。

同時要記住,你需要合適的許可權級別來操控其他進程的內存空間。但我不會在此討論保護進程(相關網址:https://www.microsoftpressstore.com/articles/article.aspx?p=2233328&seqNum=2)和許可權級別(通過Vista系統介紹,相關網址:https://msdn.microsoft.com/en-gb/library/windows/desktop/bb648648(v=vs.85).aspx);這屬於完全不同的另一個主題。

再次強調一下,正如我之前所說,DLL注入技術可以被用於合法正當的用途。比如,反病毒軟體和端點安全解決方案使用這些技術來將其軟體的代碼嵌入/攔截系統中「所有」正在運行的進程,這使得它們可以在其運行過程中監控每一個進程,從而更好地保護我們。同樣存在惡意的用途。一種經常被用到的通用技術是注入「lsass」進程來獲取口令哈希值。我們之前都這麼干過。很明顯,惡意代碼同樣廣泛應用了代碼注入技術:不管是運行shellcode代碼,運行PE文件,還是在另一個進程的內存空間中載入DLL文件以隱藏自身,等等。

基礎知識

對於每一種技術,我們都將用到微軟Windows API,因為它為我們提供了大量的函數來附加和操縱其他進程。從微軟Windows操作系統的第一個版本開始,DLL文件就是其基石。事實上,微軟Windows API中的所有函數都包含於DLL文件之中。其中,最重要的是「Kernel32.dll」(包含管理內存,進程和線程相關的函數),「User32.dll」(大部分是用戶介面函數),和「GDI32.dll」(繪製圖形和顯示文本相關的函數)。

你可能會有疑問,為什麼會有這些API介面,為什麼微軟為我們提供如此豐富的函數集來操縱和修改其他進程的內存空間?主要原因是為了擴展應用程序的功能。比如,一個公司創建了一款應用程序,並且允許其他公司來擴展或增強這個應用程序;如此,這就有了一個合法正當的用途。除此之外,DLL文件還用於項目管理,內存保護,資源共享,等等。

下圖嘗試說明了幾乎每一種DLL注入技術的流程。

如上所見,我認為DLL注入共四個步驟:

1. 附加到目標/遠程進程

2. 在目標/遠程進程內分配內存

3. 將DLL文件路徑,或者DLL文件,複製到目標/遠程進程的內存空間

4. 控制進程運行DLL文件

所有這些步驟是通過調用一系列指定的API函數來完成的。每種技術需要進行特定的設置和選項配置。我認為,每種技術都有其優點和缺點。

(1)技術介紹

我們有多種方式可以控制進程運行我們的DLL文件。最普通的應該是「CreateRemoteThread」和「NtCreateThreadEx」函數;然而,不可能僅僅向這些函數傳遞一個DLL文件作為參數,我們必須提供一個包含執行起點的內存地址。為此,我們需要分配內存,使用「LoadLibrary」載入我們的DLL文件,複製內存,等等。

我稱之為「injectAllTheThings」的工程(因為我只是單純討厭「注入器」這個名字,加上GitHub上已經有太多的垃圾「注入器」,而我不想再多一個了)包含7種不同的技術;我並不是其中任何一種技術的原創作者,而是提煉總結了這七種技術(是的,還有更多)。其中某些已經有很多文檔資料描述(像「CreateRemoteThread」),而另一些則屬於未公開API函數(像「NtCreateThreadEx」)。以下是所實現的技術的完整列表,其中每種都可以在32位和64位環境下生效。

  • CreateRemoteThread

  • NtCreateThreadEx

  • QueueUserAPC

  • SetWindowsHookEx

  • RtlCreateUserThread

  • 利用SetThreadContext找到的代碼區域

  • 反射DLL

你可能通過其他的名字了解其中某些技術。以上並不是包含每一種DLL注入技術的完整列表;正如我所說的,還有更多技術,如果之後我在某個工程中需要對其接觸學習的話我會將它們添加進來。到目前為止,這就是我在某些工程中所用到的技術列表;其中某些可以穩定利用,某些不可以。需要注意的是,不能夠穩定利用的那些技術可能是由於我所編寫代碼的自身問題。

(2)LoadLibrary

正如MSDN中所述,「LoadLibrary」函數「被用於向調用進程的地址空間載入指定模塊,而該指定模塊可能導致其他模塊被載入」。函數原型與參數說明如下所示:

換言之,該函數只需要一個文件名作為其唯一的參數。即,我們只需要為我們的DLL文件路徑分配內存,將執行起點設置為「LoadLibrary」函數的地址,之後將路徑的內存地址傳遞給函數作為參數。

正如你所知道(或不知道)的,最大的問題是「LoadLibrary」會向程序註冊已載入的DLL模塊;這意味著這種方法很容易被檢測到,但令人驚奇的是很多端點安全解決方案仍檢測不出。不管怎樣,正如我之前所說,DLL注入也有一些合法正當的用途,因此我們還要注意的是,如果一個DLL文件已經用「LoadLibrary」載入過了,則它不會再次執行。你可以試驗一下,但我沒有對任何一種技術試過。當然,使用反射DLL注入技術不會有這方面的問題,因為DLL模塊並未被註冊。不同於使用「LoadLibrary」,反射DLL注入技術將整個DLL文件載入內存,然後通過確定DLL模塊的入口點偏移來將其載入;這樣可以按照需求更隱蔽的對其進行調用。取證人員仍然能夠在內存中找到你的DLL,但會很艱難。Metasploit平台大量使用了這項技術,而且大部分端點解決方案也還樂意始終使用它。如果你想查找這方面的技術資料,或者你在攻防遊戲中處於防守方,可以參閱以下網址:

  • https://www.defcon.org/html/defcon-20/dc-20-speakers.html#King

附註一下,如果你正在折騰你的端點安全軟體,而它很好地利用了以上所有這些技術,你可能需要使用以下攻防反欺騙引擎來試試(注意,我只是嘗試輕鬆的說法,以便你能理解)。某些反欺騙工具的反Rookit性能要比某些反病毒軟體要先進得多。reddit網站上有一本書你肯定讀過,叫「黑客遊戲」,它的作者Nick Cano對其有非常深入的研究;只需了解一下他的工作,你就會理解我所談論的內容。

附加到目標/遠程進程

首先,我們需要獲取我們想要交互的進程句柄;為此我們調用「OpenProcess」API函數。函數原型如下:

如果你讀過MSDN中相關的文檔,那麼你應該知道我們需要請求獲取一系列特定的訪問許可權;訪問許可權的完整列表參閱網址:https://msdn.microsoft.com/en-gb/library/windows/desktop/ms684880(v=vs.85).aspx。

這些許可權隨微軟Windows操作系統版本不同而不同;以下調用代碼可用於幾乎每一種技術之中:

在目標/遠程進程空間分配內存

我們調用「VirtualAllocEx」函數為DLL路徑分配內存。正如MSDN中所述,「VirtualAllocEx」函數「保留,提交或改變指定進程虛擬地址空間中的一塊內存區域的狀態;函數通過置零來初始化內存。」函數原型如下:

基本上,我們進行如下的調用操作:

或者你可以更聰明一點地調用「GetFullPathName」API函數;然後,我在整個工程中都沒有調用這個API函數,僅僅是出於個人偏好或者是不夠聰明。

如果你想要為整個DLL文件分配空間,就必須進行如下操作:

複製DLL文件路徑到目標/遠程進程的內存空間中

現在,我們需要調用「WriteProcessMemory」API函數,來將我們的DLL文件路徑,或者整個DLL文件,複製到目標/遠程進程中。函數原型如下所示:

一般的調用代碼如下所示:

如果我們想要像反射DLL注入技術所做的那樣複製整個DLL文件,就需要更多的代碼,因為在將其複製到目標/遠程進程之前我們需要將其讀入內存。具體代碼如下所示:

正如之前所述,通過使用反射DLL注入技術,以及將DLL文件複製到內存中,進程不會記錄該DLL模塊。

但這樣會有一點複雜,因為當DLL模塊載入到內存中時我們需要獲取其入口點;反射DLL工程的「LoadRemoteLibraryR」函數部分為我們完成了這項工作。如有需要請參閱源碼。

需要注意的是,我們將注入的DLL文件需要使用適當的包含與選項來進行編譯,這樣它才能與ReflectiveDLLInjection方法相匹配。「InjectAllThings」工程中包含了一個名為「rdll_32.dll/rdll_64.dll」的DLL文件,可用於練習。

控制進程來運行DLL文件

(1)CreateRemoteThread

可以說,「CreateRemoteThread」是最傳統和最流行,以及最多文檔資料介紹的DLL注入技術。

它包括以下幾個步驟:

1. 使用OpenProcess函數打開目標進程

2. 通過調用GetProAddress函數找到LoadLibrary函數的地址

3. 通過調用VirtualAllocEx函數在目標/遠程進程地址空間中為DLL文件路徑開闢內存空間

4. 調用WriteProcessMemory函數在之前所分配的內存空間中寫入DLL文件路徑

5. 調用CreateRemoteThread函數創建一個新的線程,新線程以DLL文件路徑名稱作為參數來調用LoadLibrary函數

如果你看過MSDN中關於「CreateRemoteThread」函數的文檔,那麼你應該知道,我們需要一個指針,「指向將由線程執行的,類型為『LPTHREAD_START_ROUTINE』的應用程序定義函數,並且該指針代表遠程進程中線程的起始地址」。

這意味著要運行我們的DLL文件,我們只需要控制進程來做就好(譯者註:由下文可知,應該是將「LoadLibrary」函數作為線程的啟動函數,來載入待注入的DLL文件)。很簡單。

以下代碼即之前所列的全部基本步驟。

完整代碼請參閱源文件「t_CreateRemoteThread.cpp」。

(2)NtCreateThreadEx

另一個選擇是使用「NtCreateThreadEx」函數;這是一個未公開的「ntdll.dll」中的函數,未來可能會消失或改變。這種技術相比而言實現更加複雜,因為我們需要一個結構體(具體如下所示)來向函數傳遞參數,以及另一個結構體用於從函數接收數據。

網址:處的文章詳細介紹了該函數調用。設置部分與「CreateRemoteThread」非常類似;然而,相較於直接調用「CreateRemoteThread」函數,我們使用如下代碼來調用「NtCreateThreadEx」。

完整代碼請參閱源文件「t_NtCreateThreadEx.cpp」。

(3)QueueUserAPC

除了之前介紹的方法還有一種選擇,不用在目標/遠程進程中創建一個新的線程,那就是「QueueUserAPC」函數。

根據MSDN中的文檔介紹,該函數「向指定線程的APC隊列中添加一個用戶態的非同步過程調用(APC)對象」。

函數原型與參數說明如下所示。

因此,如果不想創建我們自己的線程,我們可以調用「QueueUserAPC」函數來劫持目標/遠程進程中一個已存在的線程;即,調用該函數將在指定線程的APC隊列中添加一個非同步過程調用。

我們可以使用一個真實的APC回調函數,而不使用「LoadLibrary」。事實上參數可以是指向我們想要注入的DLL文件名稱的指針,具體代碼如下所示。

如果你想試用這種技術,那麼有一點你可能注意到了,即它與微軟Windows操作系統執行APC的方式有關。沒有能夠查看APC隊列的調度器,這意味著只有線程設置成可喚醒模式才能夠檢查隊列。

因此,我們基本上劫持每一個單獨的線程,具體代碼如下所示。

這樣做的理由主要是期望其中一個線程會被設為可喚醒模式。

另外,使用「雙脈衝星」(網址:,DOUBLEPULSAR 用戶模式分析:通用反射DLL載入器)工具分析學習這項技術,是個很好的辦法。

完整代碼請參閱源文件「t_QueueUserAPC.cpp」。

(4)SetWindowsHookEx

使用這項技術的首要工作是,我們需要理解在微軟Windows操作系統中劫持的工作原理。本質上,劫持技術是攔截並干預事件的一種方式。

正如你所猜想的那樣,有很多種不同類型的劫持技術。最通用的一種可能是WH_KEYBOARD和WH_MOUSE消息攔截;沒錯,它們可被用於監控鍵盤與滑鼠的輸入。

函數「SetWindowsHookEx」將一個應用程序定義的攔截常式安裝到一個攔截鏈表中。函數原型和參數定義如下所示。

MSDN中有一段很有趣的備註如下:

「SetWindowsHookEx函數可被用於向另一個進程注入DLL文件。一個32位的DLL文件不能注入一個64位的進程,反之亦然。如果一個應用程序需要在其他進程中使用劫持技術,那麼就要求一個32(64)位的應用程序調用SetWindowsHookEx函數來將一個32(64)位的DLL文件注入到一個32(64)位的進程中。32位和64位DLL文件的名稱必須不同。」

記住以上內容。

以下代碼是實現的簡要過程。

我們需要理解的是,每一個發生的事件都要遍歷攔截鏈表,該鏈表包含一系列響應事件的常式。「SetWindowsHookEx」函數的設置工作基本上就是如何將我們自己的攔截常式植入攔截鏈表中。

以上代碼用到了待安裝的劫持消息類型(WH_KEYBOARD)常式指針,包含常式的DLL模塊句柄,以及劫持所關聯的線程標識號。

為了獲取常式指針,我們需要首先調用「LoadLibrary」函數載入DLL文件,然後調用「SetWindowsHookEx」函數,並等待我們所需的事件發生(本文中該事件是指按鍵);一旦事件發生我們的DLL就會被執行。

注意,正如我們在維基解密(網址:)中所看到的,就連聯邦調查局的人員也有可能用到「SetWindowsHookEx」函數。

完整代碼請參閱源文件「t_SetWindowsHookEx.cpp」。

(5)RtlCreateUserThread

「RtlCreateUserThread」是一個未公開的API函數。它的設置工作幾乎和「CreateRemoteThread」函數相同,相應的也和「NtCreateThreadEx」函數相同。

實際上,「RtlCreateUserThread」調用「NtCreateThreadEx」,這意味著「RtlCreateUserThread」是「NtCreateThreadEx」的一個小型封裝函數;因此,這個函數並沒有新的內容。然而,我們可能只是單純地想使用「RtlCreateUserThread」而不用「NtCreateThreadEx」。哪怕之後發生變動,我們的「RtlCreateUserThread」仍能正常工作。

正如你所知道的,不同於其他工具,mimikatz工具和Metasploit平台都用到了「RtlCreateUserThread」。如果你對此感興趣,請參閱網址:https://github.com/gentilkiwi/mimikatz/blob/d5676aa66cb3f01afc373b0a2f8fcc1a2822fd27/modules/kull_m_remotelib.c#L59 和網址:。

因此,如果mimikatz工具和Metasploit平台都使用「RtlCreateUserThread」函數,那麼(是的,他們了解自己的處理對象)聽從他們的「建議」,使用「RtlCreateUserThread」;特別是你計劃做一項相比於簡單的「injectAllTheThings」工程來說更認真的項目。

完整代碼請參閱源文件「t_RtlCreateUserThread.cpp」。

(6)SetThreadContext

實際上這是一種非常酷的方法:通過在目標/遠程進程中分配一塊內存區域,向目標/遠程進程注入一段特別構造的代碼,這段代碼的用途是載入DLL模塊。

以下是32位環境下的代碼。

對於64位環境,實際上我沒有找到任何完整的工作代碼,因此我簡單寫了我自己的代碼,如下所示。

在我們想目標進程注入這段代碼之前,以下佔位符需要修改填充:

  • 返回地址(代碼樁執行完畢之後,線程恢復應回到的地址)

  • DLL路徑名稱

  • LoadLibrary函數地址

而這也是進行劫持,掛起,注入和恢複線程這一系列操作的時機。

我們需要附加到目標/遠程進程,之後當然是在目標/遠程進程中分配內存。注意,我們需要以讀寫許可權分配內存,以便操作DLL路徑名稱和用於載入DLL文件的封裝代碼。具體代碼如下所示。

之後,我們需要獲取一個運行於目標/遠程進程之上的線程上下文(即我們將要注入封裝代碼的目標線程)。

我們調用函數「getThreadID」來找到線程,你可以在文件「auxiliary.cpp」中找到該函數。

有了線程標識號之後,我們需要設置線程上下文。具體代碼如下所示。

然後,我們需要掛起線程來獲取其上下文;一個線程的上下文是指其寄存器的狀態,我們格外關注的是EIP/RIP寄存器(根據需要也可以稱其為IP——instruction pointer,指令指針)。

由於線程已被掛起,所以我們可以改變EIP/RIP寄存器的值,控制線程在不同的路徑上(我們的代碼區域)繼續執行。具體代碼如下所示。

因此,我們中斷線程,獲取上下文,並從上下文中提取EIP/RIP值;保存的舊值用於在注入代碼執行完成時恢複線程的執行流程。新的EIP/RIP值設置為我們注入的代碼位置。

然後我們用返回地址,DLL路徑名稱地址和「LoadLibrary」函數地址填充所有的佔位符。

線程開始執行的時候,我們的DLL文件將被載入;而當注入代碼執行完成時,執行流程將返回縣城掛起點,並從此恢複線程的正常執行流程。

如果你想要調試這種技術方法來學習練習,以下是操作流程。啟動你想要注入的應用程序,在此我們以「notepad.exe」為例。使用「x64dbg」調試工具來運行「injectAllTheThings_64.exe」,如下圖所示。

使用以下命令(根據你的實際環境來調整)。

在調用「WriteProcessMemory」函數處設下斷點,如下圖所示。

繼續運行程序,當運行到斷點處時,注意寄存器RDX中的內存地址,如圖所示。如果你對為什麼這裡需要關注RDX有疑問,請去查閱x64環境下的調用約定;搞清楚再回來繼續學習。

單步步過(快捷鍵F8)調用「WriteProcessMemory」函數的過程,開啟x64dbg工具的另一個實例,並將其附加到「notepad.exe」進程通過快捷鍵「Ctrl+g」調到之前複製的地址(即RDX寄存器中的內存地址)處,你將看到我們的代碼區域程序集,如下圖所示。

很酷吧?現在在Shellcode代碼起始處設下斷點。轉向「injectAllTheThings」調試進程,並運行程序。我們的斷點被成功斷下,如下圖所示;現在我們可以步過代碼,並分析這段代碼的功能。

當我們調用「LoadLibrary」函數時,我們的DLL文件成功載入,如下圖所示。

太棒了~

我們的Shellcode代碼將返回到之前保存的RIP地址處,「notepad.exe」進程將恢復執行。

完整代碼請參閱源文件「t_suspendInjectResume.cpp」。

(7)反射DLL注入

我將Stephen Fewer(這項技術的先驅)的代碼也整合到了這個「injectAllTheThings」工程中,同時還構建了一個反射DLL文件用於這項技術。注意,我們要注入的DLL文件必須使用適當的包含和選項來進行編譯,這樣它才能與反射DLL注入技術相匹配。

反射DLL注入技術通過將整個DLL文件複製到內存中的方式來生效,因此它避免了向進程註冊DLL模塊這一行為。所有的繁瑣工作都已完成。要在DLL模塊載入到內存時獲取其入口點,我們只需要使用Stephen Fewer的代碼;他的工程中所包含的「LoadRemoteLibrary」函數為我們完成這項工作。我們使用「GetReflectLoaderOffset」函數來確定在我們進程內存中的偏移,然後我們將偏移加上目標/遠程進程(即我們寫入DLL模塊的進程)的內存基址,將該結果作為執行起始點。

太複雜?好吧,可能有點兒;以下是實現上述過程的4個主要步驟。

1. 將DLL文件頭部寫入內存

2. 將每個區塊寫入內存(通過解析區塊表)

3. 檢查輸入表,並載入任何引用的DLL文件

4. 調用DLLMain函數的入口點

相比於其他方法,這種技術有很好的隱蔽性,主要被用於Metasploit平台。

如果你想要了解更多,請前往官方GitHub庫;還想要閱讀Stephen Fewer的文章的話,請參閱網址:。

還可以參閱「MemoryModule」項目的作者Joachim Bauch所寫的「從內存中載入DLL文件」,以及一篇好文章「不調用LoadLibrary函數『手動』載入32位/64位DLL文件」。

代碼

還有一些模糊複雜的注入方法,因此我未來將對「injectAllTheThings」工程進行更新。其中某些最有趣的技術包括:

•「雙脈衝星」工具所用到的技術

•網友@zerosum0x0所編寫的工具,使用SetThreadContext和NtContinue實現的反射DLL注入,詳細描述參見網址:,可用代碼詳見網址:。

以上我所描述的所有技術,都在一個單獨的工程中實現了,我將其放在GitHub庫中;其中還包括每種技術所需的DLL文件。為了便於理解,下表簡單介紹了所實現的方法和具體用法。

需要說明的是,從安全形度出發,應該堅持使用injectAllTheThings_32.exe注入32位進程,或者injectAllTheThings_64.exe來注入64位進程;儘管你也可以使用injectAllTheThings_64.exe來注入32位進程。而實際上我並沒有這樣實現,但可能之後我會嘗試一下,具體請參考以下網址:http://blog.rewolf.pl/blog/?p=102。參考網址中的技術基本上就是Metasploit平台上「smart_migrate」工具所用到的,詳見網址:https://github.com/rapid7/meterpreter/blob/5e24206d510a48db284d5f399a6951cd1b4c754b/source/common/arch/win/i386/base_inject.c。

整個工程的代碼(包括DLL文件)都在GitHub庫中。代碼以32位/64位環境編譯,包含或不包含調試信息都可以。

轉載請註明來自看雪社區

熱門閱讀

  • 議題徵集 | 2017 安全開發者峰會

  • 實現CVE-2016-3842的堆噴

  • 惡意軟體中的逃避技術,十八般武藝樣樣齊全

  • BitDefender:由7z PPMD產生的遠程棧溢出漏洞

  • URSNIF 變種通過收集滑鼠移動軌跡判斷是否處於沙盒以繞過查殺

  • 憑藉Scapy, radamsa工具和少量明文數據包對專有協議進行Fuzzing測試



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦