매직 더 개더링: 아레나를 해킹하여 100% 승률 달성
(mayer.cool)- Magic: The Gathering Arena에서 상대를 원하는 시점에 항복 처리할 수 있어, 매치메이킹 게임에서 지지 않는 상태를 만들 수 있었음
- 카드 게임은 보통 서버가 전체 상태를 관리하는 서버 권위 구조에 적합하지만, MTGA의 봇 상대 Sparky는 로컬 클라이언트 로직으로 구현돼 있었음
- C# 기반 MTGA 클라이언트는 리플렉션으로 런타임 객체와 private 필드에 접근할 수 있었고, 디컴파일된 코드에서
JoinMatch,ConnectAndJoinMatch,HeadlessClient같은 이름이 확인됐음 - 봇 매치에서는 같은 계정의
PersonaID와 JWT로 양쪽 좌석에 접속하는 구조가 쓰였고, 이를 일반 매치에 적용해 상대 좌석에 헤드리스 클라이언트를 붙인 뒤ConcedeGame()을 호출할 수 있었음 - 서버는 이후 매치메이킹 게임에서 양쪽 좌석이 같은 계정과 JWT를 쓰지 못하도록 패치됐으며, 클라이언트 측 봇 구현과 좌석 인증 검증의 경계가 실전 보안에 직접 영향을 준 사례가 됨
카드 게임 해킹이 어려운 이유
- 카드 게임은 턴 기반이고 클라이언트와 서버 사이에 오가는 정보가 많지 않아 서버 권위형 구조에 잘 맞음
- 서버가 전체 게임 상태를 관리하고 클라이언트에는 필요한 정보만 전달하면 됨
- 상대의 손패나 덱처럼 공개되지 않은 정보는 로컬에 존재하지 않음
- 덱 순서를 바꾸거나 뽑을 카드를 조작하는 식의 시도는 서버가 동작을 수행하고 결과만 알려주기 때문에 어려움
- 1인칭 슈터와 달리 카드 게임은 플레이어 행동이 제한적이고 정해진 시점에 발생함
- FPS에서는 적 모델 위치 같은 정보가 클라이언트에 미리 캐시될 수 있어 wallhack 같은 형태가 가능함
- Riot의 anti-wallhack 블로그는 클라이언트 측 플레이어 위치 데이터를 줄이는 접근을 다룸
- 잘못된 행동을 탐지하기도 상대적으로 쉬움
- 손에 없는 카드를 내는 행동
- 자기 턴이 아닌데 행동하는 경우
분석 시작점: 네트워크와 C# 클라이언트
- 게임 해킹의 출발점으로 네트워크 통신을 살펴보는 접근을 선택함
- MMO 해킹 사례를 다룬 Manfred의 DEF CON 발표는 네트워크 프로토콜 역공학이 여러 버그 분석의 핵심임을 보여줌
- MTGA는 C#으로 작성돼 있어, 트래픽 송수신 함수를 후킹하는 대신 런타임에서 객체를 조작할 수 있었음
- .NET 리플렉션으로 private 필드와 메서드에도 접근 가능함
- 관련 기초 예제로 Unity 게임 리플렉션 해킹 튜토리얼이 연결됨
- 난독화되지 않은 .NET 어셈블리는 metadata token 덕분에 함수, 변수, 클래스 이름을 사람이 읽을 수 있는 형태로 확인할 수 있음
- 이 때문에 디컴파일 결과가 사실상 소스 코드 리뷰에 가까워짐
JoinMatch에서 발견한 Sparky 구조
- 매치 참가 초기화를 찾기 위해 관련 함수명을 검색하다가
JoinMatch함수를 발견함 JoinMatch는 200줄이 넘는 긴 함수였고, 하단에서ConnectAndJoinMatch호출을 확인함- 이 함수는 매치 설정 정보를 받아 게임 서버에 접속하는 흐름으로 보였음
- 같은 코드 흐름에는
MatchType.NPE와MatchType.Familiar분기가 있었음NPE는 신규 플레이어 경험으로 추정되는 튜토리얼 계열 매치Familiar는 표준 봇 매치
- 이 분기에서 호출되는 로직이 MTGA의 봇 상대인 Sparky와 연결돼 있었음
- Sparky는 튜토리얼과 봇 대전에서 쓰이는 MTGA의 마스코트형 상대
- 봇 매치에서는 봇 로직이 로컬 머신의 게임 클라이언트 안에서 실행됨
로컬 봇과 HeadlessClient
- 봇의 실제 로직 핸들러는
HeadlessClient클래스 안에 있었음 HeadlessClient는 게임 보드를 렌더링하지 않는 헤드리스 클라이언트로, 서버에 접속해 게임을 진행함- 로컬에서 생성된 봇 클라이언트는 게임 클라이언트와 같은 사용자 인증 정보를 사용함
- 사용자 ID 역할의
PersonaID - 로그인 뒤 게임에 부여되는 JSON web token
- 사용자 ID 역할의
- 봇 매치에서는 같은 클라이언트가 사실상 매치 양쪽에 접속해도 서버가 문제로 보지 않는 구조였음
- 좌석(seat)은 게임에서 어느 플레이어인지 구분하는 방식으로 쓰임
- 봇 매치에서는 같은 인증 정보로 서로 다른 좌석을 채우는 구성이 가능했음
일반 매치 탈취 방식
- 봇 매치 로직을 일반 매치에 적용해 양쪽 좌석에 접속할 수 있는지 테스트함
- 필요한 정보는 런타임의 게임 객체에서 가져옴
- 현재 매치 설정
- 매치 서버 호스트와 포트
controllerFabricUrimatchIdPersonaIDJwt- 카드 데이터베이스
- 매치 매니저
- 에셋 조회 시스템
- 자신의 좌석을 기준으로 상대 좌석을 계산함
- 코드에서는
man.LocalPlayerSeatId % 2U + 1U로 다른 좌석을 구함
- 코드에서는
UnityFamiliar.SpawnFamiliar_DEBUG(...)로 상대 좌석에 봇을 연결함- 생성된
UnityFamiliar객체를 찾아cheatbot.Client.Gre.ConcedeGame()을 호출해 즉시 항복시킴
결과와 패치
- 이 방식은 일반 매치에서도 동작했음
- 상대가 이미 접속해 있고 게임이 진행 중인 상태에서도 작동함
- 매치메이킹 게임이었기 때문에 인간 상대를 이긴 것처럼 보상을 받았음
- 취약점의 핵심은 일반 매치 서버가 같은 계정과 JWT로 양쪽 좌석에 접속하는 상황을 허용했다는 점임
- 서버는 이후 매치메이킹 게임에서 양쪽 좌석이 같은 계정과 JWT를 쓰지 못하도록 패치됨
- 부록의
InstaWin코드는 UnityMonoBehaviour로 GUI 버튼을 만들고, 버튼 클릭 시 현재 매치 정보를 수집해 봇을 상대 좌석에 붙인 뒤 항복시키는 흐름을 담고 있음
댓글과 토론
Hacker News 의견들
-
Linux를 처음 제대로 파고든 계기는 EverQuest용 ShowEQ로 네트워크 트래픽을 들여다본 일이었음
당시 트래픽은 암호화되지 않았고 유용한 정보가 많이 들어 있었음. 허브로 트래픽을 Linux 머신에 복제해 구역의 실시간 지도를 그렸고, 몬스터·NPC·사용자 위치를 보여줬으며, 몬스터가 가진 전리품까지 보여줘 특정 몬스터만 골라잡을 수 있었음. 수동 방식이라 탐지가 불가능했던 게 장점이었고, 결국 SOE가 알아채고 트래픽 암호화를 시작함- 데이터는 결국 복호화되어 읽혀야 하니, 클라이언트를 역공학해서 실시간 복호화 방법을 찾아내게 됨
그러면 상대는 키 기반 서명을 도입하고, 다시 클라이언트에서 키를 훔치려 하며 암호화를 깨고, 이후 부정행위 방지 시스템이 들어오면서 고양이와 쥐 게임이 시작됨 - 십대 때 Dark Age of Camelot에서도 비슷한 걸 해봤고, 네트워크 스니핑, 허브와 스위치 차이, Linux를 배우는 데 매우 유익했음
게임에서 이득을 볼 만큼 가치 있었냐면 아니었음. 진지하게 플레이하지 않아서 설치에 2주 쓰고 1주 정도 써본 게 전부였지만, 학습 경험으로는 훌륭했음 - ShowEQ는 Verant/Sony가 계속 부인하던 여러 이론과 버그를 입증하는 데 쓰였던 기억이 남
예를 들면 hell level의 존재, 인간이 아니라 하플링이 경험치 보너스를 받는다는 점과 실제로 종족/직업별 경험치 차이가 있다는 점, 초기 Shaman 연금술이 실제로 고장 나 있었다는 점 등이었음. 그리고 이게 eqemulator.org로 이어진 것 아니었나 싶음 - 암호화가 어떻게 도움이 되는지 궁금함. 클라이언트가 어차피 복호화해야 하니 거기에 얹어가면 되는 것 아닌가?
- ShowEQ는 아직도 동작하고, Windows 기반으로 메모리를 읽는 MySEQ도 있음
현재 EQ 소유자가 별로 신경 쓰지 않는 듯해서 써도 괜찮은 편임. 다만 두 앱 모두 보이는 장비를 제외하면 몬스터가 가진 전리품을 보여준 적은 없음. 세월이 지나며 일부 데이터도 바뀌어서, 예전에는 정확한 몬스터 체력이 전송됐지만 지금은 퍼센트만 보냄
- 데이터는 결국 복호화되어 읽혀야 하니, 클라이언트를 역공학해서 실시간 복호화 방법을 찾아내게 됨
-
“Magic: The Gathering의 임의 게임을 플레이할 수 있는 거의 완전한 봇이 로컬 머신에서 돌아갈 만큼 작다”는 부분이 잘 이해되지 않음
MTG AI가 고객 머신에서 돌리기 부담스러울 정도로 무겁다면 서버에서도 돌리지 않을 것 같음. 카드 게임 봇 대전은 보통 판당 과금이 아니니 비용이 커짐. 서버도 마법이 아니라 대부분 로컬 머신과 같은 x86 CPU를 쓰고, 데스크톱보다 클럭은 낮은 경우가 많음. 로컬보다 봇 턴 시간을 줄이려면 고객보다 훨씬 많은 코어를 써야 할 텐데, 플레이어당 8~16개 할당 코어라면 동시 접속 피크 때 악몽일 것 같음. CPU 플레이어가 멀티코어를 지원하지 않는다면 로컬 실행이 어쨌든 더 빨라야 함- 여기서 말한 건 처리 성능이 아니라 메모리 사용량임
봇의 규칙 엔진이 구형 iPhone이나 Android 기기 메모리 안에 들어갈 만큼 작다는 뜻임. 서버라면 상태 기계나 규칙 엔진을 메모리에 많이 올려둘 수 있고, 특정 요청을 실행하는 데는 처리 성능을 거의 쓰지 않을 수도 있음 - 데스크톱이라면 맞지만, MTGA는 휴대폰에서도 실행된다는 점도 기억해야 함
- MTG는 매우 복잡한 규칙 체계를 가진 게임
- 여기서 말한 건 처리 성능이 아니라 메모리 사용량임
-
Daniel, 다시 한 번 상위 글 축하함. 이 글이 올라온 뒤 MTGA를 해킹해봤고 GitHub에서도 조금 얘기했음 [0]
관심 있는 사람을 위해 말하자면, 지금은 거의 기능이 없는 MTGA 비공식 클라이언트를 비활성 상태로 작업 중임. 목표는 랭크 게임 플레이 자동화와 더 강한 봇 상대를 제공하는 것임. 최근 다른 일에 밀려 있었고, 명확하게 볼 수 있는 좋은 UI를 어떻게 만들지도 아직 막막해서 언제 최소한 쓸모 있게 될지조차 예상하기 어려움. 그 밖에도 Daniel이나 다른 사람들의 게임 해킹 이야기가 계속 궁금함. 이런 버그를 어떻게 찾는지, 해킹 후 밴을 걱정하지 않는 방법, @aethros가 말한 것처럼 버그를 공개하는 방식, 비공식 카드 게임 클라이언트 구조화 같은 이야기를 더 듣고 싶음
[0] https://github.com/MayerDaniel/mayerdaniel.github.io/issues/... -
MTG 같은 복잡한 게임에서 AI 상대를 만들려면 오버헤드가 클 줄 알았다는 말은 꽤 큰 과소평가임
게임이 거의 튜링 완전하고, 무한 루프를 제외해도 사람들이 그걸로 많이 놀기 때문임. 그래도 AI 전략 쪽 연구는 이미 많이 되어 있을 것 같음. 그리고 글쓴이가 직접 올린 글이니 제목에 “Show HN:”이 붙어도 될 듯함- Show HN은 블로그 글이 아니라 사람들이 직접 만져볼 수 있는 프로젝트용임: https://news.ycombinator.com/showhn.html
- “게임으로 튜링 기계를 인코딩할 수 있다”는 말과 “모든 상황에서 합법적인 행동을 할 수 있는 프로그램을 작성하기 어렵다”는 말은 전혀 같지 않음
봇 작성자에게 어려운 건 후자처럼 보임 - 실제로 튜링 완전함: https://arxiv.org/abs/1904.09828
- 새 세트가 순환하면서 게임은 계속 바뀌지만, 특정 상호작용에서는 무한 루프를 만들 수 있거나 여러 시점에 가능했던 것으로 알고 있음
예를 들면 입장/퇴장/언탭 효과로 토큰 생물을 만들고 죽이는 식임. 이런 루프가 항상 어느 한쪽에게 피해를 주는 것도 아님 - 실제 전략 중 이 복잡도에 가까운 게 있는지 궁금함
MTG를 해본 적은 없지만, 이 주장은 이기려는 목적이 아니라 일부러 상태를 가진 구조를 만들면 기술적으로 가능하다는 뜻처럼 보임
-
아들과 함께 올드스쿨 Magic 93/94를 물리 카드로 플레이하는 게 정말 즐거움
매년 Madrid에 가서 7pts Singleton 세계선수권에 참가함. 이번 여름 아들이 9위를 해서 매우 자랑스러움. 7pts Singleton은 다양한 덱 구성이 가능하고 비교적 부담 없는 비용에 균형 잡힌 게임플레이를 제공하는 훌륭한 방식임 (https://7pts-singleton.com)- Black Lotus, Ancestral Recall, Moxen이 합법인 형식을 저렴하다고 부르기는 어려움
7pts Singleton에서 전부 쓸 수는 없더라도 하나만 해도 수천에서 수만 달러가 듦. 그래도 아들과 게임을 즐긴다니 좋고, 순위권에 든 것도 축하함
- Black Lotus, Ancestral Recall, Moxen이 합법인 형식을 저렴하다고 부르기는 어려움
-
순수 클라이언트 측 게임 로직이라니. 예전에 작은 게임을 만들었을 때 반응성을 위해 클라이언트에 게임 로직을 넣어야 할 때가 있었지만, 서버에서 같은 로직을 다시 실행하지 말라는 법은 없어서 그렇게 했음
FPS나 RTS 같은 실시간 게임에서는 어려울 수 있지만, 카드 게임에서는 변명의 여지가 없음. 이런 카드 게임이라면 실제 플레이어가 볼 수 있는 정보 이상을 클라이언트에 보내지 않는 게 맞음. 예컨대 상대 손패의 카드 내용을 보내지 말고 카드 수만 보내야 함. 서버로 보내는 행동도 자기 자신에 대한 것만이어야 하므로, 상대의 항복을 선언할 수 있어서는 안 됨. “항복!”이라고 말하면 서버가 그걸 내가 항복한 것으로 해석해야 함- 글에서는 게임이 바로 그런 방식으로 작성되어 있다고 봐도 됨. 필요한 시점에 필요한 정보만 받고, 자신의 행동만 전송함
취약점은 두 번째 클라이언트를 띄워 현재 진행 중인 게임에 상대 좌석으로 접속할 수 있었다는 점임. 일단 그렇게 되면 항복을 포함해 상대의 행동을 보낼 수 있었음 - 글에 명확히 나오듯이 게임플레이는 전부 서버 측에서 처리됨
글쓴이가 이용한 건 클라이언트 측 게임 로직이 아니라 권한 부여와 게임 접속 코드의 문제였음. “상대 대신 항복을 선언할 수 없어야 한다”는 말은 글쓴이가 발견한 익스플로잇이 어떻게 작동했는지를 그대로 반복한 것임 - 글의 앞 1/3에서 게임이 그렇게 구현되어 있다고 명시적으로 말함
- 글에서는 게임이 바로 그런 방식으로 작성되어 있다고 봐도 됨. 필요한 시점에 필요한 정보만 받고, 자신의 행동만 전송함
-
League of Legends에는 특정 챔피언과 아이템 조합에서 0으로 나누기 버그가 있어 서버가 모든 플레이어를 내보낸 뒤 크래시하는 일이 있었음
악용자가 마지막에 쫓겨났기 때문에 그 팀은 승리 처리되고, 상대 팀은 정상 결과가 아니라 Loss Prevented를 받았음- 상대가 정상 결과가 아니라 패배를 받지 않는다면, 이긴 팀도 승리를 받아서는 안 됨
-
이 글은 접근하기 쉬우면서도 통찰 있는 세부사항이 좋음
다만 실제 경기 중에 봇을 연결하는 경우가 이해되지 않음. 왜 게임이 경기 중간 합류를 허용하고, 봇이 항복했을 때 왜 그게 상대의 항복으로 처리되는 걸까? 3인 게임을 만드는 것이라면 3번 플레이어가 항복해도 2번 플레이어까지 항복한 게 되면 안 됨- 3인 게임이 되는 게 아니라 계속 2인 게임으로 남음
코드가 내 계정의 좌석 인덱스를 알아낸 뒤, 봇을 다른 좌석 인덱스로 참가시키는 방식임. 문제는 참가하는 사용자가 그 좌석에 있어야 할 올바른 사용자인지 검증하지 않는다는 점임. 이미 연결된 좌석에 다시 접속을 허용하지 말아야 한다는 생각도 가능하지만, 모바일도 지원하는 게임이라면 연결 끊김 제한 시간을 꽤 길게 가져가고 싶을 것임. 잠깐 끊겼다가 제한 시간이 이전 연결이 죽었다고 인식하기 전에 재접속한 플레이어를 막고 싶지는 않기 때문임. 다른 게임에서도 재접속하려 하면 10초쯤 “진행 중인 게임에 참가할 수 없음”이 뜨다가 나중에야 재접속되는 걸 본 적 있음. 비유하자면 동네 카드숍 MTG 대회에 나타나 누군가를 자리에서 밀어내고 앉아 “항복합니다!”라고 외쳤는데, 심판이 그 의자에 앉은 사람이 선언했으니 플레이어 B가 기권했다고 인정하는 상황임 -
- 연결 끊김 후 재접속을 처리하는 방식일 가능성이 큼. 예전 연결을 종료하는 식임
- 봇이 플레이어 2를 대체한 뒤 항복을 제출하고, 서버는 이를 플레이어 2의 항복으로 기록함
- MTG: Arena는 3인 게임을 허용하지 않으므로, 봇은 상대의 좌석에 끼어드는 것임
그래서 상대 대신 항복할 수 있었던 것으로 보임. 개발자들이 누가 굳이 그런 시도를 할 이유가 있다고 생각하지 못해서 검사를 안 했을 수도 있음
- 3인 게임이 되는 게 아니라 계속 2인 게임으로 남음
-
Diablo 2 시절, 같은 접속 패킷을 재사용해서 오픈 서버 캐릭터(LAN)를 공식 bnet 인터넷 서버에 접속시킬 수 있던 때가 떠오름
오픈 서버 데이터는 전부 로컬에 저장됐기 때문에 원래 존재하면 안 되는 온갖 아이템을 만들 수 있었고, 공식 서버도 그걸 받아들였음 -
최근 MTGA로 다시 MTG에 입문했음. Unity 게임이고 il2cpp가 아니라서 빠르게 디컴파일해봤는데, 설령 il2cpp였어도 큰 보호는 아니었을 것 같고 꽤 재미있는 것들을 찾았음
Epic Launcher 빌드용 키나 문서화되지 않은 API 같은 것들이 있었음. 부정행위에 쓰고 싶은 건 아니고, 그저 전투 기록이 있었으면 좋겠음. 어떤 매치를 이기고 졌는지, 끝난 게임의 전장 상태를 다시 보는 기능 같은 것임. 이 글도 확인해볼 예정이고, 빨리 패치되길 바람- 이 취약점은 글을 쓰기 전에 이미 패치됐고, MTG에 공개했음
기록을 보고 싶다면 https://untapped.gg/en를 확인해보면 됨. 그쪽과 조금 이야기해봤는데, 사실상 원하는 일을 해줌. 대부분의 정보는 MTGA 애플리케이션 디렉터리에 있는 MTG 디버그 로그에서 가져오므로, 원하면 직접 트래커를 만들 수도 있음. 사이트에서도 이 내용을 설명함: https://help.hearthsim.net/en/articles/3620440-how-do-i-supp... - https://www.17lands.com/는 리미티드 게임의 승패 통계를 수집하고, 리미티드와 컨스트럭티드 모두에서 턴별 게임 기록도 저장함. 기여자로 참여했음
- 플레이 데이터 수집용으로는 https://mtgaassistant.net/를 가장 흔한 편으로 알고 있음
- 이 기능은 이미 있는 것으로 알고 있음. 아마 플레이어 로그일 것임
그 로그를 이용해 추적하는 앱들이 꽤 있었던 것으로 기억함
- 이 취약점은 글을 쓰기 전에 이미 패치됐고, MTG에 공개했음