APC
비동기 프로시저 호출
윈도우 운영체제에서 사용하는 매커니즘으로, 특정 스레드의 컨텍스트(흐름)에서 비동기적으로 호출되는 함수를 의미한다.
APC는 특정 조건에서 실행되며, 주로 시스템 레벨의 프로그래밍에서 효율적 작업 처리, 비동기 이벤트 처리 또는 특정 작업을 스레트 컨텍스트 단위에서 수행하기 위해 사용된다.
쉽게 설명하자면 특정 작업을 미리 정해진 시간 or 조건이 만족되었을 때 실행시키는 역할을 한다고 볼 수 있다.
- APC queue
- APC는 특정 스레드에 큐(queue) 형태로 저장된다
- APC를 실행하려면 해당 스레드가 alertable 상태여야 한다.
- SleepEx, WaitForSingleObjectEx와 같은 함수 호출 중 ALERTABLE 플래그가 설정되면 APC가 실행된다.
- APC 모드
- 커널 모드 APC: 운영 체제(OS) 커널에서 관리하며, 시스템 리소스를 관리하거나 하드웨어와 직접 상호작용하는 모드
- 유저 모드 APC: 유저 모드 애플리케이션에서 실행되며, 개발자가 직접 정의한 작업을 처리하는 모드
APC 실행 예제
golang을 공부중이어서 한번 golang으로 실습을 해 보았다.
직접적으로 Windows APC 매커니즘을 지원하지 않기 때문에 Windows API를 호출해서 APC를 구현하는 방식으로 진행을 해야한다.
☆ golang.org./x/sys/windows 패키지 사용
- 특정 스레드 지정
- 해당 스레드에 APC 등록
- 해당 스레드가 alertable 상태로 진입 -> APC 실행!
alertable 상태
alertable wait
- 유일하게 사용자가 제어할 수 있는 state
windows 운영체제에서 특정 스레드가 비동기 작업(APC) 을 실행할 준비가 된 상태
쉽게 말해, 스레드가 잠시 쉬면서 요청이 오면 -> 작업을 처리할 수 있도록 기다
먼저 syscall, golang.org/x/sys/windows 패키지를 import한다.
package main
import (
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
APC가 실행되면 호출할 함수를 작성한다.
APCFunction이라고 정의하고 함수를 작성해보자.
func APCFunction(par uintptr) uintptr { // 참고로 golang에서 uintptr은 포인터처럼 사용할 수 있는 정수형 타입으로, 메모리 주소를 저장하는데 주로 사용된다.
fmt.Printf("APC 실행중 / 전달된 값 : %d\n", par)
return 0
}
스레드를 생성한다.
windows.CreateThread 함수를 사용하여 새 Windows 스레드를 생성한 뒤, 스레드가 실행되면 Aertable 상태로 진입하여 APC를 처리하도록 만든다.
var threadId uint32 // uint 32비트 변수 선언
threadHandle, err := windows.CreateThread(
nil,
0,
threadProc,
0,
0,
&threadId, // 스레드 id
)
if err != nil {
fmt.Printf("스레드 생성 실패 %v\n", err)
return
}
defer windows.CloseHandle(threadHandle) // 함수가 종료될 때 실행
QueueUserAPC를 호출하여 타겟 스레드의 APC 큐에 작업을 등록한다.
// 스레드 함수 정의
threadProc := syscall.NewCallback(func(param uintptr) uintptr {
fmt.Println("스레드 시작...")
for i := 0; i < 5; i++ {
fmt.Printf("Alertable 상태로 진입 %d\n", i+1)
windows.SleepEx(1000, true) // Alertable 상태에서 대기
}
fmt.Println("스레드 종료")
return 0
})
.
.
.
for i := uintptr(1); i <= 3; i++ {
err := windows.QueueUserAPC(
windows.NewCallback(APCFunction), // APC 콜백 함수
threadHandle, // 타겟 스레드 핸들
i, // 전달할 값
)
if err != nil {
fmt.Printf("APC 등록 실패: %v\n", err)
return
}
}
스레드 함수에서 windows.SleepEx를 호출해서 Alertable 상태로 전환한다.
windows.WaitForSingleObject(threadHandle, windows.INFINITE)
fmt.Println("프로그램 종료")
SleepEx의 두번째 인수를 true로 설정하면 APC를 처리할 수 있는 Alertable 상태가 된다. 그러고 나서 windows.WaitForSingleObject 로 스레드 종료 때까지 대기하도록 한다.
package main
import (
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
// APC 콜백 함수
func APCFunction(par uintptr) uintptr {
fmt.Printf("APC 실행중 / 전달된 값: %d\n", par)
return 0
}
func main() {
// 스레드 함수 정의
threadProc := syscall.NewCallback(func(param uintptr) uintptr {
fmt.Println("스레드 시작")
for i := 0; i < 5; i++ {
fmt.Printf("Alertable 상태로 진입 %d\n", i+1)
windows.SleepEx(1000, true) // Alertable 상태에서 대기
}
fmt.Println("스레드 종료")
return 0
})
// 스레드 생성
var threadId uint32
threadHandle, err := windows.CreateThread(
nil, // 기본 보안 속성
0, // 기본 스택 크기
threadProc, // 스레드 함수
0, // 매개변수
0, // 실행 플래그 (0: 즉시 실행한다는 의미)
&threadId, // 스레드 id
)
if err != nil {
fmt.Printf("스레드 생성 실패: %v\n", err)
return
}
defer windows.CloseHandle(threadHandle)
fmt.Println("스레드에 APC 등록")
// APC 등록
for i := uintptr(1); i <= 3; i++ {
err := windows.QueueUserAPC(
windows.NewCallback(APCFunction), // APC 콜백 함수
threadHandle, // 타겟 스레드 핸들
i, // 전달할 값
)
if err != nil {
fmt.Printf("APC 등록 실패: %v\n", err)
return
}
}
// 스레드 종료 때까지 대기
windows.WaitForSingleObject(threadHandle, windows.INFINITE)
fmt.Println("프로그램 종료")
}
더이상 syscall 패키지를 사용하는 것이 권장되지 않는다고 에러가 뜨는 바람에(....) windows.NewLazySystemDLL 을 사용하여 함수를 호출하는 방식으로 전체적으로 다시한번 코드를 수정했다😢
또한 go 런타임은 고루틴과 os 스레드를 매핑하는 고유 스케쥴러를 가지고 있기 때문에 직접 CreateThread API 함수를 호출하는 것은 문제를 일으킬 가능성이 있다 -> 그렇기에, 고루틴을 특정 os 스레드에 고정시킨 후(고루틴과 특정 os 스레드를 연결), 해당 스레드에서 필요한 작업을 수행하도록 하였다. (해당 os 스레드에서만 고루틴이 실행됨)
package main
import (
"fmt"
"runtime"
"syscall"
"golang.org/x/sys/windows"
)
// APC 콜백 함수
func APCFunction(par uintptr) uintptr {
fmt.Printf("APC 실행중 / 전달된 값: %d\n", par)
return 0
}
// QueueUserAPC 호출을 위한 준비
var modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
var procQueueUserAPC = modKernel32.NewProc("QueueUserAPC")
// QueueUserAPC 함수 래핑
func QueueUserAPC(pfnAPC uintptr, hThread uintptr, dwData uintptr) error {
ret, _, err := procQueueUserAPC.Call(pfnAPC, hThread, dwData)
if ret == 0 {
return err
}
return nil
}
func main() {
// OS 스레드 고정
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// 현재 스레드 핸들 가져오기
currentThread, err := windows.GetCurrentThread()
if err != nil {
fmt.Printf("현재 스레드 가져오기 실패: %v\n", err)
return
}
// APC 등록
fmt.Println("스레드에 APC 등록")
for i := uintptr(1); i <= 3; i++ {
err := QueueUserAPC(
syscall.NewCallback(APCFunction), // APC 콜백 함수
uintptr(currentThread), // 타겟 스레드 핸들
i, // 전달할 값
)
if err != nil {
fmt.Printf("APC 등록 실패: %v\n", err)
return
}
}
// Alertable 상태로 진입
fmt.Println("Alertable 상태로 진입")
for i := 0; i < 5; i++ {
fmt.Printf("대기 중 (%d)...\n", i+1)
windows.SleepEx(1000, true) // Alertable 상태로 대기
}
fmt.Println("프로그램 종료")
}
- runtime.LockOSThread 사용
- Go 런타임에서 고루틴을 특정 OS 스레드와 고정
- 이렇게 하면 -> APC 작업을 처리할 때 Windows 스레드와 충돌하지 않게 된다.
- 현재 스레드 핸들 사용
- 새 스레드를 생성(CreateThread)하는 대신, 현재 OS 스레드를 이용해 APC 작업을 등록함
- windows.GetCurrentThread()로 현재 스레드 핸들을 가져옴
- APC 등록
- QueueUserAPC를 사용해 현재 스레드의 APC 큐에 작업을 등록
- Alertable 상태 유지
- windows.SleepEx를 사용해 Alertable 상태로 진입한 후, 등록된 APC 작업이 실행되도록 한다.
아무래도 golang에서 하다보니 이것저것 뜯어고쳐야될게 많았다. 다음에는 c로 실습을 진행하는게 좋을듯 하다.
코드를 실행하면(go run main.go)
golang의 런타임 스케쥴러와 Windows의 APC가 호환되지 않는다는 걸 생각하였을때, Windows 기반의 네이티브 APC 매커니즘이 필요하면 C / C++ 을 사용해서 구현해야 한다.
APC Injection?
비동기 프로시저 호출(APC, Asynchronos Procedure Calls)을 사용하여 타겟 프로세스의 스레드가 악성 DLL을 로드하도록 하는 기법
APC Injection은 주로 프로세스 스레드의 APC 큐에 로드되어 실행된다.
쉽게 말해, windows 운영 체제의 APC 매커니즘을 악용하여 특정 프로세스 내부에서 악성 코드를 실행하게 하는 기술이다.
(1) 타겟 프로세스 핸들 열기
- 권한 o 프로세스 오픈 -> OpenProcess() API 사용
- 필요 시 SE_DEBUG_PRIVILEGE 권한 상승이 필요함
(2) 스레드 선택
- OpenThread() 또는 CreateToolhelp32Snapshot()와 같은 API를 사용해 타겟 프로세스의 스레드 핸들얻기
- APC를 주입할 적절한 스레드를 찾고, 스레드가 alertable 상태인지 확인한다.
(3) 코드 삽입
- 주입할 코드는 메모리 위에 올라가야 하며, 이를 위해 VirtualAllocEx()와 WriteProcessMemory() API를 사용해야함
(4) APC 큐에 작업 추가
- QueueUserAPC() API를 호출하여 사용자 정의 APC를 스레드 큐에 추가
- 이때 실행할 코드는 우리가 주입한 코드가 위치한 메모리의 시작 주소를 알려줌 (찾아서 실행하라는 의미)
(5) APC 실행
- 타겟 스레드가 alertable 상태가 될 때, 큐에 추가된 APC가 실행
- 해당 작업은 I/O 작업 대기 중인 스레드를 이용하거나, 스레드를 강제로 alertable 상태로 전환시켜 수행되도록 한다.
다음엔 golang으로 작성된 APC injection에 대해서 포스팅해보고자 한다.
아무래도 C/C++과는 다르다보니 APC Injection도 다르게 진행되어야 될 듯 하다.
References
https://www.ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection
APC Queue Code Injection | Red Team Notes
APC Queue Code Injection This lab looks at the APC (Asynchronous Procedure Calls) queue code injection - a well known technique I had not played with in the past. Some simplified context around threads and APC queues: Threads execute code within processesT
www.ired.team
비동기 프로시저 호출 - Win32 apps
APC(비동기 프로시저 호출)는 특정 스레드의 컨텍스트에서 비동기적으로 실행되는 함수입니다.
learn.microsoft.com
https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc
3. QueueUserAPC | Malware Development
#processinjection #queueUserAPC #golang #maldev #malwaredevelopment
www.scriptchildie.com