랭킹에 이상한 점수가 올라왔다. 최대로 낼 수 있는 점수의 몇 배였다.
Table of contents
Open Table of contents
앱이 계산해서 보내고 있었다
스테이지가 끝나면 점수를 HTTP 의 GET 쿼리에 담아 libcurl 로 보내고 있었다.
"%s/api/score?uid=%s&stage=%d&score=%d"
통신이 HTTPS 도 아니었다. 이 주소를 브라우저에 그대로 치면 아무 점수나 올릴 수 있다. 앱을 안 켜도 된다. 앱이 계산한 값을 서버가 그대로 받으면 그 값을 신뢰하는 것이다.
완전히 막을 수는 없었다
점수를 서버가 계산하려면 그 재료를 서버가 알아야 한다. 어떤 적을 몇 마리 잡았는지와 걸린 시간과 사용한 아이템과 콤보 횟수다.
이걸 다 보내면 요청이 커진다. 그 값들도 앱이 만드는 것이라 똑같이 조작할 수 있다. 완전히 막을 수 없다는 것이 먼저 정리됐다. 목표를 막는 것이 아니라 쉽게 못 하게 하고 이상한 것을 걸러내는 것으로 잡았다.
여러 겹을 뒀다
가장 간단한 것부터 했다. 서버가 스테이지별 최대 점수를 알고 있으니 그것을 넘으면 거부한다. 밸런스 표가 이미 서버에 있어서 계산할 수 있었고 이걸로 명백한 것들이 걸렸다.
시간도 봤다. 스테이지 시작을 서버에 알리고 끝날 때 그 사이 시간이 최소치보다 짧으면 거부한다. 시작을 안 보내고 끝만 보내는 것도 거부한다.
값에 HMAC-SHA1 서명을 붙이고 POST 로 바꿨다. 서버가 같은 방식으로 계산해 비교하니 비밀 값을 모르면 못 만든다. 다만 그 값이 .so 안에 있으니 strings 만 돌려도 후보가 나온다. 이것도 완전하지 않다. 각각은 뚫려도 다 뚫기는 번거롭게 만드는 것이 목표였다.
거부한 것의 기록과 문구
거부한 요청을 전부 JSON 한 줄로 남겼다. 같은 uid 가 반복해서 걸리면 그 계정을 본다.
[거부] uid=*** stage=5 score=999999 (최대 42000)
[거부] uid=*** stage=3 서명 불일치
[거부] uid=*** stage=7 시작 기록 없음
기록이 없으면 얼마나 시도되는지도 모른다. 넣고 나서 하루 수십 건이 걸린다는 것을 알았다.
사용자에게 보이는 문구는 정상 오류와 같게 했다. 조작이 감지됐다고 알리면 어떤 방법이 걸리는지를 알려 주는 셈이다.
정리
- 앱이 계산한 값을 서버가 그대로 받으면 그 값을 신뢰하는 것이다
GET쿼리에 값이 들어가면 앱 없이 브라우저로도 부를 수 있다- 완전히 막을 수는 없으므로 목표를 걸러내는 것으로 잡는다
- 상한을 거는 것이 가장 간단하고 효과가 크다
- 시작과 끝을 짝으로 보면 시간 조건을 확인할 수 있다
HMAC-SHA1서명을 붙이면 손으로 요청을 만들기 어려워진다- 비밀 값이
.so에 있으면strings로 후보가 나온다 - 실패 문구는 정상 오류와 같게 해서 무엇이 걸렸는지 안 알린다