首页
社区
课程
招聘
[原创]Android APP漏洞之战(7)——信息泄露漏洞详解
发表于: 2022-1-10 19:47 30509

[原创]Android APP漏洞之战(7)——信息泄露漏洞详解

2022-1-10 19:47
30509

好久没有更新帖子了,最近一直都比较忙,这里首先祝大家腊八节快乐。本文主要围绕Android APP漏洞中的信息泄露漏洞展开描述,因为挖掘Android APP信息泄露漏洞的思路各有差异,所以本文只是基于Android APP中较为基础的信息泄露的漏洞实例开始讲述。

本文第二节主要讲述Android中存储的基本方式

本文第三节主要讲述信息泄露漏洞的分类

本文第四节主要讲述漏洞的原因和具体复现

APP 信息泄露漏洞往往和Android APP的数据存储方式有关,所以我们这里首先详细的了解Android的数据存储方式。我们知道Android中数据存储的方式共有五种,分别为:文件存储、SharedPreferences、SQLite数据库存储、ContentProvider、网络存储。

image-20220102214434734

下面我们将结合上面的思维导图依次讲解

内部存储一般存储一些应用的数据,如apk、shareprefence、database数据、webview缓存和图片缓存等等,内部存储一般存储在/data/下面,这些都需要用户获得root权限后才能访问到

image-20220102215413773

我们以root权限的模式进入:

image-20220102215509371

下面我们介绍常见的一些内部存储目录:

内部存储的特点:

Android4.4以前,手机自身的存储就叫内部存储,插入SD卡的存储叫外部存储,然而Android 4.4以后,手机自带的存储很大,因此现在的外部存储分为两部分:SD卡和扩展卡内存。外部存储一般分为两类,私有目录和公有目录,私有目录里面的数据会随着应用的卸载而删除,公有目录并不会

自身的外部存储目录:/storage/emulated/0/Android/data/packagename/files

存储卡的存储目录:/storage/extSdCard/Android/data/packagename/files

image-20220103165631522

外部存储的特点:

私有目录,在Android 4.4以上,不需要注册和用户授权SD读写的权限,就可以在应用的私有目录进行读写文件,文件不能被其他应用访问,用户删除应用时,对应的应用的私有目录也会被删除

私有目录地址:/storage/emulated/0/Android/data/packagename

相关API:

私有目录访问的API都在ContextWrapper对象上,可以直接通过Activity或Context进行调用

公共目录必须需要用户授权读写的权限,就意味需要在AndroidManifest.xml中注册用户权限

Android 6.0系统之后需要申请用户权限,并获得用户授权,才能读写文件,公共目录相对开放,我们可以访问其他APP存在公共目录下的文件,并且当APP被删除时,并不会删除应用存在公共目录下的文件

相关API:

公共目录可以通过Environment对象,访问读写公共目录的文件

补充:

写入文件:

读取文件:

SharedPreference是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态,是使用键值对的方式来存储数据,这样就可以支持多种不同的数据类型存储,进行数据持久化就会比文件方便很多

默认存储路径:/data/data/packageName/shared_prefs

获取SharedPreferences对象的方法

SharedPreference的存储:

SharedPreference数据的读取:

SharedPreferences对象与SQLite数据库相比,免去了创建数据库、创建表、写SQL语句等操作,但是其只能存储boolean,int,float,long和String五种简单的数据类型,而且SharedPreferences是以明文的形式存储密钥信息,往往存在一定的安全隐患。为此Android还提供了一个轻量级的数据库SQLite数据库,SQLite是轻量级嵌入式数据库引擎,它支持 SQL 语言,并且只利用很少的内存就有很好的性能

默认存储路径:/data/data/packagename/databases

Android 提供了SQLiteOpenHelper类帮助创建和升级数据库,SQLiteOpenHelper子类至少需要实现三个方法:

继承 SQLiteOpenHelper 创建数据库

只要我们在MainActivity中将version改为大于原来版本号,就可以让onUpgrade()方法得到执行

添加数据:

更新数据:

删除数据:

查询数据:

SQL语句操作数据库:

