악성코드와 백신/백신 개발일지

[백신 개발](7)[레지스트리 키 치료]

황올뱀 2026. 3. 20. 17:43

레지스트리 키 치료

이전까지 만든 악성코드는 그냥 .exe 파일을 삭제만 해도 치료가 되어서 그냥 치료 부분은 생략했다.

 

그러나, 악성코드 개발일지 12, 악성코드 개발일지 13에서 만든 악성코드는 단순히 삭제만 해서는 완벽한 치료라고 할 수 없고, 레지스트리 값을 다시 원복시켜줘야 한다.

 

대충 찾아보니 레지스트리 원복하는 방법은 크게 2가지가 있었다.

  1. 보안 권장 값으로 하드코딩(...)
  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로 변경되었음을 탐지하고
다시 스냅샷에 저장된대로 복원한 것을 알 수 있다!

반응형