payment

매출은 그대로인데 재화만 늘어난다면 — 인앱 결제 영수증 검증의 빈틈

"정산서랑 재화 지급 로그가 안 맞습니다" — 어느 월요일 아침의 대조 작업

월초 정산을 맞추던 운영 담당자가 이상한 점을 발견합니다. 스토어 콘솔에 찍힌 결제 건수보다 게임 서버가 지급한 유료 재화가 훨씬 많습니다. 차이는 몇 건이 아니라 수백 건입니다.

계정별로 뜯어보니 패턴이 보입니다. 어떤 계정은 같은 상품을 하루에 마흔 번 넘게 "구매"했는데 스토어 기록에는 한 건도 없습니다. 또 어떤 계정은 결제 직후 환불을 받았는데 재화는 그대로 남아 있습니다.

이 시점에서 흔히 나오는 반응이 "스토어 결제가 뚫린 건가?"입니다. 그럴 가능성은 낮습니다. Google과 Apple의 결제 처리 자체는 견고합니다. 공격자가 노리는 곳은 결제가 아니라, 결제가 끝났다고 게임이 판단하고 재화를 내주는 순간입니다.

결제 크랙은 결제가 아니라 지급을 노린다

인앱 결제는 다음과 같은 방식으로 진행됩니다.

  1. 사용자가 스토어에서 결제한다
  2. 스토어가 구매 증빙(영수증·토큰)을 클라이언트에 돌려준다
  3. 게임이 그 증빙을 확인하고 재화를 지급한다

1번과 2번은 스토어의 영역이라 손대기 어렵습니다. 문제는 3번입니다. 증빙을 누가, 어디서 확인하느냐에 따라 이 단계는 튼튼할 수도 있고 사실상 무방비일 수도 있습니다.

게임이 클라이언트 안에서만 증빙을 확인한다면, 확인하는 코드도 공격자의 손이 닿는 곳에 있습니다. 결제 우회 도구들이 하는 일이 정확히 이것입니다. 스토어를 속이는 게 아니라, 게임에게 "결제 끝났어"라고 말해 주는 것입니다.

변조가 들어오는 네 개의 문

현장에서 반복적으로 관측되는 유형은 넷입니다.

1. 클라이언트에서만 검증하는 경우

가장 흔하고 가장 크게 뚫립니다. 결제 응답을 가로채 성공으로 바꾸거나, 검증 함수의 반환값을 고정하면 그대로 통과합니다. 스토어에는 아무 기록도 남지 않습니다.

2. 영수증 재사용(replay)

한 번 정상적으로 산 영수증을 반복해서 제출하는 방식입니다. 서버가 검증은 하지만 이미 쓴 영수증인지 확인하지 않으면 같은 증빙으로 몇 번이든 재화를 받을 수 있습니다. 실제 정산과 지급량이 벌어지는 대표적인 원인입니다.

3. 환불 이후에도 남는 권한

결제하고, 재화를 받고, 환불합니다. 스토어에서는 돈이 돌아가지만 게임 안의 재화는 그대로입니다. 이 과정을 반복하면 사실상 무료로 재화를 찍어낼 수 있습니다.

4. 보류 상태를 성공으로 취급하는 경우

결제가 아직 확정되지 않은 상태가 있습니다. 이걸 성공으로 처리하면 결제가 끝내 완료되지 않아도 재화는 이미 나간 뒤입니다.

넷 다 공통점이 있습니다. 결제를 깨뜨린 공격이 하나도 없습니다. 전부 게임 쪽 검증 절차의 빈틈을 이용했을 뿐입니다.

검증 라이프사이클 — 지급 전에 끝나야 할 일들

"영수증 검증"을 한 번의 확인으로 생각하면 위의 2번과 3번을 놓칩니다. 검증은 구매 시점의 이벤트가 아니라 구매 이후까지 이어지는 절차입니다.

단계 하는 일 빠뜨리면
전달 클라이언트가 받은 증빙을 서버로 넘긴다 클라이언트 검증만 남아 1번에 노출
검증 서버가 스토어 API로 증빙의 진위를 확인한다 위조 증빙이 통과
중복 확인 이미 처리한 증빙인지 대조한다 재사용(2번)에 노출
지급 확정 상태일 때만 재화를 넣는다 보류 상태 지급(4번)
승인 스토어에 처리 완료를 알린다 자동 환불로 매출 손실
사후 환불·취소를 받아 권한을 회수한다 환불 악용(3번)에 노출

여섯 단계 모두 서버에서 일어나야 합니다. 클라이언트는 증빙을 전달하는 역할까지입니다.

Google Play: 서버에서 확인해야 할 것들

Google은 Play 결제 보안 가이드에서 백엔드 검증을 명시적으로 권장합니다. 문서가 짚는 핵심은 다음과 같습니다.

  • 클라이언트가 받은 purchaseToken을 서버로 보내고, 모든 토큰 값을 서버에 기록한다.
  • 이 토큰은 전역적으로 유일해서 기본 키로 써도 안전하다. 이전에 받은 적 있는 토큰인지 먼저 대조한다.
  • Google Play Developer API의 Purchases.products:get, Purchases.subscriptionsv2:get으로 실제 구매를 확인한다.
  • obfuscatedAccountId 또는 obfuscatedProfileId로 계정 단위의 자격과 남용 여부를 함께 본다.
  • 구매 상태가 확정(PURCHASED)일 때만 지급한다. 보류 상태에는 지급하지 않는다.
  • 유효하지 않은 구매는 Orders:refund를 revoke=true로 호출해 명시적으로 회수한다.

