动态防护技术

动态防护技术是面向App运行过程的防护,一方面可以通过App动态加固技术来实现,比如程序数据加解密保护、进程防动态调试保护、运行日志输出保护、用户信息输入保护等;另一方面需要开发者在App实现方案中采用保护技术,如客户端和服务器端通信过程的保护等。此次仅介绍App动态防护技术的实现思路,不讨论具体的实现方案细节。

1.10.1防调试

在对Android App进行逆向破解的过程中,动态调试是最常用也是最有效的方式。动态调试攻击是指攻击者利用调试器跟踪目标程序运行,查看、修改内存代码和数据,分析程序逻辑,进行攻击和破解等行为。比如对于金融类app,动态调试可以修改APP业务操作时的账号、金额等数据。相应的App防调试安全要求在之前章节已有描述,常用的动态调试工具有IDAPro gdb等,开发者通过提高App防调试的能力,能够增加App的破解难度。

由于Android平台没有禁止用于调试的ptrace系统调用,恶意程序在得到ROOT权限后,可以使用系统API对目标进程的内存、寄存器进行修改,达到执行shellcode、注入恶意模块的目的。在注入恶意模块后,攻击者就可以动态获取内存中的各种敏感信息,例如用户名、密码等。除了 ptrace系统调用外,Android 系统中的proc 文件也暴露了大量的程序进程信息,能够实现对内存的读写操作,因此对程序进行反调试的保护是非常有必要的。攻击者常常利用动态调试工具以及挂钩系统函数跟踪程序的执行流程,分析程序执行逻辑,查看并修改内存中的代码和数据。因此,本节主要介绍防调试和防挂钩方法的基本思路。

1.防调试方法

在Linux系统中,一个进程只能被附加一次,因此可以让App进程复制出子进程,然后对自已进行附加,这样就可以防止调试器在App运行过程中附加到App的进程中。

当一个进程被跟踪时,对应的进程status文件中的TracerPid字段会发生变化。当进程没有被跟踪或者调试时,TracerPid字段的默认值是0;如果进程被跟踪或者被调试,则该字段的值为跟踪进程的pid值。通过轮询/proc/app_pid/status文件,读取TracerPid的字段值,可以判断App当前是否被调试跟踪。以下是检查TracerPid的示例代码:

char file [MAX_ _LEN], line[MAX_ _LEN];snprintf (file, MAX_ LEN -1, "/proc/%d/status",getpid());/*这些地方都需要进行下列检查
/proc/<pid>/status
/proc/<pid>/task/ <chdpid>/status,
/proc/<pid>/stat
/proc/ <pid>/task/ <chdpid>/stat
/proc/<pid> /wchan
/proc/ <pid>/task/ <chdpid> /wchan
*/FILE *fp = fopen (file, "r");while (fgets (line, MAX _LEN -1,fp)) {if (strncmp (line, "TracerPid:", 10) == 0) {      if (0 != atoi (&line[10])) {//进程处于被调试之中
       }     break;
   }
}
fclose (fp);

利用Linux系统的inotify机制监测/proc目录,利用监听函数监测/proc目录是否阻塞在监听处,一旦有调试进程通过/proc文件系统对App进程内存进行读写操作,监听函数就会停止阻塞, 就可以判定有调试进程正在通过/proc文件系统。以下是inotify 监视/proc文件系统中maps是否被访问的示例代码:

bool check_ inotify(){    int ret, i;    const int BUF_SIZE = 2048;    char buf[BUF_ SIZE];    int fd, wd;
    fd_ set readfds;
    fd = inotify_init();
    sprintf(buf, "/proc/%d/maps", getpid());
    wd = inotify_add_watch(fd, buf, IN_ ALL_EVENTS);    if(wd>=0){           while (1) {
                  i= 0;                  FD_ ZERO(&readfds);                  FD_ SET(fd, &readfds);
                  ret = select(fd + 1, &readfds, 0, 0, 0); if (ret == -1) {                     break;
                 }                 if (ret) {                     while (i < read(fd, buf, BUF _SIZE)) {                           struct inotify_event *event = (struct inotify_event *) &buf[i]; 
                         if ((event->mask & IN_ACCESS) || (event->mask & IN _OPEN)) {                         // maps被访问
                         return true;
                           }
                        i+=sizeof(struct notify_event)+event->len;
                          }
                 }
         }
   }
   inotify_ rm _watch(fd, wd);
   close(fd);
}

2.防挂钩方法

