소곤소곤 ad

2014년 6월 22일 일요일

갤럭시 s4 s5 개발자옵션 설정하기

갤럭시S4의 개발자모드는 숨겨져 있다. 



디바이스 정보에서 "빌드 번호" 항목을 연속해서 7회에서 9회 정도를 연속으로 누르면 "개발자 모드가 실행 되었습니다" 라는 문구가 나오면서 활성화 된다. 상위로 가서 개발자 옵션이 활성화 되었는지 확인해 보자.







2014년 6월 17일 화요일

구글플레이 안드로이드 인앱결제 보안 이야기 #3 (public Key)

페이스북-스마트폰게임개발자그룹 에서 도움을 얻어 업데이트. 하나용님에게 감사드립니다.


서버검증할거라면 클라이언트(앱)에서 publicKey는 필요가 없다. 

위험

구글의 샘플코드에서 publicKey를 사용하는 부분들을 사용하지 않도록 수정해야 하는데, 코드상에는 두 군데가 있다. 

int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException 
{
    ....

    if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
         logDebug("Sku is owned: " + sku);
         Purchase purchase = new Purchase(itemType, purchaseData, signature);

         // Record ownership and token
         inv.addPurchase(purchase);
    }
    else {
         logWarn("Purchase signature verification **FAILED**. Not adding item.");
         logDebug("   Purchase data: " + purchaseData);
         logDebug("   Signature: " + signature);
         verificationFailed = true;
    }
    ....

아래 검증하는 부분도 단순하게 skip 한다. 
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
    ...

    Purchase purchase = null;
    try {
         purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
         String sku = purchase.getSku();

         // Verify signature
                
         // bisuit_jump
         /*
         if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
             logError("Purchase signature verification FAILED for sku " + sku);
             result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
             if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
             return true;
         }
         */
         logDebug("Purchase signature successfully verified.");
    ...

이렇게 하면, publicKey를 클라이언트에 두지 않으면서 영수증을 서버로 보내 전적으로 서버에 검증을 맡길 수 있다. 


2014년 5월 12일 월요일

안드로이드 저장공간이 부족합니다 메세지가 나올때...

앱업데이트시에 저장공간이 부족합니다.... 라고 나오는 경우

폰 저장공간이 충분히 여유가 있는 경우에도 이런 메세지가 나오는 경우 대부분은 다른앱들의 쓰레기가 내부저장공간을 차지하고 있어서인데, 사용자들에게 안쓰는앱/폴더/파일들좀 삭제하면 된다고 알려줘봐야 무슨말인지 모르는 경우가 많다.

차라리 우리앱이 다른 널찍한 곳을 찾아나서는게 나을수도 있는데, Manifest에서 ndroid:installLocation = auto 옵션을 이용하면 된다.

친절한 설명 : http://aroundck.tistory.com/139
스마트폰 저장소 정리만 잘해줘도 충분 : http://comterman.tistory.com/696

2014년 5월 6일 화요일

구글 인앱결제 크랙 / 프리덤 막기 / Simplest way to detect Freedom

Simplest way to detect Freedom

구글 인앱결제 크랙(프리덤등)을 막는 아주 간단한 방법 하나는 구매번호(Order ID) 의 형태를 확인해 보는것이다.

정상예 : 12999xxxxxxxx4705758.13175xxxxxxxx825
(내 경우는 길이가 37자리 = 숫자36개+쩜)
가짜예 : 4141975266280705695

이정도 포맷과 길이도 못 맞춰서 보내는 넘들이 무슨 해킹을 한다고...

Here is an easy way to verify IAP result. Just check the format of 'order id' within the result json string out.  Abnormal trial has abnormal format.

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단계의 서버검증에서 걸러낼 수 있다. 그러니까, 인앱결제를 넣을거라면 서버검증은 필수라는 것.


2014년 1월 27일 월요일

한글이 포함된 문자열의 실제 크기는 얼마일까?

한글이 포함된 문자열의 길이를 구해보자.

String s = "소곤소곤abcd";
System.out.println(s.length()); // 8

String.length()는 한글이건, 영문이건 상관없이 우리가 인지하는 글자 갯수를 돌려준다.

String s = "소곤소곤abcd";

byte[] buf = s.getBytes(); // using DefaultCharset

System.out.println(buf.length);  // 16
이 경우는, 메모리상에 위치할 실제 바이트수를 알아낼 수 있다.  잠깐! 한글은 2바이트니깐 결과값이 12일텐데 16이라고??? 12도 맞고 16도 맞다. 내 시스템의 eclipse는 UTF-8로 세팅되어 있고, 여기서 한글 한글자는 3바이트를 차지한다. 즉, 결과값은 이 코드가 동작하는 시스템의 기본 인코딩이 뭐냐에 달려있다.

문제는 실행파일(apk 혹은 jar)을 배포했는데, 그게 돌아가는 환경에 따라 이 값이 다르게 나온다는 것이다. 그래서, 시스템 비의존적인 실제 바이트수를 알아낼 필요가 있다.

이때 쓸 함수가 getBytes(String EncodingCharsetName)이다.
String s = "소곤소곤abcd";
System.out.println(s.getBytes("UTF-8").length);
하지만 그냥은 런타임에러가 난다. 엔코딩실패를 대비해서 try catch로 감싸주어야 한다. 함수로 만들어 보았다.
public int getRealSize(String text)
{
    int size = -1;
    try {
        size = text.getBytes("UTF-8").length;
    } catch (UnsupportedEncodingException e1){
    
    }

    return size;
}

이종시스템간 데이터교환 (특히 클라이언트-서버방식의 게임)에서는 항상 주의해야 한다.