Gh0stbo · 2016/01/21 and

0 x00 profile


Android APK can easily be reverse-engineered to decompile, leaving its code completely exposed to attackers, exposing APK to the risk of cracking, modifying software logic, inserting malicious code, and replacing advertiser ids. We can protect APK in the following ways.

0x01 Confused Protection


Obfuscation is a technique used to hide the intent of a program. It can make it harder to read code and make it harder for an attacker to fully control the internal implementation logic of an app, thus making reverse engineering and cracking more difficult and preventing intellectual property theft.

The code obfuscation technique mainly does the following:

  1. Through the code class name, function name replacement to achieve code confusion protection
  2. Simple logical branch obfuscation

There are a number of third-party apps that can be used to obfuscate our Android apps. Common ones are:

  • Proguard
  • DashO
  • Dexguard
  • DexProtector
  • ApkProtect
  • Shield4j
  • Stringer
  • Allitori

These obfuscators work at different levels in the code. The general process of Android compilation is as follows:

#! bash Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)Copy the code

Some obliquors work directly on Java source code before compilation, some on Java bytecode, and some on Dalvik bytecode. But it’s all about the Java layer.

Compared to the Dalvik VIRTUAL machine level obfuscation, there are few obfuscation options for native languages (C/C++), with the obfuscator-LLVM project being a notable exception.

The advantage of code obfuscation is that it makes the code readable and difficult to fully control the code logic. You can compress your code to make it smaller. However, it also has the following disadvantages:

  1. Can’t really protect code from decompilation;
  2. It is ineffective in dealing with dynamic debugging reverse analysis;
  3. Mechanisms to validate local signatures can easily be bypassed.

In other words, code obfuscation does not effectively protect the application itself.

www.jianshu.com/p/0c23e0a88…

0x02 Secondary Packing Prevention


2.1 Apk signature Verification

Every software needs to be signed by the developer when it is released. However, the key file used for signature is unique to the developer, and it is impossible for the cracker to have the same key file. Therefore, the method of signature verification can be used to protect APK. The Android SDK PackageManager class getPackageInfo() method can be used for software signature detection.

#! java public class getSign { public static int getSignature(PackageManager pm , String packageName){ PackageInfo pi = null; int sig = 0; Signature[]s = null; try{ pi = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); s = pi.signatures; sig = s[0].hashCode(); }catch(Exception e){handleException();}catch(Exception e){handleException(); } return sig; }}Copy the code

Main program code reference:

#! java pm = this.getPackageManager(); int s = getSign.getSignature(pm, "com.hik.getsinature"); if(s ! ORIGNAL_SGIN_HASHCODE){// Compare the current and embedded signature hashcode to be consistent system.exit (1); // If not, force program exit}Copy the code

2.2 Verifying Dex Files

Recompiling apk means recompiling the classes.dex file. The hash value of the generated classes.dex file has changed, so we can check the hash value of the classes.dex file to see if APK has been repackaged.

  1. Read the application installation directory/data/app/xxx.apkTo determine whether the client has been tampered with, compare the classes.dex file to the classes.dex hash in the.
  2. Read the application installation directory/data/app/xxx.apkFile manifest.mf in the meta-INF directory. This file records the hash values of all files in the APK package. Therefore, you can read this file to obtain the hash values of the classes.dex file. Comparing this value to the classes.dex hash value at software release time can determine whether the client has been tampered with.

To prevent cracking, the classes.dex hashes at the time of release should be stored on the server side.

#! java private boolean checkcrc(){ boolean checkResult = false; long crc = Long.parseLong(getString(R.string.crc)); // Get CRC value embedded in character resource ZipFile zf; try{ String path = getApplicationContext().getPackageCodePath(); Zf = new ZipFile(path); ZipEntry ze = zf.getentry ("classes.dex"); // get classes.dex long CurrentCRC = ze.getcrc (); // Calculate the CRC value if(CurrentCRC! // checkResult = true; } }catch(IOException e){ handleError(); checkResult = false; } return checkResult; }Copy the code

In addition, since reverse C/C++ code is much more difficult than reverse Java code, key code parts should be written using Native C/C++.

0 x03 SO protection


Android So is implemented in C/C++ code, which is much harder to decomcompile than Java code, but still very easy for experienced jailbreakers. Application critical functions or algorithms will be implemented in SO. If SO is reversed, application critical code and algorithms will be exposed. For the protection of SO, compiler optimization technology, stripping binary files and other ways can be used, and the open source SO reinforcement shell UPX can be used for reinforcement.

Compiler optimization techniques

Using compiler optimization techniques to hide core algorithms or other complex logic can help obfuscate the target code so that it is not easily decomcompiled by an attacker, making it more difficult for an attacker to understand a particular code. Such as using LLVM confusion.

Stripping binary files

Stripping native binaries is an effective way for attackers to take more time and a higher skill level to see the implementation of your application’s underlying functionality. Stripping a binary file means deleting the symbol table of a binary file so that an attacker cannot easily debug or reverse apply it. On Android, you can use technologies already used on GNU/Linux, such as Sstriping or UPX.

When UPX shells files, the software version and other related information will be written into the shell, attackers can check the shell information through static disassembly, and then find the corresponding sheller to remove the shell, making the attack easier. Therefore, we must delete this information in the UPX source code, recompile and then shell, as follows:

  1. Shell the file with the original version.
  2. Using the IDA disassembler shell file, look for the UPX shell characteristic string in the context of the disassembler file.
  3. Search the UPX source code for these character strings and delete them one by one.

www.nowsecure.com/resources/s…

0x04 Resource File Protection


If resource files are not protected, the application has two security risks:

  1. Through the resource location code, it is convenient for the application to decrypt and decompilate apK to obtain the source code. Through the resource file or the ID of the key string, the key code location is located, which provides convenience for reverse cracking applications.
  2. Replace resource files with pirated application “If you can see something, you can copy it”. Resources in Android applications, such as images and audio files, are easy to copy and steal.

Consider storing it encrypted as a binary, then loading it, decrypting it into a byte stream, and passing it to the BitmapFactory. Of course, this adds complexity to the code and has a slight performance impact.

However, the resource file is globally readable, and even though it is not packaged in APK but downloaded at first run time or when needed, it is not stored on the device, it is still easy to get the resource URL through network packet sniffing.

0x05 Reverse debugging techniques


5.1 Limiting debugger connections

Applications can prevent debuggers from being attached to the process by using specific system apis. By preventing debugger connections, an attacker’s ability to interfere with the underlying runtime is limited. An attacker must first circumvent debugging restrictions in order to attack an application from below. This further increases the complexity of the attack. Android applications should set Android:debuggable= “false” in their manifest so that they can’t be easily manipulated by attackers or malware at runtime.

5.2 Trace inspection

An application can detect whether it is being tracked by a debugger or other debugging tool. If traced, an application can perform any number of possible attack responses, such as discarding encryption keys to protect user data, notifying server administrators, or other types of self-protective responses. This can be debugged by checking the process status flag or by using other techniques such as comparing the return value attached to pTrace, checking the parent process, blacklisting the debugger process list, or calculating the difference in running time.

  • Github.com/obfuscator-…
  • www.nowsecure.com/resources/s…

A. Check the parent process

Typically, when we debug using GDB, we do it through GDB. This is to start GDB, fork out the child process and execute the target binary. Therefore, the parent process of the binary is the debugger. We can determine if the debugger forked by checking the parent process name. Sample code is as follows

#! cpp #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char buf0[32], buf1[128]; FILE* fin; snprintf(buf0, 24, "/proc/%d/cmdline", getppid()); fin = fopen(buf0, "r"); fgets(buf1, 128, fin); fclose(fin); if(! strcmp(buf1, "gdb")) { printf("Debugger detected"); return 1; } printf("All good"); return 0; }Copy the code

Here we get the PID of the parent using getppID, and then the /proc file system gets the command content of the parent and checks whether the parent is GDB by comparing strings. The actual running results are as follows:

B. Check the current running process

For example, check the android_server process. This detection can be bypassed by simply changing the name of android_server

#! java pid_t GetPidByName(const charchar *as_name) { DIR *pdir = NULL; struct dirent *pde = NULL; FILEFILE *pf = NULL; char buff[128]; pid_t pid; char szName[128]; Pdir = opendir("/proc"); if (! pdir) { perror("open /proc fail.\n"); return -1; } while ((pde = readdir(pdir))) { if ((pde->d_name[0] < '0') || (pde->d_name[0] > '9')) { continue; } sprintf(buff, "/proc/%s/status", pde->d_name); pf = fopen(buff, "r"); if (pf) { fgets(buff, sizeof(buff), pf); fclose(pf); sscanf(buff, "%*s %s", szName); pid = atoi(pde->d_name); if (strcmp(szName, as_name) == 0) { closedir(pdir); return pid; } } } closedir(pdir); return 0; }Copy the code

C. Read the process status (/proc/pid/status)

The State value T indicates the debugging status. The TracerPid value indicates the PID of the debugging process. In non-debugging cases, the State value is S or R, and TracerPid is 0

From this, we can check the value of TracerPid in the status file to see if it is being debugged. Example code is as follows:

#! cpp #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { int i; scanf("%d", &i); char buf1[512]; FILE* fin; fin = fopen("/proc/self/status", "r"); int tpid; const char *needle = "TracerPid:"; size_t nl = strlen(needle); while(fgets(buf1, 512, fin)) { if(! strncmp(buf1, needle, nl)) { sscanf(buf1, "TracerPid: %d", &tpid); if(tpid ! = 0) { printf("Debuggerdetected"); return 1; } } } fclose(fin); printf("All good"); return 0; }Copy the code

The actual running results are as follows:

It is worth noting that the /proc directory contains a lot of information about the process. In this case, we read the status file. In addition, we can obtain information about the process, including the running status, from the /proc/self.stat file.

D. read/proc/%d/wchan

In the following figure, the first red box value is the undebug value and the second red box value is the debug value:

#! cpp static int getWchanStatus(int pid) { FILEFILE *fp= NULL; char filename; char wchaninfo = {0}; int result = WCHAN_ELSE; char cmd = {0}; sprintf(cmd,"cat /proc/%d/wchan",pid); LOGANTI("cmd= %s",cmd); FILEFILE *ptr; if((ptr=popen(cmd, "r")) ! = NULL) { if(fgets(wchaninfo, 128, ptr) ! = NULL) { LOGANTI("wchaninfo= %s",wchaninfo); } } if(strncasecmp(wchaninfo,"sys_epoll\0",strlen("sys_epoll\0")) == 0) result = WCHAN_RUNNING; else if(strncasecmp(wchaninfo,"ptrace_stop\0",strlen("ptrace_stop\0")) == 0) result = WCHAN_TRACING; return result; }Copy the code

E.p trace itself or fork child processes to ptrace each other

#!cpp
if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {  
printf("DEBUGGING... Bye\n");  
return 1;  
}  
void anti_ptrace(void)  
{  
    pid_t child;  
    child = fork();  
    if (child)  
      wait(NULL);  
    else {  
      pid_t parent = getppid();  
      if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)  
            while(1);  
      sleep(1);  
      ptrace(PTRACE_DETACH, parent, 0, 0);  
      exit(0);  
    }  
}
Copy the code

F. Set the maximum running time of the program

This approach is often seen in CTF competitions. Due to breakpoints, checking and modifying memory during debugging, the running time is often much higher than the normal running time. So, if a program is running too long, it is probably being debugged.

Specifically, the timer is set through alarm when the program is started and aborted when it arrives. Example code is as follows:

#! cpp #include <stdio.h> #include <signal.h> #include <stdlib.h> void alarmHandler(int sig) { printf("Debugger detected");  exit(1); } void__attribute__((constructor))setupSig(void) { signal(SIGALRM, alarmHandler); alarm(2); } int main(int argc, char *argv[]) { printf("All good"); return 0; }Copy the code

In this case, we set the timing when the program starts by __attribute__((constructor)). In practice, when we use GDB to break the main function and resume execution after a while, SIGALRM is triggered and the debugger is detected. As shown below:

This, by the way, can easily be bypassed. We can set how GDB handles signal, and if we choose to ignore SIGALRM instead of passing it to the program, alarmHandler will not be executed, as shown below:

G. Check the fileDescriptor opened by the process

As mentioned in 2.2, if the process being debugged is started by GDB, it is obtained by GDB process fork. When fork is called, the parent process’s fd(file Descriptor) is inherited by the process. Since GDB tends to have multiple FDS open, if a process has more FDS, it may inherit from GDB, i.e. the process is being debugged.

Specifically, the FDS owned by the process are listed under /proc/self.f/. So our sample code looks like this:

#! cpp #include <stdio.h> #include <dirent.h> int main(int argc, char *argv[]) { struct dirent *dir; DIR *d = opendir("/proc/self/fd"); while(dir=readdir(d)) { if(! strcmp(dir->d_name, "5")) { printf("Debugger detected"); return 1; } } closedir(d); printf("All good"); return 0; }Copy the code

Here, we check to see if /proc/self.f/contains fd as 5. Since fd is numbered from 0, a value of 5 indicates that six files have been opened. If the program is running properly, it will not open this much, so it is used to determine whether it is debugged. The running result is shown in the following figure:

H. prevent dump