常用的挂钩工具有Xposed和CydiaSubstrate。二者的挂钩原理相似,都要替换系统中的 /system/bin/app_process文件。在系统启动过程中,init 进程通过app_process文件启动Zygote进程,从而加载Xposed劫持虚拟机的文件,实现注入Zygote进程的效果。Android系统在运行App时,会通过Zygote进程复制一个进程运行APP,那么APP进程就有了Zygote进程的完整复制, 包括Xposed的相关文件,Xposed 也就完成了App进程注入的工作。

当App进程被Xposed注入,或者运行在一个有被注入风险的移动设备上时,App的进程内存中就应该包含Xposed的相应注入文件。查看/proc/App_pidmmaps 文件,读取mmaps文件中的信息,判断App进程中是否加载了Xposed的相关文件,就可以判断App是否被挂钩。

本节的App防护措施能够解决安全测试过程中出现的防调试和防注入问题。

1.10.2 防日志输出

通过对调试日志输出的函数进行挂钩拦截,按照预设的等级允许或者阻止调试日志的输出,即可实现防日志输出的效果,统一关闭App、第三方SDK插件和so文件产生的所有调试日志。所有的Android App调试日志输出最后都会运行liblog.so中的_ android_log_ print/write()函数。函数原型如下:

int  _android_log_ print (int prio,const char *tag, const char *fmt, …);
int _android_log_write (int prio, const char *tag, const char *text);

prio泰示输出日志的优先级,tag表示日志的TAG,其他的参数为调试日志输出的字符串。若对上述函数进行挂钩,可以根据参数完成过滤,从而统一关闭调试日志。

本节的App防护措施能够解决安全测试过程中出现的日志泄露问题。在App数据生命周期中的数据处理阶段,如果APP发布时没有及时删除敏感函数功能的调试日志,就会导致后台打印日志泄露用户敏感数据或泄露代码逻辑。通过加固日志保护处理后,在APP运行期间,后台将没有敏感日志同步输出。

1.10.3安全软键盘

安全软键盘通过白盒加密技术对密钥进行保护,使用严格的加密方式对用户输入的信息进行安全处理,架构如图所示。

加固的实现过程如图所示。

安全软键盘加固的具体实现流程如下。

1)生成键盘方案:初始化键盘信息, 随机分配学符位置。加载字符对应的图片资源完成键盘的演示。

2)加密键盘输入:对通过交互界面输入的密码进行动态SM4或AES加密,点击完成后可以输出密文发送至后台解密。

(3)后台解密:后台获取到密文后,调用解密函数对加密数据进行SM4或AES解密,解密返回明文的键盘输入。

本节的App防护措施能够解决安全测试过程中出现的防系统键盘泄露敏感数据、防本地数据存储安全和防录屏/截屏的风险问题。

1.10.4防界面劫持

防界面劫持的核心是判断当前App运行的界面是否被覆盖。在Android 5.0之前,开发者可以使用ActivityManager中提供的方法操作栈,但是Android5.0之后,谷歌公司出于保护用户隐私的考虑,弱化了这个接口。ActivityManager只能管理App自身的栈,如果想管理其他App的栈,需要用户主动授权,而很多手机厂商对这个权限做了进一步限制,只能为系统App等特定的App授权。因此,Android 5.0之后就无法再通过管理栈的方式实现界面防劫持,开发者只能通过App自身的Acticity生命周期管理来达到界面防劫持的效果。

在App运行时,当运行界面发生视图焦点变化,即当前Activity 变为onPause 状态时,App应能捕获当前界面状态变化并进行相应的分析,提示用户可能出现的安全风险,同时尝试对系统输人入法进行监视,防止恶意程序诱导用户输入敏感信息。App防界面劫持的加固实现过程如下。

(1)在受保护的界面启动时,获取当前正在运行的系统后台进程CPU、内存占用信息并存储,同时启动后台服务,在服务中判断当前界面是否失去焦点。

(2)一旦检测到当前界面失去焦点,立即获取当前正在运行的程序信息,如果正在运行的程序是本App,当没有视图焦点时,比对之前获取的系统后台进程镜像和现在的系统后台进程信息,如果存在恶意行为则提示风险。

(3)如果当前运行的程序不是本App,而此时程序界面失去焦点,则获取当前正在运行的程序,提示风险。

(4)如果当前界面失去焦点,经上述检测未发现风险,则启动键盘行为检测;若发现覆盖界面短时间内出现键盘显示行为,诱导用户输入信息,则提示风险。

实现App防界面劫持的示例代码如下:

public class MyBaseActivity extends Activity {         private static Set set = new HashSet();         private boolean b= true;
         @override 
         protected void onCreate(Bundle savedInstanceState) {             if (set.size()==0) {                  try { InputStream is = getAssets(). open( "white. txt”);
                        BufferedReader br = new BufferedReader(new InputStreanReader(is,"UTF-8"));
                        while (true){
                                String write= br.redline();
                                if (write == null){
                                    break;
                                }
                                set . add(write);
                          }
                          is.close();
                          br .close();
                  }
                  catch (IOException exp) {
                          exp. printStackTrace));
                  }
           }
          set . add( getPackageName());
          super .onCreate( savedInstanceState);
   }
   protected void onPause() {
          if(b){
               String packagename=((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getRunningTask(1).get(0).topActivity.getPackageName();
              if(!set. contains( packagename)){
                     String msg=“疑似界面劫持攻击,请小心使用,并查杀病毒!”;
                     Toast toast = Toast.makeText(getApplicationContext(), msg,Toast.LENGTH_LONG); 
                     toast.setGravity(17, 0,0);
                     toast. show(); 
                     b = false;
                    }
               }
               b = true;
               super. onPause();
         }
 }

本节的App防护措施能够解决安全测试过程中出现的界面劫持问题。由于Android界面每次只能显示一个Activty,攻击者利用这个机制,使用伪造的仿冒界面或透明界面覆盖在正常界面上,诱骗用户输入敏感信息,就会导致信息泄露。通过防界面劫持加固,App一旦发现有其他页面覆盖或者程序转向后台,将会给出界面被劫持的安全风险提示,让用户提高警惕,以免上当受骗。

1.10.5防篡改

App篡改是指这样一种攻击过程: 攻击者通过逆向分析工具对APP 进行反编译后,在APP程序内添加或修改代码、替换资源文件、修改配置信息、更换图标、植入非法代码,再对篡改后的代码进行二次打包,生成各种盗版、钓鱼APP。尤其对于金融类APP来说,添加攻击代码可能导致用户登录账号、支付密码、短信验证码等敏感信息泄露,攻击者进而实施修改转账账号和金额等犯罪行为,因此非常有必要对APP进行防篡改保护。根据防篡改技术实现方式的不同,App防篡改主要有签名校验和完整性校验两种实现方式。

1.签名校验防篡改

App的数字签名可用于验证开发者身份,一旦攻击者对App进行修改并重新打包,App的数字签名一定会发生变化。因此,开发者可以获取App中证书文件的证书指纹MD5并存储在程序中,当App运行时,读取证书指纹MD5并与当前apk文件中的证书指纹MD5进行比对。如果比对结果一致,就说明App未被篡改,否则说明App已被篡改。

2.完整性校验防篡改

我们还可以采用完整性校验技术对App的安装文件进行哈希校验,再对文件内容进行交叉校验,在App中对校验数据及校验代码做加密保护。

当一个App被防篡改加固保护后,一旦攻击者对加固后的App 进行任何修改,App 就会在运行时执行校验代码,对照存储的校验数据检测App的内容是否被篡改,如果校验不通过,则表示App被篡改,程序将终止运行。

本节的App防护措施能够解决安全测试过程中发现的防篡改问题。App完整性校验技术可以防止攻击者反编译App、篡改源代码、修政改资源文件、添加恶意代码模块并二次打包,保护开发者利益。

1.10.6防截屏/录屏

截屏/录屏攻击指的是APP采用系统自带软键盘或者使用自身定制的软键盘,在用户使用软键盘进行按键过程中出现阴影、放大等特效功能,导致攻击者通过采用系统自带的截屏命令或录屏命令进行多次截屏/录屏,达到获取用户软键盘输入信息的目的。攻击者使用的截屏命令和录屏命令如下所示:

screenshot /data/local/tmp/test.jpg
screenrecord -time-limit 4 test. mp4

防截屏/录屏攻击的核心是禁止攻击者进行截屏/录屏操作,关闭程序的截屏/录屏功能,方法是在Activity中onCreate()方法的Layout初始化部分加入FLAG _SECURE实现,示例代码如下所示:

public class FlagSecureTestActivity extends Activity {         @Override
         public void onCreate(Bundle savedInstanceState) {                 super.onCreate(savedInstanceState);
                 getWindow().setFlags(LayoutParams . FLAG_ SECURE, LayoutParams . FLAG_SECURE);
                 setContentView(R. layout .main);
            }
}

本节的App防护措施能够解决安全测试过程中出现的防截屏/录屏问题。

1.10.7模拟器检测

Andoid模拟器本来是用于模拟真机的功能,让开发者即使在没有真机的环境中也能调试App的各项功能。但是,真机一般是没有经过ROOT的环境,而模拟器是已经ROOT的运行环境。因此,攻击者常常使用模拟器来调试和破解正常的App。

模拟器检测是指App在运行时结合模拟器的特点,检测运行环境是否为模拟器,并根据检测结果判断是否继续执行。模拟器的主要检测方法有以下3种

(1)检测设备的IMEL、MAC值、Device_Id以及Tlephoy_Sevive中的运营商、国家等信息,判断运行环境是否为模拟器。但是目前这部分数据不能作为判新的唯一依据,部分模拟器已经可以修改IMEI、设备信息、运营商、手机号等信息,如夜神模拟器、逍遥模拟器等。

2)检测设备的监牙状态信息来判断运行环境是否为模报器。通过系统服务获取蓝牙状态信息,如果蓝牙设备存在,但蓝牙的名称为null,说明当前运行环境不是模拟器环境,反之为模拟器环境,相关示例代码如下:

