Skip to content
isdnetworks
Go back

검증이 앱 쪽에 있었다

조건을 채우면 보상을 주는 기능에서 검증이 Android 앱 쪽에 있었다.

if (조건_충족()) {
    서버에_보상_요청(userId, rewardId);
}

앱이 판단하고 서버는 준다.

Table of contents

Open Table of contents

증상 — 서버는 조건을 안 봤다

Servlet 쪽을 열어 보니 조건을 확인하는 부분이 아예 없었다. userIdrewardId 를 받아 지급으로 바로 갔다.

무엇을 보내는지부터 봤다. 에뮬레이터의 프록시를 Fiddler 로 잡으니 요청이 그대로 보였다.

POST /reward
userId=...&rewardId=...

그 요청을 curl 로 다시 보냈더니 보상이 들어갔다. 앱을 안 거치면 조건과 무관하게 받을 수 있다는 뜻이다.

서버가 모르는 정보였다

왜 앱에 뒀는지는 짐작이 됐다. 조건이 이런 것들이었다.

특정 화면을 봤나
일정 시간 머물렀나

앱 안에서만 아는 정보다. 서버가 모르는 것으로 판단하고 있으니 판단도 앱에서 할 수밖에 없었다.

그러면 서버가 알아야 한다고 보고 각 행동을 MySQL 에 기록하게 했다. Activity 에 들어가면 그때 서버로 보낸다.

그런데 그 기록도 앱이 보낸 것이다. 안 보고도 봤다고 보낼 수 있다. 같은 문제가 한 단계 뒤로 밀린 것뿐이었다.

요청에 서명을 붙이는 방법도 찾아봤다. 키와 시각을 합쳐 HMAC-SHA256 을 만들어 같이 보내고 서버가 다시 계산해 맞춰 보는 것이다.

그런데 그 키가 APK 안에 있다. apktool 로 풀고 dex2jar 를 거쳐 JD-GUI 로 열면 Java 코드가 거의 그대로 나온다. ProGuard 를 켜도 바뀌는 것은 클래스와 메서드 이름이고 문자열 상수는 그대로 보인다.

완전히는 못 막는다

앱 안의 행동은 서버가 직접 관측할 수 없고 서버에 남은 기록은 앱이 보낸 것이다. 완전히 막는 방법이 없다.

대신 하기 어렵게는 만들 수 있었다. 세 가지를 넣었다.

① 화면 A → 화면 B → 보상   순서가 맞아야 인정
② 화면 진입에서 보상 요청까지의 간격이 너무 짧으면 이상하게 봄
③ 같은 보상은 MySQL 의 UNIQUE 로 한 번만

서명도 넣었다. 뜯으면 뚫리지만 뜯지 않고는 못 만든다.

완전히 막는 것과 어렵게 만드는 것은 다른 목표다. 앞은 불가능하고 뒤는 가능하다. 어렵게 만들면 대부분은 안 하게 된다.

얼마나 들일지는 보상 크기로 정했다. 작은 보상에 확인을 여럿 붙이면 들이는 노력과 안 맞는다.

이상한 것을 찾는 쪽

막는 것과 별개로 이상한 것을 찾는 것도 넣었다.

SELECT user_id, COUNT(*) FROM reward_log
 WHERE created_at > ...
 GROUP BY user_id HAVING COUNT(*) > 임계;

GROUP BY user_id 로 지급 건수를 세면 평소보다 많이 받은 사람이 나온다. 찾기만 하고 자동으로 막지는 않았다.

얼마부터 이상한지가 애매해서 열심히 하는 사용자와 구분이 안 됐기 때문이다. 자동으로 막으면 정상 사용자가 걸릴 수 있다. 목록만 뽑고 사람이 보게 했다. 명확한 것은 조치하고 애매한 것은 둔다.

이게 되려면 로그가 있어야 한다. Tomcat 액세스 로그와 reward_log 가 둘 다 있어야 맞대어 볼 수 있다. 그래서 순서가 정해졌다.

① 남기기 — 무조건
② 어렵게 만들기 — 할 수 있는 만큼
③ 이상한 것 찾기 — 남긴 것으로

①이 먼저였다.

정리


Share this post on:

Previous Post
같은 것이 두 개 생겨 있었다
Next Post
서버가 늘 때마다 코드를 고쳤다