当前位置: 首页 > article >正文

Android 程序静态分析

简介

静态分析是探索Android程序内幕的一种最常见的方法,它与动态调剂双剑合璧,帮助分析人员解决分析时遇到的各种“疑难”问题。

静态分析是指在不运行的情况下,采用词法分析、语法分析等各种技术手段对程序文件进行扫描从而生成程序的反汇编代码,然后阅读反汇编代码来掌握

程序功能的一种技术,它有两种方法:一种方法是阅读反汇编生成的Dalvik字节码,可以用IDA Pro分析dex文件,或者使用文本编辑器阅读baksmali反编

译生成的smali文件;另一种方法是阅读反汇编生成的java源码,可以使用dex2jar生成jar文件,然后再使用jd-gui阅读jar文件的代码。


快速定位Android程序的关键代码

特点:

1、每个apk文件中都包含有一个AndroidManifest.xml文件,它记录着软件的一些基本信息。

2、一个Android程序由一个或多个Activity以及其它组成,每个Activity都是相同级别的,不同的Activity实现不同的功能。


六种方法定位关键代码:

1、信息反馈发:先运行目标程序,然后根据程序运行时给出的反馈信息作为突破口寻找关键代码。

2、特征函数法:跟信息反馈法类似。

3、顺序查看发:从软件的启动代码开始,逐行的向下分析,掌握软件的执行流程。

4、代码注入法:手动修改apk文件的反汇编代码,加入Log输出,配合LogCat查看程序执行到特定点时的状态数据。

5、栈跟踪法:输出运行时的栈跟踪信息,然后查看栈上的函数调用序列来理解方法的执行流程。

6、方法剖析:热点分析和性能优化。


smali文件格式

每个smali文件都由若干条语句组成,所有的语句都遵循着一套语法规则。在smali 文件的头3 行描述了当前类的一些信息,格式如下:

.class < 访问权限> [ 修饰关键字] < 类名>
.super < 父类名>
.source <源文件名>
打开MainActivity.smali 文件,头3 行代码如下:

.class public Lcom/droider/crackme0502/MainActivity;     //指令指定了当前类的类名。
.super Landroid/app/Activity;				//指令指定了当前类的父类。
.source "MainActivity.java"				//指令指定了当前类的源文件名。


smali文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式如下:

# static fields
.field < 访问权限> static [ 修饰关键字] < 字段名>:< 字段类型>

实例字段的声明与静态字段类似,只是少了static关键字,它的格式如下:

# instance fields
.field < 访问权限> [ 修饰关键字] < 字段名>:< 字段类型>
比如以下的实例字段声明。
# instance fields	//baksmali 生成的注释
.field private btnAnno:Landroid/widget/Button;	//私有字段


smali 文件中方法的声明使用“.method ”指令,方法有直接方法与虚方法两种。
直接方法的声明格式如下:

# direct methods				//添加的注释
.method <访问权限> [ 修饰关键字] < 方法原型><.locals>					//指定了使用的局部变量的个数[.parameter]					//指定了方法的参数[.prologue]					//指定了代码的开始处,混淆过的代码可能去掉了该指令[.line]					//指定了该处指令在源代码中的行号
<代码体>
.end method

虚方法的声明与直接方法相同,只是起始处的注释为“virtual methods”,如果一个类实现了接口,会在smali 文件中使用“.implements ”指令指出,相应的格式声明如下:

# interfaces
.implements < 接口名>		//接口关键字如果一个类使用了注解,会在 smali 文件中使用“.annotation ”指令指出,注解的格式声明如下:
# annotations
.annotation [ 注解属性] < 注解类名>[ 注解字段 =  值]
.end annotation

注解的作用范围可以是类、方法或字段。如果注解的作用范围是类,“.annotation ”指令会直接定义在smali 文件中,如果是方法或字段,“.annotation ”指令则会包含在方法或字段定义中。例如:
# instance fields
.field public sayWhat:Ljava/lang/String;			//String 类型 它使用了 com.droider.anno.MyAnnoField 注解,注解字段info 值为“Hello my friend”.annotation runtime Lcom/droider/anno/MyAnnoField;info = "Hello my friend".end annotation
.end field

Android 程序中的类
1、内部类

Java 语言允许在一个类的内部定义另一个类,这种在类中定义的类被称为内部类(Inner Class)。内部类可分为成员内部类、静态嵌套类、方法内部类、匿名内部类。在反编译dex 文件的时候,会为每个类单独生成了一个 smali 文件,内部类作为一个独立的类,它也拥有自己独立的smali 文件,只是内部类的文件名形式为“[外部类]$[内部类].smali ”,例如:

class Outer {class Inner{}
}
反编译上述代码后会生成两个文件:Outer.smali 与Outer$Inner.smali。打开文件,代码结构如下:

.class public Lcom/droider/crackme0502/MainActivity$SNChecker;
.super Ljava/lang/Object;
.source "MainActivity.java"# annotations
.annotation system Ldalvik/annotation/EnclosingClass;value = Lcom/droider/crackme0502/MainActivity;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;accessFlags = 0x1name = "SNChecker"
.end annotation# instance fields
.field private sn:Ljava/lang/String;
.field final synthetic this$0:Lcom/droider/crackme0502/MainActivity;# direct methods
.method public constructor
<init>(Lcom/droider/crackme0502/MainActivity;Ljava/lang/String;)V
……
.end method# virtual methods
.method public isRegistered()Z
……
.end method


发现它有两个注解定义块“Ldalvik/annotation/EnclosingClass;”与“Ldalvik/annotation/ InnerClass; ”、两个实例字段sn 与this$0 、一个直接方法 init()、一个虚方法isRegistered() 。this$0 是内部类自动保留的一个指向所在外部类的引用。左边的 this 表示为父类的引用,右边的数值0 表示引用的层数。


2、监听器
Android程序开发中大量使用到了监听器,如Button的点击事件响应OnClickListener、Button的长按事件响应OnLongClickListener、ListView列表项的点击事件响应 OnItemSelected-Listener等。

实例源码以及反编译设置按钮点击事件监听器的代码如下:

 public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);btnAnno = (Button) findViewById(R.id.btn_annotation);btnCheckSN = (Button) findViewById(R.id.btn_checksn);edtSN = (EditText) findViewById(R.id.edt_sn);btnAnno.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {getAnnotations();                }});btnCheckSN.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {SNChecker checker = new SNChecker(edtSN.getText().toString());String str = checker.isRegistered() ? "注册码正确" : "注册码错误";Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();}});}

反编译如下:
.method public onCreate(Landroid/os/Bundle;)V.locals 2.parameter "savedInstanceState"…….line 32iget-object v0, p0, Lcom/droider/crackme0502/MainActivity;->btnAnno:Landroid/widget/Button;new-instance v1, Lcom/droider/crackme0502/MainActivity$1; #新建一个MainActivity$1实例invoke-direct {v1, p0}, Lcom/droider/crackme0502/MainActivity$1;
-><init>(Lcom/droider/crackme0502/MainActivity;)V # 初始化MainActivity$1
实例invoke-virtual {v0, v1}, Landroid/widget/Button;
->setOnClickListener(Landroid/view/View$OnClickListener;)V # 设置按钮点击事件
监听器.line 40iget-object v0, p0, Lcom/droider/crackme0502/MainActivity;
->btnCheckSN:Landroid/widget/Button;new-instance v1, Lcom/droider/crackme0502/MainActivity$2; #新建一个
MainActivity$2实例invoke-direct {v1, p0}, Lcom/droider/crackme0502/MainActivity$2
-><init>(Lcom/droider/crackme0502/MainActivity;)V; # 初始化MainActivity$2实例invoke-virtual {v0, v1}, Landroid/widget/Button;
->setOnClickListener(Landroid/view/View$OnClickListener;)V#设置按钮点击事件
监听器.line 50return-void
.end method

在MainActivity$1.smali 文件的开头使用了“.implements ”指令指定该类实现了按钮点击事件的监听器接口,因此,这个类实现了它的OnClick()方法,这也是我们在分析程序时关心的地方。另外,程序中的注解与监听器的构造函数都是编译器为我们自己生成的,实际分析过程中不必关心。


3、注解类



注解是Java 的语言特性,在 Android的开发过程中也得到了广泛的使用。Android系统中涉及到注解的包共有两个:一个是dalvik.annotation;另一个是 android.annotation。

例如:

# annotations
.annotation system Ldalvik/annotation/AnnotationDefault;value = .subannotation Lcom/droider/anno/MyAnnoClass;value = "MyAnnoClass".end subannotation
.end annotation

除了SuppressLint与TargetApi注解,android.annotation 包还提供了SdkConstant与Widget两个注解,这两个注解在注释中被标记为“@hide”,即在 SDK 中是不可见的。SdkConstant注解指定了SDK中可以被导出的常量字段值,Widget 注解指定了哪些类是 UI类,这两个注解在分析Android程序时基本上碰不到,此处就不去探究了。


4、自动生成的类

使用 Android SDK 默认生成的工程会自动添加一些类。例如:

public final class R {public static final class attr {      //属性}public static final class dimen {      //尺寸public static final int padding_large=0x7f040002;public static final int padding_medium=0x7f040001;public static final int padding_small=0x7f040000;}public static final class drawable {    //图片public static final int ic_action_search=0x7f020000;public static final int ic_launcher=0x7f020001;}public static final class id {        //id标识public static final int btn_annotation=0x7f080000;public static final int btn_checksn=0x7f080002;public static final int edt_sn=0x7f080001;public static final int menu_settings=0x7f080003;}public static final class layout {    // 布局public static final int activity_main=0x7f030000;}public static final class menu {    // 菜单public static final int activity_main=0x7f070000;}public static final class string {    // 字符串public static final int app_name=0x7f050000;public static final int hello_world=0x7f050001;public static final int menu_settings=0x7f050002;public static final int title_activity_main=0x7f050003;}public static final class style {    // 样式public static final int AppTheme=0x7f060000;}
}
由于这些资源类都是R 类的内部类,因此它们都会独立生成一个类文件,在反编译出的代码中,可以发现有R.smali、R$attr.smali 、R$dimen.smali、R$drawable.smali、R$id.smali、R$layout.smali、R$menu.smali 、R$string.smali 、R$style.smali 等几个文件。

阅读smali反编译的代码

smali 文件中的语句特点:

1、循环语句

在 Android开发过程中,常见的循环结构有迭代器循环、for 循环、while循环、do while 循环。我们在编写迭代器循环代码时,一般是如下形式的代码:

Iterator< 对象> <对象名> = <方法返回一个对象列表>;
for (< 对象> <对象名> : <对象列表>) {
[处理单个对象的代码体]
}
或者:
Iterator< 对象> <迭代器> = <方法返回一个迭代器>;
while (<迭代器>.hasNext()) {<对象> <对象名> = <迭代器>.next();
[处理单个对象的代码体]
}

.method private iterator()V.locals 7.prologue.line 34const-string v4, "activity"invoke-virtual {p0, v4}, Lcom/droider/circulate/MainActivity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;  # 获取ActivityManagermove-result-object v0check-cast v0, Landroid/app/ActivityManager;.line 35.local v0, activityManager:Landroid/app/ActivityManager;invoke-virtual {v0}, Landroid/app/ActivityManager;->getRunningAppProcesses()Ljava/util/List;move-result-object v2    #正在运行的进程列表.line 36.local v2, psInfos:Ljava/util/List;,"Ljava/util/List<Landroid/app/ActivityManager$RunningAppProcessInfo;>;"new-instance v3, Ljava/lang/StringBuilder;  # 新建一个StringBuilder 对象invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V  # 调用StringBuilder 构造函数.line 37.local v3, sb:Ljava/lang/StringBuilder;invoke-interface {v2}, Ljava/util/List;->iterator()Ljava/util/Iterator;#获取进程列表的迭代器move-result-object v4:goto_0 #迭代循环开始invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z #开始迭代move-result v5if-nez v5, :cond_0  # 如果迭代器不为空就跳走.line 40invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;move-result-object v4  # StringBuilder转为字符串const/4 v5, 0x0invoke-static {p0, v4, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v4invoke-virtual {v4}, Landroid/widget/Toast;->show()V # 弹出StringBuilder的内容.line 41return-void  # 方法返回.line 37:cond_0invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object; # 循环获取每一项move-result-object v1check-cast v1, Landroid/app/ActivityManager$RunningAppProcessInfo;.line 38.local v1, info:Landroid/app/ActivityManager$RunningAppProcessInfo;new-instance v5, Ljava/lang/StringBuilder;  # 新建一个临时的StringBuilderiget-object v6, v1, Landroid/app/ActivityManager$RunningAppProcessInfo;->processName:Ljava/lang/String;    #获取进程的进程名invoke-static {v6}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;move-result-object v6invoke-direct {v5, v6}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)Vconst/16 v6, 0xa #换行符invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;move-result-object v5 # 组合进程名与换行符invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;move-result-object v5invoke-virtual {v3, v5}, Ljava/lang/StringBuilder; # 将组合后的字符串添加到StringBuilder 末尾->append(Ljava/lang/String;)Ljava/lang/StringBuilder;goto :goto_0  #跳转到循环开始处
.end method

这段代码的功能是获取正在运行的进程列表,然后使用Toast弹出所有的进程名。

forCirculate() 方法如下:

.method private forCirculate()V.locals 8.prologue.line 47invoke-virtual {p0}, Lcom/droider/circulate/MainActivity;->getApplicationContext()Landroid/content/Context;move-result-object v6invoke-virtual {v6}, Landroid/content/Context;    #获取PackageManager->getPackageManager()Landroid/content/pm/PackageManager;move-result-object v3.line 49.local v3, pm:Landroid/content/pm/PackageManager;const/16 v6, 0x2000.line 48invoke-virtual {v3, v6}, Landroid/content/pm/PackageManager;->getInstalledApplications(I)Ljava/util/List;  #获取已安装的程序列表move-result-object v0.line 50.local v0, appInfos:Ljava/util/List;,"Ljava/util/List<Landroid/content/pm/ApplicationInfo;>;"invoke-interface {v0}, Ljava/util/List;->size()I  # 获取列表中ApplicationInfo对象的个数move-result v5.line 51.local v5, size:Inew-instance v4, Ljava/lang/StringBuilder;          # 新建一个StringBuilder 对象invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V  # 调用StringBuilder 的构造函数.line 52.local v4, sb:Ljava/lang/StringBuilder;const/4 v1, 0x0.local v1, i:I #初始化v1为0:goto_0 #循环开始if-lt v1, v5, :cond_0   #如果v1小于v5,则跳转到cond_0 标号处.line 56invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;move-result-object v6const/4 v7, 0x0invoke-static {p0, v6, v7}, Landroid/widget/Toast; #构造Toast->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v6invoke-virtual {v6}, Landroid/widget/Toast;->show()V #显示已安装的程序列表.line 57return-void  # 方法返回.line 53:cond_0invoke-interface {v0, v1}, Ljava/util/List;->get(I)Ljava/lang/Object; # 单个ApplicationInfomove-result-object v2check-cast v2, Landroid/content/pm/ApplicationInfo;.line 54.local v2, info:Landroid/content/pm/ApplicationInfo;new-instance v6, Ljava/lang/StringBuilder;  # 新建一个临时StringBuilder对象iget-object v7, v2, Landroid/content/pm/ApplicationInfo;->packageName:Ljava/lang/String;invoke-static {v7}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;move-result-object v7  # 包名invoke-direct {v6, v7}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)Vconst/16 v7, 0xa #换行符invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;move-result-object v6  # 组合包名与换行符invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; #转换为字符串move-result-object v6invoke-virtual {v4, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;  # 添加到循环外        的StringBuilder 中.line 52add-int/lit8 v1, v1, 0x1 #下一个索引goto :goto_0 #跳转到循环起始处
.end method

这段代码的功能是获取所有安装的程序,然后使用Toast弹出所有的软件包名。


2、switch分支语句

packedSwitch()方法的代码如下:

.method private packedSwitch(I)Ljava/lang/String;.locals 1.parameter "i".prologue.line 21const/4 v0, 0x0.line 22.local v0, str:Ljava/lang/String;  #v0为字符串,0表示nullpacked-switch p1, :pswitch_data_0  #packed-switch分支,pswitch_data_0指定case区域.line 36const-string v0, "she is a person"  #default分支.line 39:goto_0      #所有case的出口return-object v0 #返回字符串v0.line 24:pswitch_0    #case 0const-string v0, "she is a baby".line 25goto :goto_0  #跳转到goto_0标号处.line 27:pswitch_1    #case 1const-string v0, "she is a girl".line 28goto :goto_0  #跳转到goto_0标号处.line 30:pswitch_2    #case 2const-string v0, "she is a woman".line 31goto :goto_0  #跳转到goto_0标号处.line 33:pswitch_3    #case 3const-string v0, "she is an obasan".line 34goto :goto_0  #跳转到goto_0标号处.line 22nop:pswitch_data_0.packed-switch 0x0    #case  区域,从0开始,依次递增:pswitch_0  #case 0:pswitch_1  #case 1:pswitch_2  #case 2:pswitch_3  #case 3.end packed-switch
.end method

代码中的switch 分支使用的是 packed-switch 指令。p1为传递进来的 int 类型的数值,pswitch_data_0 为case 区域,在 case 区域中,第一条指令“.packed-switch”指定了比较的初始值为0 ,pswitch_0~ pswitch_3分别是比较结果为“case 0 ”到“case 3 ”时要跳转到的地址。


3、try/catch 语句

tryCatch()方法代码如下:

.method private tryCatch(ILjava/lang/String;)V.locals 10.parameter "drumsticks".parameter "peple".prologueconst/4 v9, 0x0.line 19:try_start_0  # 第1个try开始invoke-static {p2}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I#将第2个参数转换为int 型:try_end_0    # 第1个try结束.catch Ljava/lang/NumberFormatException; {:try_start_0 .. :try_end_0} :catch_1 # catch_1move-result v1  #如果出现异常这里不会执行,会跳转到catch_1标号处.line 21.local v1, i:I    #.local声明的变量作用域在.local声明与.end local 之间:try_start_1  #第2个try 开始div-int v2, p1, v1  # 第1个参数除以第2个参数.line 22.local v2, m:I    #m为商mul-int v5, v2, v1  #m * isub-int v3, p1, v5  #v3 为余数.line 23.local v3, n:Iconst-string v5, "\u5171\u6709%d\u53ea\u9e21\u817f\uff0c%d\u4e2a\u4eba\u5e73\u5206\uff0c\u6bcf\u4eba\u53ef\u5206\u5f97%d\u53ea\uff0c\u8fd8\u5269\u4e0b%d\u53ea"   # 格式化字符串const/4 v6, 0x4new-array v6, v6, [Ljava/lang/Object;const/4 v7, 0x0.line 24invoke-static {p1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;move-result-object v8aput-object v8, v6, v7const/4 v7, 0x1invoke-static {v1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;move-result-object v8aput-object v8, v6, v7const/4 v7, 0x2invoke-static {v2}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;move-result-object v8aput-object v8, v6, v7const/4 v7, 0x3invoke-static {v3}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;move-result-object v8aput-object v8, v6, v7.line 23invoke-static {v5, v6}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;move-result-object v4.line 25.local v4, str:Ljava/lang/String;const/4 v5, 0x0invoke-static {p0, v4, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v5invoke-virtual {v5}, Landroid/widget/Toast;->show()V  # 使用Toast 显示格式化后的结果:try_end_1  #第2个try 结束.catch Ljava/lang/ArithmeticException; {:try_start_1 .. :try_end_1} :catch_0   # catch_0.catch Ljava/lang/NumberFormatException; {:try_start_1 .. :try_end_1} :catch_1   # catch_1.line 33.end local v1           #i:I.end local v2           #m:I.end local v3           #n:I.end local v4           #str:Ljava/lang/String;:goto_0 return-void  # 方法返回.line 26.restart local v1       #i:I:catch_0   move-exception v0.line 27.local v0, e:Ljava/lang/ArithmeticException;:try_start_2  #第3个try 开始const-string v5, "\u4eba\u6570\u4e0d\u80fd\u4e3a0" #“人数不能为0”const/4 v6, 0x0invoke-static {p0, v5, v6}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v5invoke-virtual {v5}, Landroid/widget/Toast;->show()V  # 使用Toast 显示异    常原因:try_end_2    #第3个try 结束.catch Ljava/lang/NumberFormatException; {:try_start_2 .. :try_end_2} :catch_1goto :goto_0 #返回.line 29.end local v0           #e:Ljava/lang/ArithmeticException;.end local v1           #i:I:catch_1 move-exception v0.line 30.local v0, e:Ljava/lang/NumberFormatException;const-string v5, "\u65e0\u6548\u7684\u6570\u503c\u5b57\u7b26\u4e32" #“无效的数值字符串”invoke-static {p0, v5, v9}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v5invoke-virtual {v5}, Landroid/widget/Toast;->show()V  # 使用Toast 显示异常原因goto :goto_0 #返回
.end method


整段代码的功能比较简单,输入鸡腿数与人数,然后使用Toast弹出鸡腿的分配方案。传入人数时为了演示Try/Catch效果,使用了String 类型。代码中有两种情况下会发生异常:第一种是将String 类型转换成 int 类型时可能会发生 NumberFormatException异常;第二种是计算分配方法时除数为零的ArithmeticException异常。
在Dalvik 指令集中,并没有与Try/Catch相关的指令,在处理Try/Catch语句时,是通过相关的数据结构来保存异常信息的。

小结

静态分析是软件分析过程中最基础也是最重要的一种手段,我们向Android逆向破解又迈进了一大步。

下载
实例源码以及工具下载



http://www.lryc.cn/news/2412493.html

相关文章:

  • CIS关键工艺技术概览
  • 商城源码:建立自己的电子商务平台的利器!
  • Python3 识别判断图片主要颜色,提取指定颜色的方法
  • 计算机的起源与发展历程
  • Leetcode 483 - Smallest Good Base(二分+枚举)
  • 抬杠APP获Donews“年度最佳运营创意”大奖,跨界营销引瞩目
  • 黑苹果教程 win7+virtualbox安装Mac os搭建完美越狱环境
  • android 上传图片到服务器_画质无损,体积减半,这些图片压缩神器,你一定要知道!...
  • 【网络安全】brainpan-windows缓冲区溢出详解
  • 制作windows xp开机画面并替换
  • 全球PM2.5空气颗粒污染分布数据地图
  • DisplayTag使用指南(二) DisplayTag标签库详解
  • 2023韩顺平java从入门到精通151G 视频教程 下载
  • CSS/HTML简单静态页面
  • 开心网(kaixin001)上的X世界小游戏
  • B站15周年公布了00后最喜爱视频,前三竟然都是他?!
  • (一)JMeter性能测试,完整入门篇:性能测试操作步骤
  • 8个Python爬虫高效数据抓取技巧
  • 运维的基础介绍(附加常用108个命令行)
  • 滚动插件SuperSlide的用法
  • 各资源下载地址整理
  • 12306订票候补是个坑_官方出手了!12306屏蔽多个抢票软件
  • Nodejs使用nodemailer发邮件
  • 小龙女彤彤为何能红
  • python02
  • 日志规范多重要,这篇文章告诉你!
  • 不要轻易和少妇上床:危机是怎样产生的
  • DM7.0在VM-中标麒麟NeoKylin虚拟机上安装
  • 视频教程-Vue 2.x全家桶(Vue 3.0 新特性全解析)-Vue
  • 学生住宿管理系统