소곤소곤 ad

2015년 7월 2일 목요일

프리덤 인앱결제 크랙 막기 (서버없이)

프리덤으로 인앱 결제를 하는 것을 막기위해, 일반적으로 서버를 두고 별도 검증을 하는 것이 필수적인데, 왜 그래야 할까?
인앱결제과정에서 가짜영수증과 가짜서명이 자체 검증(Secure.verify)을 문제없이 통과하게 되는데, 프리덤에서 이 부분을 무조건 true로 리턴하는 패치를 삽입하는 듯하다. 
java.security.Signature.verify()를 따라가다 보면, abstract protected로 설정된 engineVerify 함수를 만나게 되는데, 이 부분이 굉장히 의심스럽다(고 한다).
그래서, 이 부분을 직접 구현하는 시도가 있었고 아직까지는 성공적으로 동작한다(고 한다).

원문 (http://stackoverflow.com/questions/21966369/protecting-in-app-purchases-from-freedom-hack) 을 살펴보면,

The current implementation of freedom is that it will replace (redirect) all the method calls of java.security.Signature.verify(byte[]) to a freedom's jni method which in turn just simply always return true (or 1).
Take a look at java.security.Signature.verify(byte[]):
 public final boolean verify(byte[] signature) throws SignatureException {
        if (state != VERIFY) {
            throw new SignatureException("Signature object is not initialized properly");
        }
        return engineVerify(signature);
    }
Here the engineVerify method is an abstract protected method which is first defined in java.security.SignatureSpi(Signature extends SignatureSpi). OK, that enough, because I can't believe java.security.Signature.verify(byte[]) method anymore, I would use engineVerifymethod directly. To do that, we need to use reflection. Modify the verify method of IABUtil/Security from:
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
        Signature sig;
        try {
            sig = Signature.getInstance(SIGNATURE_ALGORITHM);
            sig.initVerify(publicKey);
            sig.update(signedData.getBytes());
            if (!sig.verify(Base64.decode(signature))) {
                Log.e(TAG, "Signature verification failed.");
                return false;
            }
            return true;
        } catch (...) {
            ...
        }
        return false;
    }
To:
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
        Signature sig;
        try {
            sig = Signature.getInstance(SIGNATURE_ALGORITHM);
            sig.initVerify(publicKey);
            sig.update(signedData.getBytes());
            Method verify = java.security.SignatureSpi.class.getDeclaredMethod("engineVerify", byte[].class);
            verify.setAccessible(true);
            Object returnValue = verify.invoke(sig, Base64.decode(signature));
            if (!(Boolean)returnValue) {
                Log.e(TAG, "Signature verification failed.");
                return false;
            }
            return true;
        } catch (...) {
            ...
        }
        return false;
    }
That is simple but it works with the current implementation of freedom until they update its algorithm in the future. (BTW, if you want to test if this works or not, this is my simple app).


즉, Security의 verify를 위 코드처럼 바꾸면, 프리덤이 바꿔치기한 (무조건 true를 리턴해주는) engineVerify를 무시하고 직접 검증할 수 있다. 
서버를 두지 않고, 인앱결제를 검증하는 경우에는 아주 간단하고 유용한 방법이다. 실제로 프리덤을 막아낼까? 이 코드를 작성한 사람이 직접적용한 앱을 가지고 테스트 해보자. https://play.google.com/store/apps/details?id=com.rainbowedu.toeicguru