What is this article about
In the two previous articles, I talked about
how to debug Android applications without Java source code and
about some features of installing breakpoints . If a dear reader has not yet read these articles, I strongly recommend starting with them, and only then read this article.
It just so happened that so far I have been talking exclusively about debugging Dalvik bytecode and have not said a word about debugging native methods. And after all, it is in the native methods that the most tasty things are often hidden — tricky defenses, interesting malware features, zero-day vulnerabilities. Therefore, today I’m compressed, without “water”, I’ll tell you how to debug native methods without C / C ++ source code (or what, dear reader, they are written there).
In order to benefit from my story, you need to be a bit "in the subject." In particular, it is desirable that the reader
- Understood Smali syntax, could write his code into .smali files and could distinguish declarations and calls to native methods from ordinary methods, could reassemble .apk files using Apktool ;
- I imagined what the Java Native Interface (JNI) is and how it works ;
- He knew what the System.load (...) and System.loadLibrary (...) methods are used for, how they work in Android, and using the arguments to these methods in Smali code could independently determine in which .so libraries there are JNI functions corresponding to those or other native methods;
- able to find these JNI functions in .so libraries;
- at least at the initial level I knew the ARM assembler (the article assumes that debugging will be performed on a device with an ARM architecture or on an emulator that emulates this architecture);
- had some experience with gdb and gdbserver ;
That's probably all the knowledge and skills that will be needed by the reader. Let's go to the tools.
Instruments
Today we will need:
- gdb and gdbserver for ARM from the latest version of Android NDK . Installation is described here .
- The
adb
utility from the latest version of the Android SDK . Installation is described here . - If debugging is on a real Android device, you need root rights on it.
We turn to the preparation.
')
Training
It is assumed that the reader is experienced enough to independently perform the following preparatory actions:
- Disassemble the .apk application file using Apktool, examine the files in the subdirectory
////smali
and find out:
- what native methods are called from Dalvik bytecode;
- In which .so library are the JNI functions corresponding to these methods;
- Pull this .so library from the subdirectory
////lib
or from the device on which debugging will be performed, examine it and find out how the JNI functions are called which correspond to one or another of the Dalvik bytecode methods. - Rebuild the .apk application using Apktool with
-d
and drive it to debug in NetBeans as written in this article of mine . - Make debugging in NetBeans stop after calls to
System.loadLibrary(...)
or System.load(...)
, which load .so with JNI functions that interest us, but before the first call to any of the native methods. You can use the trick I wrote about here . - To push gdbserver on the device or the emulator on which we are debugging.
Now that all the preparations have been completed, you can proceed to debugging itself.
Debugging
The idea is to push our application directly under two debuggers: under the debugger built into NetBeans - to debug Dalvik bytecode, and under gdb - exclusively to debug calls to native methods. It sounds a little strange, but in practice it works quite well. Although not always - see the next section “Pitfalls”.
So, if the reader has completed all the preparatory steps from the previous “Preparation” section, now a reassembled application is probably running on his device or emulator, NetBeans is open on the computer, and debugging is somewhere after calling
System.load(...)
or
System.loadLibrary(...)
, but before the first call to the native method. And the reader is already aware of which library in which JNI functions correspond to which native methods. From this we begin.
Next comes step by step instructions. It was written for Windows, but I think it will work for Linux and MacOS. Please follow the instructions exactly:
- Command
abd shell
open the ADB console of your device or emulator. Find the PID of your application process using the ps
command in the ADB console. In the same console, run the command:
gdbserver :5039
where %PID%
is the PID process of your application. In response, gdbserver should output something like:
Attached; pid = %PID% Listening on port 5039
From now on, debugging in NetBeans will freeze. Those. Of course, you can click on the buttons there, but it is useless because the application that you are trying to debug in NetBeans is currently stopped under the GDB debugger. Do not panic, everything should be so! - Open a new console on your computer, run the command
adb forward tcp:5039 tcp:5039
- In the same console, run gdb from the Android NDK:
gdb libMyNativeLibrary.so
where libMyNativeLibrary.so
is the very .so library where the JNI functions of interest are located. The library should be located on your computer in a directory that for gdb is current at startup. This will open the gdb console. - In the gdb console, type the following commands:
(gdb) target remote :5039
After these manipulations, a message like this should appear in the gdb console.
Remote debugging using :5039 0x4009d58c in ?? ()
and in the ADB console (it’s still open, remember?) something like
Remote debugging from host 127.0.0.1
- In the gdb console, run
(gdb) info functions
to see the list of functions. In the list, among other functions, should be the JNI functions that interest you, something like:
0x5b5f7bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x5b5f7c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x5b5f7c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
- In the gdb console, put breakpoints to the addresses of the functions you are interested in, in our case this
(gdb) break *0x5b5f7bac (gdb) break *0x5b5f7c0c (gdb) break *0x5b5f7c1c
- Resume the execution of your application using the
c
command in the gdb console. After executing this command, debugging in NetBeans will “freeze” and you can again debug Dalvik bytecode.
Now, every time there is a call to the native method in Dalvik bytecode, like
const/high16 v8, 0x4100 invoke-static {v8}, Lmy/app/for/debug/MainActivity;->theCoolestNativeMethodEver(F)V
debugging in NetBeans will freeze, but gdb will please you with messages like
Breakpoint 1, 0x5b5f7c1c in Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver () from libMyNativeLibrary.so
That is actually it. Further
x/i $pc
,
stepi
- generally forward to debug one ARM instruction after another (remember, I said at the beginning that the ARM assembler will be needed? - well, here ...)
Underwater rocks
Oh, there are a lot of them. A whole garden of pitfalls. Here are the most memorable bugs that caught me when using GNU gdb (GDB) 7.4.1 in conjunction with GNU gdbserver (GDB) 7.4.1 on Android 4.0.3 in the Ainol Aurora device (
the old one ):
- If after some time your gdb falls off regularly from gdbserver by watchdog timeout, run
set watchdog 18000
in the gdb console - this should help. - Sometimes as a result of the
info functions
in the list of functions, not the addresses of functions in memory are displayed, but offsets in the .so file, for example:
0x000c0bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x000c0c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x000c0c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
In this case, check or libMyNativeLibrary.so
really lies in the directory that for gdb is current at startup, and restart gdb again with the same command gdb libMyNativeLibrary.so
. - Sometimes, as a result of the
info functions
, the list of functions displays the addresses of functions in memory and offsets in the .so file, for example:
0x5b5f7bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x5b5f7c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x5b5f7c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver 0x000c0bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x000c0c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x000c0c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
Ignore offsets in the .so file, put breakpoints on the addresses of functions in memory. - If the
break
command for function names works fine, you are lucky, if not ... well, actually it doesn't work for me, so in this article I put breakpoints on the function addresses. set stop-on-solib-events
may not work. It does not work for me.- From time to time you will see the words
Cannot access memory at address 0x1
. Ignore.
I am sure that this is not a complete list of glitches, which is fraught with the debugging of native methods without source codes, and that other researchers will find completely different, unique glitches that have not caught me. If anyone else finds - please share in the comments. Also in the comment please ask questions and / or make technical amendments to the text. I will try to answer as quickly as possible, but if I’m stupid, please be patient. I will try to answer all.
Happy debugging!
PS A request to the minus article unsubscribe in the comments what exactly did not like. I will try to fix it if possible.
PPS The article is a big plus, so the topic of debugging and reversing engineering is interesting to people and I will share my experience further. What were the disadvantages of the article in the beginning I did not understand - but oh well, let's write off the random fluctuations of habrasils.
PPPS If anyone needs technical assistance on the topic of the article - contact me, maybe I can help.