android逆向奇技淫巧二十七:AOSP改源码制作沙箱实现“无痕”hook和避开server端风控

  搞逆向,hook是必备的基本技能之一,常见的hook工具有xpose、frida等。这些hook框架出来的时间都有好多年了,历史悠久,用的人也多,影响范围非常大,导致app开发商也会重点检测和防御。现在规模稍微大一点的开发商都会针对xpose、frida做各种防护,导致部分场景下使用xpose、frida可能失效,无法hook,这种情况下该怎么进一步hook和trace了?

  还记得上一篇文章的分析么:既然基于android系统做开发,肯定要调用android提供的各种API,不可能啥都开发商自己实现,原因很简单:

  • 自己实现耗费时间,推迟app上线周期,可能导致被竞争的app甩开日活、月活、收入等运营数据
  • 自己实现耗费人力,只有个别大厂才能承担这些高额的成本

   上一篇是用frida hook了操作系统底层的库和API,既然android是开源的,是不是能直接把hook的代码写死在操作系统层面了?这样hook就不再需要用到frida、xpose,上层的app也无从检测,岂不完美?

  1、网络抓包

  (1)基于charles/fidder/burpsuit等抓包软件。APP为了防止被抓包,使用的功能之一是只信任系统内置的证书(可以在“设置->安全和隐私->受信任的凭据”查看系统和用户证书),不信任用户安装的证书,然而逆向人员手动安装的抓包软件证书只能位于用户区,无法进入系统区,自然是无法骗过APP检测的,这可咋整了?

   证书不是从天上掉下来的,也是操作系统厂家在出厂时内置的;既然操作系统厂家都可以内置,那么逆向人员拿到源码后不也可以内置证书么?其实很简单,把证书放在AOSP源码目录下的“system/ca-certificates/files”目录下即可!看下图第一个证书,就是我放置的charlse证书(我用的是kali,貌似没能识别出来charlse的类型......)!

        

  在正式抓包时,charlse会冒充server给client发证书,一般情况下是能够骗过client的,但有些client比较鸡贼,做了双向认证,即client也要给server发送证书来让server验明正身!这种情况下如果还想用软件抓包,必须找到client的证书导入charlse。现在面临的第一个问题就是找到client的证书!既然client既然要发给server,第一件事肯定是从磁盘把证书读到内存,肯定需要用到File函数,所以hook File类大概率是能找到client证书的路径。这都不算啥,更鸡贼的是部分app还给client的证书设置了密码,光找到证书还不够,没有密码也没法导入charles安装,密码在哪找了?既然安装证书要密码,肯定有相应的API来验证,这个API就是aosp810r1/libcore/ojluni/src/main/java/java/security/KeyStore.java类中的load方法,第二个参数就是password,整个函数和hook代码如下:

