One of the changes introduced by Mountain Lion was the removal of the old procmod convention for applications that want to access the task port of a process (aka for reversers, debuggers). Before this change, any binary that was procmod suid group set could access the task port of other processes (running as the same user). Taskgated configuration in Mountain Lion was changed and removed this possibility. Only signed binaries that contain an embedded Info.plist with a SecTaskAccess key have this feature enabled by taskgated. By embedded I mean a new section that is added to the binary at compile time, with the compiler option “–sectcreate __TEXT __info_plist Info.plist”.
Section sectname __info_plist segname __TEXT addr 0x0000000100240120 size 0x000000000000026f offset 0x240120 align 2^0 (1) reloff 0x0 nreloc 0 flags 0x00000000 reserved1 0 reserved2 0
This is true for single-file tools; application bundles have the Info.plist in the container and so the necessary key can be added there. Refer to this Apple document for additional information.
The problem arises when we want to do the same with a tool we only have the binary version. I wanted to work on a Python debugger and use the OS X supplied version, instead of compiling a new one and maybe dive directly into some kind of hell. And of course I didn’t want to modify taskgated configuration to support the old mode. So it was another opportunity to mess around with Mach-O headers.
The stupid thing is that after testing this with Python I discovered that it does not work with Python! The reason is that Python does have a bundle with a Info.plist we can modify. In Mountain Lion it is located at /System/Library/Frameworks/Python.framework/Versions/Current/Resources/Python.app (you can use fs_usage | grep taskgated to find out the correct location). You just need to edit it, add the following key and re-codesign the bundle with your own certificate.
Anyway, we can always learn something “new”. Assume the target is a single-file tool. What happens is that taskgated will read the Mach-O header, and search for the __info_plist section. If that section exists, SecTaskAccess key has the right configuration, and binary is signed by a trusted certificate, then task_for_pid() will be enabled for that binary.
The easy solution is to add that new section into the binary and problem solved. There is a small detail where codesign does not like if we add the plist data at the end of the current binary. It will refuse to re-codesign the modified binary. My solution is to use the header space to also store the Info.plist data. If you recall this post, the section information is not taken in account when executing the binary. But this trick works because taskgated reads again the header and uses the section offset information to lookup the data location. In theory this data can be located anywhere in the binary as long it passes codesign “integrity” checks. Adding to the end of the binary fails, although it might be possible to work around it (already lost too much time in this tool). The caveat here is that you must always codesign the target binary (if it not already) before injecting the Info.plist. It is due to codesign_allocate header free space algorithm that will detect the data inside the header and lower offset.
The code implements two modes. The first one that I did was to add a duplicate __TEXT segment with the __info_plist section (do not ask me why I went the hard way, my brain has this kind of things!) and the other is to just add a new section to the existing __TEXT segment (I thought this would not work because the kernel loader and dyld would not like it – does work without any problem).
The tool is called gimmedebugah and as always code is available here. The README has some additional details.
Since Python has a Info.plist, my original goal is defeated. Maybe it can be handy for something else in the future. Or just another example of tricks to play with Mach-O headers. At least I got some new ideas ;-).