민서네집

[펌] 화면캡쳐방지는 어떻게 구현될까? by lain32프로필바람처럼 본문

프로그래밍

[펌] 화면캡쳐방지는 어떻게 구현될까? by lain32프로필바람처럼

브라이언7 2021. 1. 30. 22:24

[출처] m.blog.naver.com/jeonghj66/140127950577

 

[펌] 화면캡쳐방지는 어떻게 구현될까? by lain32

http://lain32.egloos.com/m/4805054 거의 1년동안 포스팅이 뜸하다가 요즘들어 다시 포스팅을 시작하고 있...

blog.naver.com

http://lain32.egloos.com/m/4805054

 

거의 1년동안 포스팅이 뜸하다가 요즘들어 다시 포스팅을 시작하고 있습니다.
예전엔 너무 무거운 주제로 얘기를 하다보니 글이 별로 안 올라왔던 것 같아서 요즘에는 가벼운 주제인 델파이같은
어플리케이션 개발과 관련 된 사소한 부분들에 대해서도 올리고 있습니다. ( 델파이가 사소하다는게 아닙니다. -_- )

그러다가 제 블로그에 찾아오시는 분들이 거의 시스템쪽을 좋아하시는 분들이 많으시다보니 요즘 올리는 주제가 좀
제 블로그 형태에 벗어나는 것 같아서 다시 한 번 좀 더 무거운 주제로 깊이 있는 글을 써볼까 합니다.-_-

우선 시작부터는 좀 그러니 가벼운 주제로 화면캡쳐방지에 대해서 알아보도록 하겠습니다.
시작합니다.~


몇 년전부터 화면캡쳐방지를 해주는 솔루션들이 등장하기 시작했습니다.
어떻게보면 좀 오래된 분야지만 아직까지도 완벽하게 화면캡쳐방지를 막는 것은 사실상 불가능합니다.
그래도 어차피 100% 다 막을 수가 없기 때문에 적어도 99%정도는 막는다라는 취지에서 이러한 솔루션들이 등장하고 있습니다.

화면캡쳐방지에 대해 거론하기 전에 우선 화면캡쳐를 어떻게 하는지 당연히 알고 있어야 합니다.
방어를 할려면 공격방법을 먼저 알아야 되죠. 그래서 해킹보다 보안이라는것이 더 어려운 경우가 많습니다.

화면캡쳐에 기본적인 주요 API는 3가지로 구현됩니다.
GetDC, BitBlt, ReleaseDC

화면캡쳐 프로그램들은 보통 이 3가지 API를 이용하여 화면캡쳐를 합니다.
다음은 오픈캡쳐에서 사용하는 전체화면 캡쳐 방식입니다.

procedure TCaptureEngine.ScreenCapture;
const
  // ** 윈도우 2000 이상에서 반투명화 된 윈도우를 캡쳐 할 수 있는 비트 플래그 ** //
  // ** BitBlt 함수에서 OR 연산자를 통해서 사용한다 ** //
  CAPTUREBLT = $40000000;
var
  CursorInfo: TCursorInfo;
  piconinfo: TIconInfo;
  DC: HDC;
  scrRect: TRect;
  CAPTUREBIT: Integer;
begin
  DC := GetDC(0);

  scrRect := Screen.DesktopRect;
  FBackBuffer.SetSize(scrRect.Right - scrRect.Left, scrRect.Bottom - scrRect.Top);

  CAPTUREBIT := 0;

  if FTransparentCapture = True then
    CAPTUREBIT := CAPTUREBLT;

  with FBackBuffer do
    BitBlt(FBackBuffer.Canvas.Handle, 0, 0, FBackBuffer.Width,
      FBackBuffer.Height, DC, scrRect.Left, scrRect.Top,
      SRCCOPY or CAPTUREBIT);

  if FCaptureCursor = True then
  begin
    CursorInfo := CaptureCursor;
    GetIconInfo(CursorInfo.hCursor, piconinfo);

    CursorInfo.ptScreenPos := DesktopCoordinateToOriginCoordinate(CursorInfo.ptScreenPos);

    DrawIcon(FBackBuffer.Canvas.Handle,
      CursorInfo.ptScreenPos.X - piconinfo.xHotspot,
      CursorInfo.ptScreenPos.Y - piconinfo.yHotspot, CursorInfo.hCursor);
  end;

  FCaptureRect := Rect(0, 0, FBackBuffer.Width, FBackBuffer.Height);

  ReleaseDC(0, DC);