使用事务操作:

当然Android数据库中还包括LitePal数据库更加方便的操作,由于本文主要是介绍漏洞,所以这里就简单介绍到这里

前面三种方式是Android中基本的存储方式,但是由于都存在一个公共的缺点:不能实现不同应用程序之间进行数据共享,大家都知道Android是采用沙箱的管理机制,不同的应用程序之间都是独立隔离开的,这一定程度上也是为了Android 应用之间的安全性考虑,但是如果应用之间不能很好的进行交互,那么很显然就带来了明显的不便,因此为了解决这个问题,内容提供器——ContentProvider就孕育而生了,由于前面我们有专门的章节讲述这一组件,所以本文只是简单描述,不详细了解,请参考Android APP漏洞之战(4)——Content Provider漏洞详解

主要作用:用于不同的程序之间实现数据共享的功能,并通过ContentResolver进行操作

ContentResolver使用方法:

(1)内容URI

(2)使用URI对象进行数据操作

查询

插入

Android网络存储主要是通过网络来实现数据的存储和获取,这里主要调用WebService返回的数据或是解析HTTP协议实现网络数据交互,具体需要熟悉java.net.,Android.net.这两个包的内容,因为不是Android的主流存储方式,这里主要参考相应文档即可,也不是本文漏洞所关注的主要对象,就不做过多叙述了

网络存储需要开启权限:

具体可参考本文参考文献

信息泄露漏洞往往是指APP开发过程中一些不安全的开发问题导致敏感信息的泄露,那我们首先可以将敏感信息进行分类:产品敏感信息和用户敏感信息

产品敏感信息:登录密码、后台登录及数据库地址、服务器部署的绝对路径、内部ip、地址分配规则、网络拓扑、页面注释信息等

用户的个人隐私信息泄露导致被恶意人员利用获取不当信息,例如用户的密码,账户信息等等

这些敏感信息泄露往往是由于信息未加密或存储位置不当造成:

综上我们将Android中的信息泄露漏洞大致可分为:

image-20220104172608948

本文为了简单演示各个漏洞的复现情况,将会使用样本DIVA.apk和一些实际漏洞的场景,样本将放到附件中

在APP的开发过程中,为了方便调试,开发者通常会用logcat输出info、debug、error 等信息。如果在APP发布时没有去掉logcat信息,可能会导致攻击者通过查看logcat日志获得敏感信息

一般来说,LogCat敏感信息输出漏洞包括:

首先,我们安装样本DIVA.apk

image-20220104111529461

然后我们打开例题1,并开启日志监控

image-20220104112058929

我们在app表单总输入内容,check out后查看相关日志

image-20220104112212227

我们就可以发现我们输入的密钥信息,然后我们根据日志信息,可以找到漏洞代码在LogActivity.class文件,我们查看相关代码:

image-20220104112932530

这里相关代码就是将敏感信息给泄露,当然我们真实的app中不会这么简单,但这确实一个很好的思路,比如我们对我们分析的app中的敏感信息的函数进行hook或者通过插桩日志的信息,我们就可以成功获得敏感信息了

防护建议:

一些开发人员,在开发时使用硬编码的方式,导致存在一定的安全风险,硬编码一般是指将输出或输入的相关参数以常量的方式编写在源代码中,这样导致逆向分析人员可以直接通过分析源码就可以获得敏感信息

我们打开样本app的例题2

image-20220104143848066

我们分析对应app相关的逆向代码:

image-20220104143950575

我们可以发现相应的敏感信息被直接用来判断,采用硬编码的方式,我们直接将密钥输入,发现可以成功破译

image-20220104144243905

我们打开例题12

image-20220104170422773

我们分析对应的代码段

image-20220104170535933

说明key被保存在libsoName.so库中,我们将文件打开

image-20220104170746729

image-20220104171214316

经过分析,我们确定这就是我们的键值,我们输入

image-20220104171352572

说明key放在so层中也是不安全的

密钥硬编码案例:

下面为乌云平台一些APP漏洞案例,详细可做参考:

密钥硬编码

