당신의 게임은 안전한가요?

시큐어 코딩과 관련해서 경험한 이야기가 있습니다.
잡지에 있는 하나의 글처럼 가볍게 읽어주시면 감사하겠습니다.

취미로 아주 작은 게임 서버를 운영하는 친구가 있습니다.
친구는 자신이 만든 게임을 플레이해보라고 제안했습니다.
그런데 저는 친구의 의도대로 순순히 게임을 즐길 생각이 없었습니다.

사건의 시작

단기간에 강해지길 원했습니다.
노가다없이 게임 내 재화를 많이 얻고 싶었고, 가장 초보적인 방법인 메모리 변조를 해보기로 했습니다.
이 공격을 위해 만들어진 도구인 치트엔진을 사용해서 첫 공격을 시도했습니다.

첫 번째 스캔

골드를 늘리기 위해 현재 가진 골드(71497)를 입력하고 스캔했습니다.
그런데 동일한 값을 가진 항목이 여러 개 검색되어 어떤 게 골드인지 특정할 수 없었습니다.
골드를 의미하는 주소를 정확히 찾기 위해 골드를 2만큼 더 얻어서 71499로 만든 후 다시 스캔해봤습니다.

두 번째 스캔

이것을 반복하다가 결국 하나가 남게 되면 정확한 값을 찾는 데 성공한 것입니다.
찾은 값은 마음대로 변경할 수 있었고, 변경된 값이 게임 내에도 잘 반영되었습니다.

이 방법으로 스탯, 경험치, 아이템, 골드 등 모든 것을 복사하거나 얻었고, 이것을 알게 된 친구의 표정은 어두워졌습니다.

어떻게 이런 게 가능했을까요?
친구가 만든 게임 서버는 클라이언트를 100% 신뢰하고 있었고, 대부분의 로직이 클라이언트에 있었습니다.
그리고 클라이언트가 보낸 데이터를 아무런 의심 없이 게임 서버 DB에 저장해버렸죠.

게임에 필요한 모든 것을 얻었지만 여기서 멈추지 않았습니다.
값 하나를 바꾸기 위해 매번 이 작업을 하기에는 너무나 번거로웠습니다.
어떻게 하면 더 쉽게 재화를 얻을 수 있을까 고민하다가 서버와 클라이언트 사이에 오가는 값을 조작해보기로 했습니다.

본격적인 조작

값을 조작하기에 앞서 어떤 패킷들이 오가는지 확인하고 싶었습니다.
패킷 분석 도구로 와이어샤크라는 유명한 도구가 있습니다.
이 도구로 모니터링을 시작한 채 게임에 접속하자마자 패킷이 쏟아지기 시작했습니다.
사냥터에 들어가 보니 이런 내용을 가진 패킷도 보입니다.

1
2
<mon_move>177,6,4,6,11</mon_move>
<!-- 대략 몬스터가 움직인다는 뜻 -->

패킷은 전혀 암호화되어있지 않았고, 그때 저는 이런 생각이 들었습니다.

서버로 패킷을 바꿔 보내면 데이터 조작이 가능하겠구나!

바로 실행에 옮겼습니다. 게임에 로그인된 상태이니 바로 데이터를 전송해보기로 했습니다.
1
2
3
4
5
ip: 192.168.0.1
port: 12501
content: <str>9000</str>
<!-- IP가 192.168.0.1이고, 포트 번호가 12501인 곳에 힘 스텟 9000을 보냄 -->
<!-- 실제 패킷은 완전히 다르게 생겼습니다. -->

와이어샤크에서 패킷을 추출한 후 일반적인 에디터로 위와 같이 변경했고, bittwist를 사용해서 패킷을 전송해봤습니다.

1
2
3
4
5
6
7
$ bittwist -v -i eth0 test.pcap
sending packets through eth0
trace file: test.pcap

2 packets (2218 bytes) sent
Elapsed time = 0.021880 seconds
# 서버로 패킷을 보내는 데에 성공

패킷이 성공적으로 전송되었습니다.

패킷 전송 별거 아니군!

많은 기대를 하면서 게임에 접속해봤습니다.

???? 실패!!

하지만…기대와는 다르게 전혀 바뀐 게 없었습니다.