end;

GetDC(0);을 호출하게 되면 전체화면에 대한 디바이스 컨텍스트 핸들을 흭득하게 됩니다. ( 듀얼모니터 포함 )
그리고 BitBlt을 통해서 실제 화면DC에 있는 데이터를 비트맵인 FBackBuffer에 DC로 옮깁니다.
그리고 마지막으로 ReleaseDC(0, DC);를 호출하여 할당한 디바이스 컨텍스트 자원을 해제하게 됩니다.

위에서 보앗듯이 화면캡쳐는 이게 전부입니다. 추가적으로 구현되는 부분도 있겠지만 기본 베이스는 이게 끝입니다.
화면캡쳐하는 방법에 대해서 알았으니 이제 화면캡쳐방지는 어떻게 구현해야 할지 생각해볼 차례입니다.

우선적으로 떠오로는 것은 API Hooking이 필요하다라는것을 느낄것입니다.
그리고 화면캡쳐방지에 대한 3가지 주요함수에 대해서 후킹에 필요성이 느껴질것입니다.

그렇다면 어떠한 API를 후킹해야 할지 결정할 차례입니다.
일단 ReleaseDC는 제외됩니다. 단순히 자원을 해제하게 되기 때문입니다.

두 번째로 GetDC와 BitBlt이 있는데 이 함수를 다 후킹해야할지 하나만 후킹해야할지를 결정해야 합니다.
보통 화면캡쳐방지를 만들려고 하면 실수하는 부분이 GetDC를 고려한다는 점인데 실제로 GetDC는 고려할 필요조차
없습니다. 만약에 GetDC를 후킹한다면 결과적으로 그 DC를 얻을 때 마다 DC를 관리하는 리스트에 다 추가를 해줘야 
되겠고 BitBlt이 호출되면 이 때 리스트에 대한 정보를 검색하여 이전에 GetDC를 이용하여 화면캡쳐를 시도하려는 DC였다면
BitBlt을 실패시키겠죠. 그리고 ReleaseDC도 덩달아 후킹을 해야 합니다. 왜냐하면 자원이 해제되는 것을 체크하여 DC를
관리하는 리스트에서 제거를 해주어야 하기 때문입니다.

우선 왜 실수로 GetDC를 고려할려고 하였는지 생각해봐야 합니다.
GetDC를 고려하는 이유는 BitBlt 함수가 호출 될 때 이게 화면캡쳐를 하기 위해 BitBlt를 하였는지 아니면 그냥 단순히
비트맵 복사를 할려고 하였는지 알수가 없기 때문입니다.

그렇다면 오로지 BitBlt만 후킹해서 함수를 호출한 프로그램이 화면을 캡쳐하려는것인지 비트맵을 복사하려고 하는것인지
알아낼 수 있는 방법이 있는지를 찾아내야 합니다.
그리고 실제로 상용 화면캡쳐 방지 솔루션에 경우에는 오로지 BitBlt 후킹만으로 화면캡쳐방지를 구현합니다.

이것을 체크하기 위해 윈도우즈 API인 GetObjectType 함수를 이용할수 있습니다.
이 함수는 인자로 받는 매개변수가 GDIObject로써 DC도 GDIObject에 포함되기 때문에 
당연히 매개변수로 받을 수 있습니다. 그리고 이 함수가 리턴값들 중에 가장 중요한 리턴값은 

OBJ_DC로 MSDN에 보면 이것은 Device context 를 의미합니다.


즉, BitBlt를 후킹한 루틴에서는 넘어온 srcDC ( SourceDC ) 에 대해 GetObjectType으로 해당 DC에 타입을
조사하여 OBJ_DC일 경우 원본 API를 호출하지 않으면 됩니다.

다음과 같이 소스로 구현될것입니다.

