• Android4.3以前,如果系统需要备份/恢复,防火墙以及DNS解析管理,Linux内核微调等,是需要ROOT权限才能进行的。在Android4.3中,Google修改了这一策略,Google向用户提供API和扩展来完成这些事情。其中DNS解析...

    1. Change of Android4.3


    在Android4.3以前,如果系统需要备份/恢复,防火墙以及DNS解析管理,Linux内核微调等,是需要ROOT权限才能进行的。在Android4.3中,Google修改了这一策略,Google向用户提供API和扩展来完成这些事情。其中DNS解析就是这一改变中的一环。







    2. Android的DNS解析



    Bionic是Android自己的C库版本。


    在早期版本的Android中,DNS解析的方式类似于Ubuntu等发行版Linux。都是通过resovl.conf文件进行域名解析的。在老版本Android的bionic/libc/docs/overview.txt中可以看到,Android的DNS也是采用NetBSD-derived resolver library来实现,不同的是,bionic对其进行了一些修改。这些修改包括:


    1.     resovle.conf文件的位置不再是/etc/resolv.conf,在Android中改为了/system/etc/resolv.conf。

    2.     从系统属性(SystemProperties)中读取DNS服务器,比如“net.dns1”,“net.dns2”等。每一个属性必须包括了DNS服务器的IP地址。

    3.     不实现Name ServiceSwitch。

    4.     在查询时,使用一个随机的查询ID,而非每次自增1.

    5.     在查询时,将本地客户端的socket绑定到一个随机端口以增强安全性。



    3. Java与JNI层中DNS解析的公共流程



    我们从下面小例子开始分析公共流程中DNS解析所经过的函数,对于Android中JNI和JAVA等层次概念请参考最开始的那一张结构图:



    //获得www.taobao.com对应的IP地址,并通过Toast的方式打印出来
    try {
    	    InetAddress inetAddress = InetAddress.getByName("www.taobao.com");
    	    Toast.makeText(MainActivity.this, "Address is " + inetAddress.getHostAddress(), Toast.LENGTH_LONG).show();		} catch (UnknownHostException e) {
    	    			// TODO Auto-generated catch block
    	    			e.printStackTrace();
    	  }
    

    以上Java代码给出了最简单的一次DNS解析的方法。主要实现是调用InetAddress类的静态方法getByName,该方法返回一个InetAddress实例,该实例中包括很多关于域名的信息。


        public static InetAddress getByName(String host) throws UnknownHostException {
            return getAllByNameImpl(host)[0];
        }
    

    实际调用getAllByNameImpl函数。该函数内部主要进行三件事情,第一件,如果host是null,那么调用loopbackAddresses()。如果host是数字形式的地址,那么调用parseNumericAddressNoThrow解析并返回。如果是一个字符串,则使用lookupHostByName(host)返回一个InetAddress并clone一份返回。

     

    lookupHostByName函数首先host的信息是否存在在缓存当中,如果有则返回。如果没有则:


    InetAddress[] addresses = Libcore.os.getaddrinfo(host, hints);

    getaddrinfo函数是一个native本地函数,声明如下:


    public native InetAddress[] getaddrinfo(String node, StructAddrinfo hints) throws GaiException;

    在getaddrinfo对应的JNI层函数中,实际调用了下面函数:


    int rc = getaddrinfo(node.c_str(), NULL, &hints, &addressList);
    

    getaddrinfo实现自bionic的netbsd库,具体文件位于/bionic/libc/netbsd/net中,后面我们会分析Android4.2和Android4.3的代码,来观察Google在Android4.3中对DNS解析做了什么样的修改。

    除了getaddrinfo路径以外,在Java中InetAddress还有其他方式,比如

    public String getHostName() {
            if (hostname == null) {
                try {
                    hostname = getHostByAddrImpl(this).hostName;
                } catch (UnknownHostException ex) {
                    hostname = getHostAddress();
                }
            }
            return hostname;
    }
    


    上述方法,调用了getHostByAddrImpl,在getHostByAddrImpl中:


    String hostname = Libcore.os.getnameinfo(address, NI_NAMEREQD);

    调用了getnameinfo方法,该方法同样是一个native函数,在JNI层对应的函数中直接调用了getnameinfo这个bionic库的函数:


    int rc = getnameinfo(reinterpret_cast<sockaddr*>(&ss), size, buf, sizeof(buf), NULL, 0, flags);




    4. Android4.2和Android4.3 bionic中DNS解析实现的变化



    不管是getaddrinfo还是getnameinfo还是gethostbyname,都是实现在bionic库中,这里先以getaddrinfo为例分析Android4.3前后bionic在DNS解析处通用逻辑的变化。先从4.3以前版本开始。

    在getaddrinfo中,关键的一步如下:



    /*         
    * BEGIN ANDROID CHANGES; proxying to the cache
    */
    if (android_getaddrinfo_proxy(hostname, servname, hints, res) == 0) {
    return 0;
    }
    

    注意上面的注释,ANDROID_CHANGES,Google在Android4.2.2开始已经打算将所有DNS解析的方式向Netd代理的方式过渡了。后面我们还会看到ANDROID_CHANGES。

    然后在android_getaddrinfo_proxy中,我们可以看到如下代码:


    snprintf(propname, sizeof(propname), "net.dns1.%d", getpid());
    if (__system_property_get(propname, propvalue) > 0) {
    		return -1;
    	}
    // Bogus things we can't serialize.  Don't use the proxy.
    if ((hostname != NULL &&
        strcspn(hostname, " \n\r\t^'\"") != strlen(hostname)) ||
       (servname != NULL &&
        strcspn(servname, " \n\r\t^'\"") != strlen(servname))) {
    	return -1;
    }
    …
    // Send the request.
    proxy = fdopen(sock, "r+");
    if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d",
    	    hostname == NULL ? "^" : hostname,
    	    servname == NULL ? "^" : servname,
    	    hints == NULL ? -1 : hints->ai_flags,
    		hints == NULL ? -1 : hints->ai_family,
    	    hints == NULL ? -1 : hints->ai_socktype,
    		hints == NULL ? -1 : hints->ai_protocol) < 0) {
    	goto exit;
    }
    // literal NULL byte at end, required by FrameworkListener
    if (fputc(0, proxy) == EOF ||
        fflush(proxy) != 0) {
    	goto exit;
    }
    

    Android会首先尝试从系统属性(System Property)中读取DNS服务器的IP地址,然后使用这个DNS服务器来进行DNS解析。如果没有设置相关系统属性,则采用Netd的方式来进行DNS解析。由于在使用Netd方式进行解析的时候server name是不能为NULL的,所以可以看到上面将server name修改成了’^’。在分析Netd代理之前,我们最好停一停,看看Android4.3后,getaddrinfo是怎么做的。

     

    首先是从JNI层的getaddrinfo的代码开始:


    int rc = getaddrinfo(node.c_str(), NULL, &hints, &addressList);

    和Android4.2.2没有变化,直接调用了getaddrinfo,其中第二个参数是NULL。


    Int
    getaddrinfo(const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res)
    {
    	return android_getaddrinfoforiface(hostname, servname, hints, NULL, 0, res);
    }
    

    直接调用了android_getaddrinfoforiface函数。


    /* 4.3 */
    static int android_getaddrinfo_proxy(
        const char *hostname, const char *servname,
        const struct addrinfo *hints, struct addrinfo **res, const char *iface)
    {
    	int sock;
    	const int one = 1;
    	struct sockaddr_un proxy_addr;
    	FILE* proxy = NULL;
    	int success = 0;
    	*res = NULL;
    
    	if ((hostname != NULL &&
    	     strcspn(hostname, " \n\r\t^'\"") != strlen(hostname)) ||
    	    (servname != NULL &&
    	     strcspn(servname, " \n\r\t^'\"") != strlen(servname))) {
    		return EAI_NODATA;
    	}
    
    	sock = socket(AF_UNIX, SOCK_STREAM, 0);
    	if (sock < 0) {
    		return EAI_NODATA;
    	}
    
        …….
    


    很明显,Android4.3以后删掉了读取系统属性的那一段代码,这时如果任然采用添加系统属性的方法来修改DNS服务器将不会产生任何作用。

     

    Android除了使用getaddrinfo函数外,系统代码还会使用gethostbyname等其他路径。下面我们再看看gethostbyname路径在Android4.3前后发生的变化。

    在给出代码之前,先说明下gethostbyname函数内部将调用gethostbyname_internal来真正进行DNS解析。


    Android4.2.2:



    static struct hostent *
    gethostbyname_internal(const char *name, int af, res_state res)
    {
    	…
    
    	rs->host.h_addrtype = af;
    	rs->host.h_length = size;
    	/*
    	 * if there aren’t any dots, it could be a user-level alias.
    	 * this is also done in res_nquery() since we are not the only
    	 * function that looks up host names.
    	 */
    	if (!strchr(name, ‘.’) && (cp = __hostalias(name)))
    		name = cp;
    	
    /*
    	 * disallow names consisting only of digits/dots, unless
    	 * they end in a dot.
    	 */
    	if (isdigit((u_char) name[0]))
    		for (cp = name;; ++cp) {
                               …
    		}
    			if (!isdigit((u_char) *cp) && *cp != ‘.’)
    				break;
    		}
    	if ((isxdigit((u_char) name[0]) && strchr(name, ‘:’) != NULL) ||
    	    name[0] == ‘:’)
    		for (cp = name;; ++cp) {
    			if (!*cp) {
    				…
    			}
    			if (!isxdigit((u_char) *cp) && *cp != ‘:’ && *cp != ‘.’)
    				break;
    		}
    	hp = NULL;
    	h_errno = NETDB_INTERNAL;
    	if (nsdispatch(&hp, dtab, NSDB_HOSTS, “gethostbyname”,
    	    default_dns_files, name, strlen(name), af) != NS_SUCCESS) {
    		return NULL;
            }
    	h_errno = NETDB_SUCCESS;
    	return hp;
    }
    

    先不关心使用的localdns是哪个,在Android4.2.2中,gethostbyname_internal直接调用了nsdispatch来进行域名解析。

     

    下面再看看Android4.3中的变化:



    static struct hostent *
    gethostbyname_internal(const char *name, int af, res_state res, const char *iface, int mark)
    {
    …
    	proxy = android_open_proxy();
    	if (proxy == NULL) goto exit;
    
    	/* This is writing to system/netd/DnsProxyListener.cpp and changes
    	 * here need to be matched there */
    	if (fprintf(proxy, “gethostbyname %s %s %d”,
    			iface == NULL ? “^” : iface,
    			name == NULL ? “^” : name,
    			af) < 0) {
    		goto exit;
    	}
    
    	if (fputc(0, proxy) == EOF || fflush(proxy) != 0) {
    		goto exit;
    	}
    
    	result = android_read_hostent(proxy);
    
    exit:
    	if (proxy != NULL) {
    		fclose(proxy);
    	}
    	return result;
    }
    

    从上面代码可以看到,Android4.3中彻底全面使用Netd的方式进行了DNS处理。

    最后让我们再看看getnameinfo在bionic的实现。

    首先是4.2.2的代码,路径上getnameinfo会调用getnameinfo_inet,然后出现下面的代码:


    #ifdef ANDROID_CHANGES
    	struct hostent android_proxy_hostent;
    	char android_proxy_buf[MAXDNAME];
    	int hostnamelen = android_gethostbyaddr_proxy(android_proxy_buf,
    			MAXDNAME, addr, afd->a_addrlen, afd->a_af);
    	if (hostnamelen > 0) {
    		hp = &android_proxy_hostent;
    		hp->h_name = android_proxy_buf;
    	} else if (!hostnamelen) {
    		hp = NULL;
    	} else {
    		hp = gethostbyaddr(addr, afd->a_addrlen, afd->a_af);
    	}
    #else
    	hp = gethostbyaddr(addr, afd->a_addrlen, afd->a_af);
    #endif
    

    具体如何处理根据ANDROID_CHANGES宏决定,如果定义了该宏,则通过Netd的方式进行。如果没有则直接调用gethostbyaddr,该函数后面会进行实际的dns解析。

    再看看Android4.3中的实现:


    int hostnamelen = android_gethostbyaddr_proxy(android_proxy_buf,
    				MAXDNAME, addr, afd->a_addrlen, afd->a_af, iface, mark);
    


    强行使用Netd的方式完成DNS的解析。Google在Android4.3后让DNS解析全部采用Netd代理的方式进行。


    Netd是Network Daemon的缩写,Netd在Android中负责物理端口的网络操作相关的实现,如Bandwidth,NAT,PPP,soft-ap等。Netd为Framework隔离了底层网络接口的差异,提供了统一的调用接口,简化了整个网络逻辑的使用。

    简单来说就是Android将监听/dev/socket/dnsproxyd,如果系统需要DNS解析服务,那么就需要打开dnsproxyd,然后安装一定的格式写入命令,然后监听等待目标回答。


    在分析Netd前,必须知道Netd的权限和所属。




    图中可以看出,两者的owner都是root,现在就好理解为什么说Android4.3后很多原来功能不需要root的原因了,系统现在采用代理的方式,让属于同group的用户可以借助Netd来干一些原来只有root能干的事情。

    Android的初始化大致上可以分为三个部分:第一部分为启动Linux阶段,该部分包括bootloader加载kernel与kernel启动。第二部分为android的系统启动,入口为init程序,这部分包括启动service manager,启动Zygote,初始化Java世界等。第三部分为应用程序启动,主要为运行package manager。

    与Netd相关联的是第二部分,也就是init进程。init进程在初始化中会处理/init.rc以及/init.<hardware>.rc两个初始化脚本,这些脚本决定了Android要启动哪些系统服务和执行哪些动作。

    比如:


    service servicemanager /system/bin/servicemanager  
        user system  
        critical  
        onrestart restart zygote  
        onrestart restart media  
      
    service vold /system/bin/vold  
        socket vold stream 0660 root mount  
        ioprio be 2  
      
    service netd /system/bin/netd  
        socket netd stream 0660 root system  
        socket dnsproxyd stream 0660 root inet  
      
    service debuggerd /system/bin/debuggerd  
      
    service ril-daemon /system/bin/rild  
        socket rild stream 660 root radio  
        socket rild-debug stream 660 radio system  
        user root  
        group radio cache inet misc audio sdcard_rw  
    

    通过init.rc,我们可以看到netd和dnsproxy的权限和所属。直接从代码开始分析,netd源代码位于/system/netd/main.cpp,由C++编写。

    从上面框架图中可以得知,netd由四个大部分组成,一部分是NetlinkManager,一个是CommandListener,然后是DnsProxyListener和MDnsSdListener。在main函数中netd依次初始化四个部件:


    int main() {
    
        CommandListener *cl;
        NetlinkManager *nm;
        DnsProxyListener *dpl;
    MDnsSdListener *mdnsl;
    
    if (!(nm = NetlinkManager::Instance())) {
            ALOGE("Unable to create NetlinkManager");
            exit(1);
     };
    
    …
    
    cl = new CommandListener(rangeMap);
    nm->setBroadcaster((SocketListener *) cl);
    
        if (nm->start()) {
            ALOGE("Unable to start NetlinkManager (%s)", strerror(errno));
            exit(1);
        }
    setenv("ANDROID_DNS_MODE", "local", 1);
    dpl = new DnsProxyListener(rangeMap);
    
    if (dpl->startListener()) {
            ALOGE("Unable to start DnsProxyListener (%s)", strerror(errno));
            exit(1);
        }
        mdnsl = new MDnsSdListener();
        if (mdnsl->startListener()) {
            ALOGE("Unable to start MDnsSdListener (%s)", strerror(errno));
            exit(1);
        }
        if (cl->startListener()) {
            ALOGE("Unable to start CommandListener (%s)", strerror(errno));
            exit(1);
      }
    

    代码都很简单,所以不需要赘述,只不过需要注意那句setenv(“ANDROID_DNS_MODE”,”local”,1),这句在后面有大作用。如果看过bionic代码的同学可能已经有所领悟了。

     

    DnsProxyListener实际上就是pthread创造的一个线程,该线程仅仅监听dnsproxyd这个socket。

     

    其他进程如何利用dnsproxyd来进行DNS解析呢?答案很简单,看到bionic中gethostbyname_internal中的这么一句:


    if (fprintf(proxy, “gethostbyname %s %s %d”,
    			iface == NULL ? “^” : iface,
    			name == NULL ? “^” : name,
    			af) < 0) {
    		goto exit;
    	}
    

    其他进程打开dnsproxyd后(必须要同一个组),使用命令的方式来申请DNS解析。DnsProxyListener内部逻辑是很复杂的,这里没必要深究。现在看看gethostbyname这个命令如何解析。

    Netd当中每一个命令对应一个类,该类继承自NetdCommand类。除此之外,还需要一个XXXXHandler的类来做实际命令的处理工作。XXXX是命令的名称,比如对于gethostbyname就有两个类:GetHostByNameCmd

    GetHostByNameHandler。既然XXXXhandler中有两个公共方法,一个threadStart一个叫start。除此之外,还有个私有方法run。对命令的实际处理就是run方法实现的。


    void DnsProxyListener::GetHostByNameHandler::run() {
        …
        struct hostent* hp;
    
        hp = android_gethostbynameforiface(mName, mAf, mIface ? mIface : iface, mMark);
    
        bool success = true;
        if (hp) {
            success = mClient->sendCode(ResponseCode::DnsProxyQueryResult) == 0;
            success &= sendhostent(mClient, hp);
        } else {
            success = mClient->sendBinaryMsg(ResponseCode::DnsProxyOperationFailed, NULL, 0) == 0;
        }
        if (!success) {
            ALOGW("GetHostByNameHandler: Error writing DNS result to client\n");
        }
        mClient->decRef();
    }
    

    关键的两行代码是android_gethostbynameforiface和sendBinaryMsg,后者是将前者得到的结果应答给请求DNS解析的进程。


    struct hostent *
    android_gethostbynameforiface(const char *name, int af, const char *iface, int mark)
    {
    	struct hostent *hp;
    	res_state res = __res_get_state();
    
    	if (res == NULL)
    		return NULL;
    	hp = gethostbyname_internal(name, af, res, iface, mark);
    	__res_put_state(res);
    	return hp;
    }
    

    关键仍然是调用了gethostbyname_internal。看到这里,看官们可能就会奇怪了,进程向Netd申请DNS请求的时候,调用的函数就是这个gethostbyname_internal,那么此时又调用一次岂不是递归了?这里就体现了创造Android工程师的智慧了。第一次调用gethostbyname_internal的时候是进程调用,并且这个时候ANDROID_DNS_MODE没有设置。第二次调用gethostbyname_internal的时候是Netd调用的,Netd的权限是root的,而且更关键的是前面Netd初始化的时候set了ANDROID_DNS_MODE,这两个不同的地方就影响了整个逻辑。

           除此之外,上方android_gethostbynameforiface函数中调用了__res_get_state函数。该函数获得了一个和线程相关的DNS服务器信息。去哪个local dns查询就看这个函数返回的res_thread结构了。这部分内容稍后进行分析。我们继续关注gethostbyname_internal的实现。


    static struct hostent *
    gethostbyname_internal(const char *name, int af, res_state res, const char *iface, int mark)
    {
    	const char *cache_mode = getenv("ANDROID_DNS_MODE");
    	FILE* proxy = NULL;
    	struct hostent *result = NULL;
    
    	if (cache_mode != NULL && strcmp(cache_mode, "local") == 0) {
    		res_setiface(res, iface);
    		res_setmark(res, mark);
    		return gethostbyname_internal_real(name, af, res);
    	}
    

    这一次判断cache_mode的语句将为true,此时进入gethostbyname_internal_real函数来处理DNS请求,后面就不用多分析了,有兴趣的童鞋可以继续跟随代码。后面就是构建DNS请求包和发送DNS请求了。






    整个DNS解析的流程我们是清楚了,现在我们就要去想办法修改DNS服务器了。在android_gethostbynameforiface中,通过_res_thread_get函数获得__res_state。而在_res_thread_get函数中,用pthread_getspecific来获得与线程相关联的

    _res_key。此时如果pthread_getspecific返回的是NULL说明该函数是第一次被调用,那么将会通过_res_thread_alloc分配内存然后进行初始化。初始化关键语句是res_ninit,该函数由会调用__res_vinit完成具体工作。

    这里先给出__res_state结构的具体信息:


    struct __res_state {
    	char	iface[IF_NAMESIZE+1];
    	int	retrans;	 	/* retransmission time interval */
    	int	retry;			/* number of times to retransmit */
    	u_int	options;		/* option flags - see below. */
    	int	nscount;		/* number of name servers */
    	struct sockaddr_in nsaddr_list[MAXNS];	/* address of name server */
    #define	nsaddr	nsaddr_list[0]		/* for backward compatibility */
    	u_short	id;			/* current message id */
    	char	*dnsrch[MAXDNSRCH+1];	/* components of domain to search */
    	char	defdname[256];		/* default domain (deprecated) */
    	u_int	pfcode;			/* RES_PRF_ flags - see below. */
    	unsigned ndots:4;		/* threshold for initial abs. query */
    	unsigned nsort:4;		/* number of elements in sort_list[] */
    	char	unused[3];
    	struct {
    		struct in_addr	addr;
    		uint32_t	mask;
    	} sort_list[MAXRESOLVSORT];
    	res_send_qhook qhook;		/* query hook */
    	res_send_rhook rhook;		/* response hook */
    	int	res_h_errno;		/* last one set for this context */
    	int _mark;          /* If non-0 SET_MARK to _mark on all request sockets */
    	int	_vcsock;		/* PRIVATE: for res_send VC i/o */
    	u_int	_flags;			/* PRIVATE: see below */
    	u_int	_pad;			/* make _u 64 bit aligned */
    	union {
    		/* On an 32-bit arch this means 512b total. */
    		char	pad[72 - 4*sizeof (int) - 2*sizeof (void *)];
    		struct {
    			uint16_t		nscount;
    			uint16_t		nstimes[MAXNS];	/* ms. */
    			int			nssocks[MAXNS];
    			struct __res_state_ext *ext;	/* extention for IPv6 */
    		} _ext;
    	} _u;
            struct res_static   rstatic[1];
    };
    

    关键的成员是nsaddr_list,现在需要知道该成员何时何处被初始化了。答案是在前面的__res_vinit函数中,不过在深入之前必须要看看__res_ninit函数的注释部分。这一部分介绍了初始化的大概逻辑。


    /*
     * Set up default settings.  If the configuration file exist, the values
     * there will have precedence.  Otherwise, the server address is set to
     * INADDR_ANY and the default domain name comes from the gethostname().
     *
     * An interrim version of this code (BIND 4.9, pre-4.4BSD) used 127.0.0.1
     * rather than INADDR_ANY ("0.0.0.0") as the default name server address
     * since it was noted that INADDR_ANY actually meant ``the first interface
     * you "ifconfig"'d at boot time'' and if this was a SLIP or PPP interface,
     * it had to be "up" in order for you to reach your own name server.  It
     * was later decided that since the recommended practice is to always
     * install local static routes through 127.0.0.1 for all your network
     * interfaces, that we could solve this problem without a code change.
     *
     * The configuration file should always be used, since it is the only way
     * to specify a default domain.  If you are running a server on your local
     * machine, you should say "nameserver 0.0.0.0" or "nameserver 127.0.0.1"
     * in the configuration file.
     *
     * Return 0 if completes successfully, -1 on error
     */
    


    实际上这个所谓的配置文件正逐步被去掉,在__res_vinit后面有一段被#ifndefANDROID_CHANGES包围的代码,这段代码就是解析/etc/resolv.conf文件的。但是4.3后是#define了ANDROID_CHANGES的。所以ANDROID4.3以后再添加

    resolv.conf是没有意义的了。

    注释中说如果没有配置文件,则server address设为INADDR_ANY并且通过gethostname来获得默认domain name。也就是说,如果在wifi等环境下,DNS服务器都是自动获取的。



    5. 对策与思路


    Android4.3之前

    在Android4.3以前,如果需要修改DNS服务器,有很多种方法,这些方法的实质就是向系统属性中添加“net.dns1”字段的信息。这些方法的前提条件都是获得root权限。具体方法有:

    1.     在shell下,直接设置“net.dns1”等的系统属性。

    2.     在init.rc脚本中,添加对“net.dns1”等系统属性的设置。

    3.     在root权限下创建resovle.conf文件并添加相关name server信息。


    Android4.3以后


    在Android4.3以后,通过系统属性或者解析文件来手动修改DNS服务器已经是不可能了。主要有两种方法,一个是在NDK下面修改DNS解析逻辑,第二个是通过Android系统源代码修改相关逻辑,让Android4.3的新修改无效,然后重构Android。下面是一个老外基于NDK的修改方案,该方案需要以下权限:

    1.     Root权限

    2.     对/system文件夹有写权限

    3.     能修改/etc/init.d

     

    该方案重写了DnsProxyListener和bionic解析器逻辑,通过将/dev/socket/dnsproxyd改名然后自己替换它来达到目的。


    /* 等待Netd启动 */
        while (do_wait && stat(SOCKPATH, &statbuf) < 0) {
            sleep(1);
        }
        /* 将其改名 */
        if (stat(SOCKPATH, &statbuf) == 0) {
            unlink(SOCKPATH ".bak");
            rename(SOCKPATH, SOCKPATH ".bak");
            restore_oldsock = 1;
        }
    
        sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
        …
    
        /* 移花接木 */
    memset(&sock, 0, sizeof(sock));
        sock.sun_family = AF_UNIX;
        strcpy(sock.sun_path, SOCKPATH);
    
    if (bind(sockfd, (struct sockaddr *)&sock, sizeof(sock)) < 0) 
    …
    
    if (chmod(SOCKPATH, 0660) < 0) 
    …
    
    /* 使用命令行或者缺省的IP做为DNS服务器,然后剩下的逻辑就是修改DnsProxyListener了 */
    if (optind < argc)
            setup_resolver(argv[optind]);
        else
            setup_resolver("223.5.5.5");
    

    代码逻辑比较容易理解,但是如何使用呢?很简单,使用adb将NDK生成的可执行文件拷贝到system目录下面,然后./dnstool –v 223.5.5.5&即可。








    展开全文
  • 目前市场上有很多安全软件,它们拦截第三方应用广告的方式都不一样,比如说有 以so 注入方式来拦截弹出广告。 现在我们来看下这种方式的详细情况:要做到拦截,首先我们得知道广告是怎么出来的,原来第三方应用大...

    前段时间,公司制造的机器里应用装有不良广告,严重影响了儿童客户使用者的思想健康,导致被人投诉。于是乎,就有了想研发一款类似于360广告屏蔽的应用的念头。嗯,事情就是这样,现在切入主题。

    目前市场上有很多安全软件,它们拦截第三方应用广告的方式都不一样,比如说有 以so 注入方式来拦截弹出广告
    现在我们来看下这种方式的详细情况:

    要做到拦截,首先我们得知道广告是怎么出来的,原来第三方应用大部分是以加入广告jar形式加入广告插件,然后在AndroidManifest中声明广告service或者在程序中执行广告Api,广告插件再通过Http请求去加载广告。在java中,有四种访问网络的接口,如apache的http库(如下介绍),这几种方式首先都会通过getaddrinfo函数获取域名地址,然后通过connect函数连接到服务器读取广告信息。

    1. WebView(源码文件在frameworks/base/core/java/android/webkit/WebView.java)。通过WebView类的void loadUrl(String url)、void postUrl(String url, byte[] postData)、void loadData(String data, String mimeType, String encoding)、void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)、void evaluateJavascript(String script, ValueCallback resultCallback)等加载网页。
    2. apache-http(源码目录在external/apache-http/ , HttpGet 和 HttpPost类)。通过external/apache-http/src/org/apache/http/impl/client/DefaultRequestDirector.java中的DefaultRequestDirector类的HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)方法执行访问的网络的动作。
    3. okhttp(源码目录在external/okhttp/)。通过external/okhttp/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java中的HttpEngine类的private void connect(Request request) throws IOException方法连接网络。
    4. URL(源码在libcore/luni/src/main/java/java/net/URL.java)。通过libcore/luni/src/main/java/java/net/URL.java中的URL类的URLConnection openConnection() throws IOException方法和URLConnection openConnection(Proxy proxy) throws IOException方法连接网络。

    然后再来说说动态库注入,具体什么是动态库注入,以及如何注入,网上有很多文章,这里就不介绍。动态库注入拦截呢,主要是拦截getaddrinfo,根据条件返回错误来拦截网络请求,达到拦截作用。不过需要注意一点就是拦截之前要确定你所拦截的动态库是否是你需要拦截的库?例如A程序调用了动态库BO和CO,而BO和CO都调用了connect函数,此时需要拦截BO的请求,需要注入到BO动态库并修改GOT表,而不是注入到CO中。

    拦截HTTP方式广告在多数广告包中,应用程序首先会通过apache的http库或JDK中的http方法先将广告数据下载过来,然后通过WebView显示。这种方式通过注入拦截进程的/system/lib/libjavacore.so可实现广告地址拦截。

    拦截WebView方式广告广告插件也可以直接通过WebView加载URL,通过分析WebView加载流程可知它的网络处理过程均交给libchromium_net库来完成。因此通过注入libjavacore.so是无法实现拦截,而是需要注入到/system/lib/libchromium_net.so。

    通过这种方式已经完全能够拦截掉第三方APP广告,但存在一些问题:

    1.广告商可以通过JNI方式调用系统getaddrinfo与connect实现自己的解析与连接过程的动态库,从而跳过libjavacore.so导致拦截无效。
    2.拦截WebView方式广告虽然能够不显示广告,但通常仍然会有浮动框显示”网页无法打开”,从而影响美观。
    3.最重要的是我们机器是没有root权限的!!

    第三个问题直接导致了放弃了这种注入做法。
    来来去去一段时间后,目前是采用android 系统本地扫描第三方应用广告形式。具体怎么做,请往下看!

    如果对这种方式不了解的话,建议先看下这篇 Android系统扫描带广告应用的做法

    所以具体广告插件扫描方案是匹配包名+类名形式的:
    1.扫描本地所有第三方应用,列出一个应用中的所有类,将包名+类名方式与广告插件特征库进行匹配
    2.将匹配出来的应用所带广告特征,通过系统提供传入接口,将这些规则设置进去。(当然,系统代码是需要改的,做了一些处理,主要是在上面介绍中的几种访问网络方式上做了判断处理)

    这种方案的关键在于广告特征库的完善,广告插件特征库收集越全,扫描出来的广告插件就可以越准确。所幸,公司有几位大神,做过类似的事情,所以工作简单了多些。

    获取第三方应用:

       /** 
         * 查询机器内非本公司应用 
         */  
        public List<PackageInfo> getAllLocalInstalledApps() {  
            List<PackageInfo> apps = new ArrayList<PackageInfo>();  
            if(pManager == null){
                return apps;
            }
            //获取所有应用  
            List<PackageInfo> paklist = pManager.getInstalledPackages(0);  
            for (int i = 0; i < paklist.size(); i++) {  
                PackageInfo pak = (PackageInfo) paklist.get(i);
    
               //屏蔽掉公司内部应用
               //...
    
               //判断是否为非系统预装的应用程序  
                if ((pak.applicationInfo.flags & pak.applicationInfo.FLAG_SYSTEM) <= 0) {  
                    // customs applications 
                    apps.add(pak);
                }  
            }  
            return apps;  
        }

    获取某个应用的广告特征:

    public static List<String> getClassNameByDex(Context context,
                String packageName) {
    
            List<String> datalist = new ArrayList<String>();
            String path = null;
            try {
                path = context.getPackageManager().getApplicationInfo(packageName,
                        0).sourceDir;// 获得某个程序的APK路径
            } catch (NameNotFoundException e) {
                e.printStackTrace();
            }
            try {
                if(TextUtils.isEmpty(path)){
                    return datalist;
                }
                DexFile dexFile = new DexFile(path);// get dex file of APK
                Enumeration<String> entries = dexFile.entries();
                while (entries.hasMoreElements()) {// travel all classes
                    String className = (String) entries.nextElement();
                    String totalname = packageName + "."+className;
                    datalist.add(totalname);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
            return datalist;
        }

    将应用中的所有类名与特征库进行匹配:

    for (PackageInfo info : infolsit) {
            if (info == null) {
                continue;
            }
            data = getClassNameByDex(context,info.packageName);
            if(data == null){
                Log.d(TAG,"getAdFlagForLocalApp()  类名解析出错"+info.packageName);
                continue;
            }
            sgPgmap = new HashMap<String, String>();
            for (String clsname : data) {
                for (ADSInfo adinfo : flaglist) {
                    String flag = adinfo.getAdFlag();  //广告样本库的某一标识 
                    String adpg = adinfo.getAdName();  //广告样本库的某一包名
                    if (clsname.contains(adpg)) {  //匹配类名与广告特征库里的匹配符,看是否包含关系
                        sgPgmap.put(flag,info.packageName);
                    }
                }
            }
            if(sgPgmap.size() > 0){
                //AdsPgInfo  一个对应应用里包含了多少个标识
                adspginfo = new AdsPgInfo(info.packageName, sgPgmap);
                pglist.add(adspginfo);
            }
        }

    ps: 在匹配时,有一个很注意的点,有时候单单类名匹配不准,或者会漏掉某些广告,所以应该加上包名,再去匹配特征库里的匹配符,这样才能百无一漏。

    在此举例一个指智广告的特征(特征显示形式可自定义,只要符合自己的解析策略即可):

    ads.banner.zhidian#指智广告#com/adzhidian/#ad.zhidian3g.cn
    • ads.banner.zhidian 为该类型广告标识,主要是为了匹配时应用对应标识的简洁性,不用直接跟着一群特征到处跑。。
    • 指智广告 该广告名称
    • com/adzhidian/ 该广告用来匹配应用中类名的匹配符,当应用中某一(包名+类名)包含该匹配符时,说明了该应用包含该广告
    • ad.zhidian3g.cn 需要传给系统的一个规则特征。

    匹配出所有应用的所属规则特征后,接下来需要传给系统了,系统将满足需求的几个接口提供出来。这边涉及到修改系统层代码,我就主要讲下实现思路,会贴出关键的几个代码。
    实现思路:系统根据应用层传入的应用包名以及规则,将其缓存,在webview或http处请求时,对其进行判断处理。

    添加某应用规则接口:

    /**
     * add Adblock url of package pkgName
     */
     private boolean addAdblockUrlInner(String pkgName, String url) {
        synchronized (mAdblockEntries) {
          HashMap<String, UrlEntry> pkgEntry = mAdblockEntries.get(pkgName);
         if (pkgEntry == null) {
            pkgEntry = new HashMap<String, UrlEntry>();
            if (pkgEntry == null) {
                Slog.e(TAG, "addAdblockUrl():new HashMap<String, UrlEntry>() fail!");
                return false;
            }
            mAdblockEntries.put(pkgName, pkgEntry);
         }
         UrlEntry entry = pkgEntry.get(url);
         if (entry == null) {
            pkgEntry.put(url, new UrlEntry(0, false));
          } else {
            entry.deleted = false;
          }
       }
       return true;
    }

    WebView类postUrl处判断处理:

    /**
     * Loads the given URL.
     *
     * @param url the URL of the resource to load
     */
       public void loadUrl(String url) {
            checkThread();
            if (!isAddressable(url)) {
               return;
            }
           if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl=" + url);
           if(!isChromium && url.startsWith("file://")){
           Log.e("WebView.java", "loadurl setLocalSWFMode");
           mProvider.setLocalSWFMode();
       }
    
     /**
      * Returns true if the url is not included by adblock service
      */
     private boolean isAddressable(String url) {
         boolean addressable = true;
         AdblockManager adblockManager = AdblockManager.getInstance();
         if (adblockManager != null) {
           String adblockUrl =  adblockManager.containedAdblockUrl(ActivityThread.currentPackageName(), url);
         if (adblockUrl != null) {
             addressable = false;
             adblockManager.increaseNumberOfTimes(ActivityThread.currentPackageName(), adblockUrl);
          }
          }
          return addressable;
      }
    

    由于系统代码这部分的改动并非是我改的,更深细节处的理论就不清楚了。
    应用层的广告特征库为了可以持续更新,建议可以做成网络更新方式。
    据此,广告拦截功能实现就完成了,可能会有瑕疵,不过持续优化中。 有大神如果有更好的拦截实现跟策略,请您麻烦私信我,让我好好请教,非常感谢。

    展开全文
  • webview接入HttpDNS实践

    2018-05-30 15:42:56
    主要分享了GOT Hook webview中域名解析函数的方法。HttpDNS简介首先简单介绍下移动App接入HttpDNS后有什么好处,这里直接引用腾讯云文档中的说明: HttpDNS是通过将移动APP及桌面应用的默认域名解析方式,替换为通过...

    本文是对去年做的webview接入HttpDNS工作的一个总结,拖的时间有点久了。主要分享了GOT Hook webview中域名解析函数的方法。

    HttpDNS简介

    首先简单介绍下移动App接入HttpDNS后有什么好处,这里直接引用腾讯云文档中的说明:

    HttpDNS是通过将移动APP及桌面应用的默认域名解析方式,替换为通过Http协议进行域名解析,以规避由运营商Local DNS服务异常所导致的用户网络接入异常。减少用户跨网访问,降低用户解析域名时延。

    更详细的内容可以参考这篇文章:【鹅厂网事】全局精确流量调度新思路-HttpDNS服务详解

    移动端的实现原理

    域名的解析工作将在HttpDNS服务器上完成,客户端只要把待解析的域名作为参数发起一个HTTP请求,HttpDNS服务器就会把解析结果下发给客户端了。
    在客户端,默认的域名解析是系统的getaddrinfo()库函数实现的,默认的域名解析请求会走到LocalDNS。
    所以域名解析的工作必须要交给app应用层来实现。下面介绍几种实现方案。

    1、okhttp

    okhttp的实现是建立在socket之上的,并且实现了HTTP协议。对于客户端发起的http请求,okhttp首先会跟远端服务器建立socket连接,在此之前okhttp会根据http请求中url的domain做域名解析,默认的实现是java网络库提供的InetAddress.getAllByName

    如果项目中用的网络库是okhttp,所有的网络请求都是通过它完成的话就可以使用okhttp提供的DNS解析接口,实现自己的DNS resolver,代码如下:

    public class HttpDns implements Dns {
        @Override
        public List<InetAddress> lookup(String hostname) throws UnknownHostException {
            //DNSHelper完成DNS解析的具体工作,向HttpDNS服务器请求服务。
            String ip = DNSHelper.getIpByHost(hostname);
            List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
            return inetAddresses;
        }
    }

    2、native hook的方法

    通过Hook libc的getaddrinfo库函数,将函数指针指向app应用层实现的DNS解析函数地址。
    要深入了解linux native hook的技术的话,需要了解ELF文件格式和动态链接的相关知识,可参考ELF文件及android hook原理

    getaddrinfo是在libc.so中的定义的,其它库如libandroid_runtime.solibjavacore.so要使用这个函数的话,只能通过动态导入符号的形式,好在java网络库底层是就是通过这个方式实现的。

    android nativehook原理

    下面代码是arm架构的一种实现方案,具体实现参考Andrey Petrov的blog.

    #include "linker.h"  // get it from bionic
    
    unsigned elfhash(const char *_name); //hash函数
    //查找散列表。经典的链接法解决散列冲突
    
    static Elf32_Sym *soinfo_elf_lookup(soinfo *si, unsigned hash, const char *name)  
     {  
       Elf32_Sym *s;  
       Elf32_Sym *symtab = si->symtab;  
       const char *strtab = si->strtab;  
       unsigned n;  
       n = hash % si->nbucket;  
       for(n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]){  
         s = symtab + n;  
         if(strcmp(strtab + s->st_name, name)) continue;  
           return s;  
         }  
       return NULL;  
     }  
    
    
     //soname:动态库名称;
     //symbol:待hook的函数名;
     //newval:新函数地址
     int hook_call(char *soname, char *symbol, unsigned newval) {  
      soinfo *si = NULL;  
      Elf32_Rel *rel = NULL;  
      Elf32_Sym *s = NULL;   
      unsigned int sym_offset = 0;  
      //打开动态库,得到soinfo对象
      si = (soinfo*) dlopen(soname, 0);  
     //通过查找散列表找到symbol对应符号表的索引
      s = soinfo_elf_lookup(si, elfhash(symbol), symbol);  
      sym_offset = s - si->symtab;  
    
      rel = si->plt_rel;//指向plt表的起始位置  
      //遍历plt表
      for (int i = 0; i < si->plt_rel_count; i++, rel++) {  
       unsigned type = ELF32_R_TYPE(rel->r_info);  
       unsigned sym = ELF32_R_SYM(rel->r_info);  
       //加上动态库的基址,定位到该符号重定向元素的内存
       unsigned reloc = (unsigned)(rel->r_offset + si->base);  
       uint32_t page_size = 0;
       uint32_t entry_page_start = 0;
       unsigned oldval = 0;  
       if (sym_offset == sym) {  //是否是待hook的符号位置
        switch(type) {  
          case R_ARM_JUMP_SLOT:
             //修改内存页的属性为可读写  
             page_size = getpagesize();
             entry_page_start = reloc& (~(page_size - 1));
             int ret = mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_WRITE); 
    
             oldval = *(unsigned*) reloc;  
             *((unsigned*)reloc) = newval;  //成功替换这块内存的值为新函数的地址值 
             return 1;  
          default:  
             return 0;  
        }  
       }  
      }  
      return 0;  
     } 
    

    程序中调用mprotect的作用是: 修改一段指定内存区域的保护属性。以防万一,将这它改为可读写,因为后面就要对这块内存做写操作了。
    函数原型为:int mprotect(const void *start, size_t len, int prot);
    mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
    需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。

    用法:

    hook_call("libjavacore.so", "getaddrinfo", &my_getaddrinfo);  
    • 1.调用dlopen拿到so的句柄,得到soinfo,它包含了符号表、重定位表、plt表等信息。
    • 2.查找需要hook的函数的符号,得到它在符号表中的索引。
    • 3.遍历plt表,直到匹配第2步中找到的符号索引。
      如果是JUMP_SLOT类型(函数调用),替换为新的符号地址(函数指针)。
      如下图所示,my_code_func的函数地址替换了GOT表项中原来指向libc中的getaddrinfo函数地址,达到了hook的效果。

    跟进一步地,可以把设备上的libjavacore.so导出,用IDA Pro打开,观察getaddrinfo的引用关系,将有助于理解上面的代码。

    找到libjavacore.sogetaddrinfo导入符号的位置:

    定位到getaddrinfo在plt表中引用的位置:

    定位到getaddrinfo在GOT表中引用的位置:

    定位到在代码段中调用getaddrinfo的位置:

    通过分析得知,虽然getaddrinfolibc.so的导出函数,但是这种方法无法hook导出函数,没有一劳永逸的方法,只能hook导入函数,因为这种方案是通过修改GOT表项实现的,这是它的缺陷。

    3、webview

    webview作为H5的容器,在做网络请求的时候也需要做DNS域名解析,要对其接入HttpDNS的一般做法是通过拦截WebView的各类网络请求,截取URL请求的host,然后调用HttpDns解析该host,通过得到的ip组成新的URL来请求网络地址。
    不过这种方式只能处理资源,处理正常的http/https请求会存在问题。

    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 
        if (request.getMethod().equalsIgnoreCase("get")) { 
            String url = request.getUrl().toString(); 
            // HttpDns解析css文件的网络请求及图片请求 
            if (url.contains(".css") || url.endsWith(".png")) { 
            try { 
                URL oldUrl = new URL(url); 
                URLConnection connection = oldUrl.openConnection(); 
                // 获取HttpDns域名解析结果 
                String ips = MSDKDnsResolver.getInstance().getAddrByName(oldUrl.getHost()); 
                String newUrl = url.replaceFirst(oldUrl.getHost(), ip); 
                // 设置HTTP请求头Host域 
                connection = (HttpURLConnection) new URL(newUrl).openConnection(); 
                connection.setRequestProperty("Host", oldUrl.getHost()); 
                }
                return new WebResourceResponse("text/css", "UTF-8", connection.getInputStream()); 
            }
        }}

    必须要对webview的DNS域名解析函数进行拦截替换。
    webview的DNS域名解析函数具体实现是在chromiumn.so,不同版本的实现也不同,5.0版本的代码见host_resolver.h
    webview的DNS域名解析函数是否也跟java的网络库一样最终调用的libc.so动态库中getaddrinfo呢?
    通过源码注释得知确实如此。

    用Android Studio调试Framework层代码中也对其进行过断点调试。
    所以解决方法很简单,只需要hook libchromium_net.sogetaddrinfo导入符号即可。

    hook_call("libchromium_net.so", "getaddrinfo", &my_getaddrinfo);  

    机型问题

    在实践中我们发现,不同机型不同版本的android在实现DNS解析函数的导出符号是不同的,更糟糕的是调用DNS解析函数的动态库也不一定就是libjavacore.so
    我之前定位过Android5.0设备的DNS解析函数,发现它的名字改为android_getaddrinfofornet

    webview的so库位置也曾遇到过找不到的问题。

    解决方法是通过一个脚本,pull下测试设备上的所有so到主机上,然后用readelf工具查找so的导入符号,观察是否有getaddrinfo字样的导入符号。
    为此我写了一个脚本,方便自动化进行。运行如下命令即可

    $ python sofinder.py -e getaddrinfo


    在上面输出的第一行可以看到,android 5.0以上版本webview的so已经被放在system/app目录中了。
    需要写全so的路径:

    hook_call("/system/app/WebViewGoogle/lib/arm/libwebviewchromium.so", "getaddrinfo", &my_getaddrinfo);  

    参考

    【鹅厂网事】全局精确流量调度新思路-HttpDNS服务详解
    ELF文件及android hook原理
    Andrey Petrov’s blog

    我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2pfca6dje52c0

    展开全文
  • 《客厅TV-APP HttpDNS技术接入与实战》原创 2018-04-12 yujieliu 腾讯Bugly1 项目概况域名劫持大家并不陌生,从PC时代到移动互联时代,网络安全愈发重要,劫持方式更是层出不穷。现在到了智能客厅时代(意淫一下...


    《客厅TV-APP HttpDNS技术接入与实战》

    原创 2018-04-12 yujieliu 腾讯Bugly

    1 项目概况

    域名劫持大家并不陌生,从PC时代到移动互联时代,网络安全愈发重要,劫持方式更是层出不穷。现在到了智能客厅时代(意淫一下),如果说移动互联时代由于开放性和竞争性,大的厂商还是有良知的,比较注重口碑二字,但客厅由于其封闭性,无良厂商只手遮天,各类监控、各类弹窗广告、各类精简系统、各类系统级封杀等,导致客厅开发异常复杂,只有把主动权把握在自己手中,才能感觉到一丝丝安全。

    传统的域名解析是一个系统调用,解析的结果可以说控制在运营商手中,也可能控制在厂商手中(无良厂商改系统),现在我们要把域名解析的主动权收到自己的手中,于是HttpDNS技术应运而生。

    1.1 数据会说话

    我们把客厅接口数据分为两部分:一部分是客厅业务接口,一部分是客厅CDN图片接口。因为作为一个视频APP,图片量是极大的,图片的请求量要远超业务侧接口的请求量,请求量越大,接入HttpDNS技术的效果应该更好,话不多说看结论。

    1.1.1客厅业务接口数据分析

    图1展示了客厅业务接口返回码分布图,其中左图为未接入HttpDNS技术的接口返回码,右图为接入HttpDNS技术之后的接口返回码。我们重点关注错误码6和错误码7,错误码6是由于域名解析失败导致的接口错误返回码,错误码7是解析出的ip无法连接到主机导致的接口错误返回码,可以看出在接入了HttpDNS技术之后,由域名解析问题导致的错误(错误码6、7总和)降低了61%,效果非常明显。

    图1 客厅业务接口返回码分布图

    再来看一下接口耗时,图2展示了客厅业务接口耗时分布图,其中左图为未接入HttpDNS技术的接口耗时,右图为接入HttpDNS技术之后的接口耗时。可以看出在接入了HttpDNS技术之后,接口耗时降低了40%,效果非常明显。

    图2 客厅业务接口耗时分布图

    1.1.2 客厅CDN图片接口数据分析:

    图3展示了客厅CDN图片接口返回码分布图,其中左图为未接入HttpDNS技术的接口返回码,右图为接入HttpDNS技术之后的接口返回码。可以看出在接入了HttpDNS技术之后,由域名解析问题导致的错误(错误码6、7总和)降低了82%,比业务接口的接入效果更加明显。

    图3 客厅CDN图片接口返回码分布图

    再来看一下接口耗时,图4展示了客厅CDN图片接口耗时分布图,其中左图为未接入HttpDNS技术的接口耗时,右图为接入HttpDNS技术之后的接口耗时。可以看出在接入了HttpDNS技术之后,接口耗时降低了43%,比业务接口的接入效果更加明显。

    图4 客厅CDN图片接口耗时分布图

    1.2   什么是HttpDNS技术?

    首先看一下什么是域名劫持。图5简单示意了域名劫持流程:当用户向Local DNS去请求某个域名的真实ip时,运营商的Local DNS服务器回复了一个假网站或内容缓存服务器的ip,最终导致用户访问无法访问到真实ip,从而出现异常。

    图5 域名劫持流程图

    是不是非常简单明了!而HttpDNS技术正是为了解决域名劫持应运而生的。下面就来看一下HttpDNS技术的实现原理。

    图6 HttpDNS技术原理图

    图6展示了HttpDNS技术的实现原理,主要分两步:

    1.客户端向HttpDNS服务器发起请求(该请求为ip直连请求),获取与域名对应的一系列ip列表;

    2.客户端从ip列表中选取访问延迟最优的ip,直接用此ip代替域名发送请求,这样就避免了Local DNS域名解析这一步骤。

    总结HttpDNS技术的优势:

    1.根治域名解析异常:由于绕过了运营商的LocalDNS解析,客户端的请求通过ip直连,彻底解决了域名劫持问题;

    2.精准调度:HttpDNS能直接获取到用户ip,避免了DNS出口ip和业务出口ip不同网段问题;

    3.减少网络延迟:通过本地缓存ip,可以有效减少域名解析时间,降低用户网络请求的平均耗时;

    4.提升网络请求可控性和可靠性:我们采用的HttpDNS服务器依托腾讯庞大的ip地址库,利用腾讯公网交换平台的BGP Anycast网络,大大提高了网络请求的可靠性,同时是否走HttpDNS以及BGP IP的选择均由后台控制,增加了可控性。

    2 实施方案

    说起来容易做起来难!任何一种技术的尝鲜都不是一蹴而就的,我们也是从无数的坑里面爬出来,才最终打造了一套适合客厅TV的HttpDNS技术架构。曾几何时在第一版上线的时候就遇到了线上问题,也经历了暂停升级、通宵处理,历经多个版本的不断完善,才从带血的坑里爬出来。

    这儿分成三部分来介绍客厅TV-APPHttpDNS技术的接入过程:HttpDNS技术核心架构层、HttpDNS技术业务逻辑层和HttpDNS技术客户端容错处理。

    2.1 HttpDNS技术核心架构层

    无图无真相,图7展示了客厅HttpDNS技术核心实现流程图:

    1.客户端接收到域名请求,查询是否已有该域名历史解析结果缓存,如果已有解析结果缓存则转步骤2,否则转步骤3;

    2.检查该缓存是否过期,如果没有过期,则返回查询结果,域名解析成功,否则转步骤3;

    3.如果没有解析结果或缓存已过期,则向HttpDNS服务器(119.29.29.29)发起域名查询请求,如果请求成功则转步骤4,否则赚步骤5;

    4.域名查询请求成功,得到一组ip列表,通过ip优化选择最优ip并返回查询结果,同时更新解析结果缓存;

    5.域名查询请求失败,为了容错,必须再用Local DNS请求一遍,无论是否成功均返回,完成整个查询流程。

    图7 客厅HttpDNS技术核心实现流程图

    整个查询流程定下之后,开始设计各个客户端模块,图8展示了客厅HttpDNS技术模块结构图,包括查询模块、数据模块、IP优选模块、BGP-IP更新模块以及其他模块等。

    图8 客厅HttpDNS技术模块结构图

    1. 查询模块

      主要包括本地缓存查询和网络查询。查询本地是否有相应的域名缓存,如果有缓存且缓存未过期则直接返回IP;如果本地没有缓存或缓存过期,则从HttpDNS服务器查询IP,并更新域名-IP对应关系记录;如果向HttpDNS服务器查询IP失败则采用LocalDNS解析域名并返回IP,不做域名-IP对应关系更新。

    2. 数据模块

      主要包括两块数据:利用SharedPreferences缓存HttpDNS服务器的ip信息、优先级、标志位;记录每次请求的“域名-ipList”对应关系以及相关缓存数据。这儿建立的缓存非常重要,我们不可能每次都直接网络请求域名对应的ip列表,这样非常耗时。事实上,请求域名对应ip列表的接口返回数据会自带一个生存期(TTL),在该生存期内ip是有效的,可以直接访问http://119.29.29.29/d?dn=tv.aiseet.atianqi.com&ttl=1, 其中ttl=1代表返回带生存期字段,这样我们通过缓存“域名-ipList”对应数据,如果发现在生存期内再次请求该域名对应ip时,可以直接使用缓存,避免再次的网络请求。

    3. ip优选模块

      一个域名可能对应多个ip,提供ip优选是必要的。根据ipList中每个ip被选中次数,以及该ip连接耗时综合选择一个平均耗时最短的ip。图9示例了请求tv.aiseet.atianqi.com域名对应ipList的结果示例图。

    图9 域名请求示意图

    1. BGP-IP更新模块

      即HttpDNS服务器ip更新模块,如果某一天119.29.29.29这个ip不可用了,换成了另外一个ip,则我们的APP也需要支持ip更换。图10为BGP-IP更新流程图,客户端提供一个默认BGP IP: 119.29.29.29,并通过全局配置下发更新的BGP IP和是否走HttpDNS的标志位。

    图10 BGP-IP更新流程图

    1. 其他模块

      由于篇幅有限,很多地方没能详述,如果你对某一块感兴趣,可以联系作者。

    • 域名过滤功能:可以指定特定域名走HttpDNS;

    • 日志与数据上报功能:分析相关数据,确定域名解析的正确性和有效性;

    • 网络抖动监听:网络变化时需要刷新网络参数,清除内存缓存。

    简而言之,上面描述了,给我一个域名,还你一个ip这个过程。那如何给我一个域名呢?这也是一件很有趣的事,详见业务逻辑层分析。

    2.2 HttpDNS技术业务逻辑层

    一切抛弃业务谈技术的都是耍流氓。几乎所有的APP都涉及网络数据传输,这就需要有多个业务接口进行网络请求,如请求首页数据、请求列表数据、请求图片资源等,无论你是采用系统网络请求或是建有自己的网络库进行网络请求,想把域名直接替换为ip,无外乎以下方式:

    • 如果你的业务异常独立,都采用同一个域名,那你可以通过预埋ip的方式完成这一过程;

    • 如果你是自建网络库,所有的网络请求都由同一网络库发出,那么你可以在网络库中集中处理,通过提取接口中的域名,利用前面介绍的HttpDNS技术,把域名转换为ip,利用ip替换接口中的域名进行请求;

    • 如果你的业务比较分散,网络请求没有集中整理,那就很难统一给HttpDNS一个域名,于是就有了DNS HOOK技术。

    DNS HOOK技术通过拦截Android系统域名解析调用getaddrinfo请求,将其拦截到我们的HttpDNS方案之中,利用HttpDNS技术解析域名对应ip,再把解析出的ip作为返回值返回给getaddrinfo系统调用,从而完成域名解析过程。

    我们的客厅APP就采用了DNS HOOK技术,主要有以下原因:

    • 视频APP是一个庞大而复杂的APP,除了我们的主业务之外,我们还要接入播放器jar包、下载组件jar包、广告jar包、MTA数据上报jar包等等一堆外部jar包,每一个外部jar包都有自己的网络请求,请求方式也比较分散;

    • 客厅业务是要受牌照方管控的,意思就是你不能采用自己的.qq.com域名,必须采用牌照方的域名,目前我们与多个牌照方都有合作,域名都不一样,如”.gitv.tv”,  “.ottcn.com”, “.cibntv.net”, “.atianqi.com”,对于同一份APP而言,很难进行拆解处理;

    • 网络请求不一,最开始客厅APP有两套网络库,一套是java侧的采用Volley,一套是Native侧的采用CURL,逻辑分散。当然现在已经合一了,统一采用java侧请求。

    这儿再单独介绍下客厅业务为啥如此复杂:

    • 牌照管控:广电总局发布的181号文件,提出要对互联网电视进行管控:电视盒子、智能电视等产品所提供的内容,必须在CNTV、华数、上海文广(东方明珠)、南方传媒、湖南电视台、中国国际广播电台以及中央人民电台这7家国有广电系牌照商的集成播控平台上呈现,并接受上述机构的监管。我们客厅的产品就和南方传媒、CNTV、中国国际广播电台、中央人民电台4家牌照方建立了合作关系。既然要接受他们的监督,我们就无法直接使用自己的域名,必须经过他们的服务器进行中转,再通过我们的接入层接入到自己的后台之中。

    • 合作厂商:客厅这个业务和移动端不一样,移动端的大头在于应用分发,而客厅的大头却要靠软件预装实现。与咱们有合作关系的厂商超过了25家,包括主流的电视厂商,如乐视、康佳、创维、TCL等,主流的智能盒子提供商,例如京东、泰捷、微鲸、VST等。这些合作厂商都有自己的特殊需求,例如京东想把京东商城通过单独的频道展现,接口数据通过他们的后台下发,这都对我们业务的多样性和包容性提出了更高的挑战。

    于是Android侧发起的网络请求经过系统调用getaddrinfo,被拦截到HttpDNS技术方案之中,通过2.1介绍的HttpDNS技术核心架构层解析出域名对应的ip并返回给系统调用,从而完成整个HttpDNS技术的接入。

    上面介绍了Android业务侧接口的域名解析实现部分,你可能会产生疑问,那WebView和CURL又该何去何从呢?

    图11 三种域名解析过程对比图

    图11展示了Android原生域名解析、WebWiew和CURL域名解析过程,从解析过程可以看出,Android原生域名解析和WebWiew域名解析,只是调用到的系统库不同,一个是Libjavacore.so库,一个是libchromium_net.so库,都可以通过DNS HOOK技术实现解析过程。

    CURL方式比较尴尬,无法拦截到,可能水平有限没找到系统库。但方法是思考出来的,对于Native层的网络请求CURL,我们通过jni调用java侧域名解析方法InetAddress,该方法会调用到Android原生域名解析过程,通过DNS HOOK技术,采用自建的HttpDNS技术方案,把解析到的ip结果返回到Native层,再通过域名替换,从而完成CURL接入HttpDNS技术的方案。这种方案的优点是只用维护一份java侧的HttpDNS解析,不用在Native层又另外实现一套解析方案。

    至此,我们解答了”如何给我一个域名?“这个问题,总结下这一实现方案的优点:

    • Java层利用DNS HOOK技术拦截域名解析请求,Http报文结构和不使用HttpDNS技术一样,对后台完全透明。透明这个词很关键,因为如果是通过域名替换这一方式,报文结构有所差别,可能造成部分请求失败。

    • 不仅无缝完成了主业务的HttpDNS技术接入,相关依赖的业务,如播放器、广告等也都完成了无缝接入HttpDNS技术,只要是Android侧的请求,无论是自建网络库,还是系统网络调用,均直接支持。

    • CURL请求的HttpDNS技术接入也开辟了新的方式,Native层的网络请求通过jni调用Android原生域名解析来实现HttpDNS技术接入。

    2.3 HttpDNS技术客户端容错处理

    网络请求是APP的根本,一旦出现网络不通,整个APP就像失去了联系一样,无声的消失在开发人员的世界之中。而域名解析又是网络请求的一个根本所在,一旦域名解析出现问题,就会导致网络请求出现错误,从而失去一个用户。为此,在接入HttpDNS技术的过程中我们做了多重容错处理。

    1. 全局配置是否走HttpDNS技术

    这是整个HttpDNS技术是否接入的总开关,以防止出现特殊情况时我们可以整体关闭该技术的接入,走入系统处理流程之中。特殊情况如我们使用的腾讯公网BGP-IP出现不可用又没有备用IP时,这时候HttpDNS的解析一定是出错的,虽然技术本身可以保证最后一定走入到系统处理流程之中,但却浪费了一些解析的时间成本,故直接关闭整个HttpDNS技术的接入。

    2.全局配置BGP-IP更新

    一旦BGP-IP(HttpDNS服务器ip)不可用,HttpDNS的解析一定是出错的,这时候就需要有一个备用的BGP-IP来进行替换,从而走入正常流程之中。

    3.预埋ip

    通过预埋一些重要域名对应的ip列表,当解析出现错误的时候可以直接查找该域名是否有对应的预埋ip,如果查找到则采用预埋ip进行网络请求。预埋ip作为兜底处理,可以提高域名解析的正确率,但有可能出现该ip不可用或延时很长的情况。

    4.域名自动过滤

    我们的APP有需要外部依赖jar包,这些jar包中可能包含非常多的域名,然而有些域名我们可能不希望其也走入HttpDNS解析之中,于是提供了一个域名过滤的功能,只处理我们明确需要处理的域名解析。

    5.特定域名解析失败一段时间内自动屏蔽功能

    如果HttpDNS服务器出现对某个特定域名一直解析出错的情况,我们会缓存该域名的出错次数,一旦该域名解析出错三次,则禁止其在1个小时内再通过HttpDNS服务器进行解析,转而使用系统原生解析流程,从而消减解析耗时。

    原文地址:

    https://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653579768&idx=1&sn=6a3670560f48642dcb5c3a6c4cead895&chksm=84b3bbffb3c432e93da62750711eba2f26b3e71fdf0549f6d30445504d17ea1f8bf4bda06c95&mpshare=1&scene=1&srcid=04122mYF0xxTvdDgdkh6erDL&pass_ticket=dIUf1tGMuhPIt8ap8QByjgEBxT%2Fnb%2Bp4J%2BU9drEL2ZNOFYpkylNxjtp7d3s0W70Z#rd


    展开全文
  • HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。 HttpDns最佳...

    什么是HttpDns,为什么要接入HttpDns

    HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。

    HttpDns最佳实践?

    阿里云文档中,提到的HttpDns的最佳实践的几个场景,虽然在一定程度上能解决我们的问题,但是存在一定的缺陷、

    • webview,能用httpdns的请求方式有限
    • 无法做到全局替换

    那么,我们是否能够寻找一种全局替换的方案呢?或者解决的场景能够更多一点的方案呢?

    从现有的资料中寻找方案

    经过一番搜索,找到了 Android弟的这边文章 一种全局拦截并监控 DNS 的方式 以及这边文章如何为Android应用提供全局的HttpDNS服务。 在第一篇文章提到的方案中,缺陷是非常明显的

    1. 只支持7.0以上版本
    2. 不支持webview,虽然作者说支持webview,实际上是不支持的,为什么不支持,后面会给出解释

    而在第一篇文章中,提到的方案也是这样。在7.0之下 hook coonnect这个,没有相应的代码,对我我等菜鸡来说,难度太大。

    因此,现有的方案中,不合适。

    从源码中寻求突破方法-动态代理Os接口

    我们先来看下Dns解析的过程是什么样的,以API 25的SDK为例,发现下面的代码片段。

                StructAddrinfo hints = new StructAddrinfo();
                hints.ai_flags = AI_ADDRCONFIG;
                hints.ai_family = AF_UNSPEC;
                // If we don't specify a socket type, every address will appear twice, once
                // for SOCK_STREAM and one for SOCK_DGRAM. Since we do not return the family
                // anyway, just pick one.
                hints.ai_socktype = SOCK_STREAM;
                InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
    复制代码

    那么,我们继续跟踪源代码。看看Libcore.os是个什么东西。

    public final class Libcore {
        private Libcore() { }
        public static Os os = new BlockGuardOs(new Posix());
    }
    复制代码

    而Os是一个接口,代码片段如下。

    
    public interface Os {
        public FileDescriptor accept(FileDescriptor fd, SocketAddress peerAddress) throws ErrnoException, SocketException;
        public boolean access(String path, int mode) throws ErrnoException;
        public InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;
    复制代码

    Posix的代码片段如下

    public final class Posix implements Os {
        Posix() { }
        public native FileDescriptor accept(FileDescriptor fd, SocketAddress peerAddress) throws ErrnoException, SocketException;
        public native boolean access(String path, int mode) throws ErrnoException;
        public native InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;
    复制代码

    Libcore.os.android_getaddrinfo实际上是调用的Posix的android_getaddrinfo这个native方法。

    到此为止,我们可以明确,通过动态代理Os这个接口,并且替换Libcore.os这个字段,我们在调用android_getaddrinfo这个方法中,插入HttpDns解析,那么,就可以解决部分的问题(除webview以外的所有场景)。当然,我们要考虑适配的问题,在4.4版本上,android_getaddrinfo这个方法是getaddrinfo。因此,我们写下如下的代码。

    
        public static void globalReplaceByHookOs() {
            if (mHooked) {
                return;
            }
            mHooked = true;
            try {
                Class libcoreClz = Class.forName("libcore.io.Libcore");
                Field osField = libcoreClz.getField("os");
                Object origin = osField.get(null);
                Object proxy = Proxy.newProxyInstance(libcoreClz.getClassLoader(),
                        new Class[]{Class.forName("libcore.io.Os")},
                        new OsInvokeHandler(origin));
                osField.set(null, proxy);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e("xhook", "globalReplaceByHookOs: " + e.getMessage());
            }
        }
    
    
    public class OsInvokeHandler implements InvocationHandler {
    
        private Object mOrigin;
        private Field mAiFlagsField;
    
        OsInvokeHandler(Object os) {
            mOrigin = os;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("android_getaddrinfo")
                    || method.getName().equals("getaddrinfo")) {
                try {
                    if (mAiFlagsField == null) {
                        mAiFlagsField = args[1].getClass().getDeclaredField("ai_flags");
                        mAiFlagsField.setAccessible(true);
                    }
                }catch (Exception e) {
                    e.printStackTrace();
                    Log.e("xhook", "ai_flag get error ");
                }
    
                if (args[0] instanceof String && mAiFlagsField != null
                        && ((int) mAiFlagsField.get(args[1]) != OsConstants.AI_NUMERICHOST)) {
                    //这里需要注意的是,当host为ip的时候,不进行拦截。
                    String host = (String) args[0];
                    String ip = HttpDnsProvider.getHttpDnsService().getIpByHostAsync(host);
                    Log.e("xhook", "invoke: success -> host:" + host + "; ip :" + ip);
                    return InetAddress.getAllByName(ip);
                }
            }
            try {
                return method.invoke(mOrigin, args);
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
        }
    }
    
    复制代码

    但是,这种方法也是有缺陷的。

    1. 9.0 非公开API限制
    2. Webview你还没给我解决(别急,WebView放在最后,狗头保命)

    进一步优化-下沉到so hook

    既然他调用的是native的方法,那么,我们可不可以通过hook 这个native方法去达到目的呢?当然可以。我们先看下Posix的native方法对应的代码。代码在Libcore_io_Posix.cpp中,如下。

    注意看,这个实际上是调用的android_getaddrinfofornet这个方法,实际上这个方法的实现是在libc中的,但是,我们现在暂时略过inline hook这个方法,一步一步来。那么。我们能如何hook到这个方法呢?咦,好像爱奇艺最近开源了一个hook方案,试一下?捎带说一下,这些代码最后是编译成libjavacore.so的。说试就试,动手写下如下代码。PS:忽略我垃圾的c++ style

    
    static int new_android_getaddrinfofornet(const char *hostname, const char *servname,
                                             const struct addrinfo *hints, unsigned netid,
                                             unsigned mark, struct addrinfo **res) {
        LOGE("hahahha,wo hook dao l ->android_getaddrinfofornet ");
        LOGE("下面是hostname");
        LOGE(hostname, "");
        if (hints->ai_flags == AI_NUMERICHOST) {
            if (fp_android_getaddrinfofornet) {
                return fp_android_getaddrinfofornet(hostname, servname, hints, netid, mark, res);
            }
        } else {
            const char *ip = getIpByHttpDns(hostname);
            if (ip != NULL) {
                LOGE("httpdns 解析成功,直接走IP");
                LOGE("下面是ip");
                LOGE(ip, "");
                //这里就比较神奇了,传的是IP,但是ai_flags 不是ip,但是还正常,查看源码的时候,没发现在这个代码里面判断
                // ai_flags 不numberhost还是其他
                return fp_android_getaddrinfofornet(ip, servname, hints, netid, mark, res);
            } else {
                return fp_android_getaddrinfofornet(hostname, servname, hints, netid, mark, res);
            }
    
        }
    
        return 0;
    }
    
    extern "C" int hook_android_getaddrinfofornet() {
        if (fp_android_getaddrinfofornet) {
            return 0;
        }
        //libjavacore.so 这里可以换成
        int result = xhook_register(".*\\libjavacore.so$", "android_getaddrinfofornet",
                                    (void *) new_android_getaddrinfofornet,
                                    reinterpret_cast<void **>(&fp_android_getaddrinfofornet));
        xhook_refresh(1);
    #if DEBUG
        xhook_enable_sigsegv_protection(0);
        xhook_enable_debug(1);
        LOGE("built type debug");
    #elif RELEASE
        xhook_enable_sigsegv_protection(1);
        xhook_enable_debug(0);
        LOGE("built type release");
    #endif
        return result;
    }
    
    复制代码

    上面省略了一点代码(完整的代码链接放在末尾)。运行,一般场景,一点问题没有,舒服。那么,用Webview试下。结果,出问题了,没走。。好吧,我们把".*\libjavacore.so"换成 ".*\\*.so",一股脑都替换,我就不信了。再试。还是不行。。。。Webview成精了。这个方案还是没解决Webview的问题,好气!

    优点:

    1. 兼容性强,兼容4.4-9.0

    缺点:

    1. 还是不支持Webview

    寻找Webview的解决方案

    一番搜索之后,发现如下文章。webview接入HttpDNS实践 但是,结果啪啪打脸,没关系,总算给了我们点思路不是么?文中作者说我们hook掉libwebviewchromium.so 就行,ok,按照他的思路,我们去/system/app/WebViewGoogle/lib/arm/libwebviewchromium.so这个路径下看看有没有这个so(这里说明一下,我是小米6 8.0),看下结果。

    没有啊!!

    我把这个apk拉出来,用jadx反编译,看了看。发现这个库是通过System.loadLibrary去加载的

    场面一度十分尴尬。现在,我们查看下进程的maps信息。

    在关于webview的这几个so里面,都没有找到android_getinfofornet。没得办法,我们只能去看下libwebviewchromium。

    哦?也是用的libc的。爱奇艺的xHook,是PLT/GOT表hook方案,而这个so,我们又加载不到我们的进程来,没办法,只能inline hook libc.so了。

    inline hook,webview也可以了。

    我们先看下相关的代码。

    
    __BIONIC_WEAK_FOR_NATIVE_BRIDGE
    int
    getaddrinfo(const char *hostname, const char *servname,
        const struct addrinfo *hints, struct addrinfo **res)
    {
    	return android_getaddrinfofornet(hostname, servname, hints, NETID_UNSET, MARK_UNSET, res);
    }
    __BIONIC_WEAK_FOR_NATIVE_BRIDGE
    复制代码

    在上面的过程中,我们已经hook 掉了android_getaddrinfofornet,因此,我们只要hook,getaddrinfo,让这个方法调用我们自己的掉了android_getaddrinfofornet方法就可以解决了,美滋滋。

    我们现在的问题变成了,哪个inline hook的方案稳定。这个事情很恐怖,因为很多inline hook的相对稳定的方案是不开源的,我这里呢?用的是Lody的AndHook。所有,最后的代码如下。

    static int my_getaddrinfo(const char *__node, const char *__service, const struct addrinfo *__hints,
                              struct addrinfo **__result) {
    
        // 这里有用xHook的时候,把所有的的android_getaddrinfofornet方法都替换为new_android_getaddrinfofornet,
        // 因此我们这里直接调用 new_android_getaddrinfofornet就行
        if (fp_android_getaddrinfofornet != NULL) {
            return new_android_getaddrinfofornet(__node, __service, __hints, NETID_UNSET, MARK_UNSET,
                                                 __result);
        } else if (fp_android_getaddrinfoforiface != NULL) {
            return new_android_getaddrinfoforiface(__node, __service, __hints, NULL, 0,
                                                   __result);
        }
        return EAI_FAIL;
    }
    
    static int JNICALL hooj_libc_getaddrinfo(JNIEnv *, jobject) {
        static bool hooked = false;
        int result = -1;
    
        AKLog("starting native hook...");
        if (!hooked) {
            AKHook(getaddrinfo);
    
            // typical use case
            const void *libc = AKGetImageByName("libc.so");
            if (libc != NULL) {
                AKLog("base address of libc.so is %p", AKGetBaseAddress(libc));
    
                void *p = AKFindSymbol(libc, "getaddrinfo");
                if (p != NULL) {
                    AKHookFunction(p,                                        // hooked function
                                   reinterpret_cast<void *>(my_getaddrinfo),   // our function
                                   reinterpret_cast<void **>(&sys_getaddrinfo) // backup function pointer
                    );
                    AKLog("hook getaddrinfo success");
                    result = 0;
                }
                AKCloseImage(libc);
            } //if
    
            hooked = true;
        } //if
    
        return result;
    }
    
    复制代码

    编译运行、测试,ok。一切顺利。

    总结

    到此,对HttpDns全局替换的研究就告一段落了。我们最终实现的方案还是不错的。

    1. 支持4.4以上版本
    2. 支持Webview

    当然,缺点也是相当比较明显的。

    1. 依赖inline hook,inline hook的方案是相对比较复杂、兼容性也比较差的,不敢保证Lody大神的AndHook绝对稳定可靠
    2. 鬼知道国内的厂商会不会随便修改函数名、so名

    这里是全套代码,喜欢的给个star

    展开全文
  • Android DNS Resolver分析

    2020-06-24 19:59:42
    1111 /** 1112 * Given the name of a host, returns an array of its IP addresses, 1113 * based on the configured name service on the system. 1114...
  • 为什么Chrome比其他浏览器快? Google Chrome的历史和指导性原则 Google Chrome最初是2008年下半年作为Windows平台上的一个beta版本发布的。Google还将自己编写的Chrome在BSD许可下进行了开源——称为Chromium。...
  • 为什么Chrome比其他浏览器快? Google Chrome的历史和指导性原则 Google Chrome最初是2008年下半年作为Windows平台上的一个beta版本发布的。Google还将自己编写的Chrome在BSD许可下进行了开源——称为Chromium。...
1
收藏数 9
精华内容 3
关键字:

拦截android系统域名解析调用getaddrinfo