실패의 연속

왜 그랬을까? 한참 생각해보다가 이건 HTTP가 아니라 TCP 소켓 연결이라는 것을 알았습니다.
게임에 처음 접속하면 연결을 맺고, 게임을 하는 동안에는 그 연결을 유지한 채로 데이터를 주고받았습니다.
이미 클라이언트와 서버가 서로 연결되어있을 때, 그 중간에 패킷을 넣어 보내는 것은 어렵습니다.

아! 그럼 둘 다 나랑 연결하고, 데이터를 조작해서 전송하면 되겠구나!

새로운 도전을 하기로 했습니다. 이 방법은 중간자 공격이라고도 불립니다.
클라이언트와 서버 사이에 프록시 서버를 두고, 클라이언트와 서버가 통신할 때 이를 조작해서 전송할 수 있습니다.

이 작업을 쉽게 할 수 있는 도구로 Burp Suite가 있으며 이 도구는 HTTP 통신 시 값을 가로채서 조작하는 것에 자주 사용됩니다.
이것만으로는 TCP 소켓 통신을 가로챌 수 없어서 NoPE Proxy확장 프로그램도 함께 설치했습니다.
여러 글을 참고해서 이것저것 설정하고 나서 패킷이 잘 들어오나 확인해보았지만, 전혀 반응이 없었습니다.

패킷이 하나도 없어..ㅠㅠ

관련된 글을 더 찾아보니 NoPE Proxy는 DNS를 이용하는 방식이라서 서버에 도메인이 있어야 합니다.
친구의 서버는 IP만 있고, 도메인이 없어서 이 도구를 사용하는 데에 어려움이 있었습니다.
여기에 더 시간을 쏟는 것보다 다른 방법을 찾는 게 빠를 것 같아서 한참을 만지작대다가 포기했습니다.

새로운 마음가짐

적절한 도구가 더 없을까 찾아보다가 결국 직접 만들기로 했습니다. (저는 개발자니까요!)

게임 클라이언트인 것처럼 사기를 치고 연결해보자!

학창 시절에 TCP 소켓으로 채팅 프로그램을 만들었던 기억을 떠올려서 차근차근 만들었고, 직접 서버에 연결해봤습니다.

1
2
$ ./client 192.168.0.1 12501 # 예시 IP, port 입니다.
Connection Success

서버와 연결하는 데에 성공했습니다!
이제 로그인을 하고 나서 스탯 조작을 위한 명령어를 서버에 보내면 됩니다.

1
2
3
client: <str>9000</str>
server: <str>success</str>
<!-- 대략 힘 스탯을 9000으로 만들었다는 뜻 -->

이전에 와이어샤크로 수집한 패킷을 똑같이 보내보니 서버에서 정상적으로 응답이 왔습니다.
다른 스탯도 모두 9000으로 만들어서 보내봤습니다.

성공적

성공적입니다!! 이제 게임의 모든 걸 조작할 수 있습니다.
보스를 한 방에 없앨 수도 있고, 운영자 전용 아이템을 얻을 수도 있습니다.

친구는 이 상황을 실시간으로 보고 있었습니다.
입에 거품을 물고 있습니다.

여기서 멈출 수 없지

호기심은 여기서 끝나지 않았습니다. 다른 유저의 데이터도 변경해보고 싶었습니다.
앞에서는 데이터 조작을 간단하게 소개했지만, 사실 아래와 같은 순서로 이루어집니다.

서버 연결 > 로그인 > 데이터 로드 > 조작

일반적으로 A 유저로 로그인을 하면 A 유저의 데이터만 가져올 수 있는데,
A 유저로 로그인하고 나서, B 유저의 데이터를 가져오는 게 가능할지 궁금했습니다.

1
2
3
4
5
6
client: <login>userA</login>
server: <login>success</login>
<!-- A 유저로 로그인하고 나서 B 유저의 데이터 로딩 시도 -->
client: <dataload>userB</dataload>
server: <dataload>success</dataload>
<!-- B 유저 로딩 성공 -->

이게 되네!?

이제 저는 다른 유저의 데이터도 조작할 수 있습니다.
마음에 드는 유저에게 아이템을 주거나 저를 방해하는 유저의 모든 스탯을 1로 만들어버릴 수도 있습니다.