BOOL WINAPI MyBitBlt(
  HDC hdcDest, // handle to destination DC
  int nXDest,  // x-coord of destination upper-left corner
  int nYDest,  // y-coord of destination upper-left corner
  int nWidth,  // width of destination rectangle
  int nHeight, // height of destination rectangle
  HDC hdcSrc,  // handle to source DC
  int nXSrc,   // x-coordinate of source upper-left corner
  int nYSrc,   // y-coordinate of source upper-left corner
  DWORD dwRop  // raster operation code
)
{
    if (GetObjectType(hdcSrc) == OBJ_DC)
    {
        return TRUE;
    }
    BOOL Result = 0;

    RestoreAPI(g_hInst, "gdi32.dll", "BitBlt", OldBitBltCode);
  
    // ** 그리고 원래의 API 함수를 호출 ** //
    //if (hWnd == 0)
    Result = BitBlt(hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc, dwRop);

    // ** 다시 코드를 변경한다. ** //
    InterceptAPI(g_hInst, "gdi32.dll", "BitBlt", (DWORD)MyBitBlt, OldBitBltCode);

    return Result;
}

위에서 RestoreAPI는 후킹된 함수를 원래대로 되돌려주며 InterceptAPI는 다시 후킹을 하는 함수입니다.
그리고 이러한 방식은 멀티 스레드에 매우 위험한 방법으로 실제 상용DRM에서는 이렇게 허접하게 만들면 안됩니다.-_-
지금은 단순히 예제를 보여주기 위해 이렇게 코드가 작성되었다라고 이해해주시면 감사하겠습니다.
( Detour 라이브러리를 쓰는것이 좋습니다. 그러나 API Hooking하는것 자체도 설명을 같이 할것이기 때문에 나중에
  이 부분은 다시 설명하겠습니다. )

후킹을 하고 해제하는 함수는 다음과 같이 구현되어 있습니다.

BOOL InterceptAPI(HMODULE hLocalModule, const char* c_szDllName, const char* c_szApiName, DWORD dwReplaced, char *OldCode)
{ 
    DWORD dwOldProtect;
    // 해당 함수의 시작 주소를 얻는다. ** //
    DWORD dwAddressToIntercept = (DWORD)GetProcAddress(
      GetModuleHandle((char*)c_szDllName), (char*)c_szApiName);

    if (dwAddressToIntercept == 0)
    {
    //    OutputDebugString("에러");
        return FALSE;
    }

    BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;

    BYTE *pbReplaced = (BYTE *) dwReplaced;

    // ** 메모리의 권한을 변경 ** //
    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect);

    // ** 원래의 코드를 백업한다. ** //
    memcpy(OldCode, pbTargetCode, 5);

    // ** 점프 코드를 삽입한다. ** //
    *pbTargetCode++ = 0xE9; // jump rel32

    // ** 주소는 내가 설정한 함수 주소 ** //
    *((unsigned int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4);
    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect);
    FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
    
    OutputDebugString("InterceptAPI 완료");

    return TRUE;
}

BOOL RestoreAPI(HMODULE hLocalModule, const char* c_szDllName, const char* c_szApiName, char *OldCode)
{ 
    DWORD dwOldProtect;
    DWORD dwAddressToIntercept = (DWORD)GetProcAddress(
      GetModuleHandle((char*)c_szDllName), (char*)c_szApiName);

    if (dwAddressToIntercept == 0)
    {
        return FALSE;
    }

    BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;

    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect);

    // ** 원래의 코드를 복구한다. ** //
    memcpy(pbTargetCode, OldCode, 5);

    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect);
    FlushInstructionCache(GetCurrentProcess(), NULL, NULL);

    OutputDebugString("RestoreAPI 완료");;
    return TRUE;
}

그리고 후킹용 DLL이 로드 될 때
InterceptAPI(g_hInst, "gdi32.dll", "BitBlt",(DWORD)MyBitBlt, OldBitBltCode);
해제 될 때,
RestoreAPI(g_hInst, "gdi32.dll", "BitBlt", OldBitBltCode);
를 해주어야 합니다.

위와 같은 예제를 통해서 DLL을 만들어 인젝션을 시킨 후에 오픈캡쳐로 화면캡쳐를 시도하려고 하면
그냥 흰색으로 나오거나 검은색나올것입니다. BitBlt 함수자체가 동작하지 않기 때문에 아예 복사자체가 안되기 때문입니다.

