
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
加殼的實現
我是個初學者,所知有限,難免會有錯誤,如果有人發現了錯誤,還請指正。
先大致說一下加殼的原理,即在原PE文件(后面稱之為宿主文件)上加一個新的區段(也就是殼),然后從這個新的區段上開始運行;也就算是成功的加上了殼;下面我們就說一下具體的實現。
這個工程有兩個項目,一個用來生成殼的Win32項目(dll類型),另一個是實現加殼的MFC項目;
加殼的項目界面是用MFC實現的,除了原有的類外,添加了兩個新類,一個用於PE操作,
一個用於加殼。
下面說下加殼過程的實現:
先將原PE文件讀取到內存;獲取頭文件信息,獲得.text區段信息,然后對代碼段進行加密(簡單的異或加密);隨后再用LoadLibrary將生成的殼(是一個dll)加載到內存;我們需要在殼的程序里對宿主PE進行解密,並且還要修復重定位,所以要把一些必要的數據存儲到加載的殼里面;
申請空間將dll(殼)拷貝一份(此處大家可能會疑惑為什么要拷貝一份,因為我在以LoadLibrary加載進來的殼里直接修改需要重定位的地址信息時,程序運行會出錯);
然后申請內存,大小是原宿主PE文件和殼的大小的和;先將宿主程序拷貝進去;
然后修復重定位信息,這個地方應該重點說明一下,我是直接把展開的整個dll拷貝到新PE文件里,並且打算殼的部分利用系統的重定位(每次加載PE文件的時候,如果重定位沒有關掉,系統會進行一次重定位),所以在拷貝之前,要把需要重定位的地址修改成在新PE中的虛擬地址,並且我們的殼是通過LoadLibrary的方式加載的,已經被重定位過,我又是直接拷貝過來的,所以修改地址的時候要注意,后面會說到如何修改。
然后,合並PE文件和殼;設置新的OEP,既然加殼,當然要從殼的我們規定的開始位置開始執行,需要將其改成相對新PE開始位置的偏移,原理和修復重定位差不多,不過一個是相對虛擬地址,一個是虛擬地址。
如圖:
相對虛擬地址=1+2;
如果修復重定位的話就某一地址的相對虛擬地址再加個默認基址;
下面是代碼實現部分:
bool CPack::Pack(WCHAR * szPath)
{
CPe objPe;
//讀取要被加殼的PE文件
DWORD dwReadFilSize = 0;
HANDLE hFile = CreateFile(szPath,GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD dwFileSize = GetFileSize(hFile, NULL);
char * pFileBuf = new char[dwFileSize];
memset(pFileBuf, 0, dwFileSize);
ReadFile(hFile, pFileBuf, dwFileSize, &dwReadFilSize, NULL);
//獲取PE頭文件信息
PEHEADERINFO pPeHead = { 0 };
objPe.GetPeHeaderinfo(pFileBuf, &pPeHead);
//加密
IMAGE_SECTION_HEADER pTxtSection;
objPe.GetSectionInfo(pFileBuf, &pTxtSection, ".text");
objPe.XorCode((LPBYTE)(pTxtSection.PointerToRawData + pFileBuf), pTxtSection.SizeOfRawData);
//用loadLibrary加載殼文件
HMODULE pLoadStubBuf = LoadLibrary(L"..\\Release\\Stub.dll");
//存儲必要的信息
PPACKINFO PackInfoAdd = (PPACKINFO)GetProcAddress((HMODULE)pLoadStubBuf, "g_PackInfo");
PackInfoAdd->dwOriStartPoint = pPeHead.pOptionHeader->AddressOfEntryPoint; //需要跳轉的OEP
PackInfoAdd->dwImageBase = pPeHead.pOptionHeader->ImageBase; //默認加載基址
PackInfoAdd->dwXorCode = pTxtSection.VirtualAddress; //加密代碼段地址
PackInfoAdd->dwXorKey = 0xE; //加密密鑰
PackInfoAdd->dwXorSize = pTxtSection.SizeOfRawData; //加密大小
PackInfoAdd->stcPeRelocDir = pPeHead.pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; //重定位表信息
PackInfoAdd->dwSizeOfImage = pPeHead.pOptionHeader->SizeOfImage; //原PE的大小
//拷貝一份
MODULEINFO stcModInfo = { 0 };
GetModuleInformation(GetCurrentProcess(), pLoadStubBuf, &stcModInfo, sizeof(MODULEINFO));
char * pStubBuf = new char[stcModInfo.SizeOfImage];
memset(pStubBuf, 0, stcModInfo.SizeOfImage);
memcpy(pStubBuf, pLoadStubBuf, stcModInfo.SizeOfImage);
//申請新空間存儲新PE
int NewPeSize = objPe.GetAddSectionSize(pFileBuf, (char*)pLoadStubBuf, stcModInfo.SizeOfImage);
char * pNewPeBuf = new char[NewPeSize];
memset(pNewPeBuf, 0, NewPeSize);
memcpy(pNewPeBuf, pFileBuf, dwFileSize);
//修改重定位表
objPe.FixReloc(pStubBuf, pNewPeBuf);
// 添加一個區段
objPe.AddSection(pNewPeBuf, pStubBuf, stcModInfo.SizeOfImage);
//設置入口點
//自己定義的殼的開始位置的原始偏移
DWORD Offset = PackInfoAdd->dwStartPoint - (DWORD)pLoadStubBuf;
//相對於新PE的起始位置的偏移
unsigned int NewOep = Offset + pPeHead.pOptionHeader->SizeOfImage;
objPe.SetOep(pNewPeBuf, NewOep);
//保存成文件
SavePackFile(szPath, pNewPeBuf, NewPeSize);
//釋放內存
delete[]pFileBuf;
delete[]pStubBuf;
delete[]pNewPeBuf;
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
下面我們就重點說一下修復重定位,通過重定位表找到能存儲着需要重定位地址的地址,然后我們把這個地址存儲的數據給改了就行了;其實修改重定位信息,簡而言之就是把需要重定位的地址改成基於新基址的虛擬地址,這樣程序在運行時才能根據真正的加載基址進行重定位;像我們這種情況,需要算出目標地址相對於殼的起始位置的偏移,加上宿主PE的默認加載基址,再加上這個殼加到PE之后的作為新區段的相對虛擬地址,也就是基於新PE的虛擬地址了。
並且由於相對虛擬地址是從0開始,所以頭文件里的sizeofimage就是這個新區段的RVA。
比如說一個在內存中展開后大小(sizeofimage)為5000的宿主文件,相對虛擬地址為5000的地方就是新區段的起始位置(vitualaddress)。
如圖:
還有幾點應該注意,殼的部分我們要用系統進行重定位,在循環中要修改殼需要重定位的頁的起始位置相對虛擬地址(重定位表的每一個PIMAGE_BASE_RELOCATION結構體變量描述的都是某個區段一個頁的重定位信息),最后還要將重定位表的指針指向殼的重定位表,並且將重定位表的大小改成殼的重定位表的大小。
代碼實現如下:
bool CPe::FixReloc(char* StubBuf, char*PeBuf)
{
//獲取被加殼PE文件的重定位表指針
PIMAGE_DOS_HEADER pPeDos = (PIMAGE_DOS_HEADER)PeBuf;
PIMAGE_NT_HEADERS pPeNt = (PIMAGE_NT_HEADERS)(pPeDos->e_lfanew + PeBuf);
PIMAGE_DATA_DIRECTORY pPeRelocDir = &(pPeNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
//獲取殼文件的重定位表指針
PIMAGE_DOS_HEADER pStuDos = (PIMAGE_DOS_HEADER)StubBuf;
PIMAGE_NT_HEADERS pStuNt = (PIMAGE_NT_HEADERS)(pStuDos->e_lfanew + StubBuf);
PIMAGE_DATA_DIRECTORY pStuRelocDir = pStuNt->OptionalHeader.DataDirectory;
pStuRelocDir = &(pStuRelocDir[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
//獲取重定位目錄
PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)StubBuf + pStuRelocDir->VirtualAddress);
//定義一個存儲TypeOffset的結構體
typedef struct {
WORD Offset : 12;
WORD Type : 4;
}TypeOffset, *PTypeOffset;
//修復重定位信息
PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
while (pStuReloc->VirtualAddress)
{
for (DWORD i = 0; i < dwCount; i++)
{
if (*(PDWORD)(&pTypeOffset[i]) == NULL)
{
break;
}
//存儲重定位地址的相對虛擬地址
DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset; //RVA
//重定位的地址
DWORD pRelocAddr = *(PDWORD)((DWORD)StubBuf + dwRVA);
//改掉(PDWORD)((DWORD)StubBuf + dwRVA)存儲的值,也就是已經重定位的將來需要重定位的地址
*(PDWORD)((DWORD)StubBuf + dwRVA) = pRelocAddr - pStuNt->OptionalHeader.ImageBase
+ pPeNt->OptionalHeader.ImageBase + pPeNt->OptionalHeader.SizeOfImage;
}
//修改殼重定表中需要重定位的頁的起始位置的相對虛擬地址
pStuReloc->VirtualAddress += pPeNt->OptionalHeader.SizeOfImage;
pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
}
//修改PE文件的重定位表指針
pPeRelocDir->Size = pStuRelocDir->Size;
pPeRelocDir->VirtualAddress = pStuRelocDir->VirtualAddress + pPeNt->OptionalHeader.SizeOfImage;
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
接下來講一下添加區段,這個主要要注意更改PE頭文件的信息,文件才能正常運行,不用對齊,我是把整個展開的dll拷貝過來的,按0x1000進行對齊展開的代碼,按0x200對齊肯定沒問題。要注意將新區段的屬性得是可讀可寫可執行(我踩的一個坑),至於為什么后面再說;
下面是實現代碼:
void CPe::AddSection(char* pBuf, char*pNewSection, int nSize)
{
PEHEADERINFO HeaderInfo = { 0 };
DWORD NumOfSection = 0;
if (IsPeFile(pBuf) == false)
{
return;
}
//獲得PE的頭文件信息
GetPeHeaderinfo(pBuf,&HeaderInfo);
//修改區段數量
NumOfSection = HeaderInfo.pFileHeader->NumberOfSections;
HeaderInfo.pFileHeader->NumberOfSections += 1;
//新增區段信息
PIMAGE_SECTION_HEADER pLastHeader = HeaderInfo.pSectionHeader + NumOfSection - 1;
PIMAGE_SECTION_HEADER pNewSecHeader = HeaderInfo.pSectionHeader + NumOfSection;
memcpy(pNewSecHeader->Name, "aStub", 6);
pNewSecHeader->Misc.VirtualSize = nSize;
pNewSecHeader->VirtualAddress = HeaderInfo.pOptionHeader->SizeOfImage;
int a = sizeof(*pBuf);
pNewSecHeader->PointerToRawData = pLastHeader->PointerToRawData + pLastHeader->SizeOfRawData;
pNewSecHeader->SizeOfRawData = nSize;
pNewSecHeader->Characteristics = 0xE0000020;
//修改鏡像大小
HeaderInfo.pOptionHeader->SizeOfImage += nSize;
//把區段添加到PE文件中
memcpy(pBuf + pNewSecHeader->PointerToRawData, pNewSection, nSize);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
大家應該已經注意到我獲取頭文件信息 很多地方都是直接用的一個函數GetPeHeaderinfo,我是把獲取頭文件信息的操作封裝成了函數;信息保存在一個自定義結構體類型的變量里;
typedef struct PEHEADERINFO
{
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_OPTIONAL_HEADER pOptionHeader;
PIMAGE_SECTION_HEADER pSectionHeader;
}PEHEADERINFO, *PPEHEADERINFO;
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
下面就說一下殼的實現:
既然前面對宿主PE的代碼段進行加密,自然要在殼的代碼里進行解密;在執行之前我們還要人工對原PE文件進行一次重定位;要讓程序執行原PE文件;自然要跳轉到原始的入口點,前面已經將原始入口點的相對虛擬地址保存到了殼里面。只要根據加載基址算出這個入口點的地址,到這個地址去執行就可以了。那么問題來了,我們的殼利用利用入口點直接開始執行了我們的代碼,既沒加載模塊,也沒加載資源什么的,無論是人工的進行重定位,還是獲得當前的加載基址,都需要函數。所所以我們要人工的獲取以下這些函數的地址。
首先,要先將可能要用到的函數定義好:
//Stub部分用到的函數的類型定義
typedef DWORD(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName);
typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
typedef LPVOID(WINAPI *fnVirtualAlloc)(_In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);
typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
定義函數指針,保存函數地址
//需要獲取的函數
fnGetProcAddress pfnGetProcAddress = NULL;
fnLoadLibraryA pfnLoadLibraryA = NULL;
fnVirtualProtect pfnVirtualProtect = NULL;
fnVirtualAlloc pfnVirtualAlloc = NULL;
fnGetModuleHandleA pfnGetModuleHandleA = NULL;
fnMessageBox pfnMessageBox = NULL;
fnExitProcess pfnExitProcess = NULL;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
好了該說一下,我上面提到的那個坑了,我開始沒有把殼的這個區段保存成可讀可寫可執行的屬性,導致獲得的下面這些變量的值都無法保存;后來意識到這個問題,改了屬性后果然正常。
下面是獲取Kernel32.dll的代碼
DWORD GetKernel32Addr()
{
DWORD dwKernel32Addr = 0;
_asm
{
push eax
mov eax, dword ptr fs : [0x30] //eax=PEB的地址
mov eax, [eax + 0x0C] //eax=PEB_LDR_DATA的指針
mov eax, [eax + 0x1C] //eax=模塊初始化鏈表的指針,InInitializationOrderModuleList
mov eax, [eax] //eax=列表中的第二個條目
mov eax, [eax] //eax=列表中的第二個條目
mov eax, [eax + 0x08] //eax=獲取到的Kernel32.dll基址(win7下獲取的是KernelBase.dll
mov dwKernel32Addr, eax
pop eax
}
return dwKernel32Addr;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
然后用名稱遍歷導出表得到函數GetProcAddress的地址;
然后我們就可以利用GetProcAddress獲得所需函數的地址:
void Init()
{
HMODULE hKernel32 = (HMODULE)GetKernel32Addr();
pfnGetProcAddress = (fnGetProcAddress)MyGetProcessAddress();
pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress(hKernel32, "VirtualProtect");
pfnVirtualAlloc = (fnVirtualAlloc)pfnGetProcAddress(hKernel32, "VirtualAlloc");
pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress(hKernel32, "GetModuleHandleA");
pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
//獲取messagebox
HMODULE hUser32 = pfnLoadLibraryA("user32.dll");
pfnMessageBox = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA");
//加載基址
LoadBase = (DWORD)pfnGetModuleHandleA(NULL);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
下面就要利用得到的函數,對原宿主PE進行人工的重定位了,就是把需要重定位的虛擬地址改成真正的地址;用保存的地址-默認加載基址(加殼的過程中已保存)+實際的加載基址;在重定位的過程中要修改保護屬性。
bool SelfReloc()
{
//獲取重定位目錄
PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)(LoadBase + g_PackInfo.stcPeRelocDir.VirtualAddress);
//定義一個存儲TypeOffset的結構體
typedef struct {
WORD Offset : 12;
WORD Type : 4;
}TypeOffset, *PTypeOffset;
DWORD dwOldProtect = 0;
//修復重定位信息
while (pStuReloc->VirtualAddress)
{
//修改保護屬性
//按道理來修改屬性大小設置為0x1000就可以了,調試能力有限,暫時不知道為什么設置成0x2000才可以
pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, PAGE_EXECUTE_READWRITE, &dwOldProtect);
PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
for (DWORD i = 0; i < dwCount; i++)
{
if (*(PDWORD)(&pTypeOffset[i]) == NULL)
{
break;
}
DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset;
DWORD pRelocAddr = *(PDWORD)(LoadBase + dwRVA);
*(PDWORD)(LoadBase + dwRVA) = pRelocAddr - g_PackInfo.dwImageBase
+ LoadBase;
}
//恢復保護屬性
pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, dwOldProtect, &dwOldProtect);
//獲取描述下一個頁重定位信息的結構體變量
pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
}
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
加殼的時候已經把原PE文件OEP相對虛擬地址保存下來,加上加載基址就是原PE入口點現在的地址,然后從那個地址執行就可以了:
void _declspec(naked) start()
{
Init();
DeXorCode();
SelfReloc();
_asm
{
push eax;
mov eax, LoadBase;
add eax, g_PackInfo.dwOriStartPoint;
mov dword ptr[esp], eax;
ret;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 頂
- 0
- 踩
- 0