Using Inotify mechanism, /proc/pid/mem and /proc/pid/pagemap files are monitored. The inotify API provides an event mechanism for monitoring file systems, which can be used to monitor individual files, or to monitor directories. For details, please refer to: man7.org/linux/man-p…

Pseudo code:

#! cpp void __fastcall anitInotify(int flag) { MemorPagemap = flag; charchar *pagemap = "/proc/%d/pagemap"; charchar *mem = "/proc/%d/mem"; pagemap_addr = (charchar *)malloc(0x100u); mem_addr = (charchar *)malloc(0x100u); ret = sprintf(pagemap_addr, &pagemap, pid_); ret = sprintf(mem_addr, &mem, pid_); if ( ! MemorPagemap ) { ret = pthread_create(&th, 0, (voidvoid *(*)(voidvoid *)) inotity_func, mem_addr); if ( ret >= 0 ) ret = pthread_detach(th); } if ( MemorPagemap == 1 ) { ret = pthread_create(&newthread, 0, (voidvoid *(*)(voidvoid *)) inotity_func, pagemap_addr); if(ret > 0) ret = pthread_detach(th); } } void __fastcall __noreturn inotity_func(const charchar *inotity_file) { const charchar *name;[email protected]
      signed int fd;[email protected]
      bool flag;[email protected]
      bool ret;[email protected]
      ssize_t length;[email protected]
      ssize_t i;[email protected]
      fd_set readfds;[email protected]
      char event;[email protected]name = inotity_file; memset(buffer, 0, 0x400u); fd = inotify_init(); inotify_add_watch(fd, name, 0xFFFu); while ( 1 ) { do { memset(&readfds, 0, 0x80u); } while ( select(fd + 1, &readfds, 0, 0, 0) <= 0 ); length = read(fd, event, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) { if ( ! ret && ! flag ) { i = 0; do { inotity_kill((int)&event); i += *(_DWORD *)&event + 16; } while ( length > i ); } } else { while ( *(_DWORD *)_errno() == 4 ) { length = read(fd, buffer, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) } } } }Copy the code

I. Hook read

Since memory dump calls read, a memory hook is used to check whether the read data is in the space it needs to protect to prevent the dump

J. Set a single step debugging trap

#! cpp int handler() { return bsd_signal(5, 0); } int set_SIGTRAP() { int result; bsd_signal(5, (int)handler); result = raise(5); return result; }Copy the code

www.freebuf.com/tools/83509…

0x06 Applying the Hardening Technology

There are three generations of mobile application reinforcement technology:

  • The first generation is based on class loader to achieve protection;
  • The second generation realizes protection based on method substitution.
  • The third generation is based on virtual machine instruction set to achieve protection.

First generation hardening technology: class loaders

Using bang-bang reinforcement as an example, the classloader does the following:

  1. Classes.dex is fully encrypted and put into APK resources
  2. Using dynamic hijacking of virtual machine class loading engine technology
  3. The virtual machine can load and run the encrypted classes.dex

The apK loading process has changed as follows:

After the application is started, the protected code will be started first, and the protected code will start mechanisms such as anti-debugging and integrity detection, and then the real code will be loaded.

The advantages of the first-generation hardening technology lie in that it can completely protect APK and support reverse debugging and integrity check.

The disadvantage of the first generation of hardening technology is that the classes.dex file before hardening will be fully imported into memory. You can use the memory dump tool to export the unhardened classes.dex file directly.

The second generation of hardening techniques: class method replacement

The second generation of reinforcement technology adopts the technology of class method replacement:

  1. The code of all methods in the original APK is extracted and encrypted separately
  2. The runtime dynamically hijacks the code of the parsing method in the Dalvik VIRTUAL machine and delivers the decrypted code to the virtual machine execution engine

The advantages of this technology are as follows:

  1. Each method is decrypted separately and there is no complete decryption code in memory
  2. If a method is not executed, it is not decrypted
  3. The cost of dumping code in memory is high

With gen 2 hardening, the startup process adds a procedure to parse the function code, as shown below:

Third generation hardening technology: VM instruction set

The third-generation hardening technology is based on the REPLACEMENT of the VM execution engine. The main tasks are as follows:

  1. Replace all the code in the original APK with a custom instruction format
  2. The runtime dynamically hijacks the execution engine in the Dalvik VM, using the custom execution engine to execute custom code
  3. Similar to the technology used by VMProtect on the PC

The advantages of the third generation technology are as follows:

  1. Has all the benefits of 2.0
  2. Cracking requires cracking the custom command format, which is very complicated