본문 바로가기
정보공유

NDC 2017 하재승 NEXON ZERO (넥슨 제로) 점검없이 실시간으로 코드 수정 및 게임 정보 수집하기

by 날고싶은커피향 2018. 11. 21.

NDC 2017 하재승 NEXON ZERO (넥슨 제로) 점검없이 실시간으로 코드 수정 및 게임 정보 수집하기


NDC 2017 하재승 NEXON ZERO (넥슨 제로) 점검없이 실시간으로 코드 수정 및 게임 정보 수집하기
1. 넥슨 라이브인프라실 하재승 (ipknhama@gmail.com, @ipkn) 점검 없이 실시간으로 코드 수정 & 게임 정보 수집하기 NEXON ZERO
2. “이상한거 만드는거 좋아하는 덕후” 개발 10년차 바람의 나라, 메틴2, 빅샷, 던전앤파이터 서버 버너, JYP, ZERO Crow, 페블 한글화, 약속, 버블러, WDD 발표자: 하재승, ipknHama
3. What’s ZERO?
4. 여러 기술을 조합하여 라이브 서비스의 관심 있는 정보들을 개발팀 부하 없이 빠르게 수집
5. 기능 Realtime Monitoring User Behavior Analytics On-demand Data Extractor
6. Realtime Monitoring 유저의 실시간 플레이 상황 및 그래프 (시연 영상 : 다음 슬라이드)
7. Realtime Monitoring 편집 수집하려는 내용 실시간 편집 및 적용 C++ 코드 지원
8. User Behavior Analytics OnChangeRegion 호출 시 ga_screenview( … 이동한 지역) 실시간 편집 및 적용
9. On-demand Data Extractor RM / UBA로 수집가능한 데이터를 지정한 기간 동안 수집하여 .csv 또는 RDBMS 테이블로 추출 개발팀 외부에서도 사용할 수 있게 기능을 분리 개발팀 – 변수 편집 외부팀 – 사용할 변수를 선택하여 보고서 생성
10. Why “ZERO”?
11. ZERO development cost 추가 개발 없이 원하는 정보를 바로 수집하도록 설정 ZERO integration cost 개발팀이 연동을 위해 한 일: ZERO 연동 라이브러리 추가 및 초기화 함수 호출 ZERO additional patch 시연으로 확인 하였듯이, 새로운 내용을 수집하려 할 때 패치가 필요 없음 ZERO delay 실시간으로 새로운 정보를 수집하고, 실시간으로 확인 가능 ZERO risk 실수를 하더라도 게임 서비스에는 장애가 발생하지 않게 여러 겹의 보호 장치
12. NDC 2017, <즉시, 바로 살펴보자>, 송창규
13. Part 1 목차: Introduction 1. 시연 2. 소개 3. 구성 요소 4. 적용 사례 5. 향후 작업
14. Part 2 목차: Technical Details 1. PDB Inspect 2. C++ Expression Evaluator 1.Basic Operation 2.Function Call 3. Function Hooking 4. Minor issues
15. Part 1 Introduction
16. 필요성 라이브 본부 밑의 다양한 프로젝트 ETL 과정 자동화 개발팀 부하 없이 이벤트 진행 상황과 결과 빠른 확인
17. 시작 아이디어 덤프파일을 통한 사후 디버깅 실용 테크닉(김이선님, 2011) 풀덤프 dump 파일에서 특정 클래스의 오브젝트 추적 런타임에도 할 수 있지 않을까? 관련 기능을 API로 제공한다면? 서비스 중인 게임으로 확장한다면? 무엇을 더 할 수 있을까?
18. 관심 있는 정보를 빠르게 살펴보고 싶다 “무엇”을 “어떻게” 뽑아야 하는가?
19. “어떻게” – 수집할 방법 대다수 프로젝트가 C++로 작성됨 익숙한 C++ 코드를 바로 쓸 수 있게 하자 패치하더라도 다시 작업하지 않아야 한다 실시간으로 실수 해도 장애가 생기지 않게 보안 수준을 낮추지 않게
20. “무엇” – 수집할 시점 + 값 주기적으로 수집되는 값 현재 위치, 레벨, 직업, … 특정 함수가 불리는 시점에서 수집된 값 사망, 사용한 스킬, 이동한 맵, … 조합하여 표현할 수 있는 값 시간당 데미지, 시간당 돈 획득량, …
21. “무엇” – 수집할 시점 + 값 주기적으로 수집되는 값 현재 위치, 레벨, 직업, … 특정 함수가 불리는 시점에서 수집된 값 사망, 사용한 스킬, 이동한 맵, 퀘스트 완료, … 조합하여 표현할 수 있는 값 시간당 데미지, 시간당 돈 획득량, …
22. “무엇” – 수집할 시점 + 값 주기적으로 수집되는 값 현재 위치, 레벨, 직업, … 특정 함수가 불리는 시점에서 수집된 값 사망, 사용한 스킬, 이동한 맵, 퀘스트 완료, … 조합하여 표현할 수 있는 값 시간당 데미지, 시간당 돈 획득량, …
23. “무엇” – 수집할 시점 + 값 주기적으로 수집되는 값 현재 위치, 레벨, 직업, … 특정 함수가 불리는 시점에서 수집된 값 사망, 사용한 스킬, 이동한 맵, 퀘스트 완료, … 조합하여 표현할 수 있는 값 시간당 데미지, 시간당 돈 획득량, …
24. Realtime Monitoring “무엇”을 실시간으로 보여줌 한눈에 반하게! User Behavior Analytics “무엇”을 Google Analytics에 연동 On-demand Data Extractor “무엇”을 활용하기 편한 형태로 제공
25. 구성 요소
26. Realtime Monitoring User Behavior Analytics On-demand Data Extractor PDB Inspect C++ Expression Evaluator Function Hooking
27. ZERO 서버 게임 클라이언트 PDB ZERO Web C++ EE 변환 C++ EE 실행 Function Hooking PDB Inspect명령 지시 및 확인 * 게임 서버를 거치지 않고 게임 클라이언트가 직접 연결하여 관련 정보를 수집하게 구현되었습니다. 게임 서버에 연동하여 서버의 내용을 얻어오는 방식도 차후 구현될 예정입니다.
28. 적용 사례
29. 신년 방물장수 이벤트
30. Part 2 Technical Details
31. Part 2 목차: 기술 소개 1. PDB Inspect 2. C++ Expression Evaluator 1.Basic Operation 2.Function Call 3. Function Hooking 4. Minor issues
32. 전체 구조
33. ZERO 서버 게임 클라이언트 PDB ZERO Web C++ EE 변환 C++ EE 실행 Function Hooking PDB Inspect명령 지시 및 확인
34. ZERO 서버 게임 클라이언트 PDB ZERO Web C++ EE 변환 C++ EE 실행 PDB Inspect RM 변수 추가 보관 일정시간 마다 가공Canvas에 렌더링
35. PDB Inspect
36. 요약 PDB 내용을 확인하면 g_status virtualAddress 149584 type baseType btInt length 4 g_status 값을 알려면 149584 에서 4바이트를 읽자
37. PDB Program Database file 컴파일러가 어떻게 실행 파일을 생성했는지 저장 Visual Studio를 통해 디버깅하기 위해 필요 각 심볼의 이름, 타입, 주소 값 등을 저장함 dbghelp 또는 DIA SDK 를 통해 값을 읽을 수 있다.
38. DIA SDK PDB 파일을 읽기 위한 API COM 기반 트리 형태로 심볼 정보를 접근 + 심볼 ID로 검색 source = CoCreateInstance(CLSID_DiaSource) session = source->LoadDataForExe(“Game.exe”, …) session->get_globalScope(&global) http://github.com/ipkn/wdd Symbols.cpp
39. IDiaSymbol 속성 symTag 어떤 종류의 심볼인지 symIndexId session->symbolById로 검색가능한 ID name 심볼 이름 (optional) typeId 이 심볼의 타입을 나타내는 심볼의 ID virtualAddress 심볼의 주소 session->put_loadAddress 로 기준 주소를 설정가능 dataKind locationType registerId offset
40. 트리 구조 get_globalScope()로 얻은 심볼로부터 함수 .symTag = SymTagFunction 전역 변수 .symTag = SymTagData, dataKind = DataIsStaticMember 클래스 .symTag = SymTagUDT, udtKind
41. 트리 구조 - 함수 .symTag == SymTagFunction .type 함수 타입에 대한 심볼 .type.type 함수 리턴 타입 .virtualAddress 함수 시작 주소 인자 SymTagData, dataKind = DataIsParam .type 인자의 타입 로컬 변수 SymTagData, dataKind = DataIsLocal
42. 트리 구조 - 클래스 부모 클래스, 멤버 변수, 멤버 함수, … class C : public A, public B { … } c; (A*)&c (B*)&c 서로 다른 주소값 UDT(C) BaseClass(A), .offset = (A*)로 변환할때 this에 더해줘야 하는 값 BaseClass(B), .offset
43. std::string (SymTagUDT) std::_String_alloc<…> (SymTagBaseClass) … size_type (SymTagTypeDef) value_type (SymTagTypeDef) … size (SymTagFunction) 인자가 없는 멤버함수: this만 존재 this (SymTagData) (SymTagFuncDebugStart, SymTagFuncDebugEnd, SymTagInlineSite, SymTagCallee) resize (SymTagFunction) 인자가 2개 this (SymTagData) _Newsize (SymTagData) _Ch (SymTagData)
44. C++용 인터페이스 작성 각 속성을 멤버변수로, (optional 타입) findChildren을 iterator로 래핑 for(auto base : s.AllBaseClass()) { if (c.name) *c.name; }
45. DIA SDK 주의점 msdia140.dll 등록 필요 (버전에 따라 120, 100, …) symsrv.dll 심볼 서버 사용시 필요 없으면 심볼을 찾지 못한다는 에러가 리턴 symIndexId는 세션 별로 부여됨 함수 호출 순서에 따라 다른 값이 주어짐
46. 다른 언어/환경으로의 확장 모바일 기기 지원 고려 중 DWARF 포맷 – gcc, clang clang은 PDB도 지원 (mostly) C# (Unity) – 리플렉션 활용
47. C++ Expression Evaluator
48. 세 줄 요약 TSingleton<CInterfaceMgr>::GetInstance().GetPlayer ()->GetCurrentRegion() read(((call(va_to_a(17022576),9,take_addr(at(read(ca ll(va_to_a(13211376),9,take_addr(at( read(call(va_to_a(194568),8), 'p')))), 'p')))))), 'i', 4) C++ 코드를 어느 클라이언트에서든 실행할 수 있게 루아 코드로 변환 및 실행 * 이 발표에서 공개된 모든 코드는 보안상 실제 코드와는 다르게 변형되었습니다.
49. 변환: C++ 표현식 파서 C++의 매우 작은 일부분만 지원 간단한 Recursive Descent Parser (링크) 필요한 기능 만큼만 직접 구현 * 파서를 만드는 방법에 대한 내용은 컴파일러 책 등을 참고 바랍니다. C++ 식 → Lua 트랜스파일러 변수 참조, 캐스팅, 함수 호출
50. 실행: 변환된 코드를 전송 후 실행 바로 계산이 이루어지는 게 아니라 접속된 클라이언트 내에서 실행 되어야함 실행할 내용을 동적 언어 코드(Lua)로 변환 후 전송
51. C++ Expression Evaluator 실행 파트
52. 값의 표현 at(주소) 또는 Lua 값 take_addr(at(100)) === 100 at(100):member(4) === at(104) read(100, ‘i’) === 100 read(at(100), ‘i’) === *(int*)100
53. API 확장 va_to_a Symbol 주소를 실제 메모리 주소로 변환 take_addr 주소값 얻기. & 연산자 구현에 필요 at.member 멤버에 해당하는 객체 리턴 read 메모리 읽기 reg 레지스터 값 읽기 get/set 계산에 필요한 값 불러오기/저장하기 read_mbcs, read_ucs2, ga_...,
54. va_to_a 심볼에 저장된 주소 = 해당 모듈이 메모리 주소 0에 로딩 되었을 때 기준 ASLR Address space layout randomization 해킹 방지 기법 중 하나 각 클라이언트는 다른 메모리 주소에 로딩될 수 있다 GetModuleHandle(nullptr) 실행중인 exe가 로딩된 주소
55. C++ Expression Evaluator 변환 파트
56. 변환 결과물: LuaValue `C++ expr` → Lua code + type `1` → 1, IntType A a `a` → at(addr of a), A a는 전역 변수, 로컬 변수, 함수(함수처리는 이후에!)
57. 재귀적 정의 X → code(X), type(X) B X::b `&X` → take_addr(code(X)), type(X)* `*X` → at(code(X)), type(X).pointing_type() `X->b` → at(read(code(X),’p’)):member( offset of b from type(X)), B at(at(…)) 꼴을 없애려고 read 사용 `X.b` → ?
58. C가 유저 타입이면 `(C)X` → code(X), C 이런 식으로 double을 int로 캐스팅 한다면 버그가 발생하지만, 작업량 대비 효과가 적어 차후 구현. 타입 변환
59. C++ Expression Evaluator 변환 – 함수 호출
60. 함수 호출 필요성 단순히 변수 읽는 것 만으로는 부족 프로그래머가 익숙한 방식으로 사용할 수 있게됨 예) 싱글턴 Singleton<A>::m_spInstance Singleton<A>::GetInstance() * 함수 스태틱으로 구현한 경우 함수 호출 없인 아예 접근 불가
61. 실행단에선 호출할 함수의 프로토타입을 모름 - 어떻게 인자를 넘기고 - 어떻게 결과값을 받고 - Calling convention은 뭔지 모두 알려줘야 한다. 함수 호출 (1)
62. `F(A,B)` → call( type(F).addr, FunctionOption:int, code(A), ArgAOption:int, code(B), ArgBOption:int, ), type(type(F)) 함수 호출 (2)
63. FunctionOption = [CC, size] Calling Convention(CC) cdecl / thiscall size = (length of Return value) 함수 호출 (3)
64. ArgOption = [size, reg] size = 1,2,4,8 -> size-1을 3비트로 인코딩 레지스터로 전달되는 경우만 표현 가능 reg = 22개 -> 5비트로 인코딩 ES, DS, FS, GS, CS, SS, AX, BX, CX, DX, BP, SP, SI, DI, R8, R9, R10, …, R15 함수 호출 (4)
65. TSingleton<CInterfaceMgr>::GetInstance() read(((call(va_to_a(199568),8))),'p') CInterfaceMgr& * 클래스 스태틱 함수이므로 인자가 없는 전역 함수. 해당 주소를 찾아 호출하면 싱글턴의 주소를 얻을 수 있다.
66. TSingleton<CInterfaceMgr>::GetInstance() read(((call(va_to_a(199568),8))),'p') CInterfaceMgr& TSingleton<CInterfaceMgr>::GetInstance().GetPlayer() (read(((call(va_to_a(13261376),9,take_addr(at(read(call(va _to_a(199568),8),'p')))))),'p')) CCharacter* * 앞에서 얻는 CInterfaceMgr&의 값이 this 인자로 전달됨
67. TSingleton<CInterfaceMgr>::GetInstance() read(((call(va_to_a(199568),8))),'p') CInterfaceMgr& TSingleton<CInterfaceMgr>::GetInstance().GetPlayer() (read(((call(va_to_a(13261376),9,take_addr(at(read(call(va _to_a(199568),8),'p')))))),'p')) CCharacter* TSingleton<CInterfaceMgr>::GetInstance().GetPlayer() ->GetCurrentRegion() read(((call(va_to_a(18022576),9,take_addr(at(read(call(va_ to_a(13261376),9,take_addr(at( read(call(va_to_a(199568),8),'p')))),'p')))))),'i',4) int
68. 구현 - 함수 주소 얻기 오버로딩 const 버전,인자가 다른 경우 PDB에 동일한 순서로 저장된다는 보장이 없음 타입 Hinting 지원 GetField#[int] GetField#[char*]
69. 구현 - x86/x64 x86 여러 calling convention 스택, 레지스터 각각으로 인자 전달 인라인 어셈블리로 신중히 구현 x64 calling convention이 단 하나 어셈 파일 따로 작성 (inline assembly 미지원)
70. 구현 - Calling convention (x86) cdecl caller clean-up thiscall ecx : this callee clean-up caller clean-up / callee clean-up
71. Out parameter (구현 중) bool GetPosition(vector3& pos) Lua 단에서 따로 메모리를 할당한 후 해당 포인터를 전달 계산 종료 후 관련 메모리 해제 local buf = buffer(12) call(`code for GetPosition`, buf) buf:member_at(4)
72. User defined type (미해결) 포인터, 레퍼런스로는 문제없이 활용 value type으로 사용시 어려움 발생 // COW 최적화 적용된 전용 스트링 클래스 String String GetName() 소멸자를 불러주지 않으면 좀비 스트링이 생기게 된다. 인자로 넣는 경우라면? (더욱 복잡)
73. 한계 Inline 되어 사라진 함수 최적화를 통해 생각보다 많은 함수가 inline됨 버전, 컴파일러 옵션에 따라 비일관적인 동작 LTCG와 WPO 켜면 VC버전에 따라 PDB와 실제 출력된 코드가 다른 경우가 있었음 (매우 가끔) 손상된 PDB 파일이 나온 경우도 있었음 (1번) 아마도 컴파일러 버그? ㅠㅠ
74. C++ EE 추가 활용
75. 활용 - 심볼 탐색 및 식 계산 PDB와 C++ EE를 쉽게 활용하기 위한 툴 작성 및 적용 ZEROConsole x.exe x.pdb 실행중인 프로세스에 연결 ZEROConsole x.dmp x.pdb 풀덤프에 연결 ZEROConsole - x.pdb PDB 정보만 확인
76. 활용: 풀덤프 탐색 (1) 라이브 게임 서버 덤프 던전 container에서 특정 던전을 찾아 처리 중 크래쉬 원소가 남아있다면 → 메모리 덮어쓰기 버그 원소가 없다면 → 멀티 쓰레드 버그
77. 활용: 풀덤프 탐색 (2) 전역 해시테이블에 특정 원소가 있는지 확인해야 하는 상황 원소 80만개 – 손으로 확인 불가 소스 분석 STLPort 기반 unordered_map 전체 데이터는 Singly Linked List로 표현됨
78. 활용: 풀덤프 탐색 (3) Full Dump는 모든 메모리 정보를 가짐 C++ EE를 구현함에 따라 Lua 코드로 덤프를 자유자재로 탐색할 수 있게 됨 루프 돌면서 해시테이블을 확인 디버깅 시나리오 확인에 도움
79. Function Hooking
80. 함수 후킹 임의의 함수의 동작을 런타임에 변경하는 기술 함수 인자에 조작을 가하거나 아예 함수 동작을 변경 Detour, MinHook, EasyHook, ...
81. 미리 정해진 함수로 대체하는 형태를 주로 지원 런타임에 임의의 함수를 후킹하여 Lua 코드를 실행해야함 함수별로 실행할 Lua code도 다르다 관련 정보를 알려줄 방법이 필요 기존 후킹 라이브러리의 한계
82. 프롤로그 함수 동적 생성 후킹으로 대체할 함수를 동적으로 생성 처리할 내용을 담은 포인터를 스택에 담아 같이 전달 함수 내용: push <address of HookInfo> jmp <HookHelper> * 아직 32비트만 구현
83. HookHelper 함수 진입 시의 인자 값 등을 확인 C++ EE 활용 사용하기 전에 필요한 레지스터 저장하고 시작 Return address를 미리 조작해두면 함수 종료시점도 알 수 있음
84. 훅을 제거할 때 이미 내부 처리 코드를 실행 중일 수 있음 공유한 정보가 포인터 하나 락 등으로 해결하기 어렵다 삭제시 먼저 훅을 해제한 후 관련 데이터 삭제에는 5초 딜레이 주는 것으로 우회 멀티스레드 이슈
85. 여러 후킹을 합치기 여러 기능에서 같은 함수를 동시에 후킹하고 싶음 후킹된 함수를 다시 후킹하는 건 낭비 서버에서 후킹을 사용하는 모든 기능의 코드를 모아서 보냄 실제 실행하는 시점에선 하나가 에러가 나더라도 다른 기능에 영향을 주지 않게 구현
86. 그 외
87. 유저 접속 시 랜덤 키 할당, 해당 키로 분배 수집 내용별 비율이 따로 존재 특정 유저에게만 수집 내용이 몰리지 않을까 하는 우려 합쳐진 키를 사용하여 분배하게 작성 (유저 키 + 수집 내용 키) 유저 샘플링
88. MySQL JSON column On-demand Data Extractor를 위한 비정형 데이터 저장에 사용 MySQL 5.7.8 이상 VIEW를 만들면 Readonly SQL 테이블 처럼 활용 가능 • * 다른 RDBMS들도 JSON 타입을 지원하나 • 사용성이 MySQL이 좋았음
89. 장애 대비 (1) 장애가 발생해선 안됨 개발팀 외부에서 만들어진 라이브러리 아무리 좋은 물건이더라도 크래쉬를 유발하거나 성능 문제를 일으키면 적용을 유보하거나 롤백할 수 밖에 없음
90. 장애 대비 (2) 수집 쓰로틀링 지나치게 많이 수집되면 자동으로 수집양을 줄인다 의도치 않은 경우를 대비 크래쉬 상황 대비 SEH로 감싸기 기능별로 lua_State를 나눠서 사용
91. 장애 대비 (3) 여전히 실수하면 문제가 발생할 여지는 있음 적용 대상을 적은 비율부터 점차적 확대 가능하게 개발자가 배포된 클라이언트로 ZERO 기능을 테스트 해 볼 수 있는 환경 제공
92. Future Works (1) Hot Patch (실행 중 코드 수정) 실시간 버그 수정 예: 비정상적인 인자가 들어올 때 return 해버리게 JYP와 연동되어 컨텐츠별 성능 정보 추적 및 활용
93. Future Works (2) Monitoring: Time-lapse view 장기간의 유저 흐름을 살펴보기 쌓인 데이터를 분석할 수 있는 툴 제공 사실상 무궁무진
94. Future Works (3) 특허 출원 진행 중
95. 감사합니다.


반응형