Shared Preferences存储安全风险在于:开发者在创建文件时没有正确的选择合适的创建模式(MODE_PRIVATE、MODE_WOELD_READABLE以及MODE_WORLD_WRITEABBLE)进行权限控制,导致将一些用户信息、密码等敏感信息存储在Shared Preferences文件中,攻击者可以通过root来查看敏感信息

我们进入例题3:

image-20220104145137669

我们分析对应的逆向代码:

image-20220104145233158

经过我们前文的分析,很明显这里采用的是SharedPreference的存储方式

我们输入相关的账号和密码,然后我们可以进入shared_prefs查看相关的文件

image-20220104145701360

防护意见:

Database配置模式安全风险源于:

开发者在创建数据库(Database)时没有正确的选取合适的创建模式(MODE_PRIVATE、MODE_WORLD_READABLE)进行权限控制,从而导致数据库(Database)内容被恶意读写,造成账户密码、身份信息、以及其他敏感信息泄露,甚至攻击者进一步实施恶意攻击

我们进入例题4

image-20220104154306093

然后我们输入相关信息并保存

image-20220104154526963

我们将数据库给拉取下来,然后使用SQLite查看

image-20220104155824459

我们就可以发现敏感信息,我们可以分析对应的代码段

image-20220104155937819

我们上文的存取数据库名也是从对应代码出找到

防护建议:

经过上文我们讲述的文件存储,现在的手机很多的公共目录都是自身自带的存储空间,Android系统的文件一般都存储在sdCard和应用的私有目录下,任何在Android Manifest中声明读写sdcard权限的应用都可以对sdcard进行读写。

我们打开例题5

image-20220104161709796

我们分析对应部分的相关代码:

image-20220104161841048

根据代码,我们知道这里采用文件存储的方式,所以我们直接找到该文件,查看即可

image-20220104162116624

同理,我们打开例题6,查看对应的代码

image-20220104162259850

我们可以发现是存储在sd卡下,然后我们输入相关信息,之后直接去sdcard下查找,这里我们需要注意,需要给应用存取权限,否则是保存不了的

image-20220104164309655

防护意见:

开发人员在开发时对网络连接的一些敏感数据往往采用http明文传输,这样就十分容易导致恶意攻击者通过一些抓包工具进行捕获,获取敏感信息,导致信息泄露的风险

这段时间我查看了一下国内主流的音乐软件平台,发现很多音乐软件都存在http明文传输的问题,这样势必会导致升级劫持、信息泄露、任意文件下载等等漏洞,这里我们列举一个有http明文泄露导致的任意文件下载问题

首先我们随便选择几首需要VIP下载的歌曲

等他

image-20220104164309655

我们发现这些音乐都需要开通VIP才能下载

接下来我们用抓包软件抓取

image-20220104164309655

我们发现了音乐的url,我们直接访问,发现可以直接下载

还有一些http明文传输漏洞导致任意登录,一些APP也会把登录成功的Cookie保存在本地,那么只要找到相关文件复制下来这个Cookie,就可以任意登录了。

防护建议:

本文对Android App中信息泄露漏洞的常见形式做了一个基本的讲述,当然实际过程中很多APP的信息泄露漏洞可能要比这个复杂,这里只是初步的为大家介绍一下信息泄露漏洞的基础知识,本文的样例我会上传到github上,这里放一个传送门:

github地址

文件存储:

其他存储:

网络存储:

漏洞挖掘参考:

 
 
 
 
 
 
 
 
