Android软件越来越多的走向免费加广告模式了,但在我们日常应用中,还是不乏一些收费应用,一部分通过Market收费下载,另一部分将收费部分放到了软件的实现中,我们今天要讨论的就是后者,就我见到的一部分Android软件中,它们有通过序列号验证的,有通过Linsence绑定的,更有甚者是通过网络激活验证的,那它们的安全措施都有哪些?如何分析并破解它们?这将是这篇文章将要讲到的。

破解工具介绍

下表列举出了破解Android程序时可能会用到的工具:

工具名称

用途

AXMLPrinter2.jar

用来解密输出APK中加密的XML文件

dex-translator

包含dex2jar可以将APK中的classes.dex生成相应的JAR文件

JD-GUI.exe

查看dex2jar生成的JAR文件(源码级显示,不过不太准确)

DeDexer

DEX文件反编译工具,使用Jasmin格式

Smali,BakSmali

DEX文件编译与反编译工具,使用smali格式

EditPlus

查看及编辑反汇编后的文件

IDA Pro

DEX反编译查看工具,可用来查找DEX文件补丁位置

Apktool

集成了Smali与BakSmali,编译与反编译DEX更方便

Eclipse,ADT插件

编译Android补丁或测试程序

Netbeans

配合ApkTool单步调试Smali文件

AndBug

APK调试工具(只支持Linux平台)

Android SDK

这个开发与测试APK都需要,ADB,AAPT,DDMS经常要用到

Jarsigner.exe.,keytool.exe

对APK进行签名(JDK的Bin目录下)

ApkTool_GUI

这个现在用的人比较多,集成了反编译、编译、签名功能于一体

Android设备

测试程序用,没有的话模拟器或Android-x86(Android的PC版)也行

这些工具经常配合在一起使用来反编译与编译APK文件,但ApkTool_GUI的出现将这些繁琐的工作化繁为简。EditPlus主要用来高亮显示查看生成的反编译文件,为此我制作了Editplus的高亮语法文件一起打包给大家。DeDexer与BakSmali是目前广泛使用的两款DEX反编译工具,两个工具在语法上有细微的差别,我们在下面会此进行比较。

Dalvik VM & OpCodes

如同破解Windows程序需要掌握Windows程序特点一样,破解Android平台的程序需要先掌握Android程序开发的一些基本知识,一般的程序破解不需要深入的了解Android程序的开发,不过对于Android程序的运行机制应该有个大致的了解,完全不懂Android程序开发的朋友还是先打打基础吧!在Windows程序的调试时代,破解者将需要分析的程序载入Ollydbg之类的调试器中,在成千上万条汇编指令中寻找突破点,MASM语法的汇编自己也成为解密者需要掌握的基础,而我们要想熟练的分析Android程序,就必须要掌握Dalvik虚拟机的指令,听到虚拟机可能一部分人开始发怵了,其实Dalvik虚拟机与我们常说的Windows平台的VM虚拟机有着很大的区别,下面我们来看看什么是Dalvik虚拟机指令是何方神圣,它在Android程序破解中又有着怎样的地位?

Dalvik虚拟机是专门为Android平台上的程序运行而设计的一种代码运行机制,它主要的作用是对JAVA程序的ByteCode进行优化,以提高代码执行效率。可以说,从Android2.2版本开始,Android系统手机真正的拉开了市场,这背后很大一部分功劳要取决于强大的Dalvik JIT编译器,正是由于它大幅提升Android 2.2的各种性能。与传统的Java 虚拟机(JVM)基于栈有所不同,Dalvik是基于寄存器的虚拟机,这使得它们在编译的时候只需花费更短的时间。Dalvik JIT解释的JAVA ByteCode源于JVM而又优于JVM,在JAVA虚拟机中,代码是基于Method方式来运行解释编译的,而在Android2.2版本及以后的Dalvik JIT则是基于Trace解释编译的,Dalvik最多支持256个寄存器,但嵌入式CPU自身并不带那么多的寄存器,Dalvik 除了使用ARM本身的几个寄存器外,其它需要用到的寄存器会借用外部存储器来模拟,那Android程序运行时会用到多少个寄存器呢?答案是不确定的,它会在生成代码时进行计算。在这里,我们不去详细的探究Dalvik的运行机制,但掌握Dalvik指令的语法为以后的逆向工程做基础却是十分有必要的。一份详细的Dalvik opcodes表可以从Android源码中获得,在Android4.0的源码中,可以从dalvik/opcode-gen/bytecode.txt查看到完整支持的OpCodes,在dalvik/docs/dalvik-bytecode.html中可以找到Dalvik OpCodes的详细说明。早期的OpCodes使用一个字节就可以保存,意味着最多可以有256条指令,在最新的一份OpCodes列表中,OpCodes扩展了一字节,并扩充了一些指令。下面我们来看看Dalvik指令的格式,毕竟我们要看的不是这些OpCodes,而是由它们在一起组成的代码。

