Android第一代加壳的验证和测试

news2024/11/29 2:42:45

Android第一代加壳测试,网上有很多文章,本文只是在前人基础上测试和验证。因此,本文的重点在于动手和实践。

第一代加壳技术有三个项目,分别是:

  1. 加壳程序。主要是把需要加壳的原程序加密后,放在壳程序中,一般是追加到壳程序的classes.dex文件的末尾,然后对壳程序的classes.dex文件中的长度、crc校验和sha1校验字段重新计算。
  2. 壳程序。运行后,将加壳后的原程序从本程序的的classes.dex末尾释放出来,然后在Application类中,设置壳中原程序的运行环境,并加载原程序。
  3. 原程序。

(一)项目下载地址
点击下载项目
该项目中包含3个工程,其中,apkUnshell是壳程序,flashplayer程序是源程序,apkshell是加壳程序,因为笔者在android studio中创建和编译java程序屡次失败,apkshell是用eclipse开发的。

(二)简要解析
此程序的加壳和解壳主要代码来自于网上,作者是yuxin,本文只是简单修剪。
加壳程序:如上所述,功能是将源程序追加到壳程序classes.dex文件的末尾,然后对壳程序的classes.dex文件中的长度、crc校验和sha1校验字段重新计算。该模块有很多细节,比如文件长度的计算,crc校验的是那些字节,sha1的计算方式等,具体可以结合dex文件结构和源代码,详细分析之。
如下几篇详细的dex文件解析文章:
dex文件解析
dex文件解析
dex文件解析

源程序加解密是简单的异或操作,密钥是:

private static String cryptKey = "fuck all the android crackers";

其主要的计算字段有classes.dex长度,sha1校验,crc校验,主要部分如下:

   /** 
     * 修改dex头 sha1值 
     * @param dexBytes 
     * @throws NoSuchAlgorithmException 
     */  
    private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException {  
        MessageDigest md = MessageDigest.getInstance("SHA-1");  
        md.update(dexBytes, 32, dexBytes.length - 32);
        //从32为到结束计算sha-1  
        byte[] newdt = md.digest();  
        System.arraycopy(newdt, 0, dexBytes, 12, 20);
        //修改sha-1值(12-31) 
        
        //输出sha-1值,可有可无  
        String hexstr = "";  
        for (int i = 0; i < newdt.length; i++) {  
        	//Integer.toString(int i, int radix)将整数i(十进制)转化为radix进制的整数
            hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16).substring(1);
        }  
        System.out.println("new dex sha-1:" + hexstr);  
    }  
  
    /** 
     * 修改dex头 file_size值 
     * @param dexBytes 
     */  
    private static void fixFileSizeHeader(byte[] dexBytes) {
        //新文件长度
        byte[] newfs = intToByte(dexBytes.length);  
        
        byte[] refs = new byte[4]; 
        //高位在前 低位在前掉个个
        for (int i = 0; i < 4; i++) {
            refs[i] = newfs[newfs.length - 1 - i];
        }
        
        //修改(32-35)
        System.arraycopy(refs, 0, dexBytes, 32, 4);
        
        System.out.println("new dex file size:" + Integer.toHexString(dexBytes.length));
    }  
    
    /** 
     * 修改dex头,CheckSum 校验码
     * @param dexBytes 
     */  
    private static void fixCheckSumHeader(byte[] dexBytes) {  
        Adler32 adler = new Adler32();  
        adler.update(dexBytes, 12, dexBytes.length - 12);
        //从12到文件末尾计算校验码  
        int value = (int)adler.getValue();  
        byte[] newcs = intToByte(value);  
        //高位在前,低位在前掉个个  
        byte[] recs = new byte[4];  
        for (int i = 0; i < 4; i++) {  
            recs[i] = newcs[newcs.length - 1 - i];  
        }  
        
        //效验码赋值(8-11)
        System.arraycopy(recs, 0, dexBytes, 8, 4);
        
        System.out.println("new dex checksum:" +Integer.toHexString(value));
    }  
    

