PowerShell 보안 강화 ── 로그·AMSI·언어 모드·JEA

· 업데이트: · · PowerShell, 보안, Windows, 로그, 감사, 정보 시스템, 권한 관리, 운영 개선

수정 이력(8건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 맞춰 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
트랜스크립트 공유 ACL이 기록 자체를 멈추는 설정이 되어 있던 점을 고쳤습니다. `CREATOR OWNER`에 `AD,RA,REA`만 주고 있었습니다. 그러나 Windows에서 「추가만 가능」이 성립하는 것은 쓰는 쪽이 `FILE_APPEND_DATA`만 지정해 열었을 때뿐이고, `Start-Transcript`는 그렇게 열지 않습니다. PowerShell 구현은 먼저 `FileAccess.ReadWrite`로 열고, 실패하면 `FileMode.Append`+`FileAccess.Write`로 열며, .NET은 이를 `GENERIC_READ`/`GENERIC_WRITE`로 옮깁니다. `FILE_GENERIC_WRITE`에는 `FILE_WRITE_DATA`와 `SYNCHRONIZE`가 포함되므로 이 `CreateFile`은 액세스가 거부됩니다. 읽기·쓰기는 허용하고 `DE`(삭제)는 주지 않는 형태로 바꿨으며, 이 구성으로 지킬 수 있는 것은 「다른 사람 기록에 손대지 못함」과 「자기 기록도 지우지 못함」까지이고, 자기 기록 덮어쓰기는 ACL로는 막을 수 없다는 점을 명시했습니다. 덮어쓰기까지 막으려면 JEA의 `TranscriptDirectory`(Local System이 쓰고 표준 사용자는 액세스가 없음), 이벤트 전달·SIEM, 추가 전용으로 여는 자체 수집 프로세스의 세 가지를 들었습니다.
트랜스크립트 공유 권한 설정에서, 기존 폴더를 재사용할 때 직접 부여된 ACE가 남는 문제를 고쳤습니다. `SetAccessRuleProtection($true, $false)`가 제거하는 것은 상속되어 온 ACE뿐이고, 그 폴더에 직접 붙어 있는 ACE는 그대로 남습니다. `New-Item -Force`는 기존 폴더에서도 성공하므로, 예전에 `Domain Users`나 `Everyone`에 변경 권한을 직접 붙여 두었다면 이후에 `CREATOR OWNER`를 좁혀도 그 부여만 살아남아 사용자는 다른 사람 기록을 덮어쓰고 삭제할 수 있었습니다. 직접 부여를 한 번에 모두 제거한 뒤, 필요한 것만 같은 ACL에 넣어 한 번에 적용하는 형태로 바꿨습니다(빈 순간을 만들지 않기 위해).
트랜스크립트 공유 권한 설계에 `OWNER RIGHTS` ACE를 추가했습니다. 파일을 만든 사용자는 그 파일의 소유자가 되고, Windows는 소유자에게 `READ_CONTROL`과 `WRITE_DAC`를 암묵적으로 줍니다. 그래서 `WD`도 `DE`도 배포하지 않았어도 소유자는 스스로 DACL을 고쳐 덮어쓰기·삭제 권한을 다시 붙일 수 있어, 「탈취된 뒤에도 기록이 남는다」는 목적을 달성하지 못하고 있었습니다. `OWNER RIGHTS`(`S-1-3-4`) ACE가 있으면 시스템은 소유자에 대한 암묵적 권한을 무시합니다. 아울러 ACL로 지킬 수 있는 범위는 같은 공유를 쓰는 사용자끼리까지이고, 감사 증적으로 지키려면 사용자가 손댈 수 없는 수집 쪽으로 흘려보내야 한다는 점도 명시했습니다.
트랜스크립트 저장 위치의 권한 설정을 다시 검토했습니다. 사용자 허용에 상속을 붙여 쓰기를 배포하면 파일 이름만 알아도 다른 사람 기록을 덮어쓸 수 있으므로, 폴더에 대한 만들기 권한과 자신이 만든 파일에 대한 추가만으로 나누는 형태로 고쳤습니다.
대상 환경과 전제 조건을 맨 앞 표로 정리하고, JEA를 다루는 7장만 원격 처리가 전제라는 점을 장 서두에 명시했습니다. 아울러 트랜스크립트 집약 위치를 「쓸 수는 있지만 지울 수는 없는」 공유로 만드는 절차, 설정이 먹혔는지 가늠하는 방법, 다층 방어 관계도, 언어 모드 표 확충을 추가했습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175082)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「PowerShell 보안 강화 ── 로그·AMSI·언어 모드·JEA」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-security-hardening-jea/

DOI(등록된 아카이브)
10.5281/zenodo.22175082
DOI(마지막 등록 버전)
10.5281/zenodo.22175083

「침입됐을 때 PowerShell이 악용됐다」는 보고가 이어진 결과, 사내에서 PowerShell 사용을 전면 금지하려는 조직이 있습니다. 그러나 이는 실효성 면에서도 업무 영향 면에서도 수지가 맞지 않습니다. PowerShell은 Windows 관리 기반 그 자체이며, 멈추면 운영 자동화가 멈춥니다. 한편 공격 쪽은 같은 일을 다른 수단으로 실행할 수 있습니다.

현실적인 방침은 금지가 아니라 가시화와 제한입니다. 다행히 PowerShell 5.0 이후에는 방어 쪽을 위한 기능이 갖춰져 있습니다. 난독화된 코드라도 펼친 뒤의 형태로 남기는 스크립트 블록 로그, 실행 전에 바이러스 백신 제품으로 내용을 넘기는 AMSI(Antimalware Scan Interface, 스크립트 내용을 멀웨어 대응 제품이 검사하게 하는 Windows 장치), 실행 가능한 구문 자체를 제한하는 언어 모드, 그리고 「필요한 작업만 위임하는」 JEA. 이들을 조합하면 업무를 멈추지 않고 감사 가능한 상태를 만들 수 있습니다.

이 글에서는 사내 Windows 환경에서 PowerShell을 안전하게 계속 쓰기 위해 설정해야 할 항목을, 효과가 큰 순서로 정리합니다. 실행 정책과 서명에 대해서는 「PowerShell의 실행 정책과 스크립트 서명」에서 다루므로, 이 글은 그다음을 다룹니다.

대상 환경·전제 조건

항목 내용
대상 OS Windows 10/11, Windows Server. AMSI는 Windows 10 이후가 전제입니다1
대상 버전 Windows PowerShell 5.1과 PowerShell 7 모두를 대상으로 합니다. 정책 설정 키도 로그 기록 위치도 5.1과 7이 다르므로, 둘 다 들어 있는 단말에서는 둘 다 설정하십시오(제3장·제4장)
필요한 권한 제3장〜제4장의 로그 설정·PowerShell 2.0 사용 중지, 제7장의 JEA 엔드포인트 등록은 모두 관리자 권한이 필요합니다. 실제 운영에서는 한 대씩 손으로 넣지 않고 그룹 정책으로 배포합니다
로컬/원격 제3장〜제6장은 대상 단말·서버 안에서 끝납니다. 제7장의 JEA만 PowerShell 원격 처리(WinRM)가 켜져 있는 것이 전제입니다(제7장 서두를 참조)
다루지 않는 것 실행 정책과 서명의 자세한 내용은 다른 글(PowerShell의 실행 정책과 스크립트 서명)에서 다룹니다. 이 글에서는 위치만 정리합니다(제5장)

1. 먼저 결론

  • 최우선은 스크립트 블록 로그를 켜는 것입니다. 실행된 코드가 이벤트 로그에 기록되고, 난독화된 코드도 펼친 뒤의 형태로 남습니다(이벤트 ID 4104).2
  • 트랜스크립션도 함께 켭니다. 입출력을 포함한 세션 기록을 쓰기 전용 공유에 모을 수 있습니다.2
  • 로그를 켰으면 로그 크기 설정도 다시 봅니다. 기본값 그대로면 짧은 기간에 덮어씌워집니다.2
  • AMSI에 의해 실행 전 스크립트가 바이러스 백신 제품으로 넘어갑니다. PowerShell 5.0 이후, Windows 10 이후에서 유효합니다.1
  • 오래된 PowerShell 2.0 엔진은 끕니다. 남아 있으면 로그도 AMSI도 먹지 않는 옛 엔진으로 전환될 여지가 남습니다.3
  • 실행 정책은 보안 경계가 아닙니다. 공식이 명시합니다. 방어의 축으로 두지 마십시오.4
  • 언어 모드는 WDAC/AppLocker의 결과로 씁니다. 수동 설정은 우회 가능하고, 보안 기능으로 동작하지 않습니다.5
  • 권한 위임에는 JEA. 「이 명령의, 이 매개변수만」 허용하고, 관리자 권한을 나눠 주지 않고 운영을 돌릴 수 있습니다.6

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 24건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 무엇을 지키는가 ── 가시화가 먼저, 제한이 나중

이 글에서 다루는 네 가지 대책은 각각 다른 층을 담당합니다. 먼저 전체 모습입니다.

관리자·헬프데스크·자동화 스크립트(그리고 침입한 공격자)【위임】JEA(제7장)관리자 권한을 나눠 주지 않고, 허용한 작업만 넘긴다【제한】언어 모드 + WDAC/AppLocker(제6장)실행 가능한 코드와 쓸 수 있는 구문을 좁힌다【검사】AMSI(제4장)실행 직전 내용을 바이러스 백신 제품으로 넘긴다PowerShell 2.0 사용 중지로 우회 경로를 막는다PowerShell에서의 조작【기록】스크립트 블록 로그 4104·트랜스크립션(제3장)난독화를 푼 형태로 남긴다

기록은 「나중에 추적할 수 있는 상태」를 만들고, 위임은 권한 자체를 작게 하며, 검사와 제한은 실행 앞에서 멈춥니다. 넷은 서로의 대체가 아니라 담당하는 층이 다릅니다. 그 위에서 도입 순서에는 우선순위가 있습니다.

단계 할 일 효과
1. 가시화 스크립트 블록 로그, 트랜스크립션 무슨 일이 있었는지 안다. 사후 조사가 가능해진다
2. 기초적인 위험 제거 PowerShell 2.0 사용 중지, 최신 버전 유지, AMSI 확인 검사를 우회하는 경로를 막는다
3. 권한 한정 JEA로 위임, 관리자 권한 축소 피해 범위를 한정한다
4. 실행 제한 WDAC/AppLocker + 제약된 언어 모드 미승인 코드 실행 자체를 멈춘다

많은 현장에서는 1과 3만 해도 상황이 크게 좋아집니다. 4는 도입 비용이 커서, 업무 영향을 검증하며 진행하는 영역입니다.

3. 로그를 켠다 ── 4104가 가장 중요

PowerShell 로그에는 세 계층이 있습니다.2

종류 기록 내용 이벤트 ID
모듈 로그 지정 모듈의 파이프라인 실행 상세 4103
스크립트 블록 로그 실행된 코드의 텍스트(난독화 해제 후) 4104
기록 위치의 로그 5.1은 Microsoft-Windows-PowerShell/Operational, 7은 PowerShellCore/Operational
트랜스크립션 세션 입출력을 텍스트 파일에 기록 ─(파일 출력)

모두 그룹 정책의 「컴퓨터 구성 > 관리 템플릿 > Windows 구성 요소 > Windows PowerShell」에서 설정할 수 있습니다. 레지스트리로 직접 설정하는 것도 가능합니다.2

# 스크립트 블록 로그를 켠다(관리자 권한 필요. 보통은 GPO로 배포한다)
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
# -Type 은 레지스트리 공급자가 추가하는 동적 매개변수. 값의 형(DWord)을 명시할 수 있다
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

# PowerShell 7(pwsh)은 다른 정책 키를 본다. 둘 다 설정한다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

# 트랜스크립션을 켜고, 쓰기 전용 공유로 모은다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting'     -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory'         -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader'  -Value 1 -Type DWord

# 트랜스크립션도 PowerShell 7 쪽은 별도 키. 같은 값을 쓴다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting'     -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory'         -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader'  -Value 1 -Type DWord

PowerShell 7은 Windows\PowerShell 아래가 아니라 PowerShellCore 아래의 정책을 봅니다. 5.1 쪽만 설정하고 끝내면, pwsh에서 실행된 스크립트 기록이 통째로 빠집니다. 스크립트 블록 로그·트랜스크립션 모두 양쪽 키에 같은 값을 쓰십시오(GPO로 배포할 때도 각각의 템플릿에서 설정합니다).

스크립트 블록 로그의 가치는 난독화에 대한 내성에 있습니다. Base64로 인코딩된 명령이나 문자열 연결로 조립된 코드라도, 실행 시점에 펼쳐진 내용이 기록됩니다.2 공격 조사에서 「무엇이 실행됐는지」를 재현할 수 있는지는 이 설정 유무로 갈립니다.

기록 확인은 Get-WinEvent입니다(「Get-WinEvent로 이벤트 로그를 실무적으로 조사하기」).

# 최근 1일의 스크립트 블록 로그를 확인한다.
# Windows PowerShell(5.1)과 PowerShell 7은 기록되는 로그가 다르므로,
# 둘 다 대상으로 한다
Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational'
    ID        = 4104
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Select-Object TimeCreated, LogName, @{ n='Script'; e={ $_.Message } } -First 20

켜졌는지 가늠은 다음 세 가지로 합니다.

  1. 설정을 넣은 뒤, 새 PowerShell 세션을 엽니다. 스크립트 블록 로그는 활성화 이후에 시작된 세션부터 기록됩니다.2 이미 열린 창에서 시험해 「안 나온다」고 판단하지 마십시오.
  2. 표식이 되는 명령을 새 세션에서 실행하고, 그것이 4104로 돌아오는지 봅니다. 위 쿼리에서 지금 실행한 코드 조각을 포함한 이벤트가 돌아오면 유효합니다. 한 건도 안 돌아오면 Get-ItemProperty로 정책 키 값(EnableScriptBlockLogging1)과 5.1 쪽/7 쪽 어느 키에 썼는지 확인합니다.
  3. 로그 기록 위치를 잘못 보지 않았는지. pwsh로 실행했다면 기록 위치는 PowerShellCore/Operational입니다. Microsoft-Windows-PowerShell/Operational만 보고 「기록되지 않았다」고 오판하는 것이, 이 설정에서 가장 흔한 착각입니다.

켰으면 반드시 로그 크기를 다시 보십시오. 기본 크기 그대로면 수 시간〜수 일로 한 바퀴 돌아, 정작 필요할 때 남아 있지 않습니다.

Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
    Select-Object LogName, MaximumSizeInBytes, RecordCount
wevtutil sl Microsoft-Windows-PowerShell/Operational /ms:1073741824   # 1GB로 확장하는 예
wevtutil sl PowerShellCore/Operational               /ms:1073741824   # PowerShell 7 쪽도 빠뜨리지 말 것

# 반영됐는지 확인한다. MaximumSizeInBytes 가 1073741824 이면 성공
Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
    Select-Object LogName, MaximumSizeInBytes

wevtutil sl은 성공해도 아무것도 표시하지 않으므로, 설정 전후로 MaximumSizeInBytes를 견주는 것이 유일한 확인 수단입니다. 관리자 권한으로 실행하지 않으면 변경이 거부되므로, 값이 안 바뀌면 먼저 그곳을 의심하십시오.

트랜스크립트 집약 위치를 「쓸 수는 있지만 지울 수는 없는」 공유로 만들기

트랜스크립트 출력 위치는 사용자는 쓸 수 있지만 삭제할 수는 없는 공유로 두는 것이 핵심입니다. 로컬에 두면 침해당한 단말기에서는 지워집니다.

권한은 공유 액세스 허용과 NTFS 액세스 허용 둘 다 설정합니다(실효 권한은 둘 중 더 엄한 쪽입니다). 핵심은 사용자에게 「폴더에 파일 만들기」와 「자신이 만든 파일의 읽기·쓰기」만 주고, 삭제(DE)와 하위 폴더·파일 삭제(DC)는 주지 않는 것입니다.

먼저 이 구성으로 무엇이 지켜지고 무엇이 지켜지지 않는지를 분명히 해 둡니다. 지킬 수 있는 것은 「다른 사람 기록에는 전혀 손대지 못함」과 「자기 기록도 지우지 못함」 두 가지입니다. 지키지 못하는 것은 「자기 기록의 덮어쓰기」이고, 이것은 ACL만으로는 막을 수 없습니다(이유는 뒤에 나옵니다). 그래도 탈취된 계정에서 다른 사람의 증적을 지우지 못하게 되는 것만으로, 피해 원인 분리는 크게 달라집니다. 한 대 단말기를 잃어도 다른 단말기 기록은 남습니다.

# 로그 집약 서버 쪽에서 실행한다(관리자 권한 필요)
$path = 'D:\PSTranscripts'
$null = New-Item -Path $path -ItemType Directory -Force

# 공유를 만든다. 사용자 쪽은 변경(쓰기)까지, 관리는 로그 담당자만
New-SmbShare -Name 'pstranscripts$' -Path $path `
    -ChangeAccess 'EXAMPLE\Domain Users' -FullAccess 'EXAMPLE\ログ管理者'

# 부모 폴더에서의 상속을 끊는다(두 번째 인수 $false = 상속하던 ACE를 이어받지 않음).
# 상속으로 내려오는 CREATOR OWNER 의 모든 권한이 남아 있으면,
# 「자신이 만든 파일은 스스로 지울 수 있는」 상태가 되어, 집약의 의미가 없어진다.
#
# 다만 SetAccessRuleProtection 이 제거하는 것은 「상속되어 온 ACE」뿐이고,
# 그 폴더에 직접 부여된 ACE는 남습니다. 위의 New-Item -Force 는
# 기존 폴더에서도 성공하므로, 예전에 Domain Users 나 Everyone 에
# 변경 권한을 직접 붙여 두었다면, 그것은 이후 행을 모두 실행해도
# 살아남습니다. 사용자는 다른 사람 트랜스크립트를 덮어쓰고 삭제할 수 있는 채입니다.
# 그래서 직접 부여를 한 번에 모두 제거한 뒤, 필요한 것만 다시 넣습니다
$acl = Get-Acl -Path $path
$acl.SetAccessRuleProtection($true, $false)

foreach ($ace in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($ace) }

# 다 제거한 다음, 관리 쪽과 SYSTEM을 같은 $acl 에 넣은 뒤 한 번에 적용한다.
# 「비우기」와 「다시 넣기」를 따로 Set-Acl 하면, 아무도 손댈 수 없는 순간이 생긴다
foreach ($id in @('EXAMPLE\ログ管理者', 'SYSTEM')) {
    $acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
        $id, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))
}
Set-Acl -Path $path -AclObject $acl

# 사용자에게는 폴더에 대한 「만들기」만 허용한다. (OI) 를 붙이지 않는 것이 핵심이고,
# 이 허용은 아래 파일에는 상속되지 않는다
#   WD=파일 만들기  AD=폴더 만들기  X=폴더 탐색  RA=특성 읽기
#   (OI)를 붙이면 「기존 파일에 대한 데이터 쓰기」까지 배포하게 된다
icacls $path /grant 'EXAMPLE\Domain Users:(CI)(WD,AD,X,RA)'

# 쓰는 쪽이 「자신이 만든 파일만」 읽고 쓸 수 있게 한다.
# CREATOR OWNER 는 파일 생성 시 「그 생성자」로 바뀌므로,
# 다른 사람 파일에는 먹지 않는다. DE(삭제)는 주지 않는다
#   RD,WD,AD = 데이터 읽기·쓰기·추가   RA,WA  = 특성 읽기·쓰기
#   REA,WEA  = 확장 특성 읽기·쓰기                 RC     = 액세스 허용 읽기
#   S        = 동기. GENERIC_READ/GENERIC_WRITE 에 포함되므로 뺄 수 없다
# AD(추가)만으로 좁히고 싶어지지만, 그러면 트랜스크립트가 한 줄도 쓰이지 않는다(후술)
icacls $path /grant 'CREATOR OWNER:(OI)(IO)(RD,WD,AD,REA,WEA,RA,WA,RC,S)'

# 여기가 핵심입니다. 위 행만으로는 부족합니다.
# 파일을 만든 사용자는 그 파일의 「소유자」가 됩니다.
# Windows 는 소유자에게 READ_CONTROL 과 WRITE_DAC 를 암묵적으로 주므로,
# WD 도 DE 도 배포하지 않았는데, 소유자는 스스로 DACL을 고쳐
# 자신에게 덮어쓰기·삭제 권한을 다시 붙일 수 있습니다. 즉, 탈취된 계정은
# 자기 증적을 지울 수 있습니다. OWNER RIGHTS 의 ACE가 있을 때, 시스템은
# 소유자에 대한 암묵적 READ_CONTROL / WRITE_DAC 를 무시합니다
icacls $path /grant 'OWNER RIGHTS:(OI)(IO)(RA,REA)'

icacls $path        # 설정 결과를 확인한다

기존 폴더를 재사용할 때는 직접 부여된 ACE를 반드시 훑으십시오. SetAccessRuleProtection($true, $false)가 제거하는 것은 상속되어 온 ACE뿐이고, 그 폴더에 직접 붙어 있는 ACE는 그대로 남습니다.7 New-Item -Force는 기존 폴더에서도 성공하므로, 「전에 일시적으로 Domain Users에 변경 권한을 붙였다」는 이력이 있는 폴더를 재사용하면, 이후에 CREATOR OWNER를 좁혀도 그 직접 부여만 살아남아 사용자는 다른 사람 트랜스크립트를 덮어쓰고 삭제할 수 있습니다. 위 코드에서 한 번에 모두 제거한 뒤 다시 넣는 것은 이 때문입니다. 새 폴더를 만들면 필요 없지만, 넣어 두어서 손해볼 것은 없습니다. 적용 뒤에 icacls $path 출력을 눈으로 확인하고, 의도하지 않은 주체가 늘어서 있지 않은지 보십시오.

여기서 (OI)를 붙이는 방식이 대책의 성패를 가릅니다. 사용자 허용에 (OI)를 붙여 WD를 배포하면, 그 ACE가 아래 모든 파일에 상속되어 이름만 알면 다른 사람 트랜스크립트를 덮어쓰고 잘라 낼 수 있습니다.7 그러면 「단말기나 계정이 탈취된 뒤에도 기록이 남는다」는 목적을 달성하지 못합니다. 위와 같이 폴더에 대한 만들기 권한(상속시키지 않음)과, CREATOR OWNER를 통해 자신이 만든 파일만에 대한 읽기·쓰기로 나눕니다.

그리고 OWNER RIGHTS 행을 빼지 마십시오. 파일을 만든 사용자는 그 파일의 소유자가 됩니다. Windows는 소유자에게 READ_CONTROLWRITE_DAC를 암묵적으로 주므로,8 ACE로 DE를 배포하지 않았어도 소유자는 스스로 DACL을 고쳐 자신에게 삭제 권한을 다시 붙일 수 있습니다. 「자기 기록은 스스로 지울 수 있는」 상태로는 CREATOR OWNER를 좁힌 의미가 없습니다. OWNER RIGHTS(S-1-3-4) ACE를 두면 시스템은 소유자에 대한 암묵적 READ_CONTROL / WRITE_DAC를 무시하므로, 여기서 비로소 「쓸 수는 있지만 지울 수는 없다」가 성립합니다.8

AD(추가)만으로 좁히지 않는가

「덮어쓰기를 원하지 않으니 CREATOR OWNERAD만으로 충분하다」고 생각하고 싶어집니다. 그러면 트랜스크립트가 한 줄도 남지 않습니다.

Windows에서 「추가만 가능」이 성립하는 것은 쓰는 쪽이 FILE_APPEND_DATA만 지정해 파일을 열었을 때뿐입니다. Microsoft 레퍼런스는 FILE_APPEND_DATA를 「(로컬 파일에서는, FILE_WRITE_DATA 없이 이 플래그를 지정한 경우, 쓰기 작업은 기존 데이터를 덮어쓰지 않는다)」고 정의합니다.9 즉 추가 전용은 여는 방식의 성질이지, ACE에 AD를 둔다고 실현되는 것이 아닙니다.

그리고 Start-Transcript의 쓰기 입구는 추가 전용으로 열지 않습니다. PowerShell 구현은 먼저 FileMode.OpenOrCreate + FileAccess.ReadWrite로 열고, 실패했을 때만 FileMode.Append + FileAccess.Write로 다시 엽니다.10 .NET 쪽에서는 FileAccess.Read/Write가 각각 GENERIC_READ/GENERIC_WRITE로 옮겨지고, FileMode.Append도 내부에서 FileMode.OpenOrCreate로 바꾼 뒤 끝으로 시크할 뿐입니다.11 FILE_APPEND_DATA만 요구하는 경로는 없습니다.

FILE_GENERIC_WRITE에는 FILE_WRITE_DATA·FILE_WRITE_ATTRIBUTES·FILE_WRITE_EA·READ_CONTROL·SYNCHRONIZE가, FILE_GENERIC_READ에는 FILE_READ_DATA·FILE_READ_ATTRIBUTES·FILE_READ_EA·READ_CONTROL·SYNCHRONIZE가 포함됩니다.9 액세스 검사는 요구한 모든 권리를 보므로, AD,RA,REA만 가진 사용자의 이 CreateFile은 거부됩니다. 공유 쪽에서 변경(Change)을 허용해도 NTFS 쪽에서 거부됩니다. 기록을 지키려다 기록 자체를 멈추게 됩니다.

그래서 이 절의 ACL은 AD 단일이 아니라 「읽기·쓰기는 허용하되 DE(삭제)·WDAC(권한 변경)·WO(소유자 변경)는 주지 않는다」는 형태로 되어 있습니다. 소유자는 자기 트랜스크립트를 덮어쓰고 잘라 낼 수 있습니다. 여기는 ACL로는 막지 못한다고 받아들이십시오.

덮어쓰기까지 막고 싶다면, 쓰는 쪽이 손댈 수 없는 곳으로 내보내는 수밖에 없습니다. 선택지는 세 가지입니다.

  • JEA라면 세션 구성 파일의 TranscriptDirectory를 씁니다(7장). 이 출력을 쓰는 것은 접속해 온 사용자가 아니라 Local System이고, Microsoft 문서도 「표준 사용자는 이 폴더에 대한 액세스를 갖지 말 것」「감사하는 security 관리자만으로 좁힐 것」을 요구합니다.12 사용자에게 애초에 권한이 필요 없으므로, 이 절의 고민은 사라집니다
  • Windows 이벤트 전달이나 SIEM으로 가져옵니다. 스크립트 블록 로그(4104)는 이벤트 로그에 나오므로 전달 대상에 넣을 수 있습니다
  • 추가 전용으로 여는 자체 수집 프로세스를 둡니다. FILE_APPEND_DATA만 요구해 열기까지 스스로 쓰면, ACE 쪽 AD가 비로소 의미를 갖습니다

또한 ACL로 지킬 수 있는 것은 어디까지나 「그 공유에 쓰는 사용자」부터입니다. 파일 서버 관리자는 소유권을 가져갈 수 있고, 단말기 쪽이 완전히 장악되어 있으면 공유로 내보내기 전에 손댈 수 있습니다. 이 절의 권한 설계는 「같은 공유를 쓰는 사용자끼리 서로 지우지 못한다」를 담보하는 것이라고 이해하십시오.

icacls의 권한 기호(WD AD DE DC 등)와 상속 지정((OI) (CI) (IO))의 의미는 Windows 명령 레퍼런스에 목록이 있습니다.7 참고로 이 구성은 반드시 검증기에서 한 번 시험한 뒤 전개하십시오. 트랜스크립트는 실행 중인 세션이 같은 파일에 이어서 쓰므로, 권한을 너무 좁히면 기록 자체가 남지 않습니다. 「사용자 단말기에서 쓸 수 있을 것」「사용자가 다른 사람의 기존 파일을 읽지 못하고 고치지 못할 것」「사용자가 자기 파일을 삭제하지 못할 것」 세 가지를 실제로 확인하는 것이 확실합니다.

4. 우회 경로를 막는다 ── PowerShell 2.0과 AMSI

AMSI(Antimalware Scan Interface) 에 의해, PowerShell 5.0 이후에서는 스크립트 내용이 실행 직전에 바이러스 백신 제품으로 넘어가, 난독화를 해제한 상태로 검사됩니다.1 Microsoft Defender를 포함한 대응 제품을 쓰고 있으면 추가 설정 없이 동작합니다.

문제는 옛 엔진에는 이 장치가 없다는 것입니다. Windows PowerShell 2.0 엔진이 켜진 채로면 powershell.exe -Version 2로 로그도 AMSI도 먹지 않는 환경으로 전환될 여지가 남습니다. 이 기능은 더 이상 권장되지 않으며, 사용 중지가 권장됩니다.3

# PowerShell 2.0 엔진 상태를 확인하고 사용 중지한다(관리자 권한 필요·재시작이 필요한 경우 있음)
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2* |
    Select-Object FeatureName, State

Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart

참고로 PowerShell 7(pwsh)을 도입한 경우 설정 키도 로그 기록 위치도 5.1과 다릅니다(정책은 PowerShellCore 아래, 로그는 PowerShellCore/Operational). 둘 다 쓰이는 환경에서는 설정·로그 크기·조사 쿼리 모두를 둘에 대해 수행하십시오. 버전 공존에 대해서는 「Windows PowerShell 5.1과 PowerShell 7의 차이」를 참조하십시오.

5. 실행 정책의 위치를 잘못 잡지 말 것

다시 분명히 해 둡니다. 실행 정책은 보안 경계가 아닙니다. 공식 문서는, 사용자가 의도치 않게 스크립트를 실행하는 일을 막기 위한 안전 기능이며 악의적 조작을 막는 것이 아니라고 명시합니다.4

그렇다고 가치가 없는 것은 아닙니다. 서명 운영에는 「배포한 모듈이 변조되지 않았음을 확인할 수 있다」는 다른 가치가 있습니다(「PowerShell 모듈의 사내 배포와 업데이트」). 역할을 올바로 이해하고 쓰는 것이 중요하며, 「AllSigned로 했으니 안전하다」는 이해가 가장 위험합니다.

6. 언어 모드 ── 애플리케이션 제어와 조합해 쓴다

PowerShell 세션에는 언어 모드가 있어, 쓸 수 있는 언어 요소가 제한됩니다.5

모드 쓸 수 있는 것 실무에서의 위치
FullLanguage 모든 언어 요소(기본값) 보통의 세션
ConstrainedLanguage cmdlet은 모두 움직이고, 루프·조건 분기·문자열 전개·속성 참조도 쓸 수 있다. 다만 쓸 수 있는 .NET 형이 허용 목록으로 한정되며, Add-Type은 서명된 어셈블리만 로드할 수 있다 WDAC/AppLocker 아래에서 자동으로 전환되는 모드. 대화 조작이나 보통의 관리 작업은 대체로 그대로 할 수 있다
RestrictedLanguage 명령은 실행할 수 있지만 스크립트 블록을 쓸 수 없다. 변수는 $PSCulture $PSUICulture $true $false $null만, 비교 연산자는 -eq -gt -lt만이며, 대입·속성 참조·메서드 호출은 불가 모듈 매니페스트(.psd1) 로드에 쓰이는 모드. 사람이 대화로 쓰는 전제가 아니다
NoLanguage 스크립트 언어 자체가 무효. 스크립트도 변수도 쓸 수 없고, cmdlet과 네이티브 명령 호출만 가능하다 JEA 세션 구성(RestrictedRemoteServer)의 기본값. 「정해진 명령을 치기만 하는」 창구를 만들기 위한 모드

세 가지 제한 모드는 단계가 아니라 용도가 다릅니다. ConstrainedLanguage는 「사람이 스크립트를 작성해 쓰는 환경에서, 위험한 형 사용만 멈춘다」는 것이고, NoLanguage는 「애초에 스크립트를 쓰지 못하게 한다」는 것이어서, 후자는 JEA 같은 한정된 창구에서만 성립합니다.5

현재 모드는 다음으로 확인할 수 있습니다.

$ExecutionContext.SessionState.LanguageMode

중요한 것은 설정 방법입니다. 제약된 언어 모드는 WDAC(Windows Defender Application Control)나 AppLocker로 허용 목록 방식의 애플리케이션 제어를 구성했을 때, PowerShell이 자동으로 전환되는 형태로 동작합니다.5 환경 변수 등으로 수동 설정하는 방법은 우회가 쉽고, 보안 기능으로서는 동작하지 않습니다. 애플리케이션 제어를 도입하지 않고 언어 모드만 좁히는 것은, 노력에 비해 효과가 없다고 이해하십시오.

7. JEA ── 「필요한 작업만」 위임한다

현실의 피해를 좌우하는 것은 많은 경우 권한의 넓이입니다. 「헬프데스크에 관리자 권한을 넘기고 있다」「운영 담당이 전원 Domain Admin에 들어 있다」는 상태에서는, 한 대의 침해가 전사 침해가 됩니다.

JEA(Just Enough Administration)는 관리자 권한을 넘기지 않고 특정 작업만 위임하는 장치입니다.6 「애플리케이션 서비스 재시작만 헬프데스크에 맡긴다」는 요구에 잘 맞습니다.

이 장만 전제가 다르다 ── JEA는 원격 처리 위에 선다

제3장〜제6장이 로컬 설정으로 끝나는 데 비해, JEA는 PowerShell 원격 처리(WinRM) 장치 그 자체 위에 만들어져 있습니다.13 「JEA로 위임한다」는 것은 대상 서버에 전용 접속처(세션 구성)를 등록하고, 그리로 접속시키는 일입니다. 자사에서 재현할 수 있는지는 먼저 여기서 갈립니다.

필요한 것은 다음 세 가지입니다.

전제 내용
PowerShell 버전 JEA는 PowerShell 5.0 이후에서 이용할 수 있습니다13
원격 처리 활성화 대상 서버에서 PowerShell 원격 처리가 켜져 있을 것. Windows Server 2012 이후는 기본으로 켜져 있고, 꺼져 있으면 관리자 권한 PowerShell에서 Enable-PSRemoting을 실행합니다13
배치 위치 역할 기능 파일(.psrc)은 대상 서버 위 모듈의 RoleCapabilities 폴더에, 세션 구성(.pssc)은 대상 서버에서 등록합니다. 사용자 단말기에 두는 것이 아닙니다6

즉, 아래 절차 1〜3은 위임받는 쪽 서버에서, 관리자 권한으로 하는 작업입니다. 사용자 단말기에서 필요한 것은 그 서버에 접속할 수 있는 것뿐입니다. 원격 처리 자체의 구성과 안전한 설정은 「PowerShell Remoting(WinRM) 입문」에 정리해 두었습니다.

절차 1: 역할 기능 파일(.psrc)로 허용할 작업을 정의한다

역할 기능 파일은 PowerShell 모듈의 RoleCapabilities 폴더에 있어야 합니다. 폴더만 만들어서는 모듈로 인식되지 않아, 뒤에 나올 RoleDefinitions에서 이름만으로 해석하지 못합니다. 그래서 먼저 그릇이 되는 모듈(매니페스트를 가진 폴더)을 만듭니다.6

# 역할 기능을 넣을 모듈을 만든다(매니페스트가 필요)
$moduleRoot = 'C:\Program Files\WindowsPowerShell\Modules\KsJea'
$null = New-Item -Path "$moduleRoot\RoleCapabilities" -ItemType Directory -Force
# 모듈 폴더에는 폴더와 같은 이름의 파일이 하나 이상 필요
$null = New-Item -Path "$moduleRoot\KsJea.psm1" -ItemType File -Force
New-ModuleManifest -Path "$moduleRoot\KsJea.psd1" -RootModule 'KsJea.psm1'

# 역할 기능 파일 뼈대를 만든다(파일 이름이 역할 이름이 된다).
# 역할 이름은 PSModulePath 상의 모든 모듈에서 「이름만」으로 해석되므로,
# 'HelpDesk' 같은 일반적인 이름은 다른 모듈의 .psrc 와 충돌할 수 있다.
# 충돌하면 어느 쪽이 선택될지 보장이 없고, 의도하지 않은 권한이 주어진다.
# 조직 접두사를 붙인 고유한 이름으로 한다
New-PSRoleCapabilityFile -Path "$moduleRoot\RoleCapabilities\KsHelpDesk.psrc"

# 모듈로 보이는지 확인한다
Get-Module -Name KsJea -ListAvailable
# KsHelpDesk.psrc(발췌)── 「무엇을, 어디까지」 허용할지를 선언한다
@{
    GUID           = '....'
    # 읽기 전용 명령은 그대로 보여도 된다
    VisibleCmdlets = @(
        'Get-Service',
        'Get-EventLog'
    )
    # 상태를 바꾸는 Restart-Service 는 일부러 VisibleCmdlets 에 넣지 않는다(후술)
    # VisibleFunctions 는 「세션에 로드된 함수」를 좁힐 뿐이고,
    # 함수를 정의하지는 않는다. 독자 함수는 FunctionDefinitions 로 내용을 쓰고,
    # 그 위에서 VisibleFunctions 에도 이름을 올린다(둘 다 필요)
    VisibleFunctions = @('Get-KsAppStatus', 'Restart-KsAppService')
    FunctionDefinitions = @(
        @{
            Name        = 'Get-KsAppStatus'
            ScriptBlock = {
                # 함수 본체는 기본 언어 모드로 동작하므로 JEA 제약을 받지 않는다.
                # 사용자 입력을 그대로 위험한 명령에 넘기지 말 것
                Get-Service -Name 'KsAppService' |
                    Microsoft.PowerShell.Utility\Select-Object Name, Status, StartType
            }
        },
        @{
            # 재시작은 「인수로만 대상을 받는」 함수로 공개한다
            Name        = 'Restart-KsAppService'
            ScriptBlock = {
                param(
                    [Parameter(Mandatory)]
                    [ValidateSet('KsAppService', 'Spooler')]
                    [string] $Name
                )
                Microsoft.PowerShell.Management\Restart-Service -Name $Name
            }
        }
    )
    VisibleExternalCommands = @()
}

VisibleCmdletsValidateSet만으로는, 상태를 바꾸는 명령을 안전하게 좁히지 못합니다. ParametersValidateSet에 의한 제한은 그 매개변수가 실제로 바인딩됐을 때에만 평가됩니다. Restart-ServiceServiceController를 파이프라인으로 받을 수 있으므로,

Get-Service WinRM | Restart-Service

처럼 쓰면 -Name이 바인딩되지 않아 ValidateSet은 그대로 지나갑니다. 「KsAppServiceSpooler만 재시작할 수 있다」고 생각한 엔드포인트에서 임의의 서비스를 재시작할 수 있게 됩니다.

같은 일은 다른 매개변수 집합에서도 일어납니다. Get-WinEventLogNameValidateSet을 붙여도,

Get-WinEvent -ProviderName Microsoft-Windows-Security-Auditing -MaxEvents 1

처럼 쓰면 LogName은 바인딩되지 않아 제한이 동작하지 않습니다. JEA 세션은 가상 관리자로 동작하므로, 이 경우 Security 로그까지 읽을 수 있습니다.

그래서 위 예에서는 Restart-ServiceVisibleCmdlets에 넣지 않고, 인수로만 대상을 받는 래퍼 함수(Restart-KsAppService)만 공개하고 있습니다. 입력 경로가 인수뿐이면 우회할 방법이 없습니다.

ParametersValidateSet에 의한 제한은 「그 매개변수가 바인딩됐을 때」에만 동작합니다. 파이프라인 입력이나 다른 매개변수 집합으로 바인딩을 피할 수 있는 명령에서는 제한 자체가 무효가 됩니다. 인수를 좁히고 싶은 명령은 원칙적으로 래퍼 함수로 감싼다고 생각하십시오.

여기서 두 가지, 빠지기 쉬운 사양이 있습니다. 첫 번째는 VisibleFunctions가 함수를 만들지 않는다는 것입니다. 이름만 올린 함수는 세션에 존재하지 않아, 사용자에게는 「그런 명령은 없다」고 보입니다. 독자 함수는 FunctionDefinitions로 정의한 위에 VisibleFunctions에도 올리십시오.14 수가 늘어나면 스크립트 모듈로 빼고, 그 모듈의 함수를 VisibleFunctions로 공개하는 편이 관리하기 쉽습니다.

두 번째는 함수 본체가 JEA 제약을 받지 않는다는 것입니다.14 Select-Object처럼 JEA가 바꿔 끼우는 제약된 명령을 본래 동작으로 쓰고 싶다면, 위처럼 Microsoft.PowerShell.Utility\Select-Object로 정규화된 이름을 써서 호출합니다. 뒤집으면 함수 안에서는 무엇이든 할 수 있다는 뜻이므로, 사용자 입력을 Invoke-Expression에 넘기는 식의 쓰기는 절대 피하십시오.

절차 2: 세션 구성 파일(.pssc)로, 누구에게 어느 역할을 할당할지를 정의한다

# SessionType: 제한된 원격 서버(기본값 NoLanguage)
# RunAsVirtualAccount: 가상 관리자 계정으로 실행
# TranscriptDirectory: 실행 내용을 기록
$pssc = @{
    Path                = '.\KsHelpDesk.pssc'
    SessionType         = 'RestrictedRemoteServer'
    RunAsVirtualAccount = $true
    TranscriptDirectory = 'C:\JeaTranscripts'
    RoleDefinitions     = @{ 'EXAMPLE\HelpDesk' = @{ RoleCapabilities = 'KsHelpDesk' } }
}
New-PSSessionConfigurationFile @pssc

절차 3: 등록한다

Register-PSSessionConfiguration -Name 'KsHelpDesk' -Path .\KsHelpDesk.pssc -Force

사용자 쪽은 다음처럼 접속합니다.

Enter-PSSession -ComputerName 'appsrv01' -ConfigurationName 'KsHelpDesk'
# 허용된 명령만 쓸 수 있다. Restart-Service 도 지정 서비스만

JEA의 핵심은 세 가지입니다.6

  • 사용자는 관리자 권한을 갖지 않는다. 실행은 가상 계정 쪽에서 이루어진다
  • 세션은 제한된 원격 서버로 구성된다. 기본적으로 언어 모드가 제한되어, 임의 코드를 실행할 수 없다
  • 트랜스크립트로 실행 내용이 기록된다. 누가 무엇을 했는지 감사할 수 있다

또한 역할 기능 파일은 앞에서 말한 대로 모듈의 RoleCapabilities 폴더 아래에 있어야 하고, 그 모듈이 $env:PSModulePath에서 찾아지는 것이 전제입니다. RoleDefinitions에서 지정한 역할 이름이 해석되지 않으면, 먼저 Get-Module -ListAvailable로 모듈이 보이는지 확인하십시오.6

등록한 엔드포인트는 Get-PSSessionConfiguration으로 목록을 볼 수 있습니다. 서두에 적은 대로 JEA는 원격 실행 구성이 전제이므로, 접속되지 않을 때는 JEA 정의가 아니라 WinRM 쪽부터 원인을 분리하십시오(「PowerShell Remoting(WinRM) 입문」).

8. 실무 체크리스트

항목 우선도 상태
스크립트 블록 로그(4104)를 켰다 GPO로 전사 배포215
PowerShell 관련 로그 크기를 확장했다 기본값 그대로면 며칠 만에 사라진다
트랜스크립션을 켜고, 쓰기 전용 공유에 모았다 단말기 로컬에 두지 않는다2
PowerShell 2.0 엔진을 사용 중지했다 로그·AMSI 우회 경로3
바이러스 백신 제품이 AMSI에 대응한다 기본으로 유효1
실행 정책을 보안 경계로 오해하지 않았다 서명은 다른 가치4
관리자 권한 부여 범위를 재점검했다 피해 범위를 정하는 것은 권한의 넓이
정형 작업을 JEA로 위임했다 관리자 권한 축소에 직결6
WDAC/AppLocker를 검토했다 언어 모드 제한은 이와 세트5
PowerShell 7 쪽에도 로그 설정을 배포했다 5.1과 7에서 설정은 별개

9. 정리

  • PowerShell 금지는 실효성이 낮고 업무를 멈춥니다. 방침은 「가시화와 제한」입니다.
  • 최우선은 스크립트 블록 로그(4104)를 켜는 것입니다. 난독화된 코드도 펼친 뒤의 형태로 기록됩니다. 로그 크기 확장과 함께 실시하십시오.
  • 트랜스크립션은 사용자가 지울 수 없는 공유에 모으는 것이 핵심입니다.
  • PowerShell 2.0 엔진은 사용 중지합니다. 로그도 AMSI도 먹지 않는 경로를 남기지 않기 위해서입니다.
  • 실행 정책은 보안 경계가 아닙니다. 서명은 변조 탐지로서 가치가 있지만, 방어의 축으로 두지 마십시오.
  • 권한의 넓이가 피해의 넓이를 정합니다. JEA로 「필요한 작업만」 위임하면, 관리자 권한을 나눠 주지 않고 운영을 돌릴 수 있습니다.

샘플 코드 다운로드

이 글에서 다룬 코드는 그대로 돌릴 수 있는 형태로 묶어 배포합니다. 로그 설정 활성화와 JEA 엔드포인트 작성이 들어 있습니다.

샘플 코드 다운로드(zip)

이 글의 샘플은 Windows나 테넌트에 의존하므로 실행 검증은 하지 않았습니다. 구문 분석과 PSScriptAnalyzer에 의한 정적 분석까지는 모든 파일에 대해 실시했지만, 동작은 반드시 자신의 검증기에서 확인하십시오.

# 구문 분석 + 정적 분석(Windows 밖에서도 실행할 수 있다)
./Invoke-SampleTests.ps1

설정값(경로, 서버 이름, 테넌트 ID 등)은 예입니다. 그대로 운영 환경에서 실행하지 말고, 자사 환경에 맞게 바꿔 읽으십시오.

관련 글

관련 상담 영역

합동회사 고무라소프트에서는 Windows 운영 환경의 보안 설정 리뷰, 권한 위임(JEA)을 포함한 운영 설계, 감사 로그 정비와 활용 상담을 다룹니다.

참고 링크

  1. Microsoft Learn, Antimalware Scan Interface (AMSI). AMSI가 애플리케이션과 서비스에서 임의의 안티멀웨어 제품으로 콘텐츠를 넘겨 검사하게 하는 장치인 것, PowerShell을 포함한 Windows 스크립트 엔진이 통합되어 있으며, 난독화된 스크립트도 실행 시 내용으로 검사할 수 있다는 점에 대해.  2 3 4

  2. Microsoft Learn, about_Logging_Windows. 모듈 로그·스크립트 블록 로그·트랜스크립션의 세 가지 로그 기능, 그룹 정책 및 레지스트리(HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell 아래)에 의한 활성화, 스크립트 블록 로그가 이벤트 ID 4104로 난독화 해제 후 코드를 기록하는 것, 모듈 로그가 이벤트 ID 4103으로 기록되는 것, 트랜스크립션의 OutputDirectory나 EnableInvocationHeader 설정에 대해.  2 3 4 5 6 7 8 9

  3. Microsoft Learn, Windows PowerShell 2.0 사용 중단. Windows PowerShell 2.0 엔진이 더 이상 권장되지 않으며 사용 중지가 권장되는 것, Windows 옵션 기능으로 제공되어 사용·중지를 전환할 수 있다는 점에 대해.  2 3

  4. Microsoft Learn, about_Execution_Policies. 실행 정책이 보안 경계가 아니라, 사용자가 의도치 않게 스크립트를 실행하는 일을 막기 위한 안전 기능인 것, 여러 우회 수단이 존재한다는 점에 대해.  2 3

  5. Microsoft Learn, about_Language_Modes. FullLanguage·ConstrainedLanguage·RestrictedLanguage·NoLanguage 각 모드에서 이용할 수 있는 언어 요소, $ExecutionContext.SessionState.LanguageMode로 현재 모드를 확인하는 것, WDAC나 AppLocker에 의한 애플리케이션 제어가 유효한 환경에서 PowerShell이 제약된 언어 모드로 동작하는 것, 수동 언어 모드 설정이 보안 기능으로 의도되지 않았다는 점에 대해.  2 3 4 5

  6. Microsoft Learn, Just Enough Administration (JEA) 개요. JEA가 관리자 권한을 부여하지 않고 특정 관리 작업만 위임하는 장치인 것, 역할 기능 파일(.psrc)에 의한 VisibleCmdlets·매개변수와 ValidateSet에 의한 제한, 세션 구성 파일(.pssc)의 RestrictedRemoteServer·RunAsVirtualAccount·TranscriptDirectory·RoleDefinitions, Register-PSSessionConfiguration에 의한 등록, JEA 세션에서 언어 모드가 제한되는 것에 대해. 역할 기능 파일을 PowerShell 모듈의 RoleCapabilities 폴더에 배치해야 하는 것(모듈로 검색될 수 있는 상태로 둘 것, New-PSRoleCapabilityFile에 의한 작성)은 JEA Role Capabilities를 참조.  2 3 4 5 6 7

  7. Microsoft Learn, icacls. 파일과 폴더의 액세스 제어 목록을 표시·변경하는 명령인 것, /grant에 의한 권한 부여와 /remove:g에 의한 부여된 권한 삭제, 권한 마스크에 지정할 수 있는 상세 권한 기호(DE=삭제, DC=하위 폴더와 파일 삭제, WD=데이터 쓰기/파일 만들기, AD=데이터 추가/하위 폴더 만들기, WA=특성 쓰기, WEA=확장 특성 쓰기, RA=특성 읽기, X=실행/탐색, F=모든 권한 등), 상속 지정((OI)=개체 상속, (CI)=컨테이너 상속)에 대해. 공유 작성과 액세스 허용 지정은 New-SmbShare(-FullAccess / -ChangeAccess / -ReadAccess)를, 상속 무효화는 ObjectSecurity.SetAccessRuleProtection(첫 번째 인수로 상속을 보호하고, 두 번째 인수로 상속된 ACE를 이어받을지를 지정)을 참조. 이 메서드가 지정할 수 있는 것은 상속 ACE의 취급뿐이고, 그 개체에 직접 부여된 ACE는 대상 밖입니다. 직접 부여를 제거하려면 ObjectSecurity.RemoveAccessRuleSpecific 등으로 개별적으로 뺍니다.  2 3

  8. Microsoft Learn, 특수 ID 그룹(Special Identity Groups). OWNER RIGHTS(S-1-3-4)가 개체의 현재 소유자를 나타내는 그룹이며, 이 SID를 가진 ACE가 개체에 적용되어 있을 때 시스템은 소유자에 대한 암묵적 READ_CONTROLWRITE_DAC를 무시한다는 점에 대해. 뒤집으면, 이 ACE가 없으면 소유자는 암묵적으로 DACL을 고칠 수 있습니다.  2

  9. Microsoft Learn, File Access Rights Constants. FILE_APPEND_DATA가 「For a file object, the right to append data to the file. (For local files, write operations will not overwrite existing data if this flag is specified without FILE_WRITE_DATA.)」로 정의되어 있으며, 추가 전용 쓰기는 「FILE_WRITE_DATA 없이 FILE_APPEND_DATA를 지정해 열었을 때」 성립한다는 점, FILE_WRITE_DATA / FILE_ADD_FILE, FILE_DELETE_CHILD의 의미에 대해. 제네릭 액세스 권한 매핑(FILE_GENERIC_WRITE = FILE_APPEND_DATA + FILE_WRITE_ATTRIBUTES + FILE_WRITE_DATA + FILE_WRITE_EA + STANDARD_RIGHTS_WRITE + SYNCHRONIZE, FILE_GENERIC_READ = FILE_READ_ATTRIBUTES + FILE_READ_DATA + FILE_READ_EA + STANDARD_RIGHTS_READ + SYNCHRONIZE)은 File Security and Access Rights를 참조.  2

  10. PowerShell, TranscriptionOption.FlushContentToDisk(src/System.Management.Automation/engine/hostifaces/MshHostUserInterface.cs). 트랜스크립트 쓰기 입구가 new FileStream(this.Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read)로 열리고, 실패한 경우에만 new FileStream(this.Path, FileMode.Append, FileAccess.Write, FileShare.Read)로 다시 열리는 것, FILE_APPEND_DATA만 요구하는 경로가 없다는 점에 대해. 

  11. .NET, SafeFileHandle.Open(src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs). FileAccess.Read / FileAccess.Write가 각각 GENERIC_READ / GENERIC_WRITE로 옮겨지는 것, FileMode.AppendCreateFile을 부르기 전에 FileMode.OpenOrCreate로 바뀌기만 하고, 액세스 마스크에 FILE_APPEND_DATA 단일이 쓰이는 일은 없다는 점에 대해. 

  12. Microsoft Learn, JEA 세션 구성. 세션 구성 파일의 TranscriptDirectory에 폴더를 지정하면 자동으로 트랜스크립트가 기록되는 것, 「Transcripts are written to the folder by the Local System account, which requires read and write access to the directory. Standard users should have no access to the folder. Limit the number of security administrators that have access to audit the transcripts.」(표준 사용자는 이 폴더에 대한 액세스를 가져서는 안 되며, 감사하는 관리자만으로 좁힐 것)에 대해. 

  13. Microsoft Learn, JEA 전제 조건. JEA가 PowerShell 5.0 이후에서 이용할 수 있는 것, PowerShell 원격 처리가 JEA의 토대이며 JEA를 쓰기 전에 원격 처리가 켜져 있고 적절히 보호되어 있어야 하는 것, Windows Server 2012 이후에서는 PowerShell 원격 처리가 기본으로 켜져 있고, 켜져 있지 않으면 관리자 권한 PowerShell 창에서 Enable-PSRemoting을 실행해 켤 수 있다는 점에 대해.  2 3

  14. Microsoft Learn, JEA Role Capabilities. 독자 함수를 FunctionDefinitions로 정의한 위에 VisibleFunctions에도 이름을 올려야 하는 것(「Don’t forget to add the name of your custom functions to the VisibleFunctions field so they can be run by the JEA users.」), 함수 본체(스크립트 블록)가 시스템 기본 언어 모드로 실행되어 JEA 제약을 받지 않는 것, JEA가 바꿔 끼우는 제약된 명령을 본래 구현으로 쓰려면 정규화된 이름(Microsoft.PowerShell.Utility\Select-Object)이 필요한 것, 함수가 많을 때는 스크립트 모듈로 빼어 VisibleFunctions로 공개하는 방법, 모듈 폴더에 폴더와 같은 이름의 파일이 필요한 것에 대해.  2

  15. Microsoft Learn, Set-ItemProperty. 레지스트리 공급자 사용 시 -Type이 동적 매개변수로 추가되어, 레지스트리 값의 데이터 형(String / ExpandString / Binary / DWord / MultiString / QWord 등)을 지정할 수 있다는 점에 대해. 값이 없을 때 만들어진다는 점을 포함합니다. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

보안 대책으로 사내에서 PowerShell을 금지해야 할까요?
실효성이 낮고 부작용이 커서 권장하지 않습니다. PowerShell은 Windows 관리 기반 그 자체이고, 실체는 .NET 위의 기능입니다. 실행 파일을 막아도 같은 API를 다른 경로로 호출할 수 있어 공격 쪽 장애가 되기 어려운 반면, 정상적인 관리 업무와 자동화는 반드시 멈춥니다. 현실적인 방침은 금지가 아니라 「가시화와 제한」입니다. 스크립트 블록 로그로 무엇이 실행됐는지 남기고, 구버전을 끄고, 필요하면 언어 모드나 JEA로 실행 가능한 범위를 좁힙니다.
스크립트 블록 로그를 켜면 로그가 너무 많이 나오지 않나요?
실제로 늘어나므로 로그 크기 설정과 함께 켜십시오. Microsoft-Windows-PowerShell/Operational 로그의 최대 크기를 기본값 그대로 두면 짧은 기간에 덮어씌워져, 정작 필요할 때 남아 있지 않습니다. 운영에서는 로그 크기를 충분히 확보하고, 필요하면 SIEM이나 이벤트 전달로 모읍니다. 더 자세한 「호출 시작·종료」까지 기록하는 설정도 있지만 출력량이 매우 많아서, 보통은 기본 스크립트 블록 로그만 켭니다.
실행 정책을 AllSigned로 하면 보안 대책이 되나요?
실행 정책은 보안 경계가 아닙니다. 공식 문서도, 사용자가 의도치 않게 위험한 스크립트를 실행하는 일을 막기 위한 장치이지 악의적 조작을 막는 것이 아니라고 명시합니다. 우회 방법이 여러 가지 있기 때문입니다. 서명 운영에는 배포물의 무결성을 확인할 수 있다는 다른 가치가 있지만, 방어의 축은 로그로 가시화하고, AMSI로 검사하고, 언어 모드나 JEA로 권한을 제한하는 쪽에 두십시오.
제약된 언어 모드(ConstrainedLanguage)는 수동으로 설정해도 되나요?
환경 변수 등으로 수동 설정하는 것은 권장하지 않습니다. 우회가 쉽고 보안 기능으로 동작하지 않기 때문입니다. 제약된 언어 모드는 WDAC(Windows Defender Application Control)나 AppLocker로 애플리케이션 제어를 구성한 결과, PowerShell이 자동으로 전환되는 형태로 쓰는 것이 본래 설계입니다. 애플리케이션 제어 없이 언어 모드만 좁혀도 실효 있는 방어가 되지 않습니다.
헬프데스크 담당자에게 서버의 특정 서비스 재시작만 맡기고 싶습니다.
JEA(Just Enough Administration)가 그 용도를 위한 장치입니다. 역할 기능 파일에서 「이 명령의, 이 매개변수의, 이 값만 허용한다」고 정의하고 세션 구성으로 등록합니다. 사용자는 관리자 권한 없이 가상 계정을 통해 허용된 작업만 실행할 수 있습니다. 세션은 제한된 원격 서버로 구성되고 기본적으로 언어 모드도 제한되므로, 임의 코드가 실행될 여지가 없습니다. 실행 내용은 트랜스크립트로 남길 수 있습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기