Making Cocoa Plugins Using SIMBL
OSXDEV
- Beginner's Guide to Cocoa Reverse Engineering & SIMBL Plugin
목차 |
[편집] 들어가며
애플에서 제공하는 프로그램들에 사용자들이 플러그인을 만들어서 더 효율적으로 쓰는 일을 요즘 들어 흔하지 않게 볼 수 있다. 이런 플러그인을 로드하는데에 굉장히 유용하게 쓸 수 있는 환경이 SIMBL이다. 가장 흔한 예로 WebKit의 Nightly Build도 SIMBL로 Safari의 WebKit오브젝트를 수정하여 쓴다. 이 문서는 코코아 프로그램을 리버스 엔지니어링 하는 간단한 팁을 위주로, SIMBL을 사용하는 방법까지 소개한다. 리버스 엔지니어링은 그 이름만큼 복잡하여서 때에 따라 다른 만큼 자세한 과정을 설명하기 보다는 여러 방법을 설명하고, 시작하는 포인트를 잡는데에 초점을 맞추었다. SIMBL의 제작자인 Mike Solomon의 http://www.culater.net/wiki/moin.cgi/CocoaReverseEngineering의 내용을 읽고 직접 키노트용 링크백 플러그인을 만들어보면서 아주 초보자에겐 좀 헷갈리는 내용이 있고, 친절하지 못한점이 많아 그런 내용들을 정리했다. (사실 키노트용 링크백 플러그인은 King Chung Huang이라는 사람이 먼저 개발하는 바람에 중도에 포기하게 되었다) 이 문서의 많은 내용이 위 Mike Solomon의 위키페이지의 내용이므로 꼭 보길 바란다(특히 코드부분은 모두 Mike Solomon의 예제를 가져왔다). 실제로 개발을 할 생각이 없더라도 이 문서를 읽어보면 플러그인과 심볼에 대해서 자세히 이해할 수 있으므로 한번 읽어보는 것이 도움이 될 것이라고 생각된다.
[편집] 준비물
우선 이 작업을 시작하기 위해선 몇가지 준비물이 필요하다.
- SIMBL: http://www.culater.net/software/SIMBL/SIMBL.php
- 리버스 엔지니어링할 원본 프로그램
- class-dump: http://www.codethecode.com/Projects/class-dump/
- F-Script anywhere: http://www.fscript.org/ 에서 F-Script패키지를 다운받으면 그 안에 F-Script anywhere도 기본으로 들어가 있다.
- 추가적으로 SIMBL페이지에서 여러 오픈소스 플러그인을 다운받아서 스크립트를 살펴보는 것이 매우 도움이 될 것이다.
- Leopard 사용자는 F-Script anywhere, Leopard Edition: http://osiris.laya.com/blog/?p=24 을 추가로 다운받아 사용하시면 됩니다.
[편집] 시작하기
시작하기에 앞서 먼저 마음의 준비를 해야한다. 이것은 쉬운 과정이 아니며 꾸준히 코드를 보며 생각하고 고민해야 한다. 유적을 발굴하는 느낌의 느긋한 마음으로 프로그램 구조가 어떤지 호기심으로 살펴본다고 생각하면 정신적 스트레스가 좀 덜 할 것이다. 먼저 리버스 엔지니어링에 필요한 여러 도구들에 대해서 간단히 알아보자. 1~4번은 터미널에서 명령어를 입력하여 사용할 수 있다. 1~3번은 XCode가 깔려있으면 기본적으로 특별한 설치 없이 사용할 수 있다.
- nm은 Objective-C의 메세지들을 덤프해준다.
- strings는 프로그램에 있는 모든 문자열을 덤프해주며, 이것을 이용하여 가끔 Mac OS의 숨겨진 기능들을 찾아낼 수도 있다.
- gdb는 reverse engineering에서 빠져선 안되는 프로그램이다. 중간 중간 breakpoint를 잡아서 멈춰서 메모리에 있는 값들을 볼 수 있으며, 모든 커맨드의 한줄 한줄을 멈췄다가 실행시킬 수 있는 등 매우 강력한 도구이다. Reverse Engineering경험이 있는 사람들은 이 툴을 아주 능숙하게 이용할 수 있지만, 초보자들에게는 사용하기 쉽지 않은 프로그램이다.
- class-dump는 위에 준비물 섹션에 소개했던 대로 다운받아서 설치하면 된다. 어느 디렉토리에서나 class-dump 명령어로 실행시킬 수 있어야 편하므로 usr/bin 디렉토리에 이 파일을 집어넣어 놓는 것이 좋다. 이 도구는 프로그램의 헤더파일을 모두 출력해주므로 없어서는 안될 도구라고 할 수 있겠다.
- FScript/FScript Anywhere는 SIMBL 플러그인으로, 실행되고 있는 프로그램의 object구조를 살펴볼 수 있다. 이것은 위의 class-dump만큼 유용하고 강력하지만, iPhoto나 Mail 등 자동으로 데이터가 저장되는 경우에는 데이터 시스템을 망가뜨릴 수 있으므로 주의해서 써야한다. 이 도구도 필수적으로 한번은 쓰게 될 것이다.
[편집] 어디서 부터 시작해야될지 모르겠을 때
위에서 도구들을 소개했으니, 어떻게 해야할지 감이 잡힌 사람들은 바로 시작하면 된다. 하지만 필자같이 처음으로 Objective-C reverse engineering을 시작하는 사람들에게는 참 막막하다. 아주 자세한 소프트웨어 매뉴얼처럼, 보면서 따라할 수 있도록 시작하는 방법을 설명하도록 하겠다.
[편집] class-dump 설치
사실 말이 설치하기지 그냥 붙여넣기 하기만 하면 된다. 우선 위에서 소개한 대로 http://www.codethecode.com/Projects/class-dump/ 에서 class-dump를 다운받는다. DMG파일을 열어보면 Unix 실행파일인 class-dump가 뜬다. 새로운 Finder 창을 띄운후 하드디스크 디렉토리로 간다.
(그림 1) class-dump 설치하기
하드디스크 디렉토리에서 (그림 2)와 같이 이동-> 폴더로 이동을 클릭한다(또는 shift+command+G). 뜨는 시트에서 usr/bin을 입력한다. 그러면 숨겨져있던 폴더가 드러난다. 그 폴더 안에 위에서 다운받은 class-dump파일을 복사해 집어넣는다. 이제 모든 디렉토리에서 class-dump를 실행할 수 있다.
(그림 2) usr/bin으로 이동하기
[편집] 헤더 파일 만들기
헤더 파일을 만들어 내고 나면, 어떤 부분을 손 봐야 할지 감이 잡히게 된다. class-dump를 이용하면 아주 그럴듯한 헤더파일을 만들어낼 수 있다. 응용프로그램->유틸리티에 있는 터미널을 열자. 터미널에서 해당 프로그램의 디렉토리로 간다. 키노트 같은 경우 패키지 내용 보기를 하면 Contents->MacOS 폴더 안에 컴파일된 실행 파일이 있음을 알 수 있다. 터미널로 여기까지 찾아간다. 드디어 여기서 class-dump >> 원하는_파일이름.h 와 같은 방식으로 헤더파일을 생성한다. (그림 3)에서 잘 보면, 배경에 Keynote.h가 생겨남을 볼 수가 있다(Finder로 Keynote.app->Contents->MacOS를 보고 있다). XCode에서 이 파일을 열어보면 정말 놀랍게도 Keynote의 헤더파일을 볼 수 있다(그림 4). 이것만 이용해도 어떤 함수를 조작해야 하는지 감히 잡힐 것이다.
(그림 3) 배경에 Keynote.h가 생겨남을 볼 수 있다.
(그림 4) Keynote.h
[편집] FScript Anywhere 사용해보기
FScript Anywhere를 이용해보면 굉장히 유용하다는 것을 느낄 수 있다. 실제로 실행 될 때에 어떤 메세지가 어떤 역할을 하는지 알 수 있고 오브젝트들의 상관관계를 파악할 수 있다. FScript anywhere를 실행시키면 실행되고 있는 프로그램들의 목록이 나오며 오른쪽에 체크박스가 있음을 알 수 있다. 그 중 해당 프로그램의 체크박스를 (그림 5)와 같이 체크한다.
(그림 5) FScript Anywhere 설치
체크를 하고 Install을 클릭하면 해당 프로그램에 FSA라는 메뉴가 생김을 알 수 있다. 밑의 그림과 같이 FSA 메뉴에서 Browser for Target을 선택하면(그림 6) 커서가 바뀐다. 이 커서가 된 상태로 알아 보고 싶은 곳에 클릭을 한 후 살펴보고 싶은 오브젝트를 클릭하면 Finder의 윈도우와 비슷하게 생긴 브라우져가 뜬다. 이곳에서 오브젝트들을 살펴 볼 수 있으며, 메세지이름을 클릭하여 그 메세지를 실제로 보내볼 수 도 있다.
(그림 6) Browser for Target
위와 같은 방법으로 어떤 오브젝트와 어떤 메소드들이 어떻게 쓰이는지 알 수 있어서, 실제로 후에 플러그인을 만들 때 어디에 어떤 내용을 첨가/수정해야할지 알 수 있게 될 것이다.
[편집] 실제로 수정하기
먼저, XCode에서 (그림 7)처럼 새로운 프로젝트에서 Cocoa Bundle을 만든다. 그 안에서 두가지 방법으로 원본 프로그램 코드의 수정/첨가본을 짤 수 있다. Posing, 그리고 method swizzling.
(그림 7) Cocoa bundle 만들기
[편집] Posing
Posing은 말 뜻 그대로, ‘다른 사람인 척’, 즉 ‘다른 클래스 인척’하는 것이다. 다시 말하면, B라는 A의 subclass가 A라는 클래스로 pose한다 하면, 후에 A가 쓰여야 할 때 대신 B가 쓰이게 되는 것이다. 이것은 Objective-C runtime에서 이뤄지는 일이다. 다음과 같은 코드로 posing이 일어날 수 있다:
[[B class] poseAsClass:[A class]];
더 자세한 내용은 apple의 posing에 대한, 또는 +[NSObject poseAsClass:(Class)_class]에 대한 문서를 찾아보면 된다. Posing은 다음과 같은 단점이 있다:
- 1. Posing이 취해지고 난 후에 생긴 인스턴스들만 새로운 클래스의 인스턴스이다. 그 전에 만든 클래스들은 모두 원본 클래스이므로, 원본 클래스의 인스턴스가 생성되기 전에 posing을 해줘야한다. 이것은 조금만 주의하면 특별히 문제가 되지 않지만, 후에 디버깅시 도움이 될 정보이다.
- 2. 새로운 클래스에 인스턴스 변수(instance variable)를 만들 수 없다. Mike Solomon, SIMBL제작자에 의하면 이것은 클래스의 사이즈가 정해져있는 것에 대한 문제일 것이라고 생각이 된다고 한다. 하지만 다음과 같은 방법을 이용하면 쉽게 새로운 변수를 만들 수 있다:
static NSMutableDictionary* s_fakeIvars = nil;
+ (void) initialize
{
s_fakeIvars = [[NSMutableDictionary alloc] init];
}
- (id) init
{
self = [super init];
[s_fakeIvars setObject:[NSMutableDictionary dictionary] forKey:[NSNumber numberWithInt:(int)self]];
}
- (void) dealloc
{
[super dealloc];
// note: assumes 32-bit pointers
[s_fakeIvars removeObjectForKey:[NSNumber numberWithInt:(int)self]];
}
- 3. 새로운 버젼이 나오는 등의 이유로 타겟 프로그램의 사이즈가 바뀐다면 에러가 난다.
- 4. 여러 플러그인이 같은 클래스에 대하여 posing할 수 있고, 이렇게 되면 문제가 된다.
[편집] Method Swizzling
Method swizzling은 class posing과 달리 method를 바꿔주는 기술이다. DuctTape의 코드 일부를 예로 들면:
/**
* Renames the selector for a given method.
* Searches for a method with _oldSelector and reassigned
_newSelector to that
* implementation.
* @return NO on an error and the methods were not swizzled
*/
BOOL DTRenameSelector(Class _class, SEL _oldSelector, SEL _newSelector)
{
Method method = nil;
// First, look for the methods
method = class_getInstanceMethod(_class, _oldSelector);
if (method == nil)
return NO;
method->method_name = _newSelector;
return YES;
}
위의 코드는 메소드를 바꿔준다. 여기에 추가로 다음의 PitHelmet의 코드를 집어넣어 쓸 수 있다.
// never implemented, just here to silence a compiler warning
@interface WebInternalImage (PHWebInternalImageSwizzle)
- (void) _webkit_scheduleFrame;
@end
@implementation WebInternalImage (PHWebInternalImage)
+ (void) initialize
{
DTRenameSelector([self class], @selector(scheduleFrame), @selector (_webkit_scheduleFrame));
DTRenameSelector([self class], @selector(_ph_scheduleFrame), @selector(scheduleFrame));
}
- (void) _ph_scheduleFrame
{
// do something crazy...
...
// call the "super" method - this method doesn't exist
until runtime
[self _webkit_scheduleFrame];
}
@end
여기서 원본 메소드와 새로만든 메소드의 signature는 같으며, 앞에 약자를 집어넣어 구분하기 쉽게 하였음을 유념해야 한다. Method swizzling 역시 class posing과 같은 단점들을 갖고있다. 마찬가지로 인스턴스 변수를 만들 수 없으며, 이것을 해결하기 위해 class posing과 같은 방법을 이용하면 된다. Class posing과 다른점은, posing은 posing후에 생성된 인스턴스만 치환이 되지만, method swizzling의 경우, 모든 오브젝트가 메소드를 부르면, 새로 만들어진 메소드가 불려진다.
이제 코드를 작성했으면 SIMBL플러그인으로 작성하는 방법을 알아보자.
[편집] SIMBL
보통 플러그인을 만들 때 InputManager와 SIMBL 두가지 방법으로 플러그인을 만들 수 있다. InputManager를 쓰는 경우, Cocoa 프로그램이 시작되는 순간부터 로드된다. 즉, 타겟 어플리케이션이 아니더라도 어느 프로그램이라도 cocoa 프로그램이 실행되면 플러그인들이 로드되어 active상태로 존재하게 된다. 이것은 매우 소모적이며, 여러 버그가 발생할 소지가 있다. 한편 SIMBL은 해당 어플리케이션이 켜질 때만 로드되며, 버젼관리등의 관리가 되므로 매우 유용하고, 실제로 많은 플러그인들이 SIMBL로 만들어지고 있으며 100,000개가 넘는 컴퓨터에 SIMBL환경이 설치되어있다. 정확히 SIMBL의 장점을 말하자면,
- 원하는 프로그램에만 설치되도록 안전하게 로드된다.
- 라이브러리 hierarchy를 잘 검색하여 사용자 특정적으로 또는 전체 사용자를 대상으로 로드 해야할지를 결정하여 로드한다.
- 오직 하나의 버젼만 로드되도록 관리한다. (사용자 특정적으로 설정된 것이 전체 사용자 대상으로 한 것보다 우세하게 로드한다)
- 프로그램의 버젼을 확인하여 버젼변경에 따른 문제를 방지한다.
위에서 작업한 새로운 플러그인 프로젝트에서, MySamplePlugin이라는 기본 플러그인 클래스를 생성했다고 가정하면, 다음과 같은 과정에 따라 SIMBL플러그인으로 만들 수 있다(이 MySamplePlugin이 SIMBL이 로드하기 시작하는 출발점이라고 보면 된다):
- 1. Info.plist파일을 수정한다.
- NSPrincipalClass를 MySamplePlugin으로 설정한다.
- SIMBLTargetApplications라는 새로운 key를 만든다.
- SIMBLTargetApplications안에 BundleIdentifier, MaxBundleVersion, MinBundleVersion 세가지 키를 만든다. 모두 string값을 갖는다.
- BundleIdentifier값은 원본 프로그램의 것을 집어넣는다.
- MaxBundleVersion, MinBundleVersion 두 값은, 지원하는(테스트해 본) 프로그램의 최고, 최소 버젼의 CFBundleVersion 값을 넣는다. 위의 버젼이 맞지 않으면 사용자에게 지원하지 않는 버젼이라고 말하고 개발자에게 연락하라고 메세지창이 뜬다.
- 예로 사파리를 들어 다음과 같이 만들 수 있다:
<key>SIMBLTargetApplications</key>
<array>
<dict>
<key>BundleIdentifier</key>
<string>com.apple.Safari</string>
<key>MaxBundleVersion</key>
<string>412</string>
<key>MinBundleVersion</key>
<string>412</string>
</dict>
</array>
- 2. 빌드를 해본다. 참고로 load라는 메소드가 있으면 SIMBL이 알아서 플러그인을 로드하기에 적절한 타이밍에 불러주므로, 이곳에 시작하는 코드를 넣는 것이 좋다. 다음의 예를 보면 이해가 쉽다:
@implementation MySamplePlugin
/**
* A special method called by SIMBL once the application has started and all classes are initialized.
*/
+ (void) load
{
MySamplePlugin* plugin = [MySamplePlugin sharedInstance];
// ... do whatever
NSLog(@"MySamplePlugin installed");
}
/**
* @return the single static instance of the plugin object
*/
+ (MySamplePlugin*) sharedInstance
{
static MySamplePlugin* plugin = nil;
if (plugin == nil)
plugin = [[MySamplePlugin alloc] init];
return plugin;
}
- 3. 컴파일한다. 에러가 많이 나는 경우 다음과 같은 플래그들을 설정해주면 될 수 있다.(Other Linker Flags에 집어넣는다) 이 플래그들은 링크할 때 원래 정의되어져 있는 특정 함수들이 없다는 warning들을 막아준다.
- -undefined suppress
- -undefined define_a_way
- 4. 확실히 로드되는지를 확인한다. Mike Solomon에 의하면, 각 사용자에 플러그인 폴더를 만들고 플러그인 번들들을 symlink하여 플러그인들을 쉽게 껐다 켰다 할 수 있게 만든다.
mkdir -p ~/Library/Application\ Support/SIMBL/Plugins cd ~/Library/Application\ Support/SIMBL/Plugins ln -s ~/MySamplePlugin/build/MySamplePlugin.bundle
[편집] 발생할 수 있는 문제점들
- 소프트웨어 업데이트
- 위에서도 몇번 언급했듯이, 원본 프로그램이 업데이트되는 경우 class posing을 한 경우, 메모리 크기가 맞지 않아 충돌하는 경우가 많다. 그래서 언제나 새로운 버젼이 나오면 이것을 체크해 보는 것이 좋다.
- 이름 충돌
- 다른 클래스, 메소드, C 함수들과 이름이 충돌되는 경우가 많다. 그래서 언제나 앞에 고유한 약자를 붙이는 것이 좋다. CF, NS등의 약자를 피하는 것은 말할 필요도 없을 것이다.
- 버젼 체크의 필요성
- 테스트해본 버젼에서만 될 수 있도록 위에서 말한 버젼 체크 기능을 이용하는 것이 좋다. 사용자에게 도움을 주기 위한 플러그인이 오히려 해가 되는 일이 발생한다면 모두에게 나쁜 일이 될 것이다.
[편집] References
SIMBL 제작자인 Mike Solomon의 CocoaReverseEngineering(http://www.culater.net/wiki/moin.cgi/CocoaReverseEngineering)이 많은 부분의 내용을 차지하므로 꼭 들려보는 것이 좋다. 메일로 Mike Solomon의 허락을 받아 첨가했음을 밝힌다.