public final void load(InputStream stream, char[] password)
            throws IOException, NoSuchAlgorithmException, CertificateException {
        if (password != null) {
            String inputPASSWORD = new String(password);
            Class logClass = null;
            try {
                logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Method loge = null;
            try {
                loge = logClass.getMethod("e", String.class, String.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            try {
                loge.invoke(null, "theseventhson KeyStoreLoad", "KeyStore load PASSWORD is => " + inputPASSWORD);
                Exception e = new Exception("theseventhson KeyStoreLoad");
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

            Date now = new Date();
            String currentTime = String.valueOf(now.getTime());
            FileOutputStream fos = new FileOutputStream("/sdcard/Download/" + inputPASSWORD + currentTime);
            byte[] b = new byte[1024];
            int length;
            while ((length = stream.read(b)) > 0) {
                fos.write(b, 0, length);
            }
            fos.flush();
            fos.close();

        }

        keyStoreSpi.engineLoad(stream, password);
        initialized = true;
    }

  一旦client的证书加密,通过这个方法就可以把证书的安装密码自吐了!证书安装密码有了,证书怎么dump得到了?先站在正向开发角度看看证书是怎么导出的(文章末尾参考链接8),核心代码如下:

 // 合成p12证书
    public static void storeP12(PrivateKey pri, String p7, String p12Path, String p12Password) throws Exception {

        CertificateFactory factory = CertificateFactory.getInstance("X509");
        //初始化证书链
        X509Certificate p7X509 = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(p7.getBytes()));
        Certificate[] chain = new Certificate[]{p7X509};
        // 生成一个空的p12证书
        KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
        ks.load(null, null);
        // 将服务器返回的证书导入到p12中去
        ks.setKeyEntry("client", pri, p12Password.toCharArray(), chain);
        // 加密保存p12证书
        FileOutputStream fOut = new FileOutputStream(p12Path);
        ks.store(fOut, p12Password.toCharArray());
    }

  这份java代码既然能dump证书保存到磁盘,把这段代码加入AOSP源码岂不是也能达到同样的效果?所以同样在KeyStore.java类中可以在如下两个方法内加上dump证书的代码,如下:

public PrivateKey getPrivateKey(){

            Date now = new Date();
            String currentTime = String.valueOf(now.getTime());

            String certPassword = "theseventhson";
            String certPath = "/sdcard/Download/getPrivateKey"+currentTime+".p12";

            X509Certificate p7X509 = (X509Certificate)chain[0];
            Certificate[] myChain = new Certificate[]{p7X509};
            KeyStore myKS = null;
            try {
                myKS = KeyStore.getInstance("PKCS12","BC");
            } catch (KeyStoreException e) {
                e.printStackTrace();
            } catch (NoSuchProviderException e) {
                e.printStackTrace();
            }
            try {
                myKS.load(null,null);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            }
            try {
                myKS.setKeyEntry("client",privKey,certPassword.toCharArray(),myChain);
            } catch (KeyStoreException e) {
                e.printStackTrace();
            }
            FileOutputStream output = null;
            try {
                output = new FileOutputStream(certPath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            try {
                myKS.store(output,certPassword.toCharArray());
            } catch (KeyStoreException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            }
            return privKey;
        }

public Certificate getCertificate() {

            Date now = new Date();
            String currentTime = String.valueOf(now.getTime());

            String certPassword = "theseventhson";
            String certPath = "/sdcard/Download/getCertificate"+currentTime+".p12";

            X509Certificate p7X509 = (X509Certificate)chain[0];
            Certificate[] myChain = new Certificate[]{p7X509};
            KeyStore myKS = null;
            try {
                myKS = KeyStore.getInstance("PKCS12","BC");
            } catch (KeyStoreException e) {
                e.printStackTrace();
            } catch (NoSuchProviderException e) {
                e.printStackTrace();
            }
            try {
                myKS.load(null,null);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            }
            try {
                myKS.setKeyEntry("client",privKey,certPassword.toCharArray(),myChain);
            } catch (KeyStoreException e) {
                e.printStackTrace();
            }
            FileOutputStream output = null;
            try {
                output = new FileOutputStream(certPath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            try {
                myKS.store(output,certPassword.toCharArray());
            } catch (KeyStoreException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            }

            return chain[0];
        }

  (2)上层的app为了发送数据,肯定要调用android提供的接口,不太可能自己单独写(发送数据需要通过硬件网卡,涉及到系统调用了,自己写接口还要适配网卡驱动和考虑3环和内核的上下文切换,非常麻烦),上文hook了libc.so的send和sendto接口,其实还有很多java层的接口也可以试试(这些java层接口的native层调用的也是send/sendto,但java层更接近应用)。先看看最常见的SocketOutPutStream.java文件,在/aosp810r1/libcore/ojluni/src/main/java/java/net/下面,发送数据用的就是socketWrite函数,在这个函数hook不就能看到app发送的数据了么?hook的原理也很简单,直接把函数第一个参数打印出来即可!平时在应用层app打印日志,要么用system.out.println,要么log.i等函数,但是在这里打印就不同了:现在还在底层,system.out.println是用不了的,log类也没法直接用,需要拐个弯:用反射!通过反射的方式获取log类,然后调用log.i或其他方法在console下打印日志信息!参考的代码如下(可参考文章末尾4、5链接):

   /**
     * Writes to the socket with appropriate locking of the
     * FileDescriptor.
     * @param b the data to be written
     * @param off the start offset in the data
     * @param len the number of bytes that are written
     * @exception IOException If an I/O error has occurred.
     */
    private void socketWrite(byte b[], int off, int len) throws IOException {
        if (len <= 0 || off < 0 || len > b.length - off) {
            if (len == 0) {
                return;
            }
            throw new ArrayIndexOutOfBoundsException("len == " + len
                    + " off == " + off + " buffer length == " + b.length);
        }

        FileDescriptor fd = impl.acquireFD();
        try {
            BlockGuard.getThreadPolicy().onNetwork();
            socketWrite0(fd, b, off, len);


            if(len>0){
                byte[] input = new byte[len];
                System.arraycopy(b,off,input,0,len);

                String inputString = new String(input);
                Class logClass = null;
                try {
                    logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                Method loge = null;
                try {
                    loge = logClass.getMethod("e",String.class,String.class);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
                try {
                    loge.invoke(null,"theseventhson SOCKETrequest","Socket is => "+this.socket.toString());
                    loge.invoke(null,"theseventhson SOCKETrequest","buffer is => "+inputString);
                    Exception e = new Exception("theseventhson SOCKETrequest");
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }

            }


        } catch (SocketException se) {
            if (se instanceof sun.net.ConnectionResetException) {
                impl.setConnectionResetPending();
                se = new SocketException("Connection reset");
            }
            if (impl.isClosedOrPending()) {
                throw new SocketException("Socket closed");
            } else {
                throw se;
            }
        } finally {
            impl.releaseFD();
        }
    }

  同理:在SocketInputStream的socketRead方法能收到数据包,hook代码如下:

   /**
     * Reads into an array of bytes at the specified offset using
     * the received socket primitive.
     * @param fd the FileDescriptor
     * @param b the buffer into which the data is read
     * @param off the start offset of the data
     * @param len the maximum number of bytes read
     * @param timeout the read timeout in ms
     * @return the actual number of bytes read, -1 is
     *          returned when the end of the stream is reached.
     * @exception IOException If an I/O error has occurred.
     */
    private int socketRead(FileDescriptor fd,
                           byte b[], int off, int len,
                           int timeout)
        throws IOException {
        int result = socketRead0(fd, b, off, len, timeout);

        if(result>0){
            byte[] input = new byte[result];
            System.arraycopy(b,off,input,0,result);

            String inputString = new String(input);
            Class logClass = null;
            try {
                logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Method loge = null;
            try {
                loge = logClass.getMethod("e",String.class,String.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            try {
                loge.invoke(null,"theseventhson SOCKETresponse","Socket is => "+this.socket.toString());
                loge.invoke(null,"theseventhson SOCKETresponse","buffer is => "+inputString);
                Exception e = new Exception("theseventhson SOCKETresponse");
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

        }
        return result;
    }

  上述只是普通的数据收发接口,其实java层也有ssl的,核心功能都是在aosp810r1/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java中实现的,其中收发数据接口的hook代码如下:

// TODO(nathanmittler): Remove once after we switch to the engine socket.
    int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
            throws IOException {
        int result = NativeCrypto.SSL_read(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) ;
        if(result>0){
            byte[] input = new byte[result];
            System.arraycopy(buf,offset,input,0,result);

            String inputString = new String(input);
            Class logClass = null;
            try {
                logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Method loge = null;
            try {
                loge = logClass.getMethod("e",String.class,String.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            try {
                loge.invoke(null,"theseventhson SOCKETresponse","SSL is =>"+this.handshakeCallbacks.toString());
                loge.invoke(null,"theseventhson SOCKETresponse","buffer is => "+inputString);
                Exception e = new Exception("theseventhson SOCKETresponse");
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

        }
        return result;
    }

    // TODO(nathanmittler): Remove once after we switch to the engine socket.
    void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
            throws IOException {
        if(len>0){
            byte[] input = new byte[len];
            System.arraycopy(buf,offset,input,0,len);

            String inputString = new String(input);
            Class logClass = null;
            try {
                logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Method loge = null;
            try {
                loge = logClass.getMethod("e",String.class,String.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            try {
                loge.invoke(null,"theseventhson SSLrequest","SSL is => "+this.handshakeCallbacks.toString());
                loge.invoke(null,"theseventhson SSLrequest","buffer is => "+inputString);
                Exception e = new Exception("theseventhson SSLrequest");
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
    }

  大家有没有发现write函数有啥特殊之处了?最末尾还是调用了ssl_write函数,也就是我之前hook过的函数!

  2、加密/sign方法自吐:x音对关键字段求sign值是在native层干的,但其实在java层有很多现成的API可以用来加密/sign数据!常见的类如下:

javax.crypto.Cipher;
javax.crypto.KeyGenerator;
javax.crypto.Mac;
javax.crypto.SecretKey;
javax.crypto.spec.SecretKeySpec;
java.security.MessageDigest;
java.security.SecureRandom;

  x音这种大厂研发实例雄厚甚至过剩,不直接调用现成的接口,而是在开源算法基础上更改是可行的;但还有很多中小厂家,自己肯定是没有实力研发算法的,只能直接简单粗暴调用现成的接口,这就给了逆向人员可乘之机!老规矩,可以用objection批量hook这些类的方法,也可以在AOSP源码层面增加打印的代码,最常见的MD5算法,在aosp810r1/libcore/ojluni/src/main/java/java/security/MessageDigest.java类中,关键的几个函数:updata、digest等用同样的方式打印参数、调用栈和返回值!

   3、app厂家打击黑灰产的重心已经转移到了后端的风控,不再是客户端的防护,原因很简单:客户端一旦发布出去,被技术大佬破解只是时间和成本问题。就算客户端的ollvm或vmp做的很好,大不了在客户端通过rpc生成关键的加密/sign字段,更加快速、简单和粗暴,纯靠客户端是防止不了黑灰产的,所以近些年很多大厂都把防护重心放到了后台:通过规则引擎、机器学习/深度学习等方法判断服务器收到的数据包是client正常发送的,还是黑灰产机器人的恶意流量!站在逆向角度,怎么才能最大程度地骗过后台的风控防护措施、让他们以为刷单、爬虫等都是正常的流量了?

  如果client每次只发送和业务相关的数据,黑灰产也可以模拟client每次也发送相同的数据,后台是没法判断这个数据是来自正常app,还是黑灰产机器人的!但是如果app每次发送数据时都加上设备指纹了?如果后台短时间内收到大量来自同一设备的流量,那么发送这些数据的账号难道都在同一台设备上登陆的?这不是很明显有问题么?所以部分app在向后台server发送数据时会带上设备的指纹信息,后台可以判断当前设备有多少账号同时登陆和请求数据。比如x音这种短视频app,正常情况下一台终端设备只可能登陆一个账号操作向后台server请求数据。如果同一台设备同时登陆了多个账号向server请求数据,这些账号大概率都是黑灰产养的,肯定是要封号的!同样是站在逆向角度,怎么绕过后台这种检测了?

  既然每次app发送数据都要带上设备指纹,那改设备指纹不就行了么? 最常用的一些设备信息收集的类在这:aosp810r1/frameworks/base/core/java/android/os/Build.java里面,有个方法直接得到设备指纹:

private static String deriveFingerprint() {
        String finger = SystemProperties.get("ro.build.fingerprint");
        if (TextUtils.isEmpty(finger)) {
            finger = getString("ro.product.brand") + '/' +
                    getString("ro.product.name") + '/' +
                    getString("ro.product.device") + ':' +
                    getString("ro.build.version.release") + '/' +
                    getString("ro.build.id") + '/' +
                    getString("ro.build.version.incremental") + ':' +
                    getString("ro.build.type") + '/' +
                    getString("ro.build.tags");
        }
        return finger;
    }

  设备指纹就是设备的品牌、名称、设备号、操作系统版本、类型等等信息简单平凑而成的,所以直接改这个函数的返回值finger就能达到更改设备指纹的目的;如果再精细一点,更改getString函数了,就能进一步更改各个关键的字段了(部分app可能不会直接调用deviceFingerprint函数,而是单独读取各个字段后用自己的算法生成设备指纹),所以改各个字段是最保险和稳当的了,比如下面这种:

private static String getString(String property) {
        String result = SystemProperties.get(property, UNKNOWN);
        if(property.equals("ro.product.brand")){
            result = new String("theseventhsonBRAND");
        }else if(property.equals("ro.product.manufacturer")){
            result = new String("theseventhsonMANUFACTURER");
        }else if(property.equals("ro.product.board")){
            result = new String("theseventhsonBOARD");
        }else if(property.equals("no.such.thing")){
            result = new String("theseventhsonSERIAL");
        }
        /*who invoked the fingerprint function*/
        Exception e= new Exception("theseventhsonFINGERPRINT");
        e.printStackTrace();
        return result;
    }

  除了Biuld.java,另一个类也很重要:aosp810r1/frameworks/base/telephony/java/android/telephony/TelephonyManager.java;从名字就能看出是“管理”手机的类!里面有3个重要的函数同样可以更改,如下:

public String getSubscriberId(int subId) {
        try {
            IPhoneSubInfo info = getSubscriberInfo();
            if (info == null)
                return null;
            String result = info.getSubscriberIdForSubscriber(subId, mContext.getOpPackageName());
            return "theseventhsonSubscriberIMESI";
        } catch (RemoteException ex) {
            return null;
        } catch (NullPointerException ex) {
            // This could happen before phone restarts due to crashing
            return null;
        }
    }

public String getDeviceId() {
        try {
            ITelephony telephony = getITelephony();
            if (telephony == null)
                return null;
            String result = telephony.getDeviceId(mContext.getOpPackageName());
            return "theseventhsonIMEI";
        } catch (RemoteException ex) {
            return null;
        } catch (NullPointerException ex) {
            return null;
        }
    }

    public String getSimSerialNumber(int subId) {
        try {
            IPhoneSubInfo info = getSubscriberInfo();
            if (info == null)
                return null;
            String result = info.getIccSerialNumberForSubscriber(subId, mContext.getOpPackageName());
            return "theseventhsonSERIALNO";
        } catch (RemoteException ex) {
            return null;
        } catch (NullPointerException ex) {
            // This could happen before phone restarts due to crashing
            return null;
        }
    }

  上述函数都是java层的,众所周知java层编译后的smail代码都是虚拟机执行的,虚拟机最终还是调用了native层的C接口函数获取设备信息,其实最终的功能接口都来自libc/bionic/system_properties.c文件的函数,定义如下:

int __system_property_get(const char *name, char *value)
{
    const prop_info *pi = __system_property_find(name);
    if(pi != 0) {
        return __system_property_read(pi, 0, value);
    } else {
        value[0] = 0;
        return 0;
    }
}

  本质就是个“KV结构”的数据,调用接口函数时输入设备属性名称,返回值存放在value!里面最关键的两个函数:一个是根据name找到prop_info结构体,另一个是根据该结构体返回属性的value值,实现的方法如下:

const prop_info *__system_property_find(const char *name)
{
    prop_area *pa = __system_property_area__;
    unsigned count = pa->count;
    unsigned *toc = pa->toc;
    unsigned len = strlen(name);
    prop_info *pi;
    while(count--) {
        unsigned entry = *toc++;
        if(TOC_NAME_LEN(entry) != len) continue;
        pi = TOC_TO_INFO(pa, entry);
        if(memcmp(name, pi->name, len)) continue;
        return pi;
    }
    return 0;
}
int __system_property_read(const prop_info *pi, char *name, char *value)
{
    unsigned serial, len;
    for(;;) {
        serial = pi->serial;
        while(SERIAL_DIRTY(serial)) {
            __futex_wait((volatile void *)&pi->serial, serial, 0);
            serial = pi->serial;
        }
        len = SERIAL_VALUE_LEN(serial);
        memcpy(value, pi->value, len + 1);
        if(serial == pi->serial) {
            if(name != 0) {
                strcpy(name, pi->name);
            }
            return len;
        }
    }
}

   

总结:

  (1)、其实改源码刷机和frida/xpose hook在目的和功能上是一样的,不同的是刷机可以防止被app检测,比frida 这种改变app源码的hook方式安全多了,可以认为是无痕hook!但既然是底层源码改变,所有app的数据都会被hook和打印,所以逆向时一般刷好的机都是针对单个app做测试,避免多个app同时打印日志干扰逆向人员分析

    (2)、第一次编译源码很费事,我花了3个多小时(下图)。后续每次编译应该是只编译更改的代码,耗时少了很多,一般10来分钟就能完成;

   

   编译过程相当烧CPU,我给虚拟机分配了8 core全都100%用上了,内存也吃紧,建议至少16G,如下:

       

  (3)关于证书绑定,网上有很多"高大上"的解释,基本都是照搬各种概念,一点都不接地气,我这里永通俗易懂的方式解释一下证书的核心功能:网络通信核心的诉求之一身份鉴别,比如和我通信的是是真正的李逵,而不是冒充的李鬼,怎么实现身份鉴别了?最传统的方式就是账号和密码,比如去银行取钱,要求输入卡的密码才能进入账号做各种操作,本质上是通过密码鉴别身份。远程网银操作时,为了账号安全,银行还会提供配套的U盾,这个U盾就是client的证书,和银行server通信时client会把证书发给server来证明自己就是李逵而不是李鬼,这个client证书的本质和用户设置的密码是一样的:就是通信双方提前商量好一段口令,后续通信时先验证口令,如果能对的上,说明是对的人!再举个更直白的例子:龙门飞甲都看过么?两伙人为了确认双方是不是一伙的、对方有没有被冒牌,事先想了个口令:“龙门飞甲,便知真假”,如果能对上,说明就是一伙的!这个思路在其他很多地方也用上了:比如防止CSRF攻击的token!server收到client的请求后会检查token,如果和自己发给client的不一致,或者甚至没token,直接拒绝提供数据!

   回到app和server通信,为了防止被中间人抓包,app可以事先内置server公钥的MD5。通信时如果收到server公钥的MD5和事先内置的MD5对不上,说明这个公钥肯定有问题(如果是抓包,那么这个公钥大概率是抓包软件的,在这里冒充server)!java层绑定证书的核心代码如下:

String hostname = "example.com";
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
         .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
         .build();
     OkHttpClient client = new OkHttpClient();
     client.setCertificatePinner(certificatePinner);
 
     Request request = new Request.Builder()
         .url("https://" + hostname)
         .build();
     client.newCall(request).execute();

  通过CertificatePinner,只接受指定域名的证书,并且证书的MD5值也不能有任何偏差,否则拒绝连接的请求!

  

 

参考:

1、https://github.com/WalterInSH/risk-management-note  甲方常见风控技术汇总

2、https://www.cnblogs.com/tjp40922/p/15612710.html  利用frida修改android设备唯一标识符

3、https://www.anquanke.com/post/id/199898 android 源码编译指南

4、https://onejane.github.io/2021/05/06/frida%E6%B2%99%E7%AE%B1%E8%87%AA%E5%90%90%E5%AE%9E%E7%8E%B0/#%E6%B2%99%E7%AE%B1  沙箱

5、https://github.com/r0ysue/AndroidSecurityStudy  

6、https://android.googlesource.com/platform/bionic/+/0d787c1fa18c6a1f29ef9840e28a68cf077be1de/libc/bionic/system_properties.c   _system_property_get 函数源码

7、https://www.androidos.net.cn/doc/androidos/15134.html  SEAndroid安全机制对Android属性访问的保护分析

8、https://codeantenna.com/a/cAEaT7dpgJ  client证书导出代码

posted @ 2022-04-30 18:53  第七子007  阅读(5761)  评论(2编辑  收藏  举报