基于Android的http&https中间人攻击
本文所说的中间人攻击是在同一台Android手机上,通过中间人方式获取其他App的网络访问,支持http&https.
原理:首先通过本地VPN拿到其他APP的网络请求,再做进一步处理。https解密则需要导入自己的CA,绕过证书验证。
遇到的几个技术点:
1.通过VPN,拿到的是一个个的IP层数据包,比如sync包,fin包,psh包等,而http/https等是数据流,需要做好状态维护
2.https数据解密需要为每个访问动态生成证书
3.动态生成证书需要处理SNI(Server Name Identification,同一个ip对应多个域名)
针对第1点,首先看Vpn的工作原理:
参考:https://www.ibm.com/developerworks/cn/linux/l-tuntap/
简单的说就是会创建一个工作在网络层的虚拟网卡叫做TUN,然后其他APP的网卡id(Android中socket的网卡变量叫netId)都会设置为tun.
所以当其他APP往外发送数据或者读取数据时,都是通过TUN。
这样我们的VPN APP只需要从TUN这里读取数据便可以获取到其他APP往外发送的数据。
当然我们的VPN APP读到了这些数据后,还要做的事情就是代替他们去和他们的目的服务器交互,发送数据,读取回复等....
读到回复后,再将这些回复数据写入TUN,这样那些APP才能通过读取TUN接收到自己的回复。
这点有很多开源的参考,不是重点。
关于第二点,解密https数据时,首先需要生成一个CA,利用OPEN SSL命令:
参考这个:http://www.cnblogs.com/syuee/p/6482687.html
开发过程中还会用到一些其他命令,比如查看证书内容:
openssl x509 -in demoCA/cacert.pem -noout -text
查看某个网站的证书:
openssl s_client -connect 111.161.121.28:443
PEM格式转换为DER格式:
openssl x509 -outform der -in CARoot1024.crt -out ca.der
DER格式转换为PEM格式:
openssl x509 -inform der -in certificate.cer -out certificate.pem
证书导入后,接下来就是数据解密了。针对HTTP,用了一个socket队列处理,HTTPS的则需要创建一个Server,前面的socket接收到https数据后,和这个server进行tcp连接,tcp握手完成后进行https握手。在https握手时,我们就需要根据请求的地址去拿到真正的证书,再将这个证书的CA设置为我们导入的CA。
动态生成证书代码:
X509 *create_fake_certificate(struct mg_connection *conn, SSL *ssl_to_server, EVP_PKEY *key) {
X509 * server_x509 = SSL_get_peer_certificate(ssl_to_server);
if(server_x509 == NULL){
return NULL;
}
X509 * ca = SSL_CTX_get0_certificate(conn->ctx->ssl_ctx);
if(ca == NULL){
return NULL;
}
X509 * mn = X509_new();
if(mn == NULL){
return NULL;
}
X509_set_notBefore(mn, X509_get_notBefore(server_x509));
X509_set_notAfter(mn, X509_get_notAfter(server_x509));
X509_set_issuer_name(mn, X509_get_subject_name(ca));
X509_set_subject_name(mn, X509_get_subject_name(server_x509));
X509_set_serialNumber(mn, X509_get_serialNumber(server_x509));
X509_set_version(mn, 2);
int extcount = X509_get_ext_count(server_x509);
int i;
int addIndex = 0;
for (i = 0; i < extcount; ++i) {
const char *extstr;
X509_EXTENSION * ext;
ext = X509_get_ext(server_x509, i);
extstr = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));
if (strstr(extstr, "authorityKeyIdentifier")) {
continue;
}
X509_add_ext(mn, ext, addIndex);
addIndex++;
}
X509_set_pubkey(mn, key);
X509_sign(mn, key, EVP_sha256());
return mn;
}
证书生成好后,接下来需要设置ssl参数以及证书链:
if (SSL_CTX_use_PrivateKey(sslServerCtxUsed, evpKey) != 1) {
return NULL;
}
if (SSL_CTX_use_certificate(sslServerCtxUsed, fake_x509) != 1) {
return NULL;
}
if (SSL_CTX_check_private_key(sslServerCtxUsed) != 1) {
return NULL;
}
X509 * x1 = SSL_CTX_get0_certificate(conn->ctx->ssl_ctx);
if(x1 == NULL){
log_android_yassl("get_ssl_to_app2...x1 is null...");
}
if (!SSL_CTX_add1_chain_cert(sslServerCtxUsed, x1)) {
return NULL;
}
这里需要注意的是SSL_read & SSL_write 需要读满一个record才能返回,可能需要重试几次,通过如下设置让它和普通IO没有区别:
int mode = SSL_CTX_get_mode(sslServerCtxUsed);
mode |= SSL_MODE_AUTO_RETRY;
SSL_CTX_set_mode(sslServerCtxUsed, mode);
到这一步,就可以对HTTPS内容解密了,但是还有个问题,就是对同一个IP对应多个域名的情况还不能处理。
也就是第三点所说的SNI。那如何解决呢? 分为2步。
首先在我们的Server端,创建SSL Ctx时,调用如下函数设置SNI:
SSL_set_tlsext_host_name(ssl, sni);
那Server端的sni从哪里获得? 这个值其实可以在ssl握手的第一步,也就是client hello中获取,不过open ssl似乎没有提供相关的方法,只有自己解析获取。
ssl握手协议格式参考:http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session
|
|
|
Record Layer | Handshake Layer
| |
| | ...more messages
+----+----+----+----+----+----+----+----+----+------ - - - -+--
| 22 | | | | | | | | | |
|0x16| | | | | | | | |message |
+----+----+----+----+----+----+----+----+----+------ - - - -+--
/ / | \ \----\-----\ |
/ / | \ \
type: 22 / | \ handshake message length
/ type
/
length: arbitrary (up to 16k)
Handshake Type Values dec hex
-------------------------------------
HELLO_REQUEST 0 0x00
CLIENT_HELLO 1 0x01
SERVER_HELLO 2 0x02
CERTIFICATE 11 0x0b
SERVER_KEY_EXCHANGE 12 0x0c
CERTIFICATE_REQUEST 13 0x0d
SERVER_DONE 14 0x0e
CERTIFICATE_VERIFY 15 0x0f
CLIENT_KEY_EXCHANGE 16 0x10
FINISHED 20 0x14
解析sni代码如下:
#include <stdio.h>
#include <stdlib.h> /* malloc() */
#include <string.h> /* strncpy() */
#include <sys/socket.h>
#include "clienthello.h"
#include "common.h"
#define SERVER_NAME_LEN 256
#define TLS_HEADER_LEN 5
#define TLS_HANDSHAKE_CONTENT_TYPE 0x16
#define TLS_HANDSHAKE_TYPE_CLIENT_HELLO 0x01
#ifndef MIN
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#endif
static const char tls_alert[] = {
0x15, /* TLS Alert */
0x03, 0x01, /* TLS version */
0x00, 0x02, /* Payload length */
0x02, 0x28, /* Fatal, handshake failure */
};
static int parse_extensions(const char *, size_t, char *);
static int parse_server_name_extension(const char *, size_t, char *);
static const struct Protocol tls_protocol_st = {
.name = "tls",
.default_port = 443,
.parse_packet = &parse_tls_header,
.abort_message = tls_alert,
.abort_message_len = sizeof(tls_alert)
};
const struct Protocol *const tls_protocol = &tls_protocol_st;
/* Parse a TLS packet for the Server Name Indication extension in the client
* hello handshake, returning the first servername found (pointer to static
* array)
*
* Returns:
* >=0 - length of the hostname and updates *hostname
* caller is responsible for freeing *hostname
* -1 - Incomplete request
* -2 - No Host header included in this request
* -3 - Invalid hostname pointer
* -4 - malloc failure
* < -4 - Invalid TLS client hello
*/
int parse_tls_header(const char *data, size_t data_len, char *hostname) {
char tls_content_type;
char tls_version_major;
char tls_version_minor;
size_t pos = TLS_HEADER_LEN;
size_t len;
if (hostname == NULL)
return -3;
/* Check that our TCP payload is at least large enough for a TLS header */
if (data_len < TLS_HEADER_LEN)
return -1;
/* SSL 2.0 compatible Client Hello
*
* High bit of first byte (length) and content type is Client Hello
*
* See RFC5246 Appendix E.2
*/
if (data[0] & 0x80 && data[2] == 1) {
log_android_ssl("client hello Received SSL 2.0 Client Hello which can not support SNI.");
return -2;
}
tls_content_type = data[0];
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
log_android_ssl("client hello Request did not begin with TLS handshake.");
return -5;
}
tls_version_major = data[1];
tls_version_minor = data[2];
if (tls_version_major < 3) {
log_android_ssl("client hello Received SSL %d.%d handshake which can not support SNI.",
tls_version_major, tls_version_minor);
return -2;
}
/* TLS record length */
len = ((unsigned char)data[3] << 8) +
(unsigned char)data[4] + TLS_HEADER_LEN;
data_len = MIN(data_len, len);
/* Check we received entire TLS record length */
if (data_len < len)
return -1;
/*
* Handshake
*/
if (pos + 1 > data_len) {
return -5;
}
if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
//log_android_ssl("client hello Not a client hello");
return -5;
}
/* Skip past fixed length records:
1 Handshake Type
3 Length
2 Version (again)
32 Random
to Session ID Length
*/
pos += 38;
/* Session ID */
if (pos + 1 > data_len)
return -5;
len = (unsigned char)data[pos];
pos += 1 + len;
/* Cipher Suites */
if (pos + 2 > data_len)
return -5;
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
pos += 2 + len;
/* Compression Methods */
if (pos + 1 > data_len)
return -5;
len = (unsigned char)data[pos];
pos += 1 + len;
if (pos == data_len && tls_version_major == 3 && tls_version_minor == 0) {
// log_android_ssl("client hello Received SSL 3.0 handshake without extensions");
return -2;
}
/* Extensions */
if (pos + 2 > data_len)
return -5;
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
pos += 2;
if (pos + len > data_len)
return -5;
return parse_extensions(data + pos, len, hostname);
}
static int
parse_extensions(const char *data, size_t data_len, char *hostname) {
// log_android_ssl("client hello parse_extensions...");
size_t pos = 0;
size_t len;
/* Parse each 4 bytes for the extension header */
while (pos + 4 <= data_len) {
/* Extension Length */
len = ((unsigned char)data[pos + 2] << 8) +
(unsigned char)data[pos + 3];
/* Check if it's a server name extension */
if (data[pos] == 0x00 && data[pos + 1] == 0x00) {
// log_android_ssl("client hello found server name:%s",data + pos + 4);
/* There can be only one extension of each type, so we break
our state and move p to beinnging of the extension here */
if (pos + 4 + len > data_len)
return -5;
return parse_server_name_extension(data + pos + 4, len, hostname);
}
pos += 4 + len; /* Advance to the next extension header */
}
/* Check we ended where we expected to */
if (pos != data_len)
return -5;
return -2;
}
static int
parse_server_name_extension(const char *data, size_t data_len,
char *hostname) {
size_t pos = 2; /* skip server name list length */
size_t len;
while (pos + 3 < data_len) {
len = ((unsigned char)data[pos + 1] << 8) +
(unsigned char)data[pos + 2];
if (pos + 3 + len > data_len)
return -5;
switch (data[pos]) { /* name type */
case 0x00: /* host_name */
// log_android_ssl("client hello found host name:%s",data + pos + 3);
strncpy(hostname, data + pos + 3, len);
hostname[len] = '\0';
return len;
default:
log_android_ssl("client hello Unknown server name extension name type: %d",
data[pos]);
}
pos += 3 + len;
}
/* Check we ended where we expected to */
if (pos != data_len)
return -5;
return -2;
}
头文件:
//
// Created by yanchen on 17-9-24.
//
#ifndef NQMDM_CLIENTHELLO_H
#define NQMDM_CLIENTHELLO_H
#include <inttypes.h>
struct Protocol {
const char *const name;
const uint16_t default_port;
int (*const parse_packet)(const char*, size_t, char **);
const char *const abort_message;
const size_t abort_message_len;
};
const struct Protocol *const tls_protocol;
int parse_tls_header(const char *data, size_t data_len, char *hostname);
#endif //NQMDM_CLIENTHELLO_H
调用方式:
if(dataP[0] == 0x16 && dataP[1] == 0x03 && dataP[5] == 0x01){
char hostName[1024] = {0};
struct Handshake *hs = (struct Handshake *) dataP;
int result = parse_tls_header(dataP,len,hostName);
到此基本上就解决完开发中遇到的几个主要问题,可以对https进行审计等操作。
上一篇: 中间人攻击小结
下一篇: 艾叶可防疫 枸杞滋肾润肺
推荐阅读
-
Android编程基于距离传感器控制手机屏幕熄灭的方法详解
-
基于android startActivityForResult的学习心得总结
-
Android基于ImageView绘制的开关按钮效果示例
-
Android开发基于Drawable实现圆角矩形的方法
-
基于android背景选择器selector的用法汇总
-
基于android样式与主题(style&theme)的详解
-
Android编程之电池电量信息更新的方法(基于BatteryService实现)
-
Android开发之基于DialogFragment创建对话框的方法示例
-
Android下基于Iptables的一种app网络访问控制方案(一)
-
基于Android 错误信息捕获发送至服务器的详解