본문 바로가기

Unity/작업방식

Unity: Workflow. AllocConsole() 감지

프로그래밍을 시작할 때면 콘솔 프로그램부터 시작하는게 대세였던 시절이 있었습니다. 지금은 그런 시대가 아니냐는 질문에는 명확한 답변을 드리기가 쉽지가 않습니다. 아무튼, 콘솔으로 코딩에 입문하는 것 처럼 게임 해킹을 시작하고 작얼 할 때, 그래픽스 렌더러를 후킹해 원하는 대로 조작하기 전에 콘솔을 이용해 로그를 찍고 데이터를 확인하는게 보통입니다. 어렵지 않게 띄울 수 있고, 곧바로 텍스트로 결과를 알 수 있기에 흐름 진행 확인에 꽤 도움이됩니다. AllocConsole을 호출하는 DLL을 하나 생성한 다음 타겟 프로세스에 주입만 해주면 콘솔이 나타나므로 매우 간편합니다.

 

그래픽 렌더러를 후킹한 다음 imgui를 붙여 원하는 데이터를 그리는 해커의 화면

kernel32.dll 라이브러리에 콘솔관련 함수들이 포함되어 있기 때문에, 이를 이용해 간략히 구현했습니다.

유니티 기준으로 작성되어 MonoBehaviour 패턴을 사용했습니다. 

 

public class ConsoleCheck : MonoBehaviour
{
    internal static class NativeImports
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool AllocConsole();
        
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool FreeConsole();

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetConsoleWindow();
    }
    
    [ContextMenu("Open Console")]
    private void OpenConsole()
    {
        NativeImports.AllocConsole();
        Debug.Log("Console Allocated!");
    }

    [ContextMenu("Free Console")]
    private void FreeConsole()
    {
        NativeImports.FreeConsole();
        Debug.Log("Console Free!");
    }

    [ContextMenu("Check Console")]
    private void CheckConsole()
    {
        IntPtr console = NativeImports.GetConsoleWindow();
        
        Debug.Log($"Console handle : {console}");
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.F10))
        {
            OpenConsole();
        }
        
        if (Input.GetKeyDown(KeyCode.F9))
        {
            FreeConsole();
        }
    }

    private void Start()
    {
        interval = new WaitForSeconds(1.0f);
        StartCoroutine(DetectingRoutine());
    }

    private YieldInstruction interval;

    private IEnumerator DetectingRoutine()
    {
        while (true)
        {
            yield return interval;
            
            IntPtr console = NativeImports.GetConsoleWindow();
            if (console != IntPtr.Zero)
            {
                // detected!
                Debug.LogException(new Exception("Console opened!"));
                Application.Quit(10);
            }
        }
    }
}

 

엄밀하게는 AllocConsole() 함수 호출을 감지한 것은 아니라고 볼 수 있습니다. 특정 주기로 GetConsoleWindow() 메서드를 호출하고, 그 결과를 바탕으로 콘솔이 사용되고 있는지 예측하는 방식입니다. 따라서 성능에 영향을 받거나 쉽게 우회될 가능성이 있습니다. 사실 이런 방식을 채택하여 게임에 적용시킨 회사는 없을 것입니다.

 

AllocConsole() 메소드 자체를 먼저 후킹하여 호출을 감시하거나, 프로세스의 특정 메모리에 기록되어있는 콘솔의 노출 여부를 감지하는 부분을 체크하여 감지하는 방식을 택할 것입니다. 실제로 이 코드를 사용하려고 하더라도 C# 레이어에서 구현하지 않고, IL2CPP 변환 과정 후 엔트리 포인트에서 구현하고 패커를 거치는 것이 현명한 선택입니다.