xposed框架的Android网络监测模块

简介: 试图监控一个app所有的网络通信。前提 :手机root过已成功安装xposed框架XposedInstaller:https://github.

试图监控一个app所有的网络通信。

前提 :

  1. 手机root过
  2. 已成功安装xposed框架

XposedInstaller:

https://github.com/rovo89/XposedInstaller

操作步骤

  1. 添加xposed框架XposedBridgeApi-54.jar包到android项目build path

  2. assets目录下添加xposed_init文件内容为com.example.xposeddiy.Test,指示入口

  3. AndroidManifest.xml文件application标签下添加

    [[meta-data
    android:name="xposedmodule"
    android:value="true" />
    [[meta-data
    android:name="xposedminversion"
    android:value="40" />
    [[meta-data
    android:name="xposeddescription"
    android:value="network monitor" />

  4. hook android所有通信调用函数,代码如下

package com.example.xposeddiy;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;


import android.app.Activity;
import android.app.Application;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.util.Log;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodHook.MethodHookParam;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import static de.robv.android.xposed.XposedHelpers.findAndHookConstructor;

//web.loadUrl("http://1.1.1.0", param);web.loadUrl("http://1.1.1.1/");web.postUrl("","");

//DatagramSocket.send(packet) 发送
//DatagramSocket DatagramSocket.bind DatagramSocket.createSocket 监听

//startupSocket对应多个socket构造函数  sock.connect  
//ServerSocket ServerSocket.bind

//SocketChannel.open(addr)  ?SocketChannel.connect?

//outputstream.write(byte[],int,int)

//httpclient.execute(post);
//urls.openConnection(); urls.openConnection(proxy);  ?urls.setRequestProperty?


//辅助记录,避免出现调用其他包进行监听 logcat | grep xuhu >>/sdcard/a.txt

public class Test implements IXposedHookLoadPackage{
private String TARGET_APP = "com.baidu.BaiduMap";
private String[] TARGET_APPS = new String[]{"com.yek.lafaso","com.vipshop.vswxk","com.baidu.BaiduMap","com.example.t1","com.baidu.fb","com.tencent.mm","com.tencent.mtt","com.nq.mdm"};
private SharedPreferences msp = null;
private Application mApp = null;
private String LOG_FILENAME = "_test_network";
private boolean NETWORK = true;
private boolean HTTP_DATA = true;
private boolean SOCKET_DATA = true;
private boolean HTTP_RESPONSE = false;
    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
    
    if(!Arrays.asList(TARGET_APPS).contains(lpparam.packageName))return;
   
TARGET_APP = lpparam.packageName;
if(lpparam.appInfo == null || 
           (lpparam.appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) !=0){
       return;
   }else if(true){//lpparam.isFirstApplication
   
    hookOncreate(lpparam);//
    if(mApp != null){
    msp = mApp.getSharedPreferences(LOG_FILENAME, Activity.MODE_PRIVATE);
    }
    mLog("target", lpparam.packageName);

    //网络监控开始
    if(NETWORK){
    findAndHookConstructor(InetSocketAddress.class, String.class, int.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            mLog("网络地址", param.args[0]+":"+param.args[1]);
               super.beforeHookedMethod(param);
           }
       });
   
    //
    findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "send", DatagramPacket.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            DatagramPacket d = (DatagramPacket)param.args[0];
            if(SOCKET_DATA){
            mLog("udp发送", d.getAddress()+":"+d.getPort()+":"+new String(d.getData()));
            }else{
            mLog("udp发送", d.getAddress()+":"+d.getPort());
            }
               super.beforeHookedMethod(param);
           }
       });
    findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "createSocket", int.class, InetAddress.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            mLog("udp监听", ((InetAddress)param.args[1]).toString()+":"+(Integer)param.args[0]);
               super.beforeHookedMethod(param);
           }
       });
    findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "bind", SocketAddress.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            mLog("udp监听", ((SocketAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
    findAndHookConstructor(DatagramSocket.class, SocketAddress.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            mLog("udp监听", ((SocketAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
   
    //
    findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "loadUrl", String.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            String d = (String)param.args[0];
               mLog("webview", d);
               super.beforeHookedMethod(param);
           }
       });
    findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "loadUrl", String.class, Map.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            String d = (String)param.args[0];
            if(HTTP_DATA){
            Map d1 = (Map)param.args[1];
               mLog("webview", d+":"+d1.toString());
            }else{
            mLog("webview", d);
            }
           
               super.beforeHookedMethod(param);
           }
       });
    findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "postUrl", String.class, byte[].class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            String d = (String)param.args[0];
            if(HTTP_DATA){
            String d1 = new String((byte[])param.args[1]);
               mLog("webview", d+":"+d1);
            }else{
            mLog("webview", d);
            }
           
               super.beforeHookedMethod(param);
           }
       });
   
    //
    if(SOCKET_DATA){
       findAndHookMethod("java.io.OutputStream", lpparam.classLoader, "write", byte[].class, int.class, int.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            byte[] d = (byte[])param.args[0];
            mLog("socketdata", new String(d));
               super.beforeHookedMethod(param);
           }
       });
       findAndHookMethod("java.io.OutputStream", lpparam.classLoader, "write", byte[].class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            byte[] d = (byte[])param.args[0];
            mLog("socketdata1", new String(d));
               super.beforeHookedMethod(param);
           }
       });
    }
    findAndHookMethod("java.nio.channels.SocketChannel", lpparam.classLoader, "open", SocketAddress.class, new XC_MethodHook() {
      @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
            mLog("tcp连接", ((SocketAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
   
    findAndHookMethod("java.net.Socket", lpparam.classLoader, "startupSocket", InetAddress.class, int.class, InetAddress.class, int.class, boolean.class, new XC_MethodHook() { 
           @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("tcp连接", ((InetAddress)param.args[0]).toString()+":"+(Integer)param.args[1]);
               super.beforeHookedMethod(param);
           }
       });
    findAndHookMethod("java.net.Socket", lpparam.classLoader, "connect", SocketAddress.class, int.class, new XC_MethodHook() { 
           @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("tcp连接", ((SocketAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
    findAndHookConstructor(ServerSocket.class, int.class, int.class, InetAddress.class, new XC_MethodHook() { 
           @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("tcp监听", ((InetAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
       findAndHookMethod("java.net.ServerSocket", lpparam.classLoader, "bind", SocketAddress.class, int.class, new XC_MethodHook() {
    @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("tcp监听", ((SocketAddress)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
       
       //
       findAndHookMethod("java.net.URL", lpparam.classLoader, "openConnection", java.net.Proxy.class, new XC_MethodHook() {
    @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               URL url = (URL) param.thisObject;
               mLog("urlconnp", url.toString()+":"+((Proxy)param.args[0]).toString());
               super.beforeHookedMethod(param);
           }
       });
       if(HTTP_DATA){
    findAndHookMethod("java.net.URLConnection", lpparam.classLoader, "setRequestProperty", String.class, String.class, new XC_MethodHook() {
    @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("urlconnheader", (String)param.args[0]+":"+(String)param.args[1]);
               super.beforeHookedMethod(param);
           }

       });
    findAndHookMethod("java.net.URLConnection", lpparam.classLoader, "addRequestProperty", String.class, String.class, new XC_MethodHook() {
    @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               mLog("urlconnheader", (String)param.args[0]+":"+(String)param.args[1]);
               super.beforeHookedMethod(param);
           }
   
       });
       }
    findAndHookMethod("java.net.URL", lpparam.classLoader, "openConnection", new XC_MethodHook() {
    @Override
           protected void beforeHookedMethod(MethodHookParam param)
                   throws Throwable {
               URL url = (URL) param.thisObject;
               mLog("urlconn", url.toString());
               super.beforeHookedMethod(param);
           }
       });
    //
    hookHttpClient(lpparam);

    }//网络监控结束
        }
    }
    
    public void mLog(String tag, String text){
    Log.i(TARGET_APP, "xuhu"+tag+":"+text);
    if(msp != null){
    if(HTTP_DATA){
    mSharePrefer(text,tag);
    }else{
    int i = text.indexOf("?");
    if(i > 0)
    mSharePrefer(text.substring(0, i), tag);
    else
    mSharePrefer(text, tag);
    }
    }
    }
    public void mSharePrefer(String key, String value){
SharedPreferences.Editor editor = msp.edit();
editor.putString(key, value);
editor.commit();
}
    
    public void hookHttpClient(LoadPackageParam lpparam){
    findAndHookMethod("org.apache.http.impl.client.AbstractHttpClient", lpparam.classLoader,
"execute", HttpHost.class, HttpRequest.class, HttpContext.class, new XC_MethodHook() {
     
            @Override
            protected void beforeHookedMethod(MethodHookParam param)
                    throws Throwable {
            //HttpHost host = (HttpHost) param.args[0];
HttpRequest request = (HttpRequest) param.args[1];
if (request instanceof HttpGet) {
HttpGet httpGet = (HttpGet) request;
mLog("httpclientGet", httpGet.getURI().toString());
if(HTTP_DATA){
Header[] headers = request.getAllHeaders();
if (headers != null) {
for (int i = 0; i < headers.length; i++) {
mLog("getHeader", headers[i].getName() + ": " + headers[i].getValue());
}
}}
} else if (request instanceof HttpPost) {
HttpPost httpPost = (HttpPost) request;
mLog("httpclientPost", httpPost.getURI().toString());
if(HTTP_DATA){// until get header
Header[] headers = request.getAllHeaders();
if (headers != null) {
for (int i = 0; i < headers.length; i++) {
mLog("postHeader", headers[i].getName() + ":" + headers[i].getValue());
}
}
HttpEntity entity = httpPost.getEntity();
String contentType = null;
if (entity.getContentType() != null) {
contentType = entity.getContentType().getValue();
if (URLEncodedUtils.CONTENT_TYPE.equals(contentType)) {
try {
byte[] data = new byte[(int) entity.getContentLength()];
entity.getContent().read(data);
String content = new String(data, HTTP.DEFAULT_CONTENT_CHARSET);
mLog("postcontent",content);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else if (contentType.startsWith(HTTP.DEFAULT_CONTENT_TYPE)) {
try {
byte[] data = new byte[(int) entity.getContentLength()];
entity.getContent().read(data);
String content = new String(data, contentType.substring(contentType.lastIndexOf("=") + 1));
mLog("postcontent",content);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}else{
byte[] data = new byte[(int) entity.getContentLength()];
try {
entity.getContent().read(data);
String content = new String(data, HTTP.DEFAULT_CONTENT_CHARSET);
mLog("postcontent",content);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}//get header
}else{
HttpEntityEnclosingRequestBase get = (HttpEntityEnclosingRequestBase)request;
HttpEntity entity = get.getEntity();
mLog("Android-async-http", get.getURI().toString());
if(HTTP_DATA){
Header[] headers = request.getAllHeaders();
if (headers != null) {
for (int i = 0; i < headers.length; i++) {
mLog("Android-async-httpHeader", headers[i].getName() + ":" + headers[i].getValue());
}
}
if(entity!= null){
String content = EntityUtils.toString(entity);
                       mLog("Android-async-httpcontent", content);
}
}
}
                super.beforeHookedMethod(param);
            }

            @Override
            protected void afterHookedMethod(MethodHookParam param)
                    throws Throwable { 
            HttpResponse resp = (HttpResponse) param.getResult();
if (resp != null && HTTP_RESPONSE) {
mLog("Status Code", ""+resp.getStatusLine().getStatusCode());
Header[] headers = resp.getAllHeaders();
if (headers != null) {
for (int i = 0; i < headers.length; i++) {
mLog("response", headers[i].getName() + ":" + headers[i].getValue());
}
}

}
                super.afterHookedMethod(param);
            }
        });
    }
    
    public void hookOncreate(LoadPackageParam lpparam){
    String appClassName = lpparam.appInfo.className;
    if (appClassName == null) {
    Method hookOncreateMethod = null;
try {
hookOncreateMethod = Application.class.getDeclaredMethod("onCreate", new Class[] {});
} catch (NoSuchMethodException e) {
e.printStackTrace();
mLog("target0", "target");
}
    XposedBridge.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook("target0",lpparam));
    }else{
    Class[[?> hook_application_class = null;
try {
hook_application_class = lpparam.classLoader.loadClass(appClassName);
if (hook_application_class != null) {
Method hookOncreateMethod = hook_application_class.getDeclaredMethod("onCreate", new Class[] {});
if (hookOncreateMethod != null) {
XposedBridge.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook("target1",lpparam));
}
}
mLog("target11", "target");
} catch (Exception e) {
// TODO Auto-generated catch block
Method hookOncreateMethod;
try {
hookOncreateMethod = Application.class.getDeclaredMethod("onCreate", new Class[] {});
if (hookOncreateMethod != null) {
XposedBridge.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook("target2",lpparam));
}
mLog("target21", "target");
} catch (NoSuchMethodException e1) {
// TODO Auto-generated catch block
mLog("target3", "target");
e1.printStackTrace();
}

}
    }
    }
    private class ApplicationOnCreateHook extends XC_MethodHook {
    public String which;
    public LoadPackageParam lpparam;
public ApplicationOnCreateHook(String which, LoadPackageParam lpparam){
this.which = which;
this.lpparam = lpparam;
}
@Override
public void afterHookedMethod(MethodHookParam param) throws FileNotFoundException {
mLog("", which);
if (!HAS_REGISTER_LISENER) {
mApp = (lpparam.isFirstApplication)?(Application) param.thisObject:mApp;
IntentFilter filter = new IntentFilter("xuhuafeng");
mApp.registerReceiver(new CommandBroadcastReceiver(), filter);
msp = mApp.getSharedPreferences(LOG_FILENAME, Activity.MODE_PRIVATE);
HAS_REGISTER_LISENER = true;
}
}

}
    private boolean HAS_REGISTER_LISENER = false;
}


相关文章
|
1月前
|
数据库 Android开发 开发者
构建高效Android应用:采用Kotlin协程优化网络请求处理
【2月更文挑战第30天】 在移动应用开发领域,网络请求的处理是影响用户体验的关键环节。针对Android平台,利用Kotlin协程能够极大提升异步任务处理的效率和简洁性。本文将探讨如何通过Kotlin协程优化Android应用中的网络请求处理流程,包括协程的基本概念、网络请求的异步执行以及错误处理等方面,旨在帮助开发者构建更加流畅和响应迅速的Android应用。
|
1天前
|
移动开发 Java Android开发
构建高效Android应用:采用Kotlin协程优化网络请求
【4月更文挑战第24天】 在移动开发领域,尤其是对于Android平台而言,网络请求是一个不可或缺的功能。然而,随着用户对应用响应速度和稳定性要求的不断提高,传统的异步处理方式如回调地狱和RxJava已逐渐显示出局限性。本文将探讨如何利用Kotlin协程来简化异步代码,提升网络请求的效率和可读性。我们将深入分析协程的原理,并通过一个实际案例展示如何在Android应用中集成和优化网络请求。
|
1天前
|
安全 JavaScript 前端开发
第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题—B模块安全事件响应/网络安全数据取证/应用安全
该内容描述了一次网络安全演练,包括七个部分:Linux渗透提权、内存取证、页面信息发现、数字取证调查、网络安全应急响应、Python代码分析和逆向分析。参与者需在模拟环境中收集Flag值,涉及任务如获取服务器信息、提权、解析内存片段、分析网络数据包、处理代码漏洞、解码逆向操作等。每个部分都列出了若干具体任务,要求提取或生成特定信息作为Flag提交。
3 0
|
1天前
|
安全 测试技术 Linux
2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A模块安全事件响应/网络安全数据取证/应用安全
该内容描述了一个网络安全挑战,涉及Windows和Linux系统的渗透测试以及隐藏信息探索和内存取证。挑战包括使用Kali Linux对Windows Server进行服务扫描、DNS信息提取、密码获取、文件名和内容查找等。对于Linux系统,任务包括收集服务器信息、提权并查找特定文件内容和密码。此外,还有对Server2007网站的多步骤渗透,寻找登录界面和页面中的隐藏FLAG。最后,需要通过FTP获取win20230306服务器的内存片段,从中提取密码、地址、主机名、挖矿程序信息和浏览器搜索关键词。
2 0
|
1天前
|
安全 测试技术 网络安全
2024年甘肃省职业院校技能大赛中职组 “网络安全”赛项竞赛样题-C模块安全事件响应/网络安全数据取证/应用安全
涉及安全事件响应和应用安全测试。需使用Kali对Windows Server2105进行渗透测试,包括服务扫描、DNS信息提取、管理员密码、文件名与内容、图片中单词等。另外,需收集win20230305的服务器端口、页面信息、脚本、登录后信息等。在Linux Server2214上,要获取SSH端口、主机名、内核版本并进行提权操作。网络安全响应针对Server2228,涉及删除后门用户、查找SSH后门时间、恢复环境变量、识别篡改文件格式和矿池钱包地址。最后,对lin20230509进行网站渗透,获取端口号、数据库服务版本、脚本创建时间、页面路径、内核版本和root目录下的flag文件内容
3 0
|
2天前
|
SQL 安全 测试技术
2021年职业院校技能大赛“网络安全”项目 江西省比赛任务书—B模块
B模块涵盖安全事件响应和网络数据取证,涉及多项应用安全挑战。任务包括使用nmap扫描靶机、弱口令登录、生成反弹木马、权限验证、系统内核版本检查、漏洞源码利用、文件名和内容提取等。此外,还有Linux渗透测试,要求访问特定目录下的文件并提取内容。应用服务漏洞扫描涉及服务版本探测、敏感文件发现、私钥解密、权限提升等。SQL注入测试需利用Nmap扫描端口,进行SQL注入并获取敏感信息。应急响应任务包括处理木马、删除恶意用户、修复启动项和清除服务器上的木马。流量分析涉及Wireshark数据包分析,查找黑客IP、枚举测试、服务破解等。渗透测试任务涵盖系统服务扫描、数据库管理、漏洞利用模块搜索等。
9 0
|
3天前
|
Android开发
Android RIL 动态切换 4G 模块适配
Android RIL 动态切换 4G 模块适配
6 0
|
9天前
|
Android开发 开发者
Android网络和数据交互: 请解释Android中的AsyncTask的作用。
Android&#39;s AsyncTask simplifies asynchronous tasks for brief background work, bridging UI and worker threads. It involves execute() for starting tasks, doInBackground() for background execution, publishProgress() for progress updates, and onPostExecute() for returning results to the main thread.
10 0
|
9天前
|
网络协议 安全 API
Android网络和数据交互: 什么是HTTP和HTTPS?在Android中如何进行网络请求?
HTTP和HTTPS是网络数据传输协议,HTTP基于TCP/IP,简单快速,HTTPS则是加密的HTTP,确保数据安全。在Android中,过去常用HttpURLConnection和HttpClient,但HttpClient自Android 6.0起被移除。现在推荐使用支持TLS、流式上传下载、超时配置等特性的HttpsURLConnection进行网络请求。
9 0
|
21天前
|
数据采集 网络协议 API
python中其他网络相关的模块和库简介
【4月更文挑战第4天】Python网络编程有多个流行模块和库,如requests提供简洁的HTTP客户端API,支持多种HTTP方法和自动处理复杂功能;Scrapy是高效的网络爬虫框架,适用于数据挖掘和自动化测试;aiohttp基于asyncio的异步HTTP库,用于构建高性能Web应用;Twisted是事件驱动的网络引擎,支持多种协议和异步编程;Flask和Django分别是轻量级和全栈Web框架,方便构建不同规模的Web应用。这些工具使网络编程更简单和高效。

热门文章

最新文章