May, 11th 2010

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/ 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:

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:

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:

  1. After a new process is forked an execv occurs, the program is read from hard-drive and different sections are loaded in RAM, .data is loaded into "read only" pages while .text and a stack is created with "read and write" pages.
    Note: In the drawing .data's pages are mapped starting at 0x00000000 but in reality this is reserved so you get a nice "segmentation fault" upon derefencing a null pointer.

  1. 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.
  2. The loader reads the missing symbols names and search for them in the default libraries.
  3. 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:

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)();

      			func =  dlsym(RTLD_NEXT, "malloc");

    		printf("malloc(%d) is called\n", size);     

Compilation :

   $: gcc -D_GNU_SOURCE -rdynamic -shared malloc_interposer.c -o /lib/ -ldl


Execution :

   $: LD_PRELOAD=/lib/ 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:

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 root@

	// Jump in and inject
 	$ ssh -l root
	# cd /tmp
	# ./

Notice that injection is performed via a script

	cd /System/Library/LaunchDaemons/
	cp /tmp/ /System/Library/LaunchDaemons/

	launchctl unload -w /System/Library/LaunchDaemons/
	launchctl load -w /System/Library/LaunchDaemons/


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.



	[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] 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/'
	[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/ 
	# and /dev/dlci.h5-baseband.sms

Receiving a (fictional) SMS:


	# Receiving an unsollicited text message (AT+CMT). 

	[recv] AT+CMT=10307919127163385F901000B914161387976F0000066C8721E640C8B592090F28D76838661793B3C5E83D

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.

Android and pseudo-terminal MITM

Next page



Fabien Sanglard @2010