요약

  1. 친구가 아주 작은 게임 서버를 운영한다.
  2. 필자는 강해지고 싶었다.
  3. 필자는 클라이언트 메모리 변조로 모든 것을 얻었다.
  4. 친구의 표정은 어두워졌다.
  5. 클라이언트와 서버 사이의 패킷을 훔쳐보고 위조했다.
  6. 친구는 입에 거품을 물었다.
  7. 다른 유저 데이터까지 변경해버렸다.
  8. 친구는 쓰러졌다.

이를 통해 얻은 것

  1. 서버는 클라이언트가 보낸 정보를 무조건 신뢰하면 안 된다.
  2. 중요한 정보는 암호화하여 위변조가 어렵게 해야 한다.

The Context Container

원문

https://tomcat.apache.org/tomcat-8.5-doc/config/context.html#Context_Parameters

소개

기본 디렉토리로 $CATALINA_BASE 의 상대 경로를 사용하고, 설정하지 않은 경우에는 $CATALINA_HOME으로 설정됩니다. 컨텍스트 경로는 가장 긴 Request URI 부터 매칭됩니다.

속성

  • allowCasualMultipartParsing
    대상 서블릿이 @MultipartConfig 어노테이션으로 표시되지 않은 경우에도 HttpServletRequest.getPart* 또는 HttpServletRequest.getParameter*가 호출 될 때, 톰캣이 multipart / form-data 요청 본문을 자동으로 구문 분석해야하는 경우 true로 설정합니다. false가 아닌 다른 설정은 톰캣이 기술적으로 규격을 준수하지 않는 방식으로 작동하도록합니다. 기본값은 false입니다.

  • allowMultipleLeadingForwardSlashInPath

    톰캣에 여러 /를 단일 /로 정규화합니다. 이것은 URI가 파일 시스템 경로로 변환되는 것처럼 파일 시스템의 동작과 일관성을 유지하지 위함입니다. 기본값은 false이며 여러 개의 /문자를 접을 수 있습니다.

  • altDDName
    alternative deployment descriptor의 절대 경로입니다. 이는 /WEB-INF/web.xml에 있는 default deployment descriptoroverride 합니다.

  • backgroundProcessorDelay

    backgroundProcess메소드를 호출할 때 까지의 지연시간을 초 단위로 나타냅니다. 기본값은 -1이고, 컨텍스트가 상위 호스트의 백그라운드 처리 스레드에 의존함을 의미합니다.

  • className

    사용할 Java클래스 이름. 이 클래스는 org.apache.catalina.Context인터페이스를 구현해야합니다. 지정하지 않으면 표준값이 사용됩니다.

  • containerSciFilter

    SCI가 제공한 컨테이너의 정규식으로 이 컨텍스트에 사용하지 않도록 필터링합니다. 매칭은 java.util.regex.Matcher.find()를 사용하므로 정규 표현식은 필터링을 위해 SCI가 제공 한 컨테이너의 정규화 된 클래스 이름의 하위 문자열과 일치해야합니다. 지정하지 않으면 필터링이 적용되지 않습니다.

  • cookies

    브라우저가 쿠키를 지원해야하고, 세션 식별 통신에 쿠키를 사용하려면 true로 설정합니다.

  • crossContext

    이 응용 프로그램 내에서 ServletContext.getContext() 호출이 이 가상 호스트에서 실행중인 다른 웹 응용 프로그램의 요청 디스패처를 성공적으로 반환하도록하려면 true로 설정합니다. 보안이 있는 환경에서는 false (기본값)로 설정하여 getContext()가 항상 null을 반환하도록합니다.

  • docBase

    웹애플리케이션을 위한 문서 디렉토리나 웹애플리케이션의 아카이브 파일의 경로이름입니다. 절대경로나 appBase의 상대경로로 지정이 가능합니다. 컨텍스트 요소가 server.xml에 정의되어 있거나 docBase가 호스트의 appBase아래에 정의되어 있지 않다면 필드의 값을 설정하지 않아야 합니다. docBase에 심볼릭 링크가 사용되면 톰캣을 재시작하거나 컨텍스트를 재배포해야 적용됩니다.

  • dispatchersUseEncodedPaths

    인코딩 여부를 제어합니다. 이것은 톰캣이 요청 디스패처를 얻기 위해 호출을 처리하는 것과 내부적으로 요청 디스패처를 얻는 데 사용되는 경로를 생성하는 것에 모두 영향을 줍니다. 기본값은 true입니다.

  • failCtxIfServletStartFails

    서블릿이 load-on-startup >=0인 경우 컨텍스트 시작을 실패하도록 하려면 true로 설정합니다. 지정하지 않으면 상위 호스트의 구성에서 동일한 이름의 속성이 지정된 경우 그것을 사용힙니다. 그렇지 않으면 기본값이 false가 사용됩니다.

  • fireRequestListenersOnForwards

    톰캣이 요청을 전달할 때 구성된 ServletRequestListeners를 시작하려면 true로 설정합니다. 주로 ServletRequestListeners를 사용하여 요청에 필요한 환경을 구성하는 CDI 프레임 워크 사용자에게 유용합니다. 지정하지 않으면 false의 기본값이 사용됩니다.

  • logEffectiveWebXml

    애플리케이션이 시작될 때 웹애플리케이션에 사용되는 유효한 web.xml이 (INFO 수준에서) 기록되도록하려면 true로 설정합니다. 효과적인 web.xml은 톰캣이 구성한 모든 기본값과 발견 된 web-fragment.xml 파일 및 주석을 결합한 애플리케이션의 web.xml입니다. 지정하지 않으면 false의 기본값이 사용됩니다.

  • mapperContextRootRedirectEnabled

    활성화 된 경우 웹애플리케이션 컨텍스트 루트에 대한 요청은 기본 서블릿이 아닌 매퍼가 필요에 따라 리디렉션됩니다. 이것은 효율적이지만 컨텍스트 경로가 있는지 확인하는 문제가 있습니다. 지정하지 않으면 기본값 true가 사용됩니다.

  • mapperDirectoryRedirectEnabled

    활성화 된 경우 웹애플리케이션 디렉토리에 대한 요청은 기본 서블릿이 아닌 매퍼가 필요에 따라 리디렉션됩니다. 이것은 효율적이지만 디렉토리가 있는지 확인하는 문제가 있습니다. 지정하지 않으면 기본값 false가 사용됩니다.

  • override

    전역 또는 호스트 기본 컨텍스트의 설정을 무시하려면 true로 설정합니다. 기본적으로 기본 컨텍스트의 설정이 사용되지만 컨텍스트에 대해 동일한 속성을 명시적으로 설정하여 무시할 수 있습니다.

  • path

    처리 할 적절한 웹애플리케이션을 선택하기 위해 각 request URI의 시작과 대조되는 이 웹 응용 프로그램의 컨텍스트 경로입니다. 특정 호스트 내의 모든 컨텍스트 경로는 고유해야합니다. 빈 문자열 ( “”)의 컨텍스트 경로를 지정하면이 호스트에 대한 기본 웹애플리케이션을 정의하며 다른 컨텍스트에 할당되지 않은 모든 요청을 처리합니다.

    이 속성은 server.xml에서 컨텍스트를 정적으로 정의 할 때만 사용해야합니다. 다른 모든 상황에서는 경로가 .xml 컨텍스트 파일 또는 docBase에 사용 된 파일 이름에서 유추됩니다.

    server.xml에서 컨텍스트를 정적으로 정의하는 경우에도 이 속성은 docBase가 호스트의 appBase 아래에 있거나 deployOnStartupautoDeploy가 모두 false가 아닌 경우에만 설정해야합니다. 이 규칙을 지키지 않으면 이중 배포가 발생할 수 있습니다.

  • preemptiveAuthentication

    true로 설정되고 사용자가 보안 제한 조건에 의해 보호되지 않는 자원에 대한 자격을 제시하면, 인증자가 선점인증을 지원하는 경우에 사용자의 자격 증명이 처리됩니다. 지정하지 않으면 기본값 인 false가 사용됩니다.

  • privileged

    이 컨텍스트가 관리자 서블릿과 같은 컨테이너 서블릿을 사용할 수 있게 하려면 true로 설정합니다. 특별한 속성의 사용은 컨텍스트의 parent 클래스 로더를 Shared 클래스 로더 대신에 Server 클래스 로더로 변경합니다. 기본 설치에서는 Common 클래스 로더가 Server 클래스 로더와 Shared 클래스 로더 모두에 사용됩니다.

  • reloadable

    파일에 변화가 있을 때 자동으로 로딩할지 설정합니다. 기본값은 false입니다.

  • resourceOnlyServlets

    리소스가 존재하는 쉼표로 구분된 서블릿 이름의 목록입니다.

  • sendRedirectBody

    true인 경우 리디렉션의 응답에 RFC2616에서 권장하는 리디렉션의 세부 정보가 포함됩니다. 응답 본문을 포함하면 압축 필터와 같은 일부 애플리케이션 구성 요소에 문제가 발생할 수 있으므로 기본적으로 사용하지 않도록 설정합니다.

  • sessionCookieDomain

    세션쿠키에 사용할 도메인입니다.

  • sessionCookieName

    세션쿠키에 사용하는 이름입니다. 설정한 경우 웹애플리케이션에서 설정한 이름을 무시합니다.

  • sessionCookiePath

    세션쿠키의 경로입니다. 설정한 경우 웹애플리케이션에서 설정한 경로를 무시합니다.

  • sessionCookiePathUsesTrailingSlash

    Internet Explorer, Safari, Edge 브라우저에서 RFC6265를 위반하여 /foobar의 요청으로 /foo 경로를 가진 컨텍스트에 대한 세션 쿠키를 보내는 문제가 있습니다. 이 위험을 줄이려면 true로 설정합니다. 그러면 톰캣은 세션 쿠키와 연결된 경로에 후행 슬래시를 추가합니다. 위의 예에서 쿠키 경로는 /foo/가 됩니다.

    /*에 매핑 된 서블릿이 없으면 문제가되지 않습니다. 이 속성을 비활성화하려면 false로 설정합니다. 기본값은 false입니다.

  • swallowAbortedUploads

    톰캣이 중단된 업로드에 대한 추가 요청 데이터를 읽지 않고 대신 클라이언트 연결을 중단해야하는 경우 true로 설정합니다. 추가 데이터를 읽지 않으면 요청 처리 스레드가 더 빠르게 해제됩니다.

  • swallowOutput

    true인 경우에 system.outsystem.err로 출력한 바이트가 웹애플리케이션의 logger로 리디렉션 됩니다. 기본값은 false입니다.

  • tldValidation

    true인 경우에 TLD파일은 컨텍스트 시작시에 XML유효성을 검사합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 성능에 좋지 않은 영향이 있습니다.

  • useHttpOnly

    클라이언트의 스크립트가 세션 ID에 접근하지 못하도록 합니다. 기본값은 true입니다.

  • useRelativeRedirects

    HTTP 1.1 이상이고 javax.servlet.http.HttpServletResponse#sendRedirect(String)의 호출로 생성된 헤더가 절대경로나 상대경로로 리디렉션 할지 여부를 제어합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 false이고, 그렇지 않은 경우 디폴트 값은 true가 됩니다.

  • validateClientProvidedNewSessionId

    클라이언트가 새 세션ID를 제공하면 이 속성은 해당 ID가 유효한지 여부를 제어합니다. 기본값은 true입니다.

  • wrapperClass

    org.apache.catalina.Wrapper의 자바 클래스 이름입니다. 지정하지 않으면 기본값이 사용됩니다.

  • xmlBlockExternal

    true인 경우에 web.xml, web-fragment.xml, *.tld, *.jspx, *.tagx and tagPlugins.xml 파일의 파싱에 외부 엔터티를 로드할 수 없습니다. 기본값은 true입니다.

  • xmlNamespaceAware

    true인 경우에 web.xml, web-fragment.xml파일의 파싱이 namespace-aware가 됩니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 xmlValidation옵션도 true가 됩니다.

  • xmlValidation

    true인 경우에 web.xml, web-fragment.xml파일의 유효성(문법)을 검사합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 성능에 좋지 않은 영향이 있습니다.