解壳程序:此模块是加壳技术的核心。主要功能有3个,一个是在Application中,释放出源程序的apk,第二个是使用android.app.LoadedApk类中的mClassLoader方法加载apk,第三个是在onCreate方法中,进一步的将执行环境和资源转换到源程序的环境,然后加载执行。这部分我也有很多细节不太明白,只是照抄代码,如果有哪位读者能弄明白这里面的细节,请私信我,我将会给每行代码添加注释。
其主要代码如下:

package com.apkUnshell;



import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dalvik.system.DexClassLoader;

/**
 * =============================================================================
 * Copyright (c) 2017 yuxin All rights reserved.
 * Packname com.jju.yuxin.reforceapk
 * Created by yuxin.
 * Created time 2017/6/18 0018 下午 5:03.
 * Version   1.0;
 * Describe :
 * History:
 * ==============================================================================
 */

//com.google.android.apps.plus
//com.adobe.flashplayer
//com.loader
//com.setup.loader
//Applicaiton做为整个应用的上下文,会被系统第一时间调用,这也是应用开发者程序代码的第一执行点
public class MyApplication extends Application{

    private static String DEXFILENAME = "update.apk";

    private static final String appkey = "APPLICATION_CLASS_NAME";

    private static String cryptKey = "fuck all the android crackers";

    public static String PAYLOAD_ODEX = "my_payload_odex";

    public static String PAYLOAD_LIB = "my_payload_lib";

    private  static final String TAG = MyApplication.class.getSimpleName();

    private String srcDexFilePath = "";
    private String odexPath = "";
    private String libPath = "";

    private static String gIPstr = "";
    private static String gUserNameStr = "";

    private Context context = null;

    //以下是加载资源
    protected AssetManager mAssetManager = null;
    protected Resources mResources = null;
    protected Resources.Theme mTheme = null;