/data/app/    存储着我们手机上安装的apk文件
/data/data/包名/share_prefs  存储对应的应用程序中的shareprefence存储文件
/data/data/包名/cache    存储对应的应用程序中的cache缓存文件
/data/data/包名/databases  存储对应的应用程序中的数据库文件
/data/data/包名/files      存储对应的应用程序中的资源文件
/data/app/    存储着我们手机上安装的apk文件
/data/data/包名/share_prefs  存储对应的应用程序中的shareprefence存储文件
/data/data/包名/cache    存储对应的应用程序中的cache缓存文件
/data/data/包名/databases  存储对应的应用程序中的数据库文件
/data/data/包名/files      存储对应的应用程序中的资源文件
内部存储的文件和目录只能被我们的app自己所访问,别的app不能访问。
内部存储中的私有目录,当用户卸载app之后,改文件目录中关于该应用的信息就会被删除。
内部存储是可用的。
内部存储大小有限,不适合存储大量数据。
只有root的手机,才能从手机文件管理器看见,否则都是隐藏着的。
内部存储的文件和目录只能被我们的app自己所访问,别的app不能访问。
内部存储中的私有目录,当用户卸载app之后,改文件目录中关于该应用的信息就会被删除。
内部存储是可用的。
内部存储大小有限,不适合存储大量数据。
只有root的手机,才能从手机文件管理器看见,否则都是隐藏着的。
 
 
 
 
公有目录任何程序都可以访问,私有目录自身可以访问。
并不一定是可用的,因为SD卡会被挂载。
外部存储中的私有目录中的数据会随着应用的卸载而删除,公有目录则不会。
公有目录任何程序都可以访问,私有目录自身可以访问。
并不一定是可用的,因为SD卡会被挂载。
外部存储中的私有目录中的数据会随着应用的卸载而删除,公有目录则不会。
 
 
 
getExternalCacheDir(): 访问/storage/emulated/0/Android/data/应用包名/cache目录,该目录用来存放应用的缓存文件,当我们通过应用删除缓存文件的时候,该目录下的文件会被清除
getExternalFilesDir(): 访问/storage/emulated/0/Android/data/应用包名/files 目录,该目录用来存放应用的数据
getExternalCacheDir(): 访问/storage/emulated/0/Android/data/应用包名/cache目录,该目录用来存放应用的缓存文件,当我们通过应用删除缓存文件的时候,该目录下的文件会被清除
getExternalFilesDir(): 访问/storage/emulated/0/Android/data/应用包名/files 目录,该目录用来存放应用的数据
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
 
Environment.getExternalStorageDirectory() 访问外部存储设备公共根目录
Environment.getExternalStorageState() 获得外部存储SD卡的状态
Environment.getExternalStorageDirectory() 访问外部存储设备公共根目录
Environment.getExternalStorageState() 获得外部存储SD卡的状态
getRootDirectory():对应获取系统分区根路径:/system
getDataDirectory():对应获取用户数据目录路径:/data
getDownloadCacheDirectory():对应获取用户缓存目录路径:/cache
getRootDirectory():对应获取系统分区根路径:/system
getDataDirectory():对应获取用户数据目录路径:/data
getDownloadCacheDirectory():对应获取用户缓存目录路径:/cache
1)升级应用程序后的apk文件在哪?
一般我们从服务器端下载的app需要放到外部存储目录下面,而不是内部存储目录,即/storage/emulated/0/Android/data/packagename下
2)清除数据和清除缓存的区别?
清除数据清除的是保存在app中所有数据,就是上面提到的位于packagename下面的所有文件,包含内部存储(/data/data/packagename/)和外部存储(/storage/emulated/0/Android/data/packagename/),但不会影响SD卡的数据
缓存是程序运行时的临时存储空间,缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下
1)升级应用程序后的apk文件在哪?
一般我们从服务器端下载的app需要放到外部存储目录下面,而不是内部存储目录,即/storage/emulated/0/Android/data/packagename下
2)清除数据和清除缓存的区别?
清除数据清除的是保存在app中所有数据,就是上面提到的位于packagename下面的所有文件,包含内部存储(/data/data/packagename/)和外部存储(/storage/emulated/0/Android/data/packagename/),但不会影响SD卡的数据
缓存是程序运行时的临时存储空间,缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下
public void save(){
      String data = "Data to save";
      FileOutputStream out = null;
      ButteredWriter writer = null;
      try{
            out = openFileOutput("data",Context.MODE_PRIVATE);  //MODE_PRIVATE(默认):覆盖、MODE_APPEND:追加
            writer = new ButteredWriter(new OutputSreamWriter(out));
            writer.write(data);
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            try{
                  if(writer!=null){
                        writer.close();
                  }
            }catch(IOException e){
                  e.printStackTrace();
       }
}
public void save(){
      String data = "Data to save";
      FileOutputStream out = null;
      ButteredWriter writer = null;
      try{
            out = openFileOutput("data",Context.MODE_PRIVATE);  //MODE_PRIVATE(默认):覆盖、MODE_APPEND:追加
            writer = new ButteredWriter(new OutputSreamWriter(out));
            writer.write(data);
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            try{
                  if(writer!=null){
                        writer.close();
                  }
            }catch(IOException e){
                  e.printStackTrace();
       }
}
public String load(){
      FileInputStream in = null;
      ButteredReader reader = null;
      StringBuilder builder = new StringBuilder();
      try{
            in = openFileInput("data");
            reader = new ButteredReader(new InputStreamReader(in));
            String line= "";
            while((line = reader.readline()) != null){
                   builder.append();
            }
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            if(reader != null){
                    try{
                          reader.close();
                    }catch(IOException e){
                          e.printStackTrace();
                    }
             }
      }
}
public String load(){
      FileInputStream in = null;
      ButteredReader reader = null;
      StringBuilder builder = new StringBuilder();
      try{
            in = openFileInput("data");
            reader = new ButteredReader(new InputStreamReader(in));
            String line= "";
            while((line = reader.readline()) != null){
                   builder.append();
            }
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            if(reader != null){
                    try{
                          reader.close();
                    }catch(IOException e){
                          e.printStackTrace();
                    }
             }
      }
}
 
 
Context的getSharedPreferences()方法,参数一是文件名,参数二是操作模式
Activity的getPreferences()方法,参数为操作模式,使用当前应用程序包名为文件名
PreferenceManager的getDefaultSharedPreferences()静态方法,接收Context参数,使用当前应用程序包名为文件名
Context的getSharedPreferences()方法,参数一是文件名,参数二是操作模式
Activity的getPreferences()方法,参数为操作模式,使用当前应用程序包名为文件名
PreferenceManager的getDefaultSharedPreferences()静态方法,接收Context参数,使用当前应用程序包名为文件名
(1)根据Context获取SharedPreferences对象
(2)利用edit()方法获取Editor对象
(3)ditor对象存储key-value键值对数据
(4)apply()提交数据
(1)根据Context获取SharedPreferences对象
(2)利用edit()方法获取Editor对象
(3)ditor对象存储key-value键值对数据
(4)apply()提交数据
public class MainActivity extends Activity {    
 @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        //获取SharedPreferences对象
        Context ctx = MainActivity.this;       
        SharedPreferences sp = ctx.getSharedPreferences("SP", MODE_PRIVATE); //MODE_PRIVATE(默认):只有当前的应用程序才能对文件进行读写、MODE_MULTI_PROCESS:用于多个进程对同一个SharedPreferences进行读写
        //存入数据
        Editor editor = sp.edit();
        editor.putString("name", "Tom");
        editor.putInt("age", 28);
        editor.putBoolean("married", true);
        editor.apply();
 
        //返回STRING_KEY的值
        Log.d("SP", sp.getString("name", "none"));
        //如果NOT_EXIST不存在,则返回值为"none"
        Log.d("SP", sp.getString("NOT_EXIST", "none"));
     }
 
}
public class MainActivity extends Activity {    
 @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        //获取SharedPreferences对象
        Context ctx = MainActivity.this;       
        SharedPreferences sp = ctx.getSharedPreferences("SP", MODE_PRIVATE); //MODE_PRIVATE(默认):只有当前的应用程序才能对文件进行读写、MODE_MULTI_PROCESS:用于多个进程对同一个SharedPreferences进行读写
        //存入数据
        Editor editor = sp.edit();
        editor.putString("name", "Tom");
        editor.putInt("age", 28);
        editor.putBoolean("married", true);
        editor.apply();
 
        //返回STRING_KEY的值
        Log.d("SP", sp.getString("name", "none"));
        //如果NOT_EXIST不存在,则返回值为"none"
        Log.d("SP", sp.getString("NOT_EXIST", "none"));
     }
 
}
SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
String name = pref.getString("name");
int age = pref.getInt("age");
boolean isMarried = pref.getBoolean("isMarried");
SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
String name = pref.getString("name");
int age = pref.getInt("age");
boolean isMarried = pref.getBoolean("isMarried");
 
1.构造函数,调用父类SQLiteOpenHelper的构造函数。需要四个参数(上下文环境、数据库名称、查询数据的游标Cursor(通常为null)、当前数据库的版本号)
2.onCreate()方法,它需要一个 SQLiteDatabase 对象作为参数,根据需要对这个对象填充表和初始化数据
3.onUpgrage() 方法,它需要三个参数,一个 SQLiteDatabase 对象,一个旧的版本号和一个新的版本号,这样就可以方便的实现数据库的升级
1.构造函数,调用父类SQLiteOpenHelper的构造函数。需要四个参数(上下文环境、数据库名称、查询数据的游标Cursor(通常为null)、当前数据库的版本号)
2.onCreate()方法,它需要一个 SQLiteDatabase 对象作为参数,根据需要对这个对象填充表和初始化数据
3.onUpgrage() 方法,它需要三个参数,一个 SQLiteDatabase 对象,一个旧的版本号和一个新的版本号,这样就可以方便的实现数据库的升级
public class MyDatabaseHelper extends SQLiteOpenHelper {
    //1.构造方法
  MyDatabaseHelper(Context context, String name, CursorFactory cursorFactory, int version) 
  {     
    super(context, name, cursorFactory, version);     
     }     
 
     @Override    
     public void onCreate(SQLiteDatabase db) {     
         // TODO 创建数据库后,对数据库的操作     
     }     
 
     @Override    
 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {     
         // TODO 升级数据库版本
     }     
 
 @Override    
 public void onOpen(SQLiteDatabase db) {     
         super.onOpen(db);       
         // TODO 每次成功打开数据库后首先被执行     
     }     
 }
public class MyDatabaseHelper extends SQLiteOpenHelper {
    //1.构造方法
  MyDatabaseHelper(Context context, String name, CursorFactory cursorFactory, int version) 
  {     
    super(context, name, cursorFactory, version);     
     }     
 
     @Override    
     public void onCreate(SQLiteDatabase db) {     
         // TODO 创建数据库后,对数据库的操作     
     }     
 
     @Override    
 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {     
         // TODO 升级数据库版本
     }     
 
 @Override    
 public void onOpen(SQLiteDatabase db) {     
         super.onOpen(db);       
         // TODO 每次成功打开数据库后首先被执行     
     }     
 }
public class MyDatabaseHelper extends SQLiteOpenHelper{ 
    ......
    //当打开数据库时传入的版本号与当前的版本号不同时会调用该方法 
    @Override 
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
          db.execSQL("drop table if exists Book");
          onCreate(db):
    }   
}
public class MyDatabaseHelper extends SQLiteOpenHelper{ 
    ......
    //当打开数据库时传入的版本号与当前的版本号不同时会调用该方法 
    @Override 
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
          db.execSQL("drop table if exists Book");

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2022-4-12 17:02 被随风而行aa编辑 ,原因:
上传的附件:
收藏
免费 11
支持
分享
最新回复 (6)
雪    币: 1367
活跃值: (2121)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
2
来学习一个郭老板的新作
2022-1-10 20:51
0
雪    币: 7201
活跃值: (21965)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
3
危楼高百尺 来学习一个郭老板的新作
上传的附件:
2022-1-11 09:31
0
雪    币: 1
活跃值: (158)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
整个系列写的简直太棒了!建议着重讲一下webview的问题。
2022-1-14 16:46
0
雪    币: 7201
活跃值: (21965)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
5
ShadowMourne 整个系列写的简直太棒了!建议着重讲一下webview的问题。
后面会对WebView做一个专题讲解的 
2022-1-15 11:33
0
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
似懂非懂
2022-1-17 16:40
0
雪    币: 7201
活跃值: (21965)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
7
hutc5050 似懂非懂
2022-1-17 19:12
0
游客
登录 | 注册 方可回帖
返回
//