Public coolen readBlueTooth(){
         BluetoothAdapter readbt = BluetoothAdapter .getDefaultAdapter(); 
         if (readbt == null) {
                return true;
          } else {
               String name = readbt.getName();
               if (TextUtils .isEmpty(name)) {
                    return true;
               } else {
                      return false;
               }
         }
}

当然,开发者还可以增加对传感器的检测,例如检测光传感器。不过,温度、压力等传感器不能作为判断的依据,因为部分设备上不存在温度和压力传感器。

(3)根据模拟器的特征来判断运行环境是否模拟器,示例代码如下:

public boolean checkEmu() {
       if (Build. FINGERPRINT . startsWith( " Emulator")) return true;
       if (Build. MODEL. contains(" Emulator")) return true;
       if (Build. SERIAL. equalsIgnoreCase(" android")) return true;
       if (Build. BRAND. startsWith("generic")) return true;
        return false;
}

由于Android设备的严重碎片化,建议组合使用多个规则来判断运行环境是否为模拟器。

本节的App防护措施能够解决安全测试过程中出现的模拟器检测问题。如果发现在模拟器环境中运行,则弹窗提示用户App在不安全环境下运行,或者禁止在模拟器中安装或运行。

1.10.8 应用多开检测

应用多开主要用于游戏类和聊天类App,目的是使游戏玩家能够同一时间开启多个终端,实现快速升级。也有同一手机上开启多个终端,登录不同的账户,实现原理类似。应用多开检测用于识别在App运行期间是否存在应用多开的情况。下面介绍 5种检测应用多开的方法。

(1)检测files目录路径

App的私有目录是“/data/data/包名/”或“data/user/用户号/包名”,通过Context.getFilesDir()方法可以拿到私有目录下的files日录。在多开环境下,获取到的目录会变为“/data/data/多开App 的包名/xxxx”或“/data/user/用户号/多开App的包xxxx。可以通过比对files目录的数量判断应用是否多开。

(2)检测是否能在

/data/data/<package_name>/目录下创建文件

在Adroid系统中,除了ROOT管理员外,只有APP自身有权限在它沙箱中创建文件,如不能创建文件,则可以判断为应用多开环境。

(3)ps检测

原理为UID是系统分配的一个应用标识,每个App对应一个UID,应用虚拟化并未真正安装App,因此UID必定和宿主一样通过ps命令查看UID所对应的包名是否为当前App的包名,如不是,则可能为应用双开环境。通过ps命令加包名过滤得到的结果如下所示:

//正常情况下
u0_ a148 8162 423 1806036 56368 SyS_ epoll+ 0 S com. package
//多开环境下
u0_ a155 19752 422 4437612 62752 SyS_ _epoll+ 0 S com.package
u0_ a155 19758 422 564234 54356 SyS_ epoll+ 0 S duokaicom. package

(4)应用列表检测

多开App克隆了原始App,并具有同样的包名。当使用克隆App时,会检测到原始App的包名和多开App的包名一样,因此可以通过获取系统已安装的App列表来查看是否有重复的包名,如果有,则为应用多开环境。

(5) maps检测

原理是通过/proc/self/maps信息进行检测,如果当前环境为应用多开环境,则系统会加载一些多开的so文件到内存空间,示例代码如下:

Private boolean checkpkgs(List<String> pkgs){
      try{
           BufferedReader br =new BufferedReader(new InputStreamreader(new               FileInputStream(“/proc/self/maps")));
           String line:
           While((line = br.readLine())!=null){
                 for(int I=0;i<pkgs.size();i++){
                       return line.contains(pkgs.get(i));
                 }
            }
      }catch (Exception e){
              e.printStackTrace();
      }
       return false;
}

本节的App防护措施能够解决安全测试过程中出现的应用多开问题。App运行期间如果发现应用多开的情况,则弹窗提示用户App已启动,不要重复运行,进而保护App的安全。

1.10.9ROOT环境检测

手机ROOT给用户带来了非常大的自主权,让用户可以删除系统应用,查看并修改程序的运行信息,但与此同时,也给恶意软件大开方便之门,给设备信息安全带来了极大的挑战。目前许多App在启动时会进行ROOT环境检测,防止App在已经ROOT的手机环境中运行,如果发现设备已经被ROOT,会向用户提示运行环境存在安全风险,终止App的运行。

除了检测ROOT工具的安装路径,包名带有SU、supersu或superuser等关键词外,下面再介绍3种ROOT环境检测的方法。

(1)检查su命令是否存在

通常要获取ROOT权限,是使用su命令来实现的,因此可以通过检查这个命令是否存在来判断运行环境是否ROOT。

(2)检查Andorid属性

检查ro.debuggable、ro.secure这两个属性是否为true:

adb shell getprop ro. debugable
adb shell getprop ro.secure

如果以上两个属性为true,说明ApP所运行的环境很可能是ROOT环境。

(3)检查特定路径是否有写权限

具体的路径包括:

/system、/system/bin、/system/sbin、/system/xbin、/vendor/bin、/sys、/sbin、 /etc、 /proc、 /dev。

通过mount命令确认对应分区的权限是否为“rw” 。

adb shell mount | grep -w /sysfs on /sys type syfs (rw,seclabel,relatime)

不过值得注意的是,使用上述方法检测ROOT环境会存在一些误报, 开发者需要综合考虑。

本节的App防护措施能够解决安全测试过程中出现的ROOT环境检测问题。综合使用多种方法检测ROOT环境,如果发现在ROOT环境中运行,则弹窗提示用户App在不安全环境中运行,或者禁止安装并运行。

1.10.10挂钩框架检测

挂钩框架检测指的是防止攻击者利用开源的挂钩框架开发自定义的攻击模块,对程序进行挂钩算法破解、敏感API监控、网络通信过程监控等攻击。下面介绍4种检测Xposed的方法。

(1)通过检测Xposed的特征判定Xposed是否存在,示例代码如下:

StackTraceElement[] l = new Thread().getStackTrace();
for (int i= 0; i< l.length; i++) {
      if (l[i].getClassName(). contains("xposed")) return true;
}

(2)通过classloader检查系统中是否存在两个Xposed特征类:

private static final String XPOSED_ HELPERS = "de. robv.a android. xposed. XposedHelpers";
private static final String XPOSED_BRIDGE = "de.robv. android.xposed. XposedBridge";

(3)查看当前进程中是否存在和Xposed相关的动态库(so 文件):

File file = new File("/system/lib/libxposed_ art.so");
file files = new File("/system/lib64/libxposed_art.so");

(4)查看是否安装Xposed包。这种方式最简单,也最容易被绕过:private static final String pkgXposed=” de.robv. android.xposed.installer”;

本节的App防护措施能够解决安全测试过程中出现的防Xposed 问题。使用多种方法检测 Xposed 框架,如果发现App运行的设备中已安装Xposed框架,则弹窗提示用户App在不安全环境中运行,或者禁止安装运行。

1.10.11 小结

本章介绍了App的动态防护技术,包括防调试、防日志输出、安全软键盘、防界面劫持、防篡改、防截屏/录屏、模拟器检测、应用多开检测、ROOT环境检测、挂钩框架检测共10种动态防护技术的基本原理和简单实现,帮助开发者应对在程序代码安全、本地交互安全、本地数据安全等安全测试工作中发现的问题。

经过安全加固的App在安全防护能力上有了很大的提 升,但是大量的代码隐藏、代码加密、防调试等App安全防护技术也给App 的安全测试工作带来了很大的挑战。要对加固后的App开展安全测试工作,还需要开发者和安全测试工程师具备一定的脱壳能力。

文章转自公众号: Tide安全团队