이정도면 제가 보기엔 안정성을 제외하고서는 기능적으로 보았을때에는 거의 상용과 동일한 수준이라고 생각됩니다.
그러나 추가적으로 하나의 API를 더 후킹해야 할 필요가 있습니다.
일부 상용 DRM 솔루션은 바보같게도 WIndows XP에서 추가 된 새로운 API인 PrintWindow를 후킹하지 않는 바람에
이 함수를 써서 우회가 가능하더군요.

PrintWindow 함수는 화면상에 보이지 않고 가려져 있는 윈도우에 대해서도 화면을 캡쳐할 수 있는 매우 막강한 함수입니다.
그냥 후킹해서 return TRUE; 해버리면 이것도 당연히 막히겠죠.

추가적으로 상용 화면캡쳐방지 솔루션은 단순히 화면캡쳐만 막는게 아니라 화면캡쳐에 대한 영역을 보안하는 기능이 있습니다.
즉, 전체화면에 대한 캡쳐가 안되는것이 아니라 보호하고 있는 대상이 웹브라우저라면 그 웹브라우저에 대한 부분만 캡쳐가
안되도록 막는 기능입니다. 이거는 후킹하는 루틴에서 원본 함수를 호출 후 좌표계산을 통해 웹브라우저가 속한 영역에 대
해 화면캡쳐를 하지 말라는 경고그림이라던지(-_-) 그런거를 그려넣으면 되겠죠.

이전에도 말씀드렷다시피 지금 알려드린 예제는 안정성이 떨어지기 때문에 마이크로소프트사에서 만든
API Hooking용 라이브러리를 쓰는 것이 좋습니다.
http://research.microsoft.com/en-us/projects/detours/
만약에 델파이를 사용하신다면 매드훅 라이브러리를 쓰면 됩니다.
http://help.madshi.net/ApiCodeHooking.htm

Detour 라이브러리에 경우에는 인터넷에 관련 글들이 많이 있고 매드훅 라이브러리는 상용 라이브러리이기 때문에
제가 사용을 해보질 않아서 이 두 라이브러리에 대한 내용은 생략하겠습니다.

그렇다면 이걸로 끝일까요?
그렇지 않습니다. 단순히 API를 후킹했다고 해서 끝나지 않습니다.
화면캡쳐를 하는 방식은 무궁무진하기 때문에 저 두 API만 후킹한다고 해서 끝나지 않습니다.
다이렉트X를 이용한 화면캡쳐라던지 윈도우즈 미디어 API를 이용한 화면캡쳐등 여러가지가 있습니다.
이 모든 것을 막을 수 없기 때문에 추가적으로 요구되는 사항이 바로 블랙리스트 개념입니다.

블랙리스트 방식은 화면캡쳐와 관련 된 프로그램에 대한 식별할 수 있는 시그내쳐를 모두 모아서 
이 시그내쳐와 맞는 프로세스가 발견되면 즉시 강제 종료시키는 방식입니다.

이러한 시그내쳐중에 단순히 윈도우에 타이틀바 캡션정보를 이용하여 식별하는 방법이 있는데 허접하게 보일지 몰라도
의외로 강력하며 최후에 수단이라고 보여집니다.-_-
왜냐하면 일반 사용자는 직접 소스코딩을 할 수가 없기 때문에 프로그래머가 아닌 일반 사용자에 대해서는 좋은 방어
수단이 될수가 있기 때문입니다. 물론 완벽하지는 않겠지만요. 

여기까지 화면캡쳐방지에 대한 글은 마치겠습니다.
지금까지 설명한 내용은 실제로 상용으로 판매되고 있는 화면캡쳐방지 기법에 대한 기본 베이스입니다.
더 이상 설명하다가는 보안종사하시는 분들이 화를 내기 때문에 이쯤에서 글을 줄입니다.

'프로그래밍' 카테고리의 다른 글

화면 캡쳐 프로그램 만들기  (0) 2021.01.30
JSToolNpp (notepad++ Javascript 개발 플러그인)  (0) 2018.02.20
skcc blog 솔루션(Solution)  (0) 2018.02.20
git - Pull Request  (0) 2018.01.10
Private Git Repository  (0) 2018.01.10
Comments