소곤소곤 ad

2014년 2월 18일 화요일

구글플레이 안드로이드 인앱결제 보안 이야기 #1 (프리덤을 막자)

(1) public key를 숨겨라

※ 서버검증할거라면 클라이언트(앱)에서 public_key는 필요가 없다.  => 자세한내용보기


/* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY
 * (that you got from the Google Play developer console). This is not your
 * developer public key, it's the *app-specific* public key.
 *
 * Instead of just storing the entire literal string here embedded in the
 * program,  construct the key at runtime from pieces or
 * use bit manipulation (for example, XOR with some other string) to hide
 * the actual key.  The key itself is not secret information, but we don't
 * want to make it easy for an attacker to replace the public key with one
 * of their own and then fake messages from the server.
 */
String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";

인앱기능을 구현하기위해 샘플 코드들을 가져다 놓고서 열어보면 이런 부분이 있다.

위의 /* 코멘트 */ 부분은 눈에 잘 들어오지 않고 (영어라서!) "Your key PLACE_IT_HERE"라는 문구가 보인다. 얼른 복사해서 붙여넣고 넘어가려는게 일단 문제다.

그냥 붙여넣지 말자. Construct를 해서 넣어야 한다. 한번쯤은 꼬아 놓자.

String piece1 = "SDFGJKGB4UIH234WE/FRT23RS#trashdata#DF/3DFUISDFVWE";
String piece2 = "SDFGJKGB4UIHUI#trashdata#SDFVWE";
String piece3 = "BDY#trashdata#ASGBDNAWGRET24IYE23das4saGBENWKD";
String piece4 = "#trashdata#432423SDF23R/+SDDS";

mHelper = new IabHelper(this, piece1.substring(...) + piece2.substring(...) + piece3.substring(...) + piece4.substring(...));
이런식으로만 해 두어도 난이도가 올라간다. 크래커입장에서는 이 키를 자기자신의 키로 변경하고자 시도할텐데, 일반 문자열로 들어가 있으면 식은죽 먹기일테지.

권장하는 방법은 실시간으로 서버로부터 이 키를 암호화된채로 받아서 복호화해서 IabHelper를 초기화하는 것.


(2) 결제과정에서 구글지갑을 꼭 체크하자.


유저가 결제한 직후에,  launchPurchaseFlow -> QueryInventory -> Consume -> onFinishedConsume 의 절차를 거치도록 하자.

*) 결제후에, 인벤토리에 그 아이템이 있는지 검사하고, 컨슘(사용)한다음에 최종적으로 OK가 나왔을때에 서버측에 전달하여 유료아이템 구매를 처리하는 식.

구글에서 제공해주는 샘플소스를 잘 살펴보자.


(3) 구매완료후 영수증을 확인한다. (Clinet Side) 

※ 영수증을 서버에서 검증한다면 굳이 검증할 필요는 없다.



public void onConsumeFinished(Purchase purchase, IabResult result) {
    if (result.isSuccess()) {
       if(purchase != null){
           // provision the in-app purchase to the user
           verifyBilling(purchase.getSignature(), purchase.getOriginalJson());
        } else {
           LogToServer("onConsumeFinished:purchase=null");
        }
   } else {
        LogToServer("onConsumeFinished:consume=failed");
   }
}

your_publickey, json_data, signature로 검증을 할 수 있다.  (Security.Java에 잘 구현되어 있다)


(4) 서버측에서 검증한다 (Server Side)


영수증데이터를 서버로 보내 서버에서 검증하는 방법이다.

인터넷을 뒤져서 c++로 구현된 서버측 검증소스를 하나 찾아냈는데, 잘 동작하지 않는다. 게다가 이렇게 뭐가뭔지 모를 소스코드는 기겁하게 만든다.

int InappBillingVerify(const char* data, const char* signature, const char* pub_key_id)
{
    std::shared_ptr<EVP_MD_CTX> mdctx =
    std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_destroy);
    const EVP_MD* md = EVP_get_digestbyname("SHA1");

    EVP_VerifyInit_ex(mdctx.get(), md, NULL);

    EVP_VerifyUpdate(mdctx.get(), (void*)data, strlen(data));

    std::shared_ptr<BIO> b64 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b64.get(),BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(),pub_key_id);
    BIO_push(b64.get(), bPubKey.get());
    std::shared_ptr<EVP_PKEY> pubkey = 
             std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b64.get(), NULL), EVP_PKEY_free);
    std::string decoded_signature = Base64Decode(std::string(signature));

    return EVP_VerifyFinal(mdctx.get(), (unsigned char*)decoded_signature.c_str(), 

             decoded_signature.length(), pubkey.get());
}

출처 : http://kukuta.tistory.com/166

이 분의 소스를 가지고 구현해보았지만 에러가 나면서 실패. 소스코드를 보고 있으면 머리가 지끈지끈하다.

20년을 프로그래밍을 해왔는데, 이 소스는 대체 뭐란 말이냐.... 이유없이 Segmentation Fault 가 나오거나 에러코드 -1이 떨어지는데, 왜 안되는지 이유조차 알 수가 없고, 직접좀 만들어볼까 하니 외계인이 만들어 놓은것 같은 openssl 함수들과 구조체들을 보면 게보린 한통은 필요해 보인다.

결국 포기 T.T

불행한 것은 전 세계를 다 뒤졌음에도, 아직 잘 동작하는 c++소스를 못 찾았았다는 것. 결국 임시방편으로 내가 쓴 방법은 이렇다.

1) Security.Java의 verify를 호출하여 검증하는 Java프로그램을 만든후에, 실행가능한 jar로 만든다.
2) 게임서버에서 java를 호출하여 실행시킨후에 출력값을 얻어온다. (리눅스 pipe를 이용한다)

*. 이 과정에서 jar가 정상 실행되지 않거나 ClassNotFound등의 오류에 시달려야 했는데, 패키지이름을 부여하지 않아서라든지 jdk 버전이 이클립스개발용과 서버용이 달라서 생기는 문제들.


(4-1) 구글 개발자 API를 이용.

구글은 "판매자의 지갑"을 들여다 볼 수 있는 API를 제공한다.  하지만 구현이 만만치 않다. 게다가 c++ 버전의 샘플이나 라이브러리는 제공하지도 않는다.

느리고, 하루 20만회 호출로 제한되어 있으며, 가끔 응답없음이 나오기도 한다그래서 일단 패쓰.


(*) 프리덤!

구글 인앱결제를 우회하게 해주는 프리덤이라는 앱이 있는데,  (프리덤, freedom, ㅍㄹㄷ 으로 검색) 가짜 카드로 무한 결제가 가능한 무시무시한 앱이다.
4단계의 서버검증에서 걸러낼 수 있다. 그러니까, 인앱결제를 넣을거라면 서버검증은 필수라는 것.