==Phrack Inc.== Volume 0x0b, Issue 0x3f, Phile #0x10 of 0x14 |=-----=[ Reverse engineering - PowerPC Cracking on OSX with GDB ]=------=| |=-----------------------------------------------------------------------=| |=--------------------------=[ curious ]=--------------------------------=| |=-----------------------------------------------------------------------=| --[ Contents 1.0 - Introduction 2.0 - The Target 3.0 - Attack Transcript 4.0 - Solutions and Patching A - GDB, OSX, PPC & Cocoa - Some observations. B - Why can't we just patch with GDB? --[ 1.0 - Introduction This article is a guide to taking apart OSX applications and reprogramming their inner structures to behave differently to their original designs. This will be explored while uncrippling a shareware program. While the topic will be tackled step by step, I encourage you to go out and try these things for yourself, on your own programs, instead of just slavishly repeating what you read here. This technique has other important applications, including writing patches for closed source software where the company has gone out of business or is not interested, malware analysis and fixing incorrectly compiled programs. It is assumed you have a little rudimentary knowledge in this area already - perhaps you have some assembly programming or you have some cracking experience on Windows or Linux. Hopefully you'll at least know a little bit about assembly language - what it is, and how it basically works ( what a register is, what a relative jump is, etc. ) If you've never worked with PowerPC assembly on OSX before, you might want to have a look at appendix A before we set off. If you have some basic familiarity with GDB, it will also be very useful. This tutorial uses the following tools and resources - the XCode Cocoa Documentation, which is included with the OSX developer tools, a PowerPC assembly reference ( I recommend IBM's "PowerPC Microprocessor Family: The Programming Environments for 32-Bit Microprocessors" - you can get it off their website ), gcc, an editor and a hexeditor ( I use bvi ). You'll also be using either XCode/Interface Builder or Steve Nygard's "class-dump" and Apple's "otool". I'm no expert on this subject - my knowledge is cobbled together from time spent working in this area with Windows, then Linux and now OSX. I'm sure there's lots in this article that could be done more correctly / efficiently / easily, and if you know, please write to me and discuss it! Already this article is seriously indebted to the excellent suggestions and hard work of Christian Klein of Teenage Mutant Hero Coders. I had a very hard time deciding whether or not to publish this article anonymously. Recently, my country has enacted ( or threatened to enact ) DMCA style laws that represent a substantial threat to the kinds of exploration and research that this document represents - exploration and research which have important academic and corporate applications. I believe that I have not broken any laws in authoring this document, but the justice system can paint with a broad brush sometimes. Thanks for reading, --[ 2.0 - The Target The target is a shareware client for SFTP and FTP, which I was first exposed to after the automatic ftp execution controversy a few years ago ( see - ). Out of respect for the authors, I'm not going to name it explicitly, and the version analysed is now deprecated. --[ 3.0 - Attack Transcript The first step is to prompt the program to display the undesirable behavior we wish to alter, so we know what to look out for and change. From reading the documentation, I know that I have fifteen days of usage before the program will start to assert it's shareware status - after that time period, I will be unable to use the Favourites menu, and sessions will be time limited. As I didn't want to wait around fifteen days, I deleted the program preferences in ~/Library/Application Support/, and set the clock back one year. I ran the software, quit, and then returned the clock to normal. Now, when I attempt to run the software, I receive the expired message, and the sanctions mentioned above take effect. Now we need to decide where we are to make the initial incision In the program. Starting at main() or even NSApplicationMain() ( which is where Cocoa programs 'begin' ) is not always feasible in the large, object based and event driven programs that have become the norm in Cocoa development, so here's what I've come up with after a few false starts. One approach is to attack it from the Interface. If you have a look inside the application bundle ( the .app file - really a folder ), you'll most likely find a collection of nib files that specify the user interface. I found a nib file for the registration dialog, and opened it in Interface Builder. Inspecting the actions referred to there we find a promising sounding IBAction "validateRegistration:" attached to a class "RegistrationController". This sounds like a good place to start, but if the developers are anything like me, they won't have really dragged their classes into IB, and the real class names may be very different. If you didn't have any luck finding a useful nib file, don't despair. If you have class-dump handy, run it on the actual mach-o executable ( usually in .app/Contents/MacOS/ ), and it will attempt to form class declarations for the program. Have a look around there for a likely candidate function. Now that we have some ideas of where to start, let's fire up GDB and look a bit closer. Start GDB on the mach-o executable. Once loaded, let's search for the function name we discovered. If you still don't have a function name to work with ( due to no nib files and no class-dump ), you can just run "info fun" to get a list of functions GDB can index in the program. | (gdb) info fun validateRegistration | All functions matching regular expression "validateRegistration": | Non-debugging symbols: | 0x00051830 -[StateController validateRegistration:] "StateController" would appear to be the internal name for that registration controlling object referred to earlier. Let's see what methods are registered against it: | (gdb) info fun StateController | All functions matching regular expression "StateController": | Non-debugging symbols: | 0x0005090c -[StateController init] | 0x00050970 +[StateController sharedInstance] | 0x000509f8 -[StateController appDidLaunch] | 0x00050e48 -[StateController cancelRegistration:] | 0x00050e8c -[StateController findLostNumber:] | 0x00050efc -[StateController state] | 0x00050fd0 -[StateController validState] | 0x00051128 -[StateController saveState:] | 0x000512e0 -[StateController appendState:] | 0x00051600 -[StateController initState] | 0x0005165c -[StateController stateDidChange:] | 0x00051830 -[StateController validateRegistration:] | 0x00051bd8 -[StateController windowDidLoad] "validState", having no arguments ( no trailing ':' ) sounds very promising. Placing a breakpoint on it and running the program shows it's called twice on startup, and twice when attempting to possibly change registration state - this seems logical, as there are two possible sanctions for expired copies as discussed earlier. Let's dig a bit deeper with this function. Here's a commented partial disassembly - I've tried to bring it down to something readable on 75 columns, but your mileage may vary. I'm mainly providing this for those unfamiliar with PPC assembly, and it's summarized at the end. (gdb) disass 0x50fd0 Dump of assembler code for function -[StateController validState]: 0x00050fd0 <-[StateController validState]+0>: mflr r0 # Copy the link register to r0. 0x00050fd4 <-[StateController validState]+4>: stmw r27,-20(r1) # Store r27, r28, r29, r30 and r31 in five consecutive words # starting at r1 - 20 ( 0xbfffe2bc ). 0x00050fd8 <-[StateController validState]+8>: addis r4,r12,4 # r4 = r12 + 4 || 16(0) # # || = "concatenated", in this case with sixteen zeroes. # this has the effect of shifting the "four" ( 100B ) # into the high sixteen of the register. 0x00050fdc <-[StateController validState]+12>: stw r0,8(r1) # Write r0 to r1 + 8. 0x00050fe0 <-[StateController validState]+16>: mr r29,r3 # Copy r3 to r29. At the moment, this would contain # the address of the object we're being invoked on # ( a StateController instance ). 0x00050fe4 <-[StateController validState]+20>: addis r3,r12,4 # As 0x50fd8, but into r3. 0x00050fe8 <-[StateController validState]+24>: stwu r1,-96(r1) # Store Word With Update: # "address" = r1 - 96 # store r1 to "address" # r1 = "address" 0x00050fec <-[StateController validState]+28>: mr r31,r12 # Copy r12 to r31. 0x00050ff0 <-[StateController validState]+32>: lwz r4,1620(r4) # Load r4 with contents of memory address r4 + 1620 ( 0x91624 ). # r4 now contains 0x908980CC = c string, "sharedInstance". 0x00050ff4 <-[StateController validState]+36>: lwz r3,5944(r3) # Load r3 with contents of memory address r3 + 5944 ( 0x92708 ). # r3 now contains 0x92b20 = objc object, describes itself as # "Preferences". # # This seems to be an instance of the undocumented preferences # api used by mail and safari. Tut tut. 0x00050ff8 <-[StateController validState]+40>: bl 0x739d0 # r3 = [ Preferences sharedInstance ]; # (gdb) po $r3 # 0x00050ffc <-[StateController validState]+44>: lwz r0,40(r29) # Load r29 + 40 into r0. As you may recall, r29 was set # at 0x50fe0 to be the StateController instance. Hence # this offset refers to some kind of instance variable. # # In this case, it's value is nil. Guess it hasn't been # assigned yet. My theory is that this function will be # invoked several times on the same object and this, the # first run through, will do initialization. 0x00051000 <-[StateController validState]+48>: mr r27,r3 # Copy the shared instance ( herein reffered to as prefObject ) # returned in 0x50ff8 to r27. 0x00051004 <-[StateController validState]+52>: cmpwi cr7,r0,0 # Compare r0 ( the first instance variable ( herein SC:1 ) ) # with nil, store the result. # # (gdb) print /t $cr # $19 = 100100000000000100001001000010 # # cr7 occupies offset 21-24, 001B ( "equal" ). # The CR's can contain 100B ( "higher" ), 010B ( "lower" ) # or 001B ( "equal" ). 0x00051008 <-[StateController validState]+56>: bne+ cr7,0x51030 <-[StateController validState]+96> # Jump to +96 if the equal bit of cr7 is not set. # It is, so we just continue on. 0x0005100c <-[StateController validState]+60>: addis r4,r31,4 # As 0x50fd8, but into r4. Note that r31 is the new address # of the r12 address used in both of those instances. I would # say r31 contains the start of the table listing the # message names available in this program. 0x00051010 <-[StateController validState]+64>: lwz r4,5168(r4) # Load r4 + 5168 into r4. This turns out to be a c string, # "firstLaunch". 0x00051014 <-[StateController validState]+68>: bl 0x739d0 # r3 = [ prefObject firstLaunch ]; # This turns out to be an NSDate object, in this case # 2003-09-19 23:30:10 +1000. We'll refer to this as # firstLaunchDate. 0x00051018 <-[StateController validState]+72>: cmpwi cr7,r3,0 # Compare firstLaunchDate with nil, results to cr7. 0x0005101c <-[StateController validState]+76>: stw r3,40(r29) # Store r3 ( firstLaunchDate ) to r29 + 40 - you'll recall # this as being the StateController local variable referred # to 0x50ffc, SC:1. 0x00051020 <-[StateController validState]+80>: beq+ cr7,0x51030 <-[StateController validState]+96> # If the equal bit is set, jump to +96 - same location as # at 0x51008 for successful loads. Not what I was expecting. 0x00051024 <-[StateController validState]+84>: addis r4,r31,4 0x00051028 <-[StateController validState]+88>: lwz r4,2472(r4) # As we did manage to load successfully, we fall through to # here - load the message table and the string "retain". 0x0005102c <-[StateController validState]+92>: bl 0x739d0 # firstLaunchDate = [ firstLaunchDate retain ]; 0x00051030 <-[StateController validState]+96>: lwz r3,40(r29) # Here's where the divergent paths rejoin - load r3 with # the SC:1. 0x00051034 <-[StateController validState]+100>: cmpwi cr7,r3,0 0x00051038 <-[StateController validState]+104>: beq+ cr7,0x51070 <-[StateController validState]+160> # Check to see if it's nil, and if so, jump out to +160. # This would catch the case where we jumped from 0x51020 - # would have seemed to make more sense to jump directly. 0x0005103c <-[StateController validState]+108>: addis r4,r31,4 0x00051040 <-[StateController validState]+112>: lwz r4,4976(r4) # Load the message table and the string "timeIntervalSinceNow". 0x00051044 <-[StateController validState]+116>: bl 0x739d0 # r3 = [ firstLaunchDate timeIntervalSinceNow ]; # # This message returns as an NSTimeInterval, which is a double. # As a result, the function returns to f1 instead of the usual # r3. The result in my case is: # (gdb) print $f1 # $21 = -31790371.620961875 # (gdb) print $f1/60/60/24 # $22 = -367.944115983355 # # This seems as expected from what we did at the beginning. 0x00051048 <-[StateController validState]+120>: addis r2,r31,3 # Not sure what's at r31 + 3 || 0x0000. It's not the message # symbol table, and r2 is usually reserved for RTOC. 0x0005104c <-[StateController validState]+124>: lfd f0,26880(r2) # Load double at r2 + 26880 into f0. Perhaps r2 is a constants # table. It ends up being a big fat zero. 0x00051050 <-[StateController validState]+128>: fcmpu cr7,f1,f0 0x00051054 <-[StateController validState]+132>: ble+ cr7,0x51070 <-[StateController validState]+160> # Compare the time between first invocation and now with zero, # if it's less ( and it should be, unless the first invocation # was in the future! ) we jump to +160. 0x00051058 <-[StateController validState]+136>: addis r4,r31,4 0x0005105c <-[StateController validState]+140>: lwz r3,40(r29) 0x00051060 <-[StateController validState]+144>: lwz r4,1836(r4) 0x00051064 <-[StateController validState]+148>: bl 0x739d0 0x00051068 <-[StateController validState]+152>: li r0,0 0x0005106c <-[StateController validState]+156>: stw r0,40(r29) 0x00051070 <-[StateController validState]+160>: lwz r0,40(r29) # Load our ever present SC:1 into r0. 0x00051074 <-[StateController validState]+164>: addis r2,r31,4 0x00051078 <-[StateController validState]+168>: addis r28,r31,4 # Load the message symbols into both r2 and r28. 0x0005107c <-[StateController validState]+172>: lwz r3,44(r29) # Load another instance variable on the StateController - this # one is 4 more along, at +44. We'll tag it as SC:2. # # It turns out to be another NSDate, this one is # "2004-09-21 21:55:27 +1000", the time I started the current # gdb session. 0x00051080 <-[StateController validState]+176>: addis r30,r31,4 # Load the message symbols into r30. 0x00051084 <-[StateController validState]+180>: cmpwi cr7,r0,0 0x00051088 <-[StateController validState]+184>: bne- cr7,0x510cc <-[StateController validState]+252> # Compare SC:1 with 0, if it's not equal, jump to +252. # Which we do. 0x0005108c <-[StateController validState]+188>: lwz r4,5172(r2) 0x00051090 <-[StateController validState]+192>: bl 0x739d0 0x00051094 <-[StateController validState]+196>: lwz r4,1504(r30) 0x00051098 <-[StateController validState]+200>: lwz r3,5924(r28) 0x0005109c <-[StateController validState]+204>: bl 0x739d0 0x000510a0 <-[StateController validState]+208>: stw r3,40(r29) 0x000510a4 <-[StateController validState]+212>: addis r4,r31,4 0x000510a8 <-[StateController validState]+216>: lwz r4,2472(r4) 0x000510ac <-[StateController validState]+220>: bl 0x739d0 0x000510b0 <-[StateController validState]+224>: lwz r5,40(r29) 0x000510b4 <-[StateController validState]+228>: mr r3,r27 0x000510b8 <-[StateController validState]+232>: addis r4,r31,4 0x000510bc <-[StateController validState]+236>: lwz r4,5176(r4) 0x000510c0 <-[StateController validState]+240>: bl 0x739d0 0x000510c4 <-[StateController validState]+244>: li r3,1 0x000510c8 <-[StateController validState]+248>: b 0x51114 <-[StateController validState]+324> 0x000510cc <-[StateController validState]+252>: lwz r4,5172(r2) # Load r4 with r2 + 5172. r2 still has the message symbol # table from 0x51074. The string is "timeIntervalSince1970". 0x000510d0 <-[StateController validState]+256>: bl 0x739d0 # r3 still contains SC:2 from 0x5107c, the time this instance was # launched. # # f1 = [ SC:2 timeIntervalSince1970 ]; # f1 = 1095767727.4292047 # f1/60/60/24/365 = 34.746566699302541 0x000510d4 <-[StateController validState]+260>: lwz r4,1504(r30) # r30 still has the message symbol table. r4 gets # "dateWithTimeIntervalSince1970:" 0x000510d8 <-[StateController validState]+264>: lwz r3,5924(r28) # Last I saw of r28, it had the message symbol table in it # as well, but +5924 seems to contain the NSDate class object. 0x000510dc <-[StateController validState]+268>: bl 0x739d0 # r3 = [ NSDate dateWithTimeIntervalSince1970: $f1 ] # Since the first argument is a float, it will draw from f1 - # which still has the seconds since 1970 to current invocation # from 0x510d0. # We end up with an exact copy of SC:2. We'll call it # thisLaunchDate. 0x000510e0 <-[StateController validState]+272>: addis r4,r31,4 # Load the message symbol table into r4. 0x000510e4 <-[StateController validState]+276>: mr r29,r3 # Copy r3 to r29. 0x000510e8 <-[StateController validState]+280>: mr r3,r27 # Copy r27 to r3. When last sighted at 0x51000, this # held the prefs shared object. 0x000510ec <-[StateController validState]+284>: lwz r4,5168(r4) # Load string "firstLaunch" to r4. 0x000510f0 <-[StateController validState]+288>: bl 0x739d0 # r3 = [ prefObject firstLaunch ]; # As seen at 0x51014, the value returned from here was later # stored in SC:1. 0x000510f4 <-[StateController validState]+292>: addis r4,r31,4 # Load the message symbol table to r4. 0x000510f8 <-[StateController validState]+296>: mr r5,r3 # Move the NSDate just returned from prefObject to # r5 ( second argument ). 0x000510fc <-[StateController validState]+300>: mr r3,r29 # Copy r29 to r3 - r29 had the reconstituted NSDate # 'thisLaunchDate' from 0x510dc. 0x00051100 <-[StateController validState]+304>: lwz r4,3456(r4) # Load "isEqualToDate:" into r4. 0x00051104 <-[StateController validState]+308>: bl 0x739d0 # r3 = [ thisLaunchDate isEqualToDate: firstLaunchDate ]; # That's going to be a big zero unless it's the first time # you're running. 0x00051108 <-[StateController validState]+312>: addic r2,r3,-1 # r2 = r3 - 1 with carry flag. # r2 will be set to max now. # XER = 100B. 0x0005110c <-[StateController validState]+316>: subfe r0,r2,r3 # subfe r0, r2, r3 = !( r2 + r3 + XER[ carry bit ] ) # = !( max + 0 + 0 ) # = !( max ) # = 0 0x00051110 <-[StateController validState]+320>: mr r3,r0 # Move r0 to r3 - the function result. 0x00051114 <-[StateController validState]+324>: lwz r0,104(r1) 0x00051118 <-[StateController validState]+328>: addi r1,r1,96 0x0005111c <-[StateController validState]+332>: lwz r27,-20(r1) 0x00051120 <-[StateController validState]+336>: mtlr r0 0x00051124 <-[StateController validState]+340>: blr # Various housekeeping and then return. For the most # part, we reload those words we pushed into memory and # the link register we stored in the opening moves. End of assembler dump. Ok, in summary, it seems validState does something different to what it's name might indicate - it checks if it's the first time you've run the program, initializes some data structures, etc. If it returns one, a dialog box asking you to join the company email list is displayed. So it's not what we thought, but it's not a waste of time - we've uncovered two useful pieces of information - the location of the date of first invocation ( StateController + 40 ) and the location of the date of current invocation ( StateController + 44 ). These should all be set correctly anytime after the first invocation of this function. These two pieces of information are key to determining whether the software has expired or not. We have a couple of options here. Knowing the offset information of this data, we can attempt to find the code that checks to see if the trial is over, or we can attempt to intercept the initialization process and manipulate the data loading to ensure that the user is always within the trial window. As this would be perfectly sufficient, we'll try that - a discussion of other avenues might make for interesting homework or a future article. --[ 4.0 - Solutions and Patching A possible method will be to overwrite the contents of StateController + 40 with StateController + 44 ( setting the date the program was first run to the current date ) and then return zero, leaving alone the code that deals with the preferences api. Due to the object oriented methodology of Cocoa development, the chances of some other function going crazy and performing a jump into the other parts of the function are slim to nil, and so we can leave it as is. A Proposed replacement function: Obtain a register for us to use. Load the contents of StateController +44 into it, write that register to StateController +40, release the register, zero r3, return. The write is done like this as you cannot write directly to memory from memory in PPC assembler. +----- | stw r31, -20(r1) | lwz r31, 44(r3) | stw r31, 40(r3) | lwz r31, -20(r1) | xor r3, r3, r3 | blr +----- Instead of consulting with the instruction reference to assemble it by hand, I'm going to be cheap and use GCC. Paste the code into a file as follows: newfunc.s: ----- .text .globl _main _main: stw r31, -20(r1) lwz r31, 44(r3) stw r31, 40(r3) lwz r31, -20(r1) xor r3, r3, r3 blr ----- Compile it as follows: `gcc newfunc.s -o temp`, and load it into gdb: | (gdb) x/15i main | 0x1dec
: stw r31,-20(r1) | 0x1df0 : lwz r31,44(r3) | 0x1df4 : stw r31,40(r3) | 0x1df8 : lwz r31,-20(r1) | 0x1dfc : xor r3,r3,r3 | 0x1e00 : blr | 0x1e04 : mflr r0 We want to see the machine code for 24 instructions post
. | (gdb) x/24xb main | 0x1dec
: | 0x93 0xe1 0xff 0xec 0x83 0xe3 0x00 0x2c | 0x1df4 : | 0x93 0xe3 0x00 0x28 0x83 0xe1 0xff 0xec | 0x1dfc : | 0x7c 0x63 0x1a 0x78 0x4e 0x80 0x00 0x20 Now that we have our assembled bytecode, we need to paste it into our executable. GDB is ( in theory ) capable of patching the file directly, but it's a bit more complicated than it might appear ( see Appendix B for details ). The good news is, finding the correct offset for patching the file itself is not difficult. First, note the offset of the code you wish to replace, as it appears in GDB. ( In this case, that's 0x50fd0. ) Now, do the following: | (gdb) info sym 0x50fd0 | [StateController validState] in section LC_SEGMENT.__TEXT.__text | of Armed with this knowledge of what segment the code falls in ( __TEXT.__text ), we can proceed. Run "otool -l" on your binary, and search for something like this ( taken from a different executable, unfortunately ): | Section | sectname __text | segname __TEXT | addr 0x0000236c | size 0x000009a8 | offset 4972 | align 2^2 (4) | reloff 0 | nreloc 0 | flags 0x80000400 | reserved1 0 | reserved2 0 The offset to your code in the file is equal to the address of the code in memory, minus the "addr" entry, plus the "offset" entry. Keep in mind that "addr" is in hex and offset is not! Now you can just over-write the code as appropriate in your hex editor. Save and then try and run the program. It worked for me first time! --[ A - GDB, OSX, PPC & Cocoa - Some Observations. Calling Convention: When handling calls, registers 0, 1 and 2 store important housekeeping information. They are not to be fucked with unless you carefully restore their values post haste. Arguments to functions commence at r3, and return values are stored at r3 as well. Except for stuff like floats, which you might find coming back in f1, etc. One of the things that makes OSX applications such a joy to crack is the heavy reliance on neatly defined object oriented interfaces, and the corresponding heavy use of messaging. Often in disassemblies you will come across branches to . This is a reformulation of the typical calling convention: | [ anObject aMessage: anArgument andA: notherArgument ]; Into something like this: | objc_msgSend( anObject, "aMessage:andA:", anArgument, notherArgument ); Hence, the receiving object will occupy r3, the selector will be a plain string at r4, and subsequent arguments will occupy r5 onwards. As r4 will contain a string, interrogate it with "x/s $r4", as the receiver will be an object, "po $r3", and for the types of subsequent arguments, I recommend you consult the xcode documentation where available. "po" is shorthand for invoking the description methods on the receiving object. GDB Integration: Due to the excellent Objective C support in GDB, not only can we breakpoint functions using their [] message nomenclature, but also perform direct invocations of methods as such: if r5 contained a pointer to an NSString object, the following is quite reasonable: | (gdb) print ( char * ) [ $r5 cString ] | $3 = 0x833c8 " \t\r\n" Very useful. Don't forget that it's available if you want to test how certain functions react to certain inputs. -- [ B - Why can't we just patch with GDB? As some of you probably know, GDB can, in principle, write changes out to core and executable files. This is not really practical in the scenario we're dealing with here, and I'll explain why. First, Mach-O binaries have memory protection. If you're going to overwrite parts of the __TEXT.__text segment, you're going to have to reset it's permissions. Christian Klein has written a program to do this ( see . ) You can also, once the program is running and has an execution space, do things like: | (gdb) print (int)mprotect(
, , 0x1|0x2|0x4 ) However, even when this is done, this only lets you write to the process in memory. To actually make changes to the disk copy, you need to either invoke GDB as 'gdb --write', or execute: | (gdb) set write on | (gdb) exec-file The problem is, OSX uses demand paging for executables. What this means is that the entire program isn't loaded into memory straight away - it's lifted off disk as needed. As a result, you're not allowed to execute a file which is open for writing. The upshot is, if you try and do it, as soon as you run the program in the debugger, it crashes out with "Text file is busy". |=[ EOF ]=---------------------------------------------------------------=|