세부 정보 탭을 이름순으로 정렬하면 conhost.exe 가 화면 한 장을 넘게 차지한다. 확인한 시점에 내 PC 에서는 49개였다. 창을 하나도 띄운 기억이 없는데 "콘솔 창 호스트"가 이만큼 있다는 건 꽤 이상해 보인다. 몇 분 뒤 다시 세었더니 47개로 줄어 있었다. 숫자가 계속 움직인다는 것 자체가 힌트다.
49개가 모두 같은 파일이었다
먼저 경로와 실행 인수를 모아 보았다.
Get-CimInstance Win32_Process -Filter "Name='conhost.exe'" |
Group-Object ExecutablePath | Select-Object Count, Name
49개 중 48개의 경로가 C:\Windows\System32\conhost.exe 였다. 나머지 하나는 경로와 명령줄이 빈 값으로 나왔는데, 서비스 쪽 세션(세션 0)에서 돌고 있는 것이라 일반 권한으로는 안을 읽을 수 없었다. 관리자 권한으로 보면 같은 경로가 나온다.
명령줄은 두 종류로 갈렸다. 32개는 conhost.exe 0x4 처럼 숫자 하나만 붙어 있었고, 16개는 conhost.exe --headless --width 80 --height 24 --signal 0x... --server 0x... 였다. 파일 자체는 약 980KB, 파일 설명은 "콘솔 창 호스트", 버전은 10.0.26100.9949, 서명은 Microsoft Windows 로 유효했다.
메모리는 하나당 작업 집합 0.912.8MB, 개인 메모리 1.26.6MB 였다. 49개의 개인 메모리를 다 더해도 약 99MB 였다. 작업 집합 합계는 406MB 로 더 크게 나오지만, 여러 프로세스가 같이 쓰는 Windows 라이브러리가 프로세스마다 중복으로 잡힌 값이다.
콘솔 프로그램 하나에 conhost 하나
conhost.exe 는 명령 프롬프트 같은 콘솔 프로그램의 "창 쪽"을 맡는 프로세스다. 콘솔 프로그램은 글자를 쓰고 키 입력을 받을 뿐이고, 실제로 화면에 글자를 그리고 키보드 입력을 받아 넘겨 주는 일은 conhost 가 한다. Windows 7 무렵부터 이 일을 csrss.exe 에서 떼어 내 사용자 권한의 conhost.exe 로 옮긴 것으로 알려져 있다. 시스템 핵심 프로세스가 콘솔 창 그리기까지 떠맡지 않게 하려는 구조다.
그래서 콘솔이 하나 생길 때마다 conhost 도 하나 생긴다. 중요한 건 "창이 보이느냐"와 상관이 없다는 점이다. 프로그램이 창 없이 백그라운드에서 cmd.exe 나 powershell.exe, 명령줄 도구를 실행해도 콘솔은 만들어지고, 그 콘솔을 맡을 conhost 가 같이 뜬다. 개발 도구나 백그라운드 서비스가 명령줄 도구를 자주 부르는 PC 일수록 conhost 가 많아지는 이유다.
--headless 가 붙은 16개는 성격이 조금 다르다. 이것은 의사 콘솔(ConPTY)이라고 부르는 방식으로, 터미널 프로그램이 콘솔 화면을 직접 그리고 싶을 때 conhost 를 화면 없이 띄워 중계만 맡기는 것이다. 코드 편집기 안에 들어 있는 터미널 창이 대표적이다. 화면에 보이는 터미널 탭 하나 뒤에 headless conhost 가 하나씩 붙어 있다고 보면 된다.
Windows Terminal 은 이 구조를 자기 쪽으로 가져간 경우다. Windows Terminal 은 conhost 대신 같은 소스에서 나온 OpenConsole.exe 를 패키지 안에 넣어 쓰는 것으로 알려져 있다. 내 PC 에는 Windows Terminal 1.24 가 설치되어 있었지만, 확인한 시점에는 실행 중이 아니어서 OpenConsole.exe 도 보이지 않았다. Windows Terminal 을 기본 터미널로 쓰는 PC 에서는 conhost 대신 OpenConsole 이 늘어나는 것으로 보인다.
누가 띄웠는지는 부모로 찾는다
conhost 자체는 "누가 시켰는지"를 말해 주지 않는다. 대신 부모 프로세스 번호가 남아 있어서, 그걸 따라가면 어느 프로그램이 콘솔을 만들었는지 알 수 있다.
$all = Get-CimInstance Win32_Process
$byId = @{}; $all | ForEach-Object { $byId[$_.ProcessId] = $_ }
$all | Where-Object Name -eq 'conhost.exe' |
ForEach-Object { $byId[$_.ParentProcessId].Name } |
Group-Object | Sort-Object Count -Descending | Select-Object Count, Name
내 PC 에서 나온 결과를 묶으면 이렇다.
| 부모 | 개수 |
|---|---|
| 터미널·개발 도구 (headless 16개 포함) | 37 |
| cmd.exe | 4 |
| wslhost.exe (WSL) | 4 |
| powershell.exe | 1 |
| 그래픽 드라이버의 보조 프로그램 | 1 |
| 서비스 세션의 프로세스 | 1 |
| 부모가 이미 끝난 것 | 1 |
대부분이 개발 도구가 내부에서 명령줄 도구를 부를 때 생긴 것이었다. 개발용 PC 가 아니라면 이렇게 많을 일은 드물고, 대개 그래픽·오디오 드라이버의 보조 프로그램이나 업데이트 도구, 백신 같은 것이 부모로 나온다. 부모가 이미 끝난 conhost 는 처음 콘솔을 만든 프로그램은 종료됐지만 그 콘솔을 이어받은 다른 프로세스가 아직 붙어 있는 경우로 보인다.
작업 관리자에서도 볼 수 있다. 세부 정보 탭의 열 머리글을 오른쪽 클릭하고 "열 선택"에서 "명령줄"을 켜 두면 headless 여부가 바로 보인다. 다만 부모 프로세스 열은 작업 관리자에 없어서 부모까지 보려면 위 명령이 편하다.
가짜 구별과 끄면 안 되는 이유
conhost.exe 는 이름이 흔하고 늘 여러 개라 악성코드가 숨기 좋은 이름이기도 하다. 확인할 것은 두 가지면 충분하다. 경로가 C:\Windows\System32\conhost.exe 인지, 그리고 서명이 Microsoft Windows 로 유효한지다.
Get-AuthenticodeSignature C:\Windows\System32\conhost.exe |
Select-Object Status, @{n='Signer'; e={$_.SignerCertificate.Subject}}
AppData, Temp, ProgramData 같은 폴더에 같은 이름의 파일이 있다면 Windows 의 것이 아니다. 또 정상 conhost 는 거의 CPU 를 쓰지 않는다. 하나가 계속 CPU 를 잡고 있다면 그 conhost 자체보다 부모 쪽 콘솔 프로그램이 화면에 글자를 쉬지 않고 쏟아내고 있는 경우가 많다. 이때도 부모를 찾는 것이 먼저다.
경로와 서명이 정상이라면 conhost 를 직접 끝내지 않는 것이 좋다. conhost 를 끝내면 그 콘솔에 붙어 있던 프로그램도 같이 끝난다. 백그라운드에서 돌던 도구가 작업 도중 끊기는 셈이다. 개수가 거슬린다면 부모로 찾은 프로그램을 정리하는 쪽이 맞다. 프로그램을 닫으면 conhost 도 따라서 사라진다.
다음 편은 이름이 더 수수께끼 같은 dllhost.exe, 작업 관리자에 "COM Surrogate"로 보이는 프로세스다. 명령줄의 긴 ID 를 레지스트리에서 찾아 정체를 밝혀 본다.