新建一个Android工程命名为HelloAndroid,HelloAndroidActivity.java文件使用默认生成的代码:

package cn.feicong.HelloAndroid;import android.app.Activity;
import android.os.Bundle;public class HelloAndroidActivity extends Activity {/** Called when the activity is first created. */@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);}
}

编译生成APK文件,接下来看看反编译后的结果。
首先是DeDexer,进入命令行并定位到HelloAndroid/bin目录下,在命令行下运行java -jar ddx.jar -d outdir classes.dex (可从http://sourceforge.net/projects/dedexer/ 下载DeDexer)会在outdir目录中生成六个ddx文件,如图1所示:

打开HelloAndroidActivity.ddx文件代码如下:

.class public cn/feicong/HelloAndroid/HelloAndroidActivity
.super android/app/Activity
.source HelloAndroidActivity.java.method public <init>()V
.limit registers 1
; this: v0 (Lcn/feicong/HelloAndroid/HelloAndroidActivity;)
.line 6invoke-direct    {v0},android/app/Activity/<init>  ; <init>()Vreturn-void
.end method.method public onCreate(Landroid/os/Bundle;)V
.limit registers 3
; this: v1 (Lcn/feicong/HelloAndroid/HelloAndroidActivity;)
; parameter[0] : v2 (Landroid/os/Bundle;)
.line 10invoke-super    {v1,v2},android/app/Activity/onCreate   ; onCreate(Landroid/os/Bundle;)V
.line 11const/high16    v0,32515invoke-virtual  {v1,v0},cn/feicong/HelloAndroid/HelloAndroidActivity/setContentView ; setContentView(I)V
.line 12return-void
.end method

.class是程序的类名,.super为它的父类,.source为源文件。.method表示是一个类的方法,后面的大写'V'表示Void,即无返回值,<init表示是构造函数,.limit registers 1表示用到了一个寄存器,分号后面是注释,.line表示行号,可有可无,invoke-direct与return-void是Dalvik的OpCode,前者调用一个类的方法,后者直接返回。

看看BakSmali的反编译代码,运行apktool.jar d HelloAndroid.apk outdir2 在outdir2目录会生成一个smali目录,同样在相应的目录会生成六个Smali结尾的文件,如图2所示:

打开HelloAndroidActivity..smali文件代码如下:

.class public Lcn/feicong/HelloAndroid/HelloAndroidActivity;
.super Landroid/app/Activity;
.source "HelloAndroidActivity.java"# direct methods
.method public constructor <init>()V.locals 0.prologue.line 6invoke-direct {p0}, Landroid/app/Activity;-><init>()Vreturn-void
.end method
# virtual methods
.method public onCreate(Landroid/os/Bundle;)V.locals 1.parameter "savedInstanceState".prologue.line 10invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V.line 11const/high16 v0, 0x7f03invoke-virtual {p0, v0}, Lcn/feicong/HelloAndroid/HelloAndroidActivity;->setContentView(I)V.line 12return-void
.end method

可以看出Smali文件的内容与ddx文件内容差别不是很大,ddx文件中有一些寄存器跟踪信息,用注释标了出来,而Smali则只对条件判断进行寄存器跟踪,ddx使用十进制表示数值,Smali则使用十六进制(Android资源ID也使用十六进制,我们搜索起来更方便),其它的不做太多比较了,不过目前使用Smali格式来分析DEX文件的人占多数,这一方面因为它代码漂亮,回编成功率高,更重要的是它能与Netbeans配合对Smali进行单步调试!!这不能不说是一个大亮点,详细的内容不是这篇文章的重点,可能以后会跟大家讨论。

上面的介绍部分就到这里了,下面正式进行实战破解环节。Let's GO!

破解实战

先说说APK的一般破解步骤,

  1. 对APK进行反编译。
  2. 对反编译的Smali文件进行分析或调试。
  3. 找到突破口,对Smali文件进行修改。(还有一种方法,更高效,我正在测试中,成功后与大家分享)
  4. 回编生成破解后的DEX文件。
  5. 重新打包生成APK并签名。
  6. 测试效果,不成功转到第二步操作。

试炼品我选择了国内著名公司金山出品的WPS Android4.0.3版,此版本的WPS是一个限制试用版,在安装运行后程序会提示软件过期。而目前最新的WPS已经免费了(可能并不好卖,呵呵)。
先安装运行程序,观察它的运行状态,执行这个软件会给出如图3的提示:

程序很大方的结出提示说已经过期了,我们点击更新即可以升级到最新版(在这里,我们就不升级了,因为升级后是免费的,对于我们练手就没搞头了),我们点击取消,程序退出。
下面我们对这个APK文件进行反编译。使用ApkTool_GUI,运行如图4:

将APK拖到第一个编辑框中,点击反编译APK,稍等片刻解压完成后会生成与文件名同名的文件夹,进入里面可以发现多出了一个smali文件夹,这个文件夹里面存放的就是我们需要分析的Smali文件。我们双击打开,可以发现里面竟然有几个文件夹及几千个smali文件!!我首次打开时也吃了一惊,如此多的文件让人怎么分析啊(大家可以发怵一下),如图5所示:

而且每个文件的文件名也很诡异,全是一些a-z的字母组合,打开任意一个smali文件看看,发现里面的方法名也是如此。分析过C#程序的人可能会觉得这里似乎很类似,是的,这些代码如同C#程序里面的混淆器混淆过一样,在Android程序开发的SDK中,提供了一套名叫Proguard的代码保护机制,它的作用就是通过随机化改变类中的方法及变量名,去除无用的虚方法、注释等手段让代码变得难以阅读,以此来达到保护程序的目的,在这里可能很多人再次发怵,本来就没弄过Android程序,现在一上来就是个混淆过的款,叫人怎么活啊!不要急,其实,被Proguard混淆过的代码还是有办法弄的,如有主要的几个类,如Service、Activity等类名是不能被混淆的,不过这不是我们今天要讲的重点。

我们接着分析,回想一下,我们在安装运行程序时,程序弹出了一个Message,想想我们在OD中破解Windows程序时用到的字符串参考,在这里是否可用呢?回答是:可以!我们在编译Android程序时,定义的字符串都保存在一个名收Strings.xml的文件中,编译生成APK后它也被加密打包到程序中了,而程序通过为每个字符串分配一个ID来进行调用,这些ID被保存在public.xml文件中。我们在刚才反编译的XML文件中找找就可以发现在res/values下有这两个文件,我们打开strings.xml并查找之,很快可以发现如下两行:

 <string name="expired_title">产品过期</string><string name="expired_message">这是一个有使用期限的版本,从2012年1月1日开始此版本已过期, 请确保网络通畅,并立即检查升级。</string>

这两行字符串正是前面运行程序时弹出的MessageBox的标题与文字。我们在这里记下它们的名字为expired_title与expired_message。打开strings.xml文件查找expired_title与expired_message则可以找到下面两行:

<public type="string" name="expired_title" id="0x7f060166" />
<public type="string" name="expired_message" id="0x7f060167" />

找到了这两个ID就好办了,我们下面来看看Android程序中是如何构建一个字符串的,修改刚才的HelloAndroid代码如下:

public class HelloAndroidActivity extends Activity {/** Called when the activity is first created. */@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);ShowString();}public void ShowString(){String str = getString(R.string.hello);Toast.makeText(getApplicationContext(), str,Toast.LENGTH_SHORT).show();}
}

