Tracing the baseband: Part 1
I was reading an article on planetbeing's blog the other day and my curiosity was tipped off when he mentioned that phones don't run only one operating system but two. I decided to learn a bit how all this really works and here are my notes with the source code associated. Hopefully it will help someone investigating the subject.
The smart and the phone
Modern smartphones are made of two parts: The "smart" part and the "phone" part. They are very independent from each other, on iPhone for example MacOSX can crash during a call but user will still be able to pursue a conversation. Those two part use separate boards, processors, run different operating system started with different bootloader and of course don't use the same RAM. More interesting is that they are "poorly" coupled and communicate with each other via an UART serial line to pass commands, the same old way a 386 was communicating with a modem plugged on a port COM 14 years ago. The protocol (Hayes Command Set) is 30 years old, human readable and extendable: even relatively new function such as "unlocking" are done over AT-Commands.
This architecture is valid for both the Androids and the iPhones:
A kernel module exposes the serial line over an UNIX pseudo-terminal in the /dev
folder. On Androids there is only one pseudo terminal: /dev/smd0
but on iPhone the UART is divided by a kernel module and several pseudo-terminals are exposed: /dev/mux.h5-baseband.reg
, /dev/dlci.h5-baseband.call
or /dev/dlci.h5-baseband.sms
. The user land process can then open
any terminal and perform I/O commands with simple read
and write
.
Note: That's why the iPhone hackers use two words for their activity:
- "Jaibreak" which refers to open the "smart" MacOS X access.
- "Unlock" which refer to allowing the modem to use any SIM card, even is the SIM's MMC/MNC are not matching the operator's MMC/MNC.
Getting in the middle
I wanted to take a look at a real stream of communication between the smart part and the phone part. It seemed there was three ways to do it:
- From within the CommCenter/RILd, using tricks such as:
- Library preloading
- Method interposition
- From the smart OS, dealing right with the unix pseudo-terminal
- Pseudo-terminal MITM (Also called Termial in the middle)
Library preloading & Method interposition: Theory
In MacOS X/Linux, programs are stored on hard-drive with missing parts referencing methods and functions from bigger libraries (libc
,zlib
,...). Only when they are started the missing symbols are linked, it is called dynamic linking and it's done by ld
on Linux and dyld
on MacOS X:
-
After a new process is forked an
execv
occurs, the program is read from hard-drive and different sections are loaded in RAM,.text
is loaded into "read only" pages while.data
and a stack are created with "read and write" pages.
Note: In the drawing.data
's pages are mapped starting at0x00000000
but in reality this is reserved so you get a nice "segmentation fault" upon derefencing a null pointer.
- Once the program's different section are all in pages, the kernel reads which loader should be used and integrate it in the process address space. The loader is usually already resident in memory somewhere on the system so it is not loaded from hard-drive but mapped by adding an entry in the progress's page table. Execution control is them transferred to the loader, passing via parameters where the different program sections are.
- The loader reads the missing symbols names and search for them in the default libraries.
- The libraries are usually also resident in memory so there is no need to read them from the hard-drive. They are mapped in the process address space via the process's page table and symbols resolution occurs. If all symbols are resolved, execution of the program can begin.
Library preloading is a way to get in front of the libraries when the loader is looking for symbols (step 3). This is done by instructing the loader to lookup for missing symbols in a library we wrote before looking anywhere else:
Note that the loader is performing symbols resolution (intercepted by our library) at launchtime. The interceptor library then uses the loader to create a hook at runtime.
It is not complicated to do:
- Write a library with function prototyp matching the one you want to intercept (let's say:
void *malloc(size_t size)
) - Compile as a shared library
- Instruct the loader to lookup this library before anything else:
- On linux this is done via the
LD_PRELOAD
environment variable. - On MacOS X it is slighty different because
lyld
uses a two-level namespaces ( a symbol not only features a method's name but also the name of the library) hence you have to specify the name of your interceptor library viaDYLD_INSERT_LIBRARIES
but also instructlyld
to use a flat lookup system viaDYLD_FORCE_FLAT_NAMESPACE
Of course to make the program run you also have to get a hold on the "real" function via
dlsym(RTLD_NEXT, "malloc")
and relay the call so everything is transparent to the program.Dummy source code (malloc_interposer.c):
#include <stdio.h> #include <dlfcn.h> #include <sys/types.h> #include <sys/stat.h> void *malloc(size_t size) { static void * (*func)(); if(!func) func = dlsym(RTLD_NEXT, "malloc"); printf("malloc(%d) is called\n", size); return(func(size)); }
Compilation :
$: gcc -D_GNU_SOURCE -rdynamic -shared malloc_interposer.c -o /lib/malloc_interposer.so.1.0 -ldl
$: LD_PRELOAD=/lib/malloc_interposer.so.1.0 cat /dev/null malloc(20) is called
Library preloading & Method interposition: Practice
While the method described previously works very well on Linux, MacOS X tend to behave poorly when you flatten the lookup system of lyld
. Lukily there is an other way to place a hook on MacOS X and this method is described in Amit Singh's gem: MacOS X Internals as method interposition:
By placing a special sub-section __interpose
in the data
portion of the executable, dyld
will perform all the interceptions automatically. Here is an example hooking open,close,write and read.
static const interposer_t interposers[] __attribute__ ((section("__DATA, __interpose")))= { { (void*)my_open, (void*)open }, { (void*)my_close, (void*)close}, { (void*)my_read, (void*)read}, { (void*)my_write, (void*)write}, }; int my_open (const char* path, int flags, mode_t mode){..} int my_close (int d){..} int my_read (int handle, void *buffer, int nbyte ){..} int my_write (int handle, void *buffer, int nbyte ){..}
With this trick, it was easy to identify the pseudo-terminals used by placing a hook on open
and close
. Then hook read
and write
. The tracing is performed by maintaining a mapping between file descriptor returned by <fcntl.h>
and FILE*
's
. Here is the resulting source code: fdinterceptor.c and a zip containing a plist and the script to inject: injector.zip.
Toolchain in action:
// Build the tools $ cd /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin $ gcc-4.2 -arch armv6 -dynamiclib -isysroot ../../SDKs/iPhoneOS3.1.3.sdk -o fdinterceptor.dylib fdinterceptor.c // Send the tools $ scp fdinterceptor.dylib injectCommCenter.sh com.apple.CommCenter.plist root@192.168.1.103:/tmp // Jump in and inject $ ssh -l root 192.168.1.103 # cd /tmp # ./injectCommCenter.sh
Notice that injection is performed via a script injectCommCenter.sh
:
cd /System/Library/LaunchDaemons/ cp com.apple.CommCenter.plist com.apple.CommCenter.plist.vanilla cp /tmp/com.apple.CommCenter.plist /System/Library/LaunchDaemons/com.apple.CommCenter.plist launchctl unload -w /System/Library/LaunchDaemons/com.apple.CommCenter.plist launchctl load -w /System/Library/LaunchDaemons/com.apple.CommCenter.plist cp com.apple.CommCenter.plist.vanilla com.apple.CommCenter.plist
The launchctl
lines are not very interesting as they merely unload and reload the CommCenter deamon. But what is done before and afer is a bit more worth mentioning: Because the CommCenter not only handles the modem but also the WIFI connection, once you unload the CommCenter your SSH terminal will HANG. You are literally sawing off the branch you are sitting on. It is hence a necessity to script the re-loading....but there is more:
Because we modified the plist and it is saved on hard-drive: if we have a bug in our interceptor library we may potentially brick the device and require a full DFU restore ! So in order to take into account a worse case scenario the script also remove the interceptor library from the plist, this way the device can restart safely: This is just an idiot proof security.
Results:
Booting
[send] at # Modem Are you there ? [send] at [send] at [send] at [recv] AT # Yes I am ! [send] ate0 # Set modem to "no echo" mode [recv] ate0 OK [send] at+cmee=1 # Require error code to be returned as code (opposed to verbose at+cmee=2) [recv] OK [send] at+ipr=750000 # Set the terminal speed [recv] OK [send] at+xdrv=0,41,25 # Call method 41 on device 0 (speakers) [recv] +XDRV: 0,41,1,0 [recv] OK RV: 0,41,1,0 [send] at+xtransportmode # Switch to binary code instead of commands [recv] OK [send] at+cscs="HEX" # Set the TE character set to HEX [recv] OK [send] at+xthumb? [recv] +XTHUMB: "1E2834B6CE739AB36EF9454B7997FCD30208398C","E93B43F3EF6DAED516A2D4B9BAD5494DC81E92D3" [recv] OK [send] at+xgendata # Request modem's firmware description [recv] +XGENDATA: "","DEV_ICE_MODEM_04.05.04_G","EEP_VERSION:208","EEP_REVISION:1","BOOTLOADER_VERSION:3.9_M3S2" [recv] OK [send] at+xdrv=10,2 # Call a function for a device, format is at+xdrv:deviceId,functionId,params ... [recv] :+XDRV: 10,2,0 [recv] OK [send] at+xl1set="psvon" [send] at+cmux=0,0,0,1500 # Set the multiplexing mode [recv] OK [open] '/dev/dlci.h5-baseband.call' [open] '/dev/dlci.h5-baseband.reg' [open] '/dev/dlci.h5-baseband.sms' [open] '/dev/dlci.h5-baseband.low' [open] '/dev/dlci.h5-baseband.pdp_ctl' [open] '/dev/dlci.h5-baseband.chatty' [open] '/dev/dlci.h5-baseband.pdp_0' [open] '/dev/dlci.h5-baseband.pdp_1' # The rest of the registration occurs in /dev/dlci.h5-baseband.reg # The two main used pseudo terminal after this are of course /dev/dlci.h5-baseband.call # and /dev/dlci.h5-baseband.sms
Receiving a (fictional) SMS:
# Receiving an unsollicited text message (AT+CMT). [recv] AT+CMT=10307919127163385F901000B914161387976F0000066C8721E640C8B592090F28D76838661793B3C5E83D 0657959079AD2D36C3628EDA697E5E539BD4C06A5DD203A3A3D07C1DFF3343DFD76837E202ABA0E92C1E86850339C0 7C96031180846D3C9642ED80C046F8350C72675154B01
Text message are PDU encoded, you can find plenty of online decoder. Here is the plain text version
SMSC: +19726133589 Receiver: +1416839XXXX Payload: Hey Fab, John Carmack here: Still interested in this position ? Thu 20th May 2010 04:22.03PM
Receiving a call :
[recv] RING # Trigger the phone to ring [recv] +CLIP: "",128,,,,2 # No caller ID :/ ! [recv] +XCALLSTAT: 1,4 [recv] RING[recv] +CLIP: "",128,,,,2 [send] ata # Local user decided to accept the incoming call [recv] +XCALLSTAT: 1,0 # Reporting call status is enabled (1), voice is active (0) [recv] OK [recv] +XCALLSTAT: 1,6 # Reporting call status is enabled (1), voice is disconnected (6) [send] at+ceer # Local user hang up [recv] NO CARRIER # Connection is indeed terminated from the other hand [recv] +CEER: "Release","Normal call clearing" #Collect informations on call [recv] OK
Note : I was surprised to find RING command notifications note only in the call
channel but also in the sms
channel but it actually makes a lot of sense when connect to EDGE/GPRS: Since text message and call are not supported simultaneously the sms pseudo-terminal must remain silent during a call.
Fails :
Overall, I was pretty happy with the result and I managed to understand better the AT communications but I was unable to capture properly a text message sent or the name of the carrier (the response associated via a AT+COPS=?
request). There is probably a bug in my parsing as I stop tracing every message after the first CR character. The fact that the pseudo-terminal is not setup as raw did not help at all.