iOS에서의 SSL Pinning Bypass(with frida)

피닝 적용이 된 앱들이 좀 있다보니 모바일 앱 분석에선 언피닝이 거의 필수 코스가 되어가고 있습니다. 보통 Frida 코드(짜거나 codeshare에서 가져다 쓰거나)로 우회합니다.

#> frida --codeshare dki/ios10-ssl-bypass -U “앱이름"

안드로이드는 피닝을 위해 CertificateFactory로 만들어지는 인증서 저장소에 burp 인증서 같이 분석용 인증서를 생성해 넣거나 시스템 인증서 저장 공간을 바라보도록 하는데요. iOS는 어떤 방식으로 우회해서 사용하는지 정리해둘까 합니다.

How to use?

여러 방법이 있겠지만, 솔직히 프리다가 가장 편합니다. 특이사항이 없는 경우 codeshare에서 끌어쓰는게 가장 간편합니다. (코드 작성해서 사용해도, codeshare쪽 보면서 작성하시는게 헷갈리지 않고 도움됩니다)

갓리다!

iOS는 버전별로 우회방법이 좀 다릅니다. 그리고 작성한 방법 이외에도 충분히 많은 방법들이 있을겁니다.

iOS 11 & iOS 10

iOS 10, 11 버전에선 TLS 쪽 코드 중 tls_helper_craete_peer_trust 부분이 실제 검증되는 부분이고 frida를 통해 후킹하여 이 부분의 내용을 재정의하거나 변조하면 우회할 수 있습니다.

Kill Switch(https://github.com/nabla-c0d3/ssl-kill-switch2/blob/master/SSLKillSwitch/SSLKillSwitch.m) 쪽 코드를 보면 이렇습니다.

NSProcessInfo *processInfo = [NSProcessInfo processInfo];
if ([processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] && [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){11, 0, 0}])
{
// Support for iOS 11
void* handle = dlopen("/usr/lib/libnetwork.dylib", RTLD_NOW);
void *tls_helper_create_peer_trust = dlsym(handle, "nw_tls_create_peer_trust");
if (tls_helper_create_peer_trust)
{
MSHookFunction((void *) tls_helper_create_peer_trust, (void *) replaced_tls_helper_create_peer_trust, (void **) &original_tls_helper_create_peer_trust);
}
}

else if ([processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] && [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10, 0, 0}])
{
// Support for iOS 10
void *tls_helper_create_peer_trust = dlsym(RTLD_DEFAULT, "tls_helper_create_peer_trust");
MSHookFunction((void *) tls_helper_create_peer_trust, (void *) replaced_tls_helper_create_peer_trust, (void **) &original_tls_helper_create_peer_trust);

10, 11 모두 각각 tls_helper_craete_peer_trust, nw_tls_helper_craete_peer_trust 라이브러리에 인증서 검증 로직이 존재하고 이 부분을 패스하면 피닝 우회가 가능합니다. (진짜 인증서인지 검증 못하니깐)

좀 더 자세히 보면 app developers에는 이렇게 정의되어 있습니다.

OSStatus tls_helper_create_peer_trust(tls_handshake_t hdsk, bool server, SecTrustRef *trustRef);

(https://opensource.apple.com/source/coreTLS/coreTLS-121.1.1/coretls_cfhelpers/tls_helpers.h.auto.html)

tls_handshake_t, bool, SecTrustRef(req 검색, 인증서 체인 검증을 위해..)의 값들이 들어가고 OSStatus 형의 return을 가집니다. OSSStatus는 Apple에서 사용하는 상태 정보에 대한 자료형(https://developer.apple.com/documentation/kernel/osstatus?language=objc)입니다.

그럼 다시 이야기하면 tls_helper_craete_peer_trust 를 읽어서 frida의 replace로 코드를 변경하여 원래 코드를 안거치고 바로 return을 넘겨줍니다. (물론 return 포맷은 맞춰야합니다. OSStatus)

return errSecSuccess; 
// var errSecSuccess: OSStatus { get }

그렇게되면 인증서 검증 로직을 거치지 않기 때문에 우회가 가능합니다. 이렇게 보면 안드로이드의 CertificateFactory와 비슷하긴 하네요.

Frida 코드로 바꾸면 이렇습니다.

function gggo() {
    // ios 10, 11에 따라 tls_helper_create_peer_trust, nw_tls_helper_create_peer_trust 를 읽어오고.. 
    // 훅 이후 replace로 무조건 errSecSuccess를 리턴하도록 바꿔줍니다. 
   
    Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) {
        return errSecSuccess;
    }, 'int', ['pointer', 'bool', 'pointer']));
    console.log("SSL certificate validation bypass active");
}

iOS 9

iOS 9버전 이하는.. 조금 더 많은 변경 사항이 필요한데요. 3가지 함수를 동일한 방법으로 바꿔줍니다.

SSLHandshake SSLSetSessionOption SSLCreateContext

아래 문서 읽어보시면 좋습니다. https://nabla-c0d3.github.io/blog/2013/08/20/ios-ssl-kill-switch-v0-dot-5-released/

#pragma mark SecureTransport hooks - iOS 9 and below
// Explanation here: https://nabla-c0d3.github.io/blog/2013/08/20/ios-ssl-kill-switch-v0-dot-5-released/
static OSStatus (*original_SSLSetSessionOption)(SSLContextRef context,
SSLSessionOption option,
Boolean value);
static OSStatus replaced_SSLSetSessionOption(SSLContextRef context,
SSLSessionOption option,
Boolean value)
{
// Remove the ability to modify the value of the kSSLSessionOptionBreakOnServerAuth option
if (option == kSSLSessionOptionBreakOnServerAuth)
{
return noErr;
}
return original_SSLSetSessionOption(context, option, value);
}

static SSLContextRef (*original_SSLCreateContext)(CFAllocatorRef alloc,
SSLProtocolSide protocolSide,
SSLConnectionType connectionType);

static SSLContextRef replaced_SSLCreateContext(CFAllocatorRef alloc,
SSLProtocolSide protocolSide,
SSLConnectionType connectionType)
{
SSLContextRef sslContext = original_SSLCreateContext(alloc, protocolSide, connectionType);
// Immediately set the kSSLSessionOptionBreakOnServerAuth option in order to disable cert validation
original_SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
return sslContext;
}

static OSStatus (*original_SSLHandshake)(SSLContextRef context);
static OSStatus replaced_SSLHandshake(SSLContextRef context)
{

OSStatus result = original_SSLHandshake(context);
// Hijack the flow when breaking on server authentication

if (result == errSSLServerAuthCompleted)
{
// Do not check the cert and call SSLHandshake() again
return original_SSLHandshake(context);
}
return result;
}