레지스트리 키 치료
이전까지 만든 악성코드는 그냥 .exe 파일을 삭제만 해도 치료가 되어서 그냥 치료 부분은 생략했다.
그러나, 악성코드 개발일지 12, 악성코드 개발일지 13에서 만든 악성코드는 단순히 삭제만 해서는 완벽한 치료라고 할 수 없고, 레지스트리 값을 다시 원복시켜줘야 한다.
대충 찾아보니 레지스트리 원복하는 방법은 크게 2가지가 있었다.
- 보안 권장 값으로 하드코딩(...)
- 스냅샷과 비교해 복원
지금은 바뀐 레지스트리가 2개 정도라 하드코딩을 해도 되지만...
스냅샷을 비교하는 것으로 구현해 보겠다
python에서는 winreg라는 라이브러리로 레지스트리에 쉽게 접근 가능하다!
take_snapshot
경로를 입력받아서 레지스트리 키를 딕셔너리 형태로 반환한다
- path, value_name을 받은 경우: 해당 path의 value_name key값을 딕셔너리로 저장
- path만 받은 경우: 해당 path의 모든 key 값을 가져옴
def take_snapshot(abspath, value_name=None):
# 맨 앞에 붙어있는 HKEY~~ 분리
parts = abspath.split('\\', 1)
reg_path = getattr(winreg, parts[0].upper())
path = parts[1]
snapshot_data = {}
try:
# 일단 path까지 registry를 열고,
with winreg.OpenKey(reg_path, path, 0, winreg.KEY_READ) as key:
if value_name: # value_name이 있으면 그 key값 받아오기
try:
value, type_id = winreg.QueryValueEx(key, value_name)
snapshot_data[value_name] = (value, type_id)
print(f"[+] snapshot DONE! (single value): {path} -> {value_name}")
except OSError:
print(f"[-] no registry found: {value_name}")
else: # path만 있으면 path의 모든 key 순회하며 값 받아오기
i = 0
while True:
try:
name, value, type_id = winreg.EnumValue(key, i)
snapshot_data[name] = (value, type_id)
i += 1
except OSError:
break
print(f"[+] snapshot DONE! (all values): {path} (total {len(snapshot_data)} values)")
except FileNotFoundError:
print(f"[-] no registry path found: {path}")
return snapshot_data
save_snapshot_to_json
경로를 입력받아서 레지스트리 키를 딕셔너리 형태로 반환한다
- 그냥 w 모드로 저장하는 경우, 파일이 꺠지는 등 오류가 발생하면
master_data={}로 덮어쎠져 스냅샷이 사라질 수 있으므로,
.tmp 파일을 만들고 교체하는 방식으로 구현
import os
import json
def save_snapshot_to_json(path, snapshot_data, filename="registry_snapshot.json"):
# json 형태로 레지스트리 원본 저장
print(f"[AV] registry of '{path}' is saved to '{filename}'")
# 기존 데이터 읽기
master_data = {}
if os.path.exists(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
master_data = json.load(f)
except Exception as e:
# 기존 JSON이 깨져있을 경우 빈 딕셔너리로 덮어쓰는 것을 방지하기 위해 백업
backup_name = filename + ".bak"
print(f"[-] Warning: Failed to read existing '{filename}'. Creating backup as '{backup_name}'...")
try:
os.replace(filename, backup_name)
except Exception as backup_e:
print(f"[-] Backup failed: {backup_e}")
print(f"[-] Read error details (new file will be generated): {e}")
# 2. 데이터 변환
processed_data = {}
for key_name, (value, type_id) in snapshot_data.items():
if isinstance(value, bytes):
processed_data[key_name] = {
"value": value.hex(),
"type_id": type_id,
"is_bytes": True
}
else:
processed_data[key_name] = {
"value": value,
"type_id": type_id,
"is_bytes": False
}
# namespace 할당
master_data[path] = processed_data
# tmp 파일에 저장하고 교체
temp_filename = filename + ".tmp"
try:
with open(temp_filename, 'w', encoding='utf-8') as f:
json.dump(master_data, f, indent=4, ensure_ascii=False)
os.replace(temp_filename, filename)
print("[+] successfully stored!\n")
except Exception as e:
if os.path.exists(temp_filename):
os.remove(temp_filename)
print(f"[-] error in storing data... Original data is safe. Details: {e}\n")
load_snapshot_from_json
특정 path에 있는 데이터를 가져옴
def load_snapshot_from_json(path, filename="registry_snapshot.json"):
print(f"[AV] from '{filename}', loading snapshot of '{path}'")
loaded_snapshot = {}
if not os.path.exists(filename):
print(f"[-] no file found: {filename}\n")
return loaded_snapshot
try:
with open(filename, 'r', encoding='utf-8') as f:
master_data = json.load(f)
# namespace가 파일 안에 존재하는지 확인
if path not in master_data:
print(f"[-] no snapshot found: {path}\n")
return loaded_snapshot
processed_data = master_data[path]
# 추출한 텍스트를 다시 파이썬 객체 및 바이트 타입으로 역직렬화
for key_name, data_dict in processed_data.items():
val = data_dict["value"]
if data_dict["is_bytes"]:
val = bytes.fromhex(val)
loaded_snapshot[key_name] = (val, data_dict["type_id"])
print(f"[+] loading snapshot success! (total {len(loaded_snapshot)})\n")
except Exception as e:
print(f"[-] error in loading snapshot: {e}\n")
return loaded_snapshot
compare_and_restore
original_snapshot과 현재 레지스트리를 비교해서 복원
- value_name이 주어졌다면, (예전에 이 키가 있다는 것을 보장받음)
레지스트리 내용이 바뀌었다면, 스냅샷의 내용으로 되돌리기
레지스트리 키 자체가 삭제되었다면, 스냅샷의 정보로 새로운 키 생성 - path만 주어진다면,
레지스트리 내용이 새로 생긴게 있다면, (삭제)
레지스트리 내용 수정/삭제라면, 스냅샷으로 원복
def compare_and_restore(original_snapshot, path, value_name=None):
target_info = f"{path} -> {value_name}" if value_name else f"{path} (total)"
print(f"[AV] compare present registry with snapshot: {target_info}")
# getattr를 이용한 루트 키 분리 (take_snapshot 로직 적용)
parts = path.split('\\', 1)
try:
root_key = getattr(winreg, parts[0].upper())
sub_path = parts[1] if len(parts) > 1 else ""
except AttributeError:
print(f"[-] invalid registry root: {parts[0]}")
return
try:
# 분리해낸 root_key와 sub_path를 사용해 레지스트리 열기
with winreg.OpenKey(root_key, sub_path, 0, winreg.KEY_READ | winreg.KEY_SET_VALUE) as key:
if value_name: # value_name 특정 키 비교
if value_name not in original_snapshot:
print(f"[-] no data in '{value_name}' snapshot, just skipped")
return
snap_val, snap_type = original_snapshot[value_name]
try:
current_val, current_type = winreg.QueryValueEx(key, value_name)
# 레지스트리가 오염되었다면 복원
if current_val != snap_val or current_type != snap_type:
print(f"[!] polluted registry detected!: ({current_val} -> {snap_val}) ")
winreg.SetValueEx(key, value_name, 0, snap_type, snap_val)
except FileNotFoundError:
# 아예 레지스트리가 지워진 경우 정상으로 복원
print(f"[!] deleted registry detected!: {value_name}")
winreg.SetValueEx(key, value_name, 0, snap_type, snap_val)
else: # path 전체와 compare
current_state = {}
i = 0
while True:
try:
name, value, type_id = winreg.EnumValue(key, i)
current_state[name] = (value, type_id)
i += 1
except OSError:
break
# 새로운 registry가 있으면 삭제 (악성 의심)
for name in current_state:
if name not in original_snapshot:
print(f"[!] new registry detected!: {name}")
winreg.DeleteValue(key, name)
# 변조, 삭제 레지스트리 처리
for name, (snap_val, snap_type) in original_snapshot.items():
if name not in current_state or current_state[name][0] != snap_val or current_state[name][1] != snap_type:
print(f"[!] polluted/deleted registry detected!: {name}")
winreg.SetValueEx(key, name, 0, snap_type, snap_val)
except Exception as e:
print(f"[-] error in recovery: {e}")
print("[+] compare & restore done!\n")
이 함수들을 가지고 악성코드 개발일지 13에서 만든 레지스트리 바이러스를 치료해보자
if __name__ == "__main__":
run_path = r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run"
adv_path = r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
run_snap = take_snapshot(run_path)
adv_snap = take_snapshot(adv_path, "Hidden")
save_snapshot_to_json(run_path, run_snap)
save_snapshot_to_json(adv_path, adv_snap)
input("wait for pollution...")
loaded_run = load_snapshot_from_json(run_path)
loaded_adv = load_snapshot_from_json(adv_path)
compare_and_restore(loaded_run, run_path)
compare_and_restore(loaded_adv, adv_path, "Hidden")

원래 상태에선
run에는 아무것도 없어야 하고,
advanced의 hidden은 2가 들어있어야 한다.
json 파일에 잘 저장된걸 볼 수 있다.

스냅샷이 성공적으로 저장되었다면
이제 바이러스를 실행시켜 레지스트리를 오염시키자...
바이러스를 실행 후 compare_and_restore을 돌려보았다.

It_isnot_SUS라는 새로운 레지스티리 키가 생긴 것과,
advanced/hidden의 값이 2로 변경되었음을 탐지하고
다시 스냅샷에 저장된대로 복원한 것을 알 수 있다!
반응형
'악성코드와 백신 > 백신 개발일지' 카테고리의 다른 글
| [백신 개발](6)[Import table을 이용한 탐지] (0) | 2026.03.17 |
|---|---|
| [백신 개발](5)[시그니처 탐지] (0) | 2026.03.13 |
| [백신 개발](4)[바이러스 db] (0) | 2026.03.05 |
| [백신 개발](3)[EICAR 변종 탐지] (0) | 2026.03.04 |
| [백신 개발](2)[EICAR test file] (0) | 2026.03.03 |