여기서 놓치기 쉬운 운영 함정이 하나 있습니다. 구매를 승인(acknowledge)하지 않은 채 3일이 지나면 자동으로 환불됩니다. 검증 로직은 멀쩡한데 승인 호출만 빠뜨려 매출이 새는 경우가 실제로 있습니다.

App Store: 오래된 방식을 그대로 쓰고 있지는 않은지

Apple 쪽에서 확인할 것은 조금 다릅니다. 검증 방식 자체가 바뀌었습니다.

예전에는 영수증 데이터를 Apple 서버의 verifyReceipt 종단점에 보내 확인했습니다. 이 방식은 레거시가 되었고, 현재는 App Store Server API로 거래 정보를 조회하는 방식이 표준입니다. 이 API는 거래 정보를 JWS로 서명된 형태로 돌려주므로, 서버가 Apple의 공개 인증서로 서명을 검증할 수 있습니다.

오래된 블로그 글이나 몇 년 전 샘플 코드를 그대로 옮겨 쓴 프로젝트라면, 지금 어떤 방식으로 검증하고 있는지 한 번 확인해 볼 만합니다. 구체적인 절차와 최신 상태는 반드시 공식 문서에서 확인하세요. 결제 관련 API는 이 글보다 빠르게 바뀝니다.

지급 전에 점검할 항목

해당하지 않는 항목이 있다면 그곳이 지금 열려 있는 문입니다.

검증 절차

  • 영수증·토큰 검증을 서버에서 수행한다.
  • 스토어 API로 실제 구매 여부를 확인한다.
  • 처리한 토큰을 저장하고, 지급 전에 중복 여부를 대조한다.
  • 구매 상태가 확정일 때만 지급하고, 보류 상태는 지급하지 않는다.

구매 이후

  • 지급 후 스토어에 승인 처리를 보낸다.
  • 환불·취소 알림을 받아 재화나 권한을 회수한다.
  • 유효하지 않은 구매를 발견하면 명시적으로 회수한다.

계정과 운영

  • 구매를 계정에 묶어 기록하고, 같은 계정의 비정상적인 반복 구매를 확인할 수 있다.
  • 정산 금액과 지급량을 주기적으로 대조한다.
  • 결제 요청을 보낸 클라이언트가 변조되지 않았는지 함께 판단한다.

OZero Security가 맡는 영역과 맡지 않는 영역

이 주제에서는 저희가 하지 않는 일부터 말씀드리는 게 맞겠습니다.

맡지 않는 영역

  • 영수증과 구매 토큰의 검증을 대신하지 않습니다. 스토어 API를 호출해 구매의 진위와 상태를 확인하는 일은 여러분의 서버가 해야 합니다. 저희가 그 자리를 대신할 수 없고, 대신한다고 주장하지도 않습니다.
  • 중복 처리 방지, 환불 회수, 정산 대조 같은 절차도 마찬가지로 서버와 운영의 몫입니다.

맡는 영역

  • 검증을 요청하는 클라이언트 자체가 신뢰할 만한지를 판단합니다. 실행 파일 바이너리 변조, 디버거 부착과 후킹, 에뮬레이터·루팅 같은 비신뢰 실행 환경을 식별해 우회 난이도를 높입니다.
  • MOD APK 리패키징과 재서명 변조를 감지하고, 프로젝트 정책에 따라 대응합니다(로그·콜백·종료). 결제 로직을 바꿔 재배포한 빌드가 여기에 해당합니다.
  • Pro Add-on 연동 시 의심 클라이언트의 텔레메트리를 수집해 백오피스에서 운영 정책에 맞게 대응할 수 있도록 지원합니다.

두 영역은 순서가 있습니다. 서버 검증이 먼저입니다. 서버가 증빙을 제대로 확인하지 않으면 클라이언트를 아무리 단단히 해도 소용이 없습니다. 반대로 서버 검증을 갖춘 뒤에는, 그 요청을 보내는 클라이언트가 변조된 빌드인지 아닌지가 다음 문제가 됩니다.

결론: 결제를 지키는 게 아니라 지급을 지키는 일입니다

인앱 결제 보안을 "결제가 뚫리지 않게 하는 일"로 생각하면 방향이 어긋납니다. 스토어의 결제는 이미 튼튼합니다. 지켜야 할 것은 결제가 끝났다는 주장을 게임이 어떻게 확인하느냐입니다.

정산과 지급량이 벌어지고 있다면, 대개 원인은 위 여섯 단계 중 하나가 비어 있는 것입니다. 특히 중복 확인과 환불 회수는 처음 만들 때 빠뜨리기 쉬우면서 피해가 조용히 누적되는 항목입니다.

다음 단계

  1. 보호 범위 확인하기 — 플랫폼별 지원 사양과 플랜(Standard / Pro)별 방어 범위를 제품 페이지에서 확인하세요.
  2. 빠른 통합 시작하기 — Package Manager의 My Assets(Asset Store 구매분) 또는 .unitypackage(포털 다운로드분)로 기존 프로젝트에 연동할 수 있습니다.
  3. 기술 문의 및 상담 — 결제 관련 변조 사례나 라이브 운영 이슈가 있다면 문의를 남겨 주세요.

서버 검증부터 점검하시고, 그다음에 그 요청을 보내는 클라이언트를 보시면 됩니다.

검증을 요청하는 클라이언트부터 신뢰할 수 있게 — OZero Security의 Native 무결성 검증.

함께 읽으면 좋은 글