重新生成APK后并编译代码如下:

# virtual methods
.method public ShowString()V.locals 3.prologue.line 16const/high16 v1, 0x7f04invoke-virtual {p0, v1}, Lcn/feicong/HelloAndroid/HelloAndroidActivity;->getString(I)Ljava/lang/String;move-result-object v0.line 17.local v0, str:Ljava/lang/String;invoke-virtual {p0}, Lcn/feicong/HelloAndroid/HelloAndroidActivity;->getApplicationContext()Landroid/content/Context;move-result-object v1const/4 v2, 0x0invoke-static {v1, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v1invoke-virtual {v1}, Landroid/widget/Toast;->show()V.line 18return-void
.end method.method public onCreate(Landroid/os/Bundle;)V.locals 1.parameter "savedInstanceState".prologue.line 11invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V.line 12const/high16 v0, 0x7f03invoke-virtual {p0, v0}, Lcn/feicong/HelloAndroid/HelloAndroidActivity;->setContentView(I)V.line 13invoke-virtual {p0}, Lcn/feicong/HelloAndroid/HelloAndroidActivity;->ShowString()V.line 14return-void
.end method

可以看到const/high16 v1, 0x7f04将V1寄存器的高16位设置为0x7f04,低位未设置则为0,然后调用getString()将结果保存到v0并设置到str中,在这里注意一下invoke-virtual之类调用的格式,在OpCode后面的括号中是参数列表,用逗号分隔,后面'L'打头的是相应参数的数据类型,用分号隔开,大写字符'I’表示为int类型。我们在反编译的文件夹中搜索调用了ID为0x7f060167文件。发现R$string.smali与DocumentManager.smali两个文件中有调用,第1个为代码编写时自动生成的,可以无视,第2个就是重点了,我们打开DocumentManager.smali文件查找调用处,代码为:

.method static synthetic e(Lcn/wps/moffice/documentmanager/DocumentManager;)V.locals 3iget-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPj:Lcn/wps/moffice/writer/view/beans/g;if-eqz v0, :cond_0    //如果g对象获取失败就跳到过期提示,这里是爆破点(g就是我们见到的过期提示框)iget-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPj:Lcn/wps/moffice/writer/view/beans/g;invoke-virtual {v0}, Lcn/wps/moffice/writer/view/beans/g;->isShowing()Z //g对象是否已经显示move-result v0if-nez v0, :cond_1  //如果g显示了就直接返回就直接返回:cond_0new-instance v0, Lcn/wps/moffice/writer/view/beans/g;  //创建一个g的实例invoke-direct {v0, p0}, Lcn/wps/moffice/writer/view/beans/g;-><init>(Landroid/content/Context;)V //构造函数const v1, 0x7f060166  //字符串常量产品过期invoke-virtual {v0, v1}, Lcn/wps/moffice/writer/view/beans/g;->eb(I)Lcn/wps/moffice/writer/view/beans/g;move-result-object v0     //调用g的eb()方法,应该是设置弹出框的标题const v1, 0x7f060167  //字符串常量这是一个有使用期限的版本,从2012年1月1日开始此版本已过期, 请确保网络通畅,并立即检查升级。invoke-virtual {p0, v1}, Lcn/wps/moffice/documentmanager/DocumentManager;->getString(I)Ljava/lang/String;move-result-object v1 //取得字符串invoke-virtual {v0, v1}, Lcn/wps/moffice/writer/view/beans/g;->be(Ljava/lang/String;)Lcn/wps/moffice/writer/view/beans/g;move-result-object v0//调用g的be()方法,应该是设置弹出框的内容const v1, 0x7f060112new-instance v2, Lcn/wps/moffice/documentmanager/DocumentManager$4;invoke-direct {v2, p0}, Lcn/wps/moffice/documentmanager/DocumentManager$4;-><init>(Lcn/wps/moffice/documentmanager/DocumentManager;)V         //构造DocumentManagerinvoke-virtual {v0, v1, v2}, Lcn/wps/moffice/writer/view/beans/g;->a(ILandroid/content/DialogInterface$OnClickListener;)Lcn/wps/moffice/writer/view/beans/g;     //设置按钮监听器move-result-object v0const v1, 0x7f060165new-instance v2, Lcn/wps/moffice/documentmanager/DocumentManager$7;invoke-direct {v2, p0}, Lcn/wps/moffice/documentmanager/DocumentManager$7;-><init>(Lcn/wps/moffice/documentmanager/DocumentManager;)V //DocumentManager对象invoke-virtual {v0, v1, v2}, Lcn/wps/moffice/writer/view/beans/g;->b(ILandroid/content/DialogInterface$OnClickListener;)Lcn/wps/moffice/writer/view/beans/g;      //设置按钮监听器move-result-object v0iput-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPj:Lcn/wps/moffice/writer/view/beans/g;iget-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPj:Lcn/wps/moffice/writer/view/beans/g;const/4 v1, 0x0invoke-virtual {v0, v1}, Lcn/wps/moffice/writer/view/beans/g;->setCancelable(Z)V //设置是否可取消iget-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPj:Lcn/wps/moffice/writer/view/beans/g;invoke-virtual {v0}, Lcn/wps/moffice/writer/view/beans/g;->show()V  //显示g,也就是过期对话框:cond_1return-void
.end method

这个e方法看得出来是重要的地方,我做了详细的注释,功能就是首先g是否已经显示,如果没显示就初始化并显示,我们爆破就简单了,直接if-eqz v0, :cond_0改成if-eqz v0, :cond_1。保存文件并退出,拖动不整个文件夹到ApkTool_Gui第二个编辑框重建APK。完成后拿到Android设备上测试。会发现程序弹出个Toast提示软件过期,然后就退出了,看来还有地方需要处理。按照上面的方法如法炮制搜索 0x7f060166(软件过期字符串的ID),发现有两处调用,而且两处调用都是类似的代码:

.method public final a(Lcn/wps/moffice/documentmanager/DocumentManager$a;Ljava/lang/String;)V.locals 4invoke-static {}, Lcn/wps/moffice/OfficeApp;->zc()Lcn/wps/moffice/OfficeApp;move-result-object v0invoke-virtual {v0}, Lcn/wps/moffice/OfficeApp;->zd()Z  ///对程序进行验证move-result v0if-eqz v0, :cond_1 //验证为0就跑去运行程序const v0, 0x7f060166    //软件过期字符串IDinvoke-virtual {p0, v0}, Lcn/wps/moffice/documentmanager/DocumentManager;->getString(I)Ljava/lang/String;move-result-object v0const/4 v1, 0x0invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v0invoke-virtual {v0}, Landroid/widget/Toast;->show()V    //弹出了Toastiget-object v0, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->DY:Lcn/wps/moffice/documentmanager/history/HistoryFiles;iget-object v1, p0, Lcn/wps/moffice/documentmanager/DocumentManager;->cPk:Ljava/lang/Runnable;const-wide/16 v2, 0x7d0invoke-virtual {v0, v1, v2, v3}, Lcn/wps/moffice/documentmanager/history/HistoryFiles;->postDelayed(Ljava/lang/Runnable;J)Z  //这里就OVER了:cond_0:goto_0return-void:cond_1invoke-static {}, Lcn/wps/moffice/OfficeApp;->zc()Lcn/wps/moffice/OfficeApp;move-result-object v0invoke-virtual {p1}, Lcn/wps/moffice/documentmanager/DocumentManager$a;->toString()Ljava/lang/String;move-result-object v1invoke-virtual {v0, v1}, Lcn/wps/moffice/OfficeApp;->bO(Ljava/lang/String;)Z
move-result v0

同样,两处的修改很简单,只需要将if-eqz v0, :cond_1 改成if-nez v0, :cond_1就可以了。保存文件后重新回编,安装测试就会发现程序被破解掉了。运行效果如图6:

到这里,本文就结束了,总结一下,内容不多,主要介绍了Android程序分析需要用到的工具及Android程序的一般分析方法,另外,在实战中也看到了,混淆并不可怕,可怕的是没有耐心坚持下去。关于Android程序的破解分析我会继续下去,目前已知有另一种更高级的方法,不过我目前测试还没成功,等小有成果就拿来与大家分享。

【移动安全实战篇】————3、Android应用程序破解入门相关推荐

  1. 实战篇-制作微信小程序码宣传海报

    一.制作前的说明 上一篇文章讲了微信官方提供的动态生成小程序码的一些说明与注意事项. 本篇文章就带大家一起把绘制带参小程序码的宣传海报给画出来以及如何保存已经生成的小程序码,方便下次复用. 注意: 上 ...

  2. 黑客攻防技术宝典web实战篇:攻击应用程序逻辑习题

    猫宁!!! 参考链接:http://www.ituring.com.cn/book/885 随书答案. 1. 何为强制浏览?可以通过它确定哪些漏洞? 强制浏览包括避开浏览器导航对应用程序功能访问顺序实 ...

  3. 微信小程序|开发实战篇之十三---小程序布局/组件/屏幕适配问题

    适配除去一些固定组件的屏幕高度 1.小程序屏幕适配 2.解决方案 2.1 屏幕适配 2.2 radio的大小调整 3.使用效果 1.小程序屏幕适配 例如:iPhone5.iPhone6/7/8.iPh ...

  4. 安卓开发实战(1)之程序员入门代码,Hello,world!

    系列文章目录 文章目录 系列文章目录 前言 一.新建一个安卓项目 二.创建安卓虚拟器 1.点击右上角后,点击create device,选择自己需要的版本即可 2.点击运行 总结,自己在运行中出现的问 ...

  5. Android应用程序组件Content Provider的启动过程源代码分析(6)

        Step 17. ActivityThread.installProvider         这个函数定义在frameworks/base/core/java/android/app/Act ...

  6. Android应用程序组件Content Provider的共享数据更新通知机制分析(3)

            3. 数据更新通知的发送过程        在前面这篇文章Android应用程序组件Content Provider应用实例介绍的应用程序Acticle中,当调用ArticlesAda ...

  7. Android应用程序组件Content Provider的启动过程源代码分析(1)

             通过前面的学习,我们知道在Android系统中,Content Provider可以为不同的应用程序访问相同的数据提供统一的入口.Content Provider一般是运行在独立的进 ...

  8. Android应用程序安装过程源代码分析(1)

    Android系统在启动的过程中,会启动一个应用程序管理服务PackageManagerService,这个服务负责扫描系统中特定的目录,找到里面的应用程序文件,即以Apk为后缀的文件,然后对这些文件 ...

  9. android java广播,[原]Android应用程序发送广播(sendBroadcast)的过程分析

    前面我们分析了Android应用程序注册广播接收器的过程,这个过程只完成了万里长征的第一步,接下来它还要等待ActivityManagerService将广播分发过来.ActivityManagerS ...

最新文章

  1. SAP MM 特殊库存之T库存
  2. git报错:Pull is not possible because you have unmerged files解决方法
  3. WordPress的body_class()函数详解
  4. 计算机系统基础:虚拟存储管理知识笔记
  5. 数据传输完整性_电缆监测数据传输系统分析与设计
  6. 5.1特辑 | 为什么显示有票你却抢不到?技术揭秘12306如何保证车票不超卖
  7. java bitmap取出数据库_bitmap一般如何取出其所表示的数据(以java为例)
  8. 【Luogu1341】无序字母对(并查集联通,欧拉路模板)
  9. YUV 后面数字的含义_奔富红酒“Bin”后的数字,是什么意思?
  10. tomcat 改端口 运维最最重要的就是有看日志的习惯
  11. 富士相机设置传原图_「富士相机」机身设置分享,摄影小白也可以拍摄到胶片感的照片...
  12. java多线程下载图片_java多线程批量下载图片(通用版)
  13. mysql 多表查询 like_MYSQL多表联合查询的问题
  14. Axure 注册码(亲测可用)
  15. SecureCRT通过vim打开文件时显示行号
  16. python中sys是什么意思_python里的sys是什么意思
  17. ZJNU 2314-Sleepy Cow Herding
  18. 曹操为何杀死神医华佗?
  19. Win11如何清理C盘垃圾文件
  20. 中科软测试面试题2019_中科软笔试题和面试题

热门文章

  1. android 截图模糊,Adobe XD导出图片模糊?设置错误啦(切图详解)
  2. 第二百四十一节,Bootstrap进度条媒体对象和 Well 组件
  3. Android电视直播 v5.6,龙龙直播app下载|龙龙直播apk2019官方版下载 v5.6.1 安卓版 - 数码资源网...
  4. 数据库与MPP数仓(十四):招标采购系统的数据仓库构建
  5. 黑裙掉盘共享文件夹重新创建
  6. 夜游神安卓模拟器安装
  7. python关于 unittest的常见用法:前置条件与后置条件
  8. [Ynoi2015]即便看不到未来
  9. 冰天雪地摄影技巧创意色彩指南
  10. DMA普通模式(normal)发送通过串口连续发送数据