    //why run 2 times?
    @SuppressWarnings("rawtypes")
    @Override
    protected void attachBaseContext(Context base) {

        super.attachBaseContext(base);

        //getApplicationContext() 返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁
        //Activity.this的context 返回当前activity的上下文,属于activity ,activity 摧毁他就摧毁
        //getBaseContext()  返回由构造函数指定或setBaseContext()设置的上下文
        //this.getApplicationContext()取的是这个应 用程序的Context,Activity.this取的是这个Activity的Context,
        //这两者的生命周期是不同 的,前者的生命周期是整个应用,后者的生命周期只是它所在的Activity。
        context = base;

        Log.e(TAG,"attachBaseContext");

        try {
//        	/data/user/0/com.apkunshell/app_payload_odex
            File odexPathFile = this.getDir(PAYLOAD_ODEX, MODE_PRIVATE);
//        	/data/user/0/com.apkunshell/app_payload_libs
            File libsPathFile = this.getDir(PAYLOAD_LIB, MODE_PRIVATE);

            //用于存放源apk释放出来的dex
            odexPath = odexPathFile.getAbsolutePath();
            //用于存放源Apk用到的so文件
            libPath = libsPathFile.getAbsolutePath();
            //用于存放解密后的apk
            srcDexFilePath = odexPathFile.getAbsolutePath() + "/" + DEXFILENAME;

//            String apppath = this.getFilesDir().getParent() + "/";
//            InputStream is = this.getAssets().open(APKFILENAME);
//            int size = is.available();
//            byte []buffer = new byte[size];
//            is.read(buffer);
//            is.close();
//            OutputStream os = new FileOutputStream(apppath + APKFILENAME);
//            os.write(buffer);
//            os.close();

            File srcDexFile = new File(srcDexFilePath);
            //第一次加载
            if (srcDexFile.exists() == false)
            {
                Log.e(TAG, "beFirstLoading");

                srcDexFile.createNewFile();
                //拿到dex文件
                byte[] dexdata = this.readDexFileFromApk();
                //取出源APK解密后放置在/payload.apk,及其so文件放置在payload_lib/下
                this.splitPayLoadFromDex(dexdata);
            }

            // 配置动态加载环境
            //反射获取主线程对象,并从中获取所有已加载的package信息,并中找到当前的LoadApk对象的弱引用
             配置动态加载环境 获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
                    "currentActivityThread",new Class[] {}, new Object[] {});
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mPackages");
            String packageName = this.getPackageName();
            WeakReference wr = (WeakReference) mPackages.get(packageName);

            ///创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
            //创建一个新的DexClassLoader用于加载源Apk,传入apk路径,dex释放路径,so路径,及父节点的DexClassLoader使其遵循双亲委托模型
            ClassLoader fathercl = (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader");
            DexClassLoader dLoader = new DexClassLoader(srcDexFilePath, odexPath,libPath, fathercl);

            //getClassLoader()等同于 (ClassLoader) RefInvoke.getFieldOjbect(),但是为了替换掉父节点我们需要通过反射来获取并修改其值

            //将父节点DexClassLoader替换
            把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",wr.get(), dLoader);

            //Object actObj = dLoader.loadClass(LOADCLASSNAME);

            //Log.e(TAG, "get class object:" + actObj);

        } catch (Exception e) {
            Log.e(TAG, "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }


    //java.lang.RuntimeException:
    //Unable to create application com.loader.sRelease: java.lang.NullPointerException:
    //expected receiver of type android.content.ContentProvider, but got null
    //at com.loader.sRefInvoke.setFieldOjbect(sRefInvoke.java:178)
    //why run 2 times?
    @SuppressWarnings("rawtypes")
    public void onCreate() {
        try {
            Log.e(TAG, "onCreate");

            Log.e(TAG,"Application:" + context +
                    ",BaseContext:" + getBaseContext() +
                    ",ApplicationContext:" + getApplicationContext() +
                    ",Activity:" + this);

            if(context == null){
                context = this;
                if(context == null){
                    context = Utils.getContext();
                }
            }

            Utils.setValue(context,"paramConfig.json","username",gUserNameStr);
            Utils.setValue(context,"paramConfig.json","ip",gIPstr);

            //加载源apk资源
            loadResources(srcDexFilePath);

            //获取配置在清单文件的源Apk的Application路径
            String appClassName = null;
            try {
                ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(),PackageManager.GET_META_DATA);
                Bundle bundle = ai.metaData;
                if (bundle != null && bundle.containsKey(appkey)) {
                    appClassName = bundle.getString(appkey);	//className 是配置在xml文件中的
                }else {
                    Log.e(TAG, "not found application class name in bundle");
                    return;
                }
            } catch (Exception e) {
                Log.e(TAG, "error:"+Log.getStackTraceString(e));
                e.printStackTrace();
                return;
            }

            //获取当前壳Apk的ApplicationInfo
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
                    "currentActivityThread",new Class[] {}, new Object[] {});

            Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mBoundApplication");

            Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData",mBoundApplication, "info");

            //将LoadedApk中的ApplicationInfo设置为null
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",loadedApkInfo, null);

            //获取currentActivityThread中注册的Application
            Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mInitialApplication");

            //获取ActivityThread中所有已注册的Application,并将当前壳Apk的Application从中移除
            @SuppressWarnings("unchecked")
            ArrayList<Application> mAllApplications =
                    (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread",
                            currentActivityThread, "mAllApplications");
            mAllApplications.remove(oldApplication);

            ApplicationInfo appinfo_In_LoadedApk =
                    (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,"mApplicationInfo");

            ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread$AppBindData",mBoundApplication, "appInfo");

            //替换原来的Application
            appinfo_In_LoadedApk.className = appClassName;
            appinfo_In_AppBindData.className = appClassName;

            //注册Application
            Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication",
                    loadedApkInfo,new Class[] { boolean.class, Instrumentation.class },new Object[] { false, null });

            //替换ActivityThread中的Application
            RefInvoke.setFieldOjbect("android.app.ActivityThread","mInitialApplication", currentActivityThread, app);

            ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mProviderMap");
            Iterator it = mProviderMap.values().iterator();
            while (it.hasNext()) {
                Object providerClientRecord = it.next();
                Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider");
                RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app);
            }

            Log.e(TAG, "app:"+app);

            app.onCreate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
        //取被加壳apk的长度
        int sdlen = shelldexdata.length;
        byte[] bytedexlen = new byte[4];
        System.arraycopy(shelldexdata, sdlen - 4, bytedexlen, 0, 4);

        ByteArrayInputStream bais = new ByteArrayInputStream(bytedexlen);
        DataInputStream dis = new DataInputStream(bais);
        int readInt = dis.readInt();
        Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));

        //取出apk
        byte[] encryptdata = new byte[readInt];
        System.arraycopy(shelldexdata, sdlen - 4 - readInt, encryptdata, 0, readInt);

        //对源程序Apk进行解密
        byte[] flatdata = xorcrypt(encryptdata);

        int offset = 0;
        byte [] byteunamelen = new byte[4];
        System.arraycopy(flatdata, offset, byteunamelen, 0, 4);
        offset += 4;

        int unamelen = Utils.bytesToInt(byteunamelen);
        byte[] username = new byte[unamelen];
        System.arraycopy(flatdata , offset, username, 0, unamelen);
        offset += unamelen;

        gUserNameStr = new String(username);

        byte [] byteiplen = new byte[4];
        System.arraycopy(flatdata, offset, byteiplen, 0, 4);
        offset += 4;

        int iplen = Utils.bytesToInt(byteiplen);
        byte[] ip = new byte[iplen];
        System.arraycopy(flatdata , offset, ip, 0, iplen);
        offset += iplen;

        gIPstr = new String(ip);

        //写入源apk文件
        File file = new File(srcDexFilePath);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(flatdata,offset,readInt - offset);
            localFileOutputStream.close();
        } catch (IOException localIOException) {
            throw new RuntimeException(localIOException);
        }

        //分析源apk文件
        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(file)));
        while (true) {
            ZipEntry ze = zis.getNextEntry();
            if (ze == null) {
                break;
            }

            //依次取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
            String zfn = ze.getName();
            if (zfn.startsWith("lib/") && zfn.endsWith(".so")) {
                File sofile = new File(libPath + zfn.substring(zfn.lastIndexOf('/')));
                sofile.createNewFile();
                FileOutputStream fos = new FileOutputStream(sofile);
                byte[] readbuf = new byte[0x4000];
                while (true) {
                    int readlen = zis.read(readbuf);
                    if (readlen == -1){
                        break;
                    }
                    fos.write(readbuf, 0, readlen);
                }
                fos.flush();
                fos.close();
            }
            zis.closeEntry();
        }
        zis.close();
    }


    /**
     * 拿到自己apk文件中的dex文件
     * @return
     * @throws IOException
     */
    private byte[] readDexFileFromApk() throws IOException {

        ByteArrayOutputStream dexbaos = new ByteArrayOutputStream();

        //getApplicationInfo().sourceDir == /data/user/0/com.adobe.flashplayer/base.apk
        //BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据
        //无其他用途
        //ZipInputStream zis = new ZipInputStream(new FileInputStream(this.getApplicationInfo().sourceDir));

        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(this.getApplicationInfo().sourceDir)));

        while (true) {
            ZipEntry ze = zis.getNextEntry();
            if (ze == null) {
                break;
            }

            //拿到dex文件
            if (ze.getName().equals("classes.dex")) {
                byte[] readbuf = new byte[0x10000];
                while (true) {
                    int readlen = zis.read(readbuf);
                    if (readlen == -1){
                        zis.closeEntry();
                        break;
                    }

                    dexbaos.write(readbuf, 0, readlen);
                }
                zis.closeEntry();
                break;
            }else{
                zis.closeEntry();
            }
        }

        zis.close();
        return dexbaos.toByteArray();
    }


    private static byte[] xorcrypt(byte[] srcdata){
        byte[] key = cryptKey.getBytes();
        int keylen = cryptKey.length();
        for(int i = 0,j = 0; i<srcdata.length; i++){
            srcdata[i] = (byte)(key[j] ^ srcdata[i]);
            j ++;
            if(j >= keylen){
                j = 0;
            }
        }
        return srcdata;
    }


    protected void loadResources(String srcApkPath) {
        //创建一个AssetManager放置源apk的资源
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, srcApkPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            Log.i(TAG, "inject:loadResource error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

}

源程序:本人自己写的小程序模块,跟此项目关系不大。

(三)加壳的注意事项

  1. 壳程序中的AndroidManifest.xml文件可以没有自己的内容,而全部复制来源于源程序中的AndroidManifest.xml程序,但是其中要添加如下条目: <meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.adobe.flashplayer.MyApplication"/>
    该字段用于壳程序加载原程序。

  2. 壳程序的Androidmanifest.xml文件中,所有Activity、Service 、ContentProvider、Broadcast、Application等,在声明中,全部都必须用源程序中的全类名,而不能用如同.MainActivity这样的缩写格式。同时,也应注意到,壳程序中的.MyApplication和源程序中的.MyApplication虽然名字相同,但不是用一个Application,修改后没有影响,这从日志输出中可以看出。
    在这里插入图片描述

  3. 壳程序包名最好不要跟源程序相同,在测试中发现,如果相同会导致系统卡死。

  4. 加壳后的程序,在使用autosign重新签名以前,必须删除原来的META-INF文件夹,否则,会因为autosign的签名错误,导致签名后的apk包安装不上。否则,如下执行查询apk签名信息的命令会失败: jarsigner -certs -verbose -verify apk路径

  5. 壳程序最好不要有启动界面,否则,程序运行时,首先会跳转壳的界面。因此,所有的Activity不要有如下属性:

                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
    

(四)加壳自动化

用eclipse下编译加壳程序apkshell.jar,编写.bat批处理文件,构建源程序并拷贝到apkshell-cmd目录下,执行run.bat程序后,自动调用apkshell.jar将源程序加密后塞进壳程序,调用autosign程序对打包后的壳程序签名,签名后的程序默认为mytest.apk。
该模块是本文中我的主要工作之一。
到此,各位看官可以直接使用此apk安装测试,或者用于其他目的。
在这里插入图片描述

run.bat如见如下:

set path = "./"

rd /q /s .\apkunshell
del .\apkUnshell.apk_new.apk
del .\autosign\update.apk
del .\autosign\update_signed.apk

java -jar apkshell.jar ./apkunshell.apk ./app-release.apk ./apkunshell jy 47.101.204.4

copy .\apkunshell.apk_new.apk .\autosign\update.apk

java -jar ./autosign/signapk.jar ./autosign/testkey.x509.pem ./autosign/testkey.pk8 ./autosign/update.apk ./autosign/update_signed.apk

copy .\autosign\update_signed.apk .\mytest.apk

del .\apkUnshell.apk_new.apk
del .\autosign\update.apk
del .\autosign\update_signed.apk
rd /q /s .\apkunshell

pause

(五)加壳方案的优劣
优点:经过本人多次测试,该加壳方式简单、快捷,易于移植,兼容性比较好,加壳后,源程序未发现因加壳引起的其他异常。

缺点:此种加壳方案,会在安装程序主目录下,生成两个文件夹:my_payload_odex和my_payload_lib,其中会包含远程的apk文件,当攻击者拿到apk后,也就意味着加壳方案的失败。综合来看此种方案虽然无法抵御例如xposed等插桩调试,但是依然会增加破解者的调试难度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第12章:视图

一、视图 1.常见的数据库对象 ①表table&#xff1a;表是存储数据的逻辑单元&#xff0c;行和列形式存在。列是字段&#xff0c;行是记录。 ②数据字典&#xff1a;系统表&#xff0c;存放数据库相关信息的表。系统表的数据通常是数据库系统维护。 ③约束constraint&#x…

怎样自己开发制作微信小程序?费用多少?

随着移动互联网的深入发展&#xff0c;各行各业都在寻求转型&#xff0c;希望通过线上软件系统来助力传统企业更好的发展&#xff0c;于是各种APP、小程序软件系统层出不穷。微信小程序是诸多软件中最流行的一种&#xff0c;凭借强大的流量基础、随用随走的便捷性和简单易操作的…

【C++】数据结构的恶龙set和map来了~

下一篇AVL树难点中的难点~ 文章目录 前言一、set的介绍二、map的介绍 题目练习总结 前言 1.关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、 forward_list(C11)等&#xff0c;这些容器统称为序列式容…

vue3【抛弃vuex,使用pinia】

Pinia Pinia 中文文档 一个全新的用于Vue的状态管理库下一个版本的vuex&#xff0c;也就是vuex5.0vue2 和vue3都支持在vue2中pinia使用vuex的现有接口&#xff0c;所以不能与vuex一起使用相比vuex有更完美的TypeScript支持支持服务器端渲染 Pinia核心概念 Pinia 从使用角度和…

【论文笔记】Learning Latent Dynamics for Planning from Pixels

论文及代码解读&#xff1a;Learning Latent Dynamics for Planning from Pixels 文章目录 论文及代码解读&#xff1a;Learning Latent Dynamics for Planning from Pixels3. Recurrent State Space ModelLatent dynamicsVariational encoderTraining objectiveDeterministic …

Linux系统编程学习 NO.2 ——环境配置和基础指令的学习

操作系统根据使用方式分类 操作系统按照使用类型可分为图形化操作系统和指令操作系统。图形化操作系统的本质其实也是根据指令来操作的。指令更贴近操作系统的底层。而我在学习Linux系统编程时&#xff0c;采用命令行的方式来学习。 补充一个小知识&#xff1a;早期命令行操作…

原装RS罗德与施瓦茨FSW85、FSW50,FSW67信号+频谱分析仪

Rohde & Schwarz FSW85 2Hz至85GHz信号和频谱分析仪 特征 10 kHz 偏移&#xff08;1 GHz 载波&#xff09;时的低相位噪声为 –137 dBc (1 Hz) WCDMA ACLR 测量的 -88 dBc 动态范围&#xff08;带噪声消除&#xff09; 高达 5 GHz 的分析带宽 < 0.4 dB 总测量不确定度高…

投资回报率业内最高!FortiGate在CyberRatings防火墙独立测试中收获近乎完美表现

对于需参考客观产品数据以制定明智采购决策的企业 IT 采购方而言&#xff0c;公正的第三方测试数据不失为一项关键参考。幸运的是&#xff0c;国际第三方安全测评机构CyberRatings始终秉持公平公正的独立测试理念&#xff0c;致力于量化网络安全风险&#xff0c;为多种网络安全…

用gost实现远程端口映射

gost 是一个非常优秀的tunnel. 支持多种形式的端口映射。 本文只介绍远程端口映射方式的tunnel. 远程端口映射的意思就是&#xff0c;将本地端的某个服务的端口A&#xff08;tcp/udp&#xff09;映射到远程的某个端口P上&#xff0c; 用户通过访问远程的端口P来访问本地端的这…

Linux多路转接之poll

文章目录 一、poll的认识二、编写poll方案服务器三、poll方案多路转接的总结 一、poll的认识 多路转接技术是在不断更新进步的&#xff0c;一开始多路转接采用的是select方案&#xff0c;但是select方案存在的缺点比较多&#xff0c;所以在此基础上改进&#xff0c;产生了poll…

怎么缩小照片的kb,压缩照片kb的几种方法

缩小照片的KB大小是我们日常工作生活中遇到的常见问题。虽然听起来十分专业&#xff0c;但其实很简单。照片的KB是指照片文件的大小&#xff0c;通常以“KB”为单位表示。缩小照片的KB就是减小照片文件的大小&#xff0c;以便占用更少的磁盘空间或更快地上传和下载照片。在实际…

什么是BI ?BI 能给企业带来什么价值?

目前&#xff0c;社会数字化程度还在不断加深&#xff0c;数据量也伴随着一同高速增长&#xff0c;许多人预测未来将是数据处理时代&#xff0c;而作为数据类解决方案的商业智能BI也会持续扩张市场&#xff0c;朝着不同行业BI商业智能的方向发展。 利用BI工具系统&#xff0c;…

MySQL 的 varchar 存储原理:InnoDB 记录存储结构

1. InnoDB 是干嘛的&#xff1f; InnoDB 是一个将表中的数据存储到磁盘上的存储引擎。 2. InnoDB 是如何读写数据的&#xff1f; InnoDB 处理数据的过程是发生在内存中的&#xff0c;需要把磁盘中的数据加载到内存中&#xff0c;如果是处理写入或修改请求的话&#xff0c;还…

统计学01: 中心极限定律、正态分布、z-score

<~生~信~交~流~与~合~作~请~关~注~公~众~号生信探索> 中心极限定律 中心极限定律&#xff1a;当样本样足够大时&#xff08;n≥30&#xff09;&#xff0c;样本的mean等于总体的mean 例如&#xff0c;对学校的学生身高抽样&#xff0c;100组每组30人&#xff0c;每组的身…

JavaScript沙箱

1、什么是沙箱 在计算机安全中&#xff0c;沙箱&#xff08;Sandbox&#xff09;是一种用于隔离正在运行程序的安全机制&#xff0c;通常用于执行未经测试或者不受信任的程序或代码&#xff0c;它会为待执行的程序创建一个独立的执行环境&#xff0c;内部程序的执行不会影响到…

【JOSE约瑟 JZS-7E14/11静态可调延时中间继电器 自动控制电路 接通、分断电路】

JZS-7E14/11静态可调延时中间继电器品牌:JOSEF约瑟名称:静态可调延时中间继电器型号:JZS-7E14/11额定电压:6220VDC&#xff1b;6380VAC触点容量:10A/250V10A/220VDC功率消耗:≤6W 一 用途 JZS-7E系列中间继电器用于各种保护和自动控制装置中,以增加保护和控制回路的触点容量. …

Java面试知识点(全)-数据结构和算法

Java面试知识点(全) 导航&#xff1a; https://nanxiang.blog.csdn.net/article/details/130640392 注&#xff1a;随时更新 基础的数据结构 数组 数组的下标寻址十分迅速&#xff0c;但计算机的内存是有限的&#xff0c;故数组的长度也是有限的&#xff0c;实际应用当中的数据…

伙伴云CEO戴志康:低代码与GPT,是赛车手和领航员的角色

GPT来的突然&#xff0c;不仅打了那些对AI冷眼相待的人们一个措手不及&#xff0c;也顺势带动了全民”AIGC”讨论热潮&#xff0c;让大众开始期待它的到来&#xff0c;能为这个人间添上多少精彩.... 万众期待下&#xff0c;GPT也没谦虚&#xff0c;大笔一挥间便融入了到了协同办…

Java集合常见面试题

1、Java集合概述 Java集合&#xff0c;也叫作容器。由两大接口派生而来&#xff1a;Collection接口&#xff0c;用于存放单一元素&#xff1b;Map接口&#xff0c;主要用于存放键值对。对于Collection接口&#xff0c;下面又有三个主要的子接口&#xff1a;List、Set、Queue 2…

桌面远程工具推荐

目前市面上的远程工具多如牛毛&#xff0c;很多人不知道怎么选择&#xff0c;下面小编介绍两种桌面远程工具&#xff0c;它们都是跨平台的&#xff0c;均支持Windows&#xff0c;Mac OS&#xff0c;IOS和安卓&#xff0c;分别是RayLink&#xff0c;VNC&#xff0c;好用&#xf…