首先让我们先了解一下一个完整的Android应用程序都由哪些文件组成。解压一个apk包,我们可以看到一下的这些文件及文件夹:
每个文件及文件夹的作用如下表所示。
这里说明下META-INF文件夹下3个文件的关系:
(1) 首先对apk包中每个文件做一次算法(数据摘要+Base64编码),然后保存到MANIFEST.MF文件中
(2) 然后对MANIFEST.MF整个文件同样做一次算法(数据摘要+Base64编码),存放到CERT.SF文件的头属性中,再对MANIFEST.MF文件中各个属性块做一次算法(数据摘要+Base64编码),存放到CERT.SF文件中
(3) 最后对CERT.SF文件做签名,内容保存到CERT.RSA中
TODO:META-INF目录下三个文件中SHA-1值的计算
我们看一下APK打包的完整流程:
上图中涉及到的工具及其作用如下:
TODO:单独学习APK打包流程
Dex文件整体结构如下:
dex文件结构的详细分析可以回看第四章-Dex文件结构分析。这里我们在dex整体加密,暂时只用到Dex文件头,所以我们主要介绍下Dex文件头的结构。
这里面,有3个成员我们需要特别关注,这在后面加固里会用到,它们分别是checksum、signature和fileSize。
checksum是校验码字段,占4bytes,主要用来检查从该字段(不包含checksum字段,也就是从12bytes开始算起)开始到文件末尾,这段数据是否完整,也就是完整性校验。它使用alder32算法校验。
signature是SHA-1签名字段,占20bytes,作用跟checksum一样,也是做完整性校验。之所以有两个完整性校验字段,是由于先使用checksum字段校验可以先快速检查出错的dex文件,然后才使用第二个计算量更大的校验码进行计算检查。
占4bytes,保存classes.dex文件总长度。
这3个字段当我们修改dex文件的时候,这3个字段的值是需要更新的,否则在加载到Dalvik虚拟机的时候会报错。
替换classloader时机
Application:attachBaseContext()
ContentProvider:onCreate()
APplication:onCreate()
加载原始application
1.获取原始application的class name
2.加载原始application并生成对象
3.替换API层所有Application引用
4.设置baseContext并调用原始application的onCreate()
Dex 文件整体加固原理如下:
在该过程中涉及到三个对象,分别如下:
l 源程序
源程序也就是我们的要加固的对象,这里面主要修改的是原apk文件中的classes.dex文件和AndroidManifest.xml文件。
l 壳程序
壳程序主要用于解密经过加密了的dex文件,并加载解密后的原dex文件,并正常启动原程序。
l 加密程序
加密程序主要是对原dex文件进行加密,加密算法可以是简单的异或操作、反转、rc4、des、rsa等加密算法。
该加固过程可以分为如下4个阶段:
(1) 加密阶段
(2)合成新的dex文件
(3)修改原apk文件并重打包签名
(4)运行壳程序加载原dex文件
加密阶段主要是讲把原apk文件中提取出来的classes.dex文件通过加密程序进行加密。加密的时候如果使用des对称加密算法,则需要注意处理好密钥的问题。同样的,如果采用非对称加密,也同样存在公钥保存的问题。
这一阶段主要是讲上一步生成的加密的dex文件和我们的壳dex文件合并,将加密的dex文件追加在壳dex文件后面,并在文件末尾追加加密dex文件的大小数值。这样我们就合成了新的classes.dex文件。由于我们修改了dex文件,这时候
当然,还可以这样处理。将加密的dex文件保存到其他目录下,如assets资源目录。那么这时候我们新的classes.dex文件就直接是壳程序,不需要合成。两种方案都可以。
前面讲了讲加密dex文件保存到文件末尾,以及保存到资源目录下2中方法。其实还有一种更复杂的方案,就是讲加密dex文件保存到dex header中。我们回顾10.1.3节 Dex文件结构。Dex Header中有一个字段:header_size,这个字段是记录dex文件头的长度,一般情况下dex头文件大小为112bytes。我们可以将header_size修改为header_size和加密dex文件长度两者的总长度,并将加密dex文件嵌入到dex header末尾来达到隐藏原dex文件的目的。但是这里有个问题,就是原来dex文件头部以下的一些类、方法、字段等位移可能发生改变需要修改,这个修复工程很复杂,或者我们可以尝试使用修改dx工具的源码来实现。
在壳程序里面,有个重要的类:ProxyApplication类,该类继承Application类,也是应用程序最先运行的类。所以,我们就是在这个类里面,在原程序运行之前,进行一些解密dex文件和加载原dex文件的操作。
在这一阶段,我们首先将apk解压,会看到如下图的6个文件和目录。其中,我们需要修改的只有2个文件,分别是classes.dex和AndroidManifest.xml文件,其他文件和文件加都不需要改动。
首先,我们把解压后apk目录下原来的classes.dex文件替换成我们在0x02上一步合成的新的classes.dex文件。然后,由于我们程序运行的时候,首先加载的其实是壳程序里的ProxyApplication类。所以,我们需要修改AndroidManifest.xml文件,指定application为ProxyApplication,这样才能正常找到识别ProxyApplication类并运行壳程序。这里,需要注意的是。
完成上述两个文件的替换后,我们就重新打包apk并签名。到此,我们就完成了dex文件的整体加固。
通过上面3个步骤,我们了解了dex文件整体加固的流程。那么,当这个加固后的dex文件加载到Dalvik虚拟机中,它又是如何工作的呢?下面我们就来讲解下壳程序在这里面发挥的重要作用。
首先,Dalvik虚拟机会加载我们经过修改的新的classes.dex文件,并最先运行ProxyApplication类。在这个类里面,有2个关键的方法:attachBaseContext和onCreate方法。ProxyApplication显示运行attachBaseContext再运行onCreate方法。
在attachBaseContext方法里,主要做两个工作:
读取classes.dex文件末尾记录加密dex文件大小的数值,则加密dex文件在新classes.dex文件中的位置为:len(新classes.dex文件) – len(加密dex文件大小)。然后将加密的dex文件读取出来,加密并保存到资源目录下
然后使用自定义的DexClassLoader加载解密后的原dex文件
在onCreate方法中,主要做两个工作:
通过反射修改ActivityThread类,并将Application指向原dex文件中的Application
创建原Application对象,并调用原Application的onCreate方法启动原程序
具体的app启动过程可以回看第2章的内容。
源程序我们使用NDK开发一个简单的demo,并在assets目录存放一个txt文件读取。这样,我们就可以得到跟10.1.1一样的文件结构,也可以检测下加固后的文件是否影响lib文件的和资源目录下文件的操作。
工程结构如下:
MyApplication.java关键代码:
1. public class MyApplication extends Application{
4. @Override
6. public void onCreate() {
8. // TODO Auto-generated method stub
10. super.onCreate();
12. }
16. @Override
18. protected void attachBaseContext(Context base) {
20. // TODO Auto-generated method stub
22. super.attachBaseContext(base);
24. }
26. }
MainActivity.java关键代码:
1. public class MainActivity extends Activity
3. {
5. /** Called when the activity is first created. */
9. public native void test();
13. @Override
15. public void onCreate(Bundle savedInstanceState)
17. {
19. super.onCreate(savedInstanceState);
21. setContentView(R.layout.main);
25. System.loadLibrary("demo1");
27. test();
31. Log.v("demo", getFromAssets("test.txt"));
35. }
39. public String getFromAssets(String fileName){
41. String Result="";
43. try {
45. InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
47. BufferedReader bufReader = new BufferedReader(inputReader);
49. String line="";
53. while((line = bufReader.readLine()) != null)
55. Result += line;
59. } catch (Exception e) {
61. e.printStackTrace();
63. }
65. return Result;
67. }
69. }
demo1.cpp关键代码:
1. #include "com_demo_MainActivity.h"
3. #include <android log.h="">
5. #include <stdio.h>
9. #define LOG_TAG "AndroidNDK"
11. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
15. JNIEXPORT void JNICALL Java_com_demo_MainActivity_test (JNIEnv * env, jobject obj)
17. {
19. LOGI("Hello World...\n");
21. }
22. </stdio.h></android>
加密程序是个java工程。
Main.java源码:
1. public static void main(String[] args) throws Exception {
3. if(args.length != 2){
4. System.out.println("Error! Please input 2 parameters!");
5. return ;
6. }
8. //参数1:脱壳classes.dex
9. //参数2:TargetApk.zip
10. File tuokeDexFile = new File(args[0]);
11. File targetFile = new File(args[1]);
13. byte[] tuokeDexFileArray = readFileBytes(tuokeDexFile);
14. byte[] targetFileArray = encrypt(readFileBytes(targetFile));
15. int tuokeDexFileLen = tuokeDexFileArray.length;
16. int targetFileLen = targetFileArray.length;
17. int totalLen = targetFileLen + tuokeDexFileLen + 4;
18. byte[] newdex = new byte[totalLen];
20. //添加脱壳classes.dex文件
21. System.arraycopy(tuokeDexFileArray, 0, newdex, 0, tuokeDexFileLen);
23. //添加目标TargetApk.zip文件
24. System.arraycopy(targetFileArray, 0, newdex, tuokeDexFileLen, targetFileLen);
26. //添加目标zip文件大小
27. System.arraycopy(intToByte(targetFileLen), 0, newdex, tuokeDexFileLen + targetFileLen, 4);
29. //修改Dex file size文件头
30. fixFileSizeHeader(newdex);
32. //修改Dex SHA1文件头
33. fixSHA1Header(newdex);
35. //修改Dex CheckSum文件头
36. fixCheckSumHeader(newdex);
38. File file = new File("classes.dex");
39. if(!file.exists()){
40. file.createNewFile();
41. }
43. FileOutputStream out = new FileOutputStream(file);
44. out.write(newdex);
45. out.flush();
46. out.close();
48. System.out.println("Enforce apk successfully!");
50. }
修改dex头的fileSize字段
1. private static void fixSHA1Header(byte[] dexBytes)
3. throws NoSuchAlgorithmException {
5. MessageDigest md = MessageDigest.getInstance("SHA-1");
7. md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
9. byte[] newdt = md.digest();
11. System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
13. //输出sha-1值,可有可无
15. String hexstr = "";
17. for (int i = 0; i < newdt.length; i++) {
19. hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16).substring(1);
21. }
23. }
修改dex头的checkSum字段
修改dex头的signature字段
1. private static void fixCheckSumHeader(byte[] dexBytes) {
3. Adler32 adler = new Adler32();
4. long value = adler.getValue();
5. int va = (int) value;
6. byte[] newcs = intToByte(va);
8. //高位在前,低位在前掉个个
9. byte[] recs = new byte[4];
11. for (int i = 0; i < 4; i++) {
12. recs[i] = newcs[newcs.length - 1 - i];
13. }
15. System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)
17. }
加密:
1. private static byte[] encrypt(byte[] srcdata){
3. for(int i = 0;i < srcdata.length;i++){
5. srcdata[i] = (byte)(0xFF ^ srcdata[i]);
7. }
9. return srcdata;
11. }
读取二进制文件内容
1. private static byte[] readFileBytes(File file) throws IOException {
3. byte[] arrayOfByte = new byte[1024];
5. ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
7. FileInputStream fis = new FileInputStream(file);
9. while (true) {
11. int i = fis.read(arrayOfByte);
13. if (i != -1) {
15. localByteArrayOutputStream.write(arrayOfByte, 0, i);
17. } else {
19. return localByteArrayOutputStream.toByteArray();
21. }
23. }
25. }
int转byte数组
1. public static byte[] intToByte(int number) {
3. byte[] b = new byte[4];
5. for (int i = 3; i >= 0; i--) {
7. b[i] = (byte) (number % 256);
9. number >>= 8;
11. }
13. return b;
15. }
壳程序我们主要分析ProxyApplication类:
首先分析attachBaseContext方法:
1. protected void attachBaseContext(Context base) {
3. super.attachBaseContext(base);
4. Log.v("demo", "[JiaguApk]=>attachBaseContext() start...");
6. try {
8. File odex = this.getDir("assets", MODE_PRIVATE);
9. String odexPath = odex.getAbsolutePath();
10. String targetFilename = odexPath + "/TargetApk.zip";
11. File targetApkZipFile = new File(targetFilename);
13. if(!targetApkZipFile.exists()){
14. //从classes.dex中提取TargetApk.zip
15. targetApkZipFile.createNewFile();
16. byte[] classesDexData = readClassesDexFromApk();
18. extractTargetZipFileFromDex(classesDexData, targetFilename);
19. }
21. Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});
22. String packageName = getPackageName();
23. Map mPackages = (Map) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages");
24. WeakReference wr = (WeakReference) mPackages.get(packageName);
26. DexClassLoader dLoader = new DexClassLoader(odexPath + "/TargetApk.zip", odexPath, "/data/data/" + packageName + "/lib", base.getClassLoader().getParent());
28. //替換成TargetApk.dex的ClassLoader
29. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);
31. } catch (Exception e) {
32. Log.v("demo", "[JiaguApk]=>attachBaseContext() " + Log.getStackTraceString(e));
34. }
36. Log.v("demo", "[JiaguApk]=>attachBaseContext() end...");
38. }
在该方法中,我们首先获取加密dex文件,并保存到assets资源目录下,然后进行解密。
由于运行加固后的apk文件时,应用程序使用的是加固后dex文件的类加载器,而不是原dex文件的类加载器。所以,我们需要事先将类加载器替换成员dex文件的类加载器。
这里,我们通过反射获取ActivityThread,通过其mPackages成员获取LoadedApk对象。通过第二章的app启动过程分析,我们知道类加载器的创建是在LoadedApk中完成,这也是为什么我们需要获取LoadedApk对象。通过该对象的mClassLoader成员,我们可以修改该成员指向我们自定义的DexClassLoader,这个类加载器就是原dex中的类加载器。这样我们就可以在后面的步骤中用该类加载器加载原dex文件。
1. public void onCreate() {
3. try {
5. // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
6. String appClassName = "com.demo.MyApplication";
8. /**
9. * 调用静态方法android.app.ActivityThread.currentActivityThread
10. * 获取当前activity所在的线程对象
11. */
13. Object currentActivityThread = RefInvoke.invokeStaticMethod(
14. "android.app.ActivityThread", "currentActivityThread",
15. new Class[] {}, new Object[] {});
16. /**
17. * 获取currentActivityThread中的mBoundApplication属性对象,该对象是一个
18. * AppBindData类对象,该类是ActivityThread的一个内部类
19. */
20. Object mBoundApplication = RefInvoke.getFieldOjbect(
21. "android.app.ActivityThread", currentActivityThread,
22. "mBoundApplication");
23. /**
24. * 获取mBoundApplication中的info属性,info 是 LoadedApk类对象
25. */
27. Object loadedApkInfo = RefInvoke.getFieldOjbect(
28. "android.app.ActivityThread$AppBindData",
29. mBoundApplication, "info");
31. if(null == loadedApkInfo){
32. Log.v("demo", "[JiaguApk]=>onCreate()=>loadedApkInfo is null!!!");
33. }else{
34. Log.v("demo", "[JiaguApk]=>onCreate()=>loadedApkInfo:" + loadedApkInfo);
35. }
37. /**
38. * loadedApkInfo对象的mApplication属性置为null
39. */
40. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
41. loadedApkInfo, null);
45. /**
46. * 获取currentActivityThread对象中的mInitialApplication属性
47. * 这货是个正牌的 Application
48. */
50. Object oldApplication = RefInvoke.getFieldOjbect(
51. "android.app.ActivityThread", currentActivityThread,
52. "mInitialApplication");
55. /**
56. * 获取currentActivityThread对象中的mAllApplications属性
57. * 这货是 装Application的列表
58. */
60. ArrayList<application> mAllApplications = (ArrayList<application>) RefInvoke
61. .getFieldOjbect("android.app.ActivityThread",
62. currentActivityThread, "mAllApplications");
65. //列表对象终于可以直接调用了 remove调了之前获取的application 抹去记录的样子
66. mAllApplications.remove(oldApplication);
68. /**
69. * 获取前面得到LoadedApk对象中的mApplicationInfo属性,是个ApplicationInfo对象
70. */
71. ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
72. .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
73. "mApplicationInfo");
76. /**
77. * 获取前面得到AppBindData对象中的appInfo属性,也是个ApplicationInfo对象
78. */
80. ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
81. .getFieldOjbect("android.app.ActivityThread$AppBindData",
82. mBoundApplication, "appInfo");
86. //把这两个对象的className属性设置为从meta-data中获取的被加密apk的application路径
87. appinfo_In_LoadedApk.className = appClassName;
89. /**
90. * 调用LoadedApk中的makeApplication 方法 造一个application
91. * 前面改过路径了
92. */
94. Application app = (Application) RefInvoke.invokeMethod(
95. "android.app.LoadedApk", "makeApplication", loadedApkInfo,
96. new Class[] { boolean.class, Instrumentation.class },
97. new Object[] { false, null });
99. RefInvoke.setFieldOjbect("android.app.ActivityThread",
100. "mInitialApplication", currentActivityThread, app);
102. if(null == app){
103. Log.v("demo", "[JiaguApk]=>onCreate()=>app is null!!!");
104. }else{
105. app.onCreate();
106. Log.v("demo", "[JiaguApk]=>onCreate() success!");
107. }
109. } catch (Exception e) {
111. Log.v("demo", "[JiaguApk]=>onCreate() " + Log.getStackTraceString(e));
113. }
115. Toast.makeText(this, "Enforced by 01hackcode", Toast.LENGTH_LONG).show();
117. }
118. </application></application>
在onCreate方法中,我们主要做的是还原Application对象。由于在加固dex文件的时候,我们讲原来的Application替换成了我们定义的ProxyApplication对象。所以,在这一步里,我们需要还原它。至于怎么还原呢?同样参考第二章的app启动流程,我们知道,Application对象的创建发生在ActivityThread类的handleBindApplication方法中,该方法其实是调用LoadedApk类的makeApplication方法进行创建,而在该方法中,最终是调用Instrumentation类的newApplication方法完成Application对象的创建。最后调用原Application对象的onCreate方法完成启动过程。
在这个过程中,我们要将所有引用到Application的地方都进行修改,这也是为什么上面那么多种反射修改操作。通过第二章的分析,我们发现引用了Application的类有3个地方。
(1) ActivityThread类的mInitialApplication成员,它是Application类,我们需要将替换成原dex文件的Application对象
(2) ActivityThread类的mAllApplication成员,每次创建完Application对象后,就会将该对象添加到mAllApplication列表中,所以我们要讲该列表中的ProxyApplication对象移除。在后面我们通过反射调用makeApplication方法的时候会将创建的原dex文件的Application添加到该列表中
(3) LoadedApk类中的mApplicationInfo对象的className成员记录了ProxyApplication的类名,这里我们也需要修改,替换成原dex文件的Application类名
(4) 最后是ActivityThread的内部类AppBindData类对象mBoundApplication成员的appInfo对象的className成员也记录了ProxyApplication类名,我们也将它修改成员dex原dex文件的Application类名。
至此,我们就差不多修改完了所有引用ProxyApplication对象的地方。最后还差一步,我们需要反射获取ActivityThread类的makeApplication方法完成原Dex文件中Application的创建工作,并调用该Application对象的onCreate方法。到这里,我们才可以正常运行原程序。
ActivityThread.java文件:
1. public final class ActivityThread{
3. …
5. Application mInitialApplication;
7. final ArrayList<application> mAllApplications = new ArrayList<application>();
9. final ArrayMap<string, weakreference<loadedapk="">> mPackages
11. = new ArrayMap<string, weakreference<loadedapk="">>();
13. …
15. static final class AppBindData {
17. …
19. ApplicationInfo appInfo;
21. …
23. }
25. }
26. </string,></string,></application></application>
LoadedApk.java文件:
1. public final class LoadedApk {
3. …
5. private final ApplicationInfo mApplicationInfo;
7. …
9. }
ApplicationInfo.java文件:
1. public class ApplicationInfo extends PackageItemInfo implements Parcelable {
3. …
5. /**
7. * Class implementing the Application object. From the "class"
8. * attribute.
9. */
11. public String className;
13. …
15. }
文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文
文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作 导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释: cwy_init/init_123..._达梦数据库导入导出
文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js
文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6
文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输
文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...
文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure
文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割
文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答
文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。
文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入
文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf