[ 개발 기록용으로 남기는 글입니다 ]
iOS 앱 개발을 하다보면 디버깅이나 개발에 도움이 되는 툴들을 찾게 된다. 실제로 생산성도 엄청나게 늘어나고..
나같은 경우 여러가지 프로그램들을 다 사용해봤는데 너무 신기해보이는 툴이 있어서 한번 비슷하게 만들어보려 한다
Sherlock이라는 툴인데.. 아무런 설정도 필요없이 시뮬레이터에 설치된 앱들을 분석하고 실시간으로 수정할 수 있는 프로그램이다. 원리가 너무 궁금해서 자료들을 다 뒤져보면서 언젠가 한번 직접 만들어봐야겠다라는 생각을 가졌었고 직접 만들어보기로 했다!(궁금한 건 못참는 사람)
일단 Sherlock을 키게 되면 Simulator가 재부팅되면서 실시간으로 수정할 수 있게되는데 어떤 원리인지는 파악해야하므로 현재 앱에 load된 dylib들을 살펴보았다.
살펴보니 bundlePath에 io.inspiredcode.Sherlock을 갖고 있는 Watson이라는 dylib가 로드되어있었다!
추측으로는 Watson이라는 라이브러리가 macOS에 설치된 앱과 통신을 하는 것 같은데… bundle이 저장된 곳을 열어봤다.
총 3가지 파일이 저장되어 있는데 Watson말고 나머지는 뭐하는 얘들인지 잘 모르겠다… 일단 Watson을 어떻게 로드하는건지 알아봐야 할 것 같아서 시뮬레이터 관련 자료를 전부 뒤져보았다.
일단 dylib 파일을 로드하는 방법은 아래와 같이 있었다.
일단 첫번째는 내가 개발한 앱에 대해서만 dlopen 함수를 쓸 수 있을테니… 이건 패스!
두번째는 왠지 macOS/iOS 보안 전문가들이 쓸법한데… 셜록이 이런 방식으로 구현했을것 같진 않았다. (돈 받고 파는 프로그램인데 설마 언제든지 막힐 수 있는 이 방법을 사용했을까?)
세번째 방법은 구글링을 통해 겨우겨우 찾았던 방법이다. WWDC 2019에서 공개된 simctl 명령어를 통해 특정 프로세스에 본인이 만든 dylib를 주입할 수 있다.
블로그 글을 따라해보면 dylib를 생성해서 SpringBoard 프로세스에 내가 만든 dylib를 주입할 수 있는데… 근데 이 방법으로는 모든 프로세스에는 주입할 수 없었다!
그래서 다른 방법을 찾아야해서 탈옥 관련 글 iOS 보완 관련 글들을 전부 뒤적거렸다.
뒤적거리다가 어느 보안 글에서 RunningBoard가 언급되었는데 ‘앱을 직접 실행하려면 RunningBoard를 거쳐야한다는 글이였다. 보자마자 바로 요놈이다!!! 싶어서 iOS Framework Runtime Header를 찾아봤다.
iOS에서 모든 프로세스를 실행할 때 해당 메소드를 거치게 된다. 해당 메소드를 Swizzling하면 해당 프로세스에 내가 원하는 dylib도 주입할 수 있지 않을까?? 싶어서 본격적으로 삽을 들었다.
일단 RBProcessManager에 있는 메소드를 아래의 코드로 스위즐링할 수 있다. (스위즐링에 관해서는 다음에 블로그로 한번 설명하는 글을 작성해보는걸로…)
#pragma mark - RBProcessManager
@interface RBProcessManager (Swizzle)
@end
@implementation RBProcessManager (Swizzle)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(executeLaunchRequest:withError:);
SEL swizzledSelector = @selector(alt_executeLaunchRequest:withError:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
그리고 아래와 같이 내가 낚아챌 메소드를 구현한다.
- (id)alt_executeLaunchRequest:(id)arg1 withError:(NSError **)error {
RBSLaunchRequest *launchRequest;
if ([arg1 isKindOfClass:[RBSLaunchRequest class]]) {
launchRequest = arg1;
NSLog(@"Magician: launchRequest: %@", launchRequest);
}
id result = [self alt_executeLaunchRequest:arg1 withError:error];
return result;
}
실제로 arg1이 어떤 객체로 들어올 지 몰라서 isKindOfClass로 RBSLaunchRequest 클래스가 맞는지 확인하고 출력하도록 해두었다.
이제 해당 dylib을 SpringBoard에 로드시켜보면…
위와 같이 모든 프로세스가 실행될때마다 로그가 찍히는 걸 확인할 수 있다!
일단 아무 앱을 실행시키지 않아도 xpcservice 라는 프로세스들이 전부 다 찍히는 걸 볼 수 있는데 앱만 찍히도록 해야한다. 이 역시 Runtime Header를 찾아보면 체크할 수 있는 방법이 있다.
이건 RunningBoard가 아닌 RunningBoardServices에 정의되어 있는데 해당 BOOL 값으로 application인지 아닌지 확인할 수 있다. 해당 값이 true인 경우에만 작동할 수 있게 분기문을 추가해주자.
근데 아직 정작 중요한 내가 원하는 dylib를 주입할 수 있는 함수를 찾지못했다… RBSLaunchRequest의 값들을 더 살펴보니 RBSLaunchContext라는 객체가 있고 additionalEnvironment라는 Dictionary를 갖고 있었다.
Apple 기본 앱에서도 FLEX가 나타난다! (dylib가 제대로 로드되었다는 뜻)
기본적인 로직은 구현된 것 같으니 다음은 macOS와 iOS에서 로드된 dylib 파일을 소켓 통신 시킬 예정이다.