• 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证书导出代码

  • 相关阅读:
    javascript中!=、!==、==、===操作符总结
    轮询、长轮询与Web Socket的前端实现
    C#中Enum用法小结
    浅谈Javascript 中几种克隆(clone)方式
    JS数组sort比较函数
    为Jquery类和Jquery对象扩展方法
    自定义滚动条mCustomScrollbar
    T-SQL 控制流语句
    sql case 用法总结
    Selenium2+python自动化19-单选框和复选框(radiobox、checkbox)【转载】
  • 原文地址:https://www.cnblogs.com/theseventhson/p/16186234.html
Copyright © 2020-2023  润新知