PowerShell
Step 11. 레지스트리 탐구 — 키와 값을 읽고 시작항목 기록하기
Level 0 — 컴퓨터 조작과 구조의 이해 | 난이도 ★★★☆☆ | 예상 소요 시간 2–3시간
「Windows 내부 들여다보기」 유닛의 시작입니다. 선수 학습: Step 10. Windows PowerShell 5.1 또는 Windows의 PowerShell 7을 사용합니다. 인터넷은 제품의 공식 자료를 찾을 때 필요합니다.
- 준비: Windows PC, PowerShell, 레지스트리 편집기(regedit).
- 실습 범위: 레지스트리는 조회만 합니다. 보고서는 새 파일로 저장합니다. 시작항목을 실행·삭제·수정하거나 권한을 바꾸지 않습니다.
- 판단 원칙: 낯선 이름은 확인 대상입니다. 이름·경로·서명 하나만으로 악성 또는 정상이라고 단정하지 않습니다.
오늘은 “무엇이 로그인 시 실행되도록 등록되어 있는가”를 기록합니다. 실제로 실행됐는지, 사용자가 허용했는지, 안전한 동작인지는 별도 근거가 필요합니다. 원본 목록과 조사자의 판단을 분리하면 나중에 상태가 바뀌었을 때 비교하기 쉽습니다.
1. 학습 목표
- 키·하위 키·값의 이름·형식·데이터를 구분한다.
- HKCU와 HKLM, 32비트와 64비트 조회 조건을 설명한다.
- Get-ChildItem과 Get-ItemProperty로 구조와 값을 읽는다.
- regedit에서 같은 키와 값을 확인한다.
- 두 Run 키의 원본 보고서와 항목별 조사 메모를 작성한다.
2. 배경 지식 — 설정 트리와 시작항목
2-1. 키와 값
레지스트리는 Windows와 여러 프로그램이 사용하는 계층형 설정 저장소입니다. 모든 설정이나 파일이 여기 들어 있는 것은 아닙니다. 프로그램은 설정 파일·별도 데이터베이스도 사용합니다. 키는 폴더와 비슷한 가지이고, 값은 키에 연결된 이름·형식·데이터입니다. 값 자체가 파일처럼 하위 경로를 이루는 것은 아닙니다.
예를 들어 REG_SZ는 문자열, REG_EXPAND_SZ는 환경 변수 참조를 담을 수 있는 문자열, REG_DWORD는 32비트 정수입니다. 기본값은 이름이 빈 문자열인 값이며 regedit는 “(기본값)”으로 표시합니다. 실제 기본값이 저장되지 않아도 편집기는 “(기본값) — (값 설정 안 됨)” 행을 보여 줄 수 있습니다. 이 표시 행은 실제 값 이름을 열거한 보고서의 개수에 포함되지 않습니다. 이름이 낯설거나 GUID처럼 보인다는 이유만으로 삭제된 프로그램의 흔적이라고 판단할 수는 없습니다.
2-2. HKCU·HKLM과 조회 조건
HKCU:는 해당 프로세스가 실행되는 사용자 계정 영역, HKLM:은 컴퓨터 전체 영역을 가리키는 PowerShell 드라이브입니다. 실제 접근 가능 여부는 키의 권한에 따라 달라집니다. HKCU라도 모든 키를 자유롭게 읽거나 쓸 수 있다는 보장은 없습니다.
regedit 맨 위의 루트 이름과, 파일로 뒷받침되는 키·값 묶음인 하이브는 정확히 같은 개념이 아닙니다. 64비트 Windows에서는 일부 레지스트리 경로가 32·64비트 프로세스에 서로 다른 뷰를 제공합니다. 비교할 때 계정·전체 키 경로·프로세스 비트 수·조회 시각을 맞추세요.
2-3. Run 키와 지속성
Run 키는 로그인 시 프로그램을 실행하도록 등록하는 위치 중 하나입니다. 부팅 직후 무조건 실행되거나 즉시 실행된다는 뜻은 아닙니다. 시작항목 사용 안 함 설정, 정책, 파일 유무 등에 따라 실행되지 않을 수 있습니다. HKCU는 해당 사용자, HKLM은 컴퓨터의 사용자들에게 적용되는 범위를 가집니다.
재로그인·재부팅 뒤에도 실행 기회를 유지하려는 동작을 지속성이라고 부릅니다. Run 키는 정상 프로그램과 악성 프로그램 모두 사용할 수 있습니다. 오늘 보는 두 Run 키 외에도 RunOnce, 서비스, 예약 작업, 시작프로그램 폴더 등이 있습니다. 두 키가 비어 있다고 전체 자동 실행이나 악성코드가 없다고 결론 내리지 않습니다.
3. 따라 하기
3-1. 레지스트리 드라이브와 구조
Get-PSDrive -PSProvider Registry
[Environment]::Is64BitProcess
Get-ChildItem -LiteralPath HKCU:\ | Select-Object Name
HKCU와 HKLM 드라이브, 프로세스의 64비트 여부, 현재 사용자 영역의 바로 아래 키 이름을 확인합니다. 계정별 목록은 다릅니다. 이어서 Software 바로 아래에서 최대 12개 키 이름만 읽습니다.
Get-ChildItem -LiteralPath 'HKCU:\Software' | Select-Object -First 12 Name
3-2. HKCU Run의 값 읽기
$userRun = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
try {
Get-ItemProperty -LiteralPath $userRun -ErrorAction Stop | Format-List
} catch {
"Query failed: $($_.Exception.Message)"
}
왼쪽은 값 이름, 오른쪽은 데이터입니다. Run 데이터에는 실행 파일 경로뿐 아니라 인수가 포함될 수 있습니다. PSPath·PSParentPath·PSChildName·PSDrive·PSProvider는 제공자가 붙이는 정보입니다. “PS로 시작하면 전부 무시”하는 일반 규칙을 만들면 실제 값 이름까지 빠질 수 있습니다. 뒤의 보고서는 실제 값 이름을 별도로 열거합니다.
출력 형식 설명용 가상 예시입니다. 아래 명령줄을 실행하거나 실제 레지스트리에 추가하지 않습니다.
ExampleSync : "C:\Program Files\ExampleSync\sync.exe" --background
3-3. 하위 키와 값은 다르다
try {
$subkeys = @(Get-ChildItem -LiteralPath $userRun -ErrorAction Stop)
"Subkey count: $($subkeys.Count)"
$subkeys | Select-Object Name
} catch {
"Query failed: $($_.Exception.Message)"
}
조회에 성공했고 Subkey count가 0이면 그 시점에 하위 키가 없는 것입니다. 값이 없다는 뜻은 아닙니다. Run 키에 하위 키를 만들 수 있으므로 모든 컴퓨터에서 항상 0이라고 단정하지 않습니다. 키 없음·접근 거부·값 없음도 서로 다른 상태입니다.
3-4. 같은 위치를 regedit에서 보기
시작 메뉴에서 “레지스트리 편집기”를 열거나 아래 명령으로 실행합니다. UAC 창이 나타나면 앱 이름과 게시자를 확인하고 직접 승인해야 합니다. 조직의 제한을 우회하거나 다른 계정 자격증명을 쓰지 않습니다.
regedit
주소 표시줄에 다음 전체 경로를 입력하고 Enter를 누릅니다. 또는 왼쪽 트리를 차례로 펼치세요.
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
- 오른쪽의 이름·종류·데이터를 PowerShell 결과와 비교합니다. 기본값의 표시 방식이나 줄바꿈은 다를 수 있습니다.
- 같은 계정·같은 뷰의 같은 키인지 먼저 확인합니다. 존재하지 않는 키를 비교 목적으로 만들지는 않습니다.
- 값을 더블클릭하면 편집 창이 열릴 수 있습니다. 편집을 시작했다면 취소합니다. 레지스트리 편집기는 전체 변경을 한꺼번에 저장하는 문서 편집기가 아니므로, 창을 닫는다고 이미 적용된 변경이 취소되지는 않습니다.
3-5. HKLM Run도 읽기
$machineRun = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
try {
Get-ItemProperty -LiteralPath $machineRun -ErrorAction Stop | Format-List
} catch {
"Query failed: $($_.Exception.Message)"
}
이 영역에 등록되어 있다는 사실만으로 시스템 필수 구성이라고 단정하지 않습니다. 일반 프로그램의 알림·업데이트 도우미도 등록될 수 있습니다. HKLM을 조회할 수 있는지와 수정할 수 있는지는 다른 권한 문제입니다. 조회가 거부되면 오류를 기록하며, 이 실습을 위해 SAM 같은 민감한 영역에 접근하거나 권한을 확대할 필요는 없습니다.
3-6. 항목의 정체를 확인하는 순서
- 원본 값 이름·전체 명령줄·조회 위치를 기록합니다. 명령줄을 복사해서 실행하지 않습니다.
- 경로와 인수를 구분합니다. powershell.exe·rundll32.exe 같은 호스트가 지정되었다면 뒤의 스크립트·DLL도 확인 대상입니다.
- 파일 존재 여부, 제품 정보, 서명 게시자와 상태를 확인하고 공식 제품 문서와 비교합니다. 파일 이름·제품 정보만으로 출처가 검증되는 것은 아닙니다.
- 서명 Valid는 검사 시점의 서명 검증 결과입니다. 안전한 동작이나 사용자 설치 동의를 보증하지 않습니다. NotSigned도 곧바로 악성이라는 뜻은 아닙니다.
- Temp·숨김 창·정책 옵션은 조사 단서입니다. Program Files·Windows 폴더나 익숙한 이름도 정상 보증은 아닙니다. 원본·근거·판단·다음 확인을 분리합니다.
한 파일의 메타데이터를 읽는 예입니다. 명령줄 데이터 전체를 실행하지 않으며, 아래는 Windows에 있는 PowerShell 실행 파일 자체만 조회합니다. 이 파일의 서명이 유효해도 그 프로그램이 실행할 다른 스크립트의 안전성은 별개입니다.
$exampleFile = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
Get-Item -LiteralPath $exampleFile | Select-Object FullName, Length, VersionInfo
Get-AuthenticodeSignature -LiteralPath $exampleFile | Select-Object Status, StatusMessage, SignerCertificate
4. 미션 — 시작항목 감사 보고서
두 Run 키를 저장한 뒤 각 실제 값에 조사 메모를 붙이세요. 결과가 없는 이유를 구분하고, 모르는 항목을 억지로 “정상”으로 표시하지 않습니다. 확인한 사실과 아직 확인하지 못한 설치 주체·필요성은 별개입니다.
- 원본 데이터: startup-audit.json과 startup-audit.txt
- 조사 메모: review-notes.txt — 각 항목의 정체·근거·판단·다음 확인
- 약 한 달 뒤 같은 계정과 같은 프로세스 뷰로 다시 수집할 계획을 메모에 남기기. 기존 파일을 덮어쓰지 않기.
해설과 완료 기준 · 펼치기/접기
5. 모범 답안과 완료 기준
두 키를 구분해 저장하기
아래 전체는 터미널에서 실행할 수 있습니다. 새 폴더를 만들고 보고서 세 개를 저장하며 레지스트리는 수정하지 않습니다. 실제 값 이름을 열거하므로 제공자 부가 정보와 섞이지 않습니다. 문자열·배열·숫자·빈 문자열을 JSON으로 보존하고, REG_EXPAND_SZ도 원래 환경 변수 표기 그대로 읽습니다.
$auditlab = Join-Path $HOME ('infosec-step11-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $auditlab -ErrorAction Stop | Out-Null
$audit = @()
foreach ($path in @(
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
)) {
$key = $null
try {
$key = Get-Item -LiteralPath $path -ErrorAction Stop
$names = @($key.GetValueNames() | Sort-Object)
if ($names.Count -eq 0) {
$audit += [pscustomobject]@{Key=$path;Status='Empty';Name=$null;Type=$null;Data=$null;Error=$null}
}
foreach ($name in $names) {
$audit += [pscustomobject]@{
Key=$path; Status='Read'; Name=$name
Type=$key.GetValueKind($name).ToString()
Data=$key.GetValue($name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
Error=$null
}
}
} catch {
$status = 'Error'
if ($_.CategoryInfo.Category -eq 'ObjectNotFound') { $status = 'Missing' }
elseif ($_.CategoryInfo.Category -eq 'PermissionDenied') { $status = 'AccessDenied' }
$audit += [pscustomobject]@{Key=$path;Status=$status;Name=$null;Type=$null;Data=$null;Error=$_.Exception.Message}
} finally {
if ($null -ne $key) { $key.Close() }
}
}
$record = [ordered]@{
CollectedAt=(Get-Date).ToString('o')
User=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
PowerShell=$PSVersionTable.PSVersion.ToString()
Is64BitProcess=[Environment]::Is64BitProcess
Scope='Only the two Run keys in this process registry view; not all startup mechanisms.'
Entries=$audit
}
$jsonPath = Join-Path $auditlab 'startup-audit.json'
$textPath = Join-Path $auditlab 'startup-audit.txt'
$record | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $jsonPath -Encoding UTF8 -Width 4096
$lines = @("CollectedAt: $($record.CollectedAt)", "User: $($record.User)",
"PowerShell: $($record.PowerShell); 64-bit process: $($record.Is64BitProcess)", $record.Scope)
foreach ($row in $audit) {
$lines += ''
$lines += "Key: $($row.Key)"
$lines += "Status: $($row.Status)"
$lines += 'Name (JSON): ' + (ConvertTo-Json -InputObject $row.Name -Compress)
$lines += "Type: $($row.Type)"
$lines += 'Data (JSON): ' + (ConvertTo-Json -InputObject $row.Data -Compress)
if ($row.Error) { $lines += "Error: $($row.Error)" }
}
$lines | Out-File -LiteralPath $textPath -Encoding UTF8 -Width 4096
$notesPath = Join-Path $auditlab 'review-notes.txt'
$notes = @('For each Read entry, record identity, evidence, disposition, and next check.',
'Keep raw JSON/TXT unchanged; do not execute the command lines in them.',
'Repeat collection in the same account and process view in about one month; retain both snapshots.')
foreach ($row in $audit | Where-Object {$_.Status -eq 'Read'}) {
$notes += @('', "Entry: $($row.Key) :: $($row.Name)",
'Identity: ', 'Evidence: ', 'Disposition: NeedsReview', 'Next check: ')
}
$notes | Out-File -LiteralPath $notesPath -Encoding UTF8 -Width 4096
"Read entries: $(@($audit | Where-Object {$_.Status -eq 'Read'}).Count)"
"Non-Read rows: $(@($audit | Where-Object {$_.Status -ne 'Read'}).Count)"
$auditlab
Status의 Read는 값을 읽었다는 뜻이고, Empty는 키가 있지만 값이 없다는 뜻입니다. Missing은 키 없음, AccessDenied는 접근 거부, Error는 그 밖의 조회 실패입니다. Non-Read rows가 0이 아니면 상태를 직접 읽어야 하며 모든 키를 정상 수집했다고 단정하지 않습니다.
PowerShell/.NET의 형식 이름 String·ExpandString·DWord는 regedit의 REG_SZ·REG_EXPAND_SZ·REG_DWORD와 대응합니다. 텍스트의 Data (JSON)는 원본 문자가 보존되도록 따옴표·역슬래시가 이스케이프되어 보일 수 있습니다. 이를 새 실행 명령으로 사용하지 않습니다.
저장 결과와 조사 메모 확인
Get-ChildItem -LiteralPath $auditlab -File | Select-Object Name, Length
Get-Content -LiteralPath $textPath -Encoding UTF8
$savedAudit = Get-Content -LiteralPath $jsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
@($savedAudit.Entries | Where-Object {$_.Status -eq 'Read'}).Count
원본 JSON/TXT를 고치지 말고 review-notes.txt를 텍스트 편집기로 열어 조사 결과를 적습니다. 모든 Read 항목을 빠짐없이 검토하고, 판정은 “근거로 정체를 설명함”, “추가 확인 필요”, “조회 불가”처럼 나눕니다. 검색은 공개 제품명·게시자·파일명으로 하고 개인 경로·전체 명령줄·토큰이 들어갈 수 있는 자료는 검색창이나 외부 검사 사이트에 그대로 올리지 않습니다.
가상 조사 메모 예: ExampleSync — 공식 제품 설명과 파일 서명 게시자 일치, 기능 확인. 다른 미서명 스크립트 — 파일 내용을 읽어 기능은 확인했으나 설치 주체 미확인, 관리 기록 확인 필요. 두 번째 항목을 근거 없이 정상으로 바꾸지 않아도 조사 기록은 완성할 수 있습니다.
연습문제와 해설
- Get-ChildItem과 Get-ItemProperty의 차이는? → 전자는 하위 키, 후자는 그 키의 값과 제공자 속성을 읽습니다.
- HKCU와 HKLM의 차이는? → 프로세스 사용자 영역과 컴퓨터 전체 영역입니다. 악성코드가 어느 쪽을 “반드시 선호한다”고 일반화하지 않습니다.
- Subkey count가 0이면? → 조회 성공 시 하위 키가 없다는 뜻이며 값의 수와 다릅니다. 키 없음·권한 오류도 따로 확인합니다.
- Temp의 updt.exe를 보면? → 이름과 위치는 확인할 단서입니다. 출처·서명·파일 내용·실행 기록 등 없이 악성이라고 확정하지 않습니다.
- regedit를 닫으면 변경이 취소되나요? → 이미 적용한 변경은 남습니다. 오늘은 수정하지 않고 실수로 열린 편집 대화상자는 취소합니다.
완료 체크리스트
- [ ] 키와 값, 값 이름·형식·데이터를 구분한다.
- [ ] HKCU·HKLM을 같은 조건에서 조회하고 계정·비트 수를 기록했다.
- [ ] regedit에서 같은 키의 이름·종류·데이터를 확인했다.
- [ ] 원본 보고서와 항목별 조사 메모를 분리해 저장했다.
- [ ] 모든 Read 항목에 근거와 판정, 필요한 후속 확인을 적었다.
- [ ] 조회 오류·확인 불가를 숨기지 않고 다음 비교 계획을 기록했다.
6. 막혔을 때
- 경로 없음: CurrentVersion 철자와 역슬래시, 현재 계정·뷰를 확인합니다. 설치 상태에 따라 키 자체가 없을 수 있으므로 “99% 오타”로 단정하지 않습니다.
- 접근 거부: 오류를 기록합니다. 이 실습을 끝내려고 키 소유권·ACL을 바꾸거나 자격증명 저장 영역을 열지 않습니다.
- GUI와 목록이 다름: 계정, 전체 경로, 32·64비트 뷰, 갱신 시각, 기본값 표시를 확인합니다. 목록이 실시간으로 바뀌었을 가능성도 있습니다.
- 긴 명령이 잘림: 화면의 표만 복사하지 말고 JSON/TXT 원본을 사용합니다. 형식화 출력과 실제 문자열은 다를 수 있습니다.
- .reg 백업: 내보낸 .reg를 가져오면 내용이 병합될 수 있어 이후 추가한 키·값까지 자동 제거하는 완전한 원상 복원은 아닙니다. 권한 등 모든 상태의 백업도 아닙니다. 오늘은 내보내기·가져오기 실습을 하지 않습니다.
7. 정리
이번 실습의 결과는 두 Run 키에 대한 시점별 기록과 검토 근거입니다. 컴퓨터 전체에 악성코드가 없다는 판정이나 모든 자동 실행 위치의 수집 결과는 아닙니다. Sysinternals Autoruns는 더 많은 자동 시작 위치를 살펴볼 때 쓰는 공식 도구이며, 존재를 알아 두되 오늘의 두 키 감사와 범위를 구분하세요.
실습 검증 기록
2026-09-10, Windows PowerShell 5.1.26100.9444와 PowerShell 7.6.5에서 실제 Run 키 조회와 보고서 저장을 검증했습니다. 이름·형식·원본 데이터를 확인하고 실제 파일의 메타데이터와 서명을 읽었으며 등록된 명령줄은 실행하지 않았습니다. 게시 예시는 개인 컴퓨터의 시작항목 목록을 그대로 공개하지 않도록 가상 데이터로 교체했습니다.
2026-09-11에는 실제 regedit의 같은 HKCU Run 키 화면에서 등록 항목, REG_SZ 종류와 화면에 보이는 데이터를 대조했습니다. 긴 이름·데이터가 열 너비 때문에 잘리는 현상과, 저장된 값이 없는 기본값 행의 표시도 확인했습니다. 전체 문자열 검증은 별도로 저장한 원본 JSON/TXT를 기준으로 했습니다.
Microsoft: Registry provider · Microsoft: Run and RunOnce · Microsoft: Registry views · Microsoft: Registry hives · Microsoft: Authenticode signature · Microsoft: Autoruns · Microsoft: reg import
한 단계 더 가까이
수업을 마쳤나요?
본문의 완료 기준을 확인한 뒤 직접 표시하세요.