@lniwn
2017-09-08T20:43:04.000000Z
字数 5049
阅读 1083
Windows
DNS
关于DNS劫持的概念,网上很多介绍,这里就不细说了。最近正好在做这方面的预研,把过程记录下来。
DNS查询协议走的53端口,默认是UDP协议,可以自己按照MSDN描述来构造协议包,而Windows系统已经提供了相关的API,用于进行DNS查询。
std::pair<std::wstring, std::string> CDNSHelper::QueryIpAndCanonname(const wchar_t* cname)
{
std::pair<std::wstring, std::string> result;
std::wstring host_name = cname;
PDNS_RECORD pDnsRecord = NULL;
DNS_STATUS dnsStatus = ::DnsQuery(
host_name.c_str(),
DNS_TYPE_A,
DNS_QUERY_NO_HOSTS_FILE|DNS_QUERY_BYPASS_CACHE|DNS_QUERY_TREAT_AS_FQDN,
pSrvList_,
&pDnsRecord,
NULL);
//if (DNS_ERROR_RCODE_NAME_ERROR == dnsStatus)
//{
// // #TODO
// // DNS name does not exist.
//}
if (ERROR_SUCCESS != dnsStatus)
{
return result;
}
bool needBreak = false;
for (PDNS_RECORD cursor = pDnsRecord; cursor != NULL && !needBreak; cursor = cursor->pNext)
{
if (cursor->Flags.S.Section != DnsSectionAnswer)
{
continue;
}
if (::DnsNameCompare(cursor->pName, host_name.c_str()))
{
switch(cursor->wType)
{
case DNS_TYPE_CNAME:
{
host_name = cursor->Data.CNAME.pNameHost;
}
break;
case DNS_TYPE_A:
{
IN_ADDR ipaddr;
ipaddr.S_un.S_addr = cursor->Data.A.IpAddress;
result = std::make_pair(host_name, ::inet_ntoa(ipaddr));
needBreak = true;
}
break;
default:
break;
}
}
}
::DnsRecordListFree(pDnsRecord, DnsFreeRecordListDeep);
return result;
}
DnsQuery的第1个参数为要查询的目标域名;第2个参数为查询类型,这里我们要查询A记录,所以使用了DNS_TYPE_A
;第3个参数为查询选项,DNS_QUERY_NO_HOSTS_FILE
标识是否使用本地host文件,在实际产品中,可以使用检测条件(比如环境变量)来决定是否使用本地host文件,DNS_QUERY_BYPASS_CACHE
标识是否使用本地缓存,因为windows系统会缓存查询过的域名,可以通过ipconfig /displaydns
来查看所有已经缓存的记录;第4个参数为PIP4_ARRAY
数组,可以通过下面方式初始化
bool CDNSHelper::Init(const char** dns_array, unsigned int length)
{
if (length < 1)
{
return false;
}
pSrvList_ = static_cast<PIP4_ARRAY>(::LocalAlloc(LPTR, sizeof(IP4_ARRAY) + sizeof(IP4_ADDRESS) * (length-1)));
pSrvList_->AddrCount = length;
for (unsigned int index = 0; index < length; ++index)
{
pSrvList_->AddrArray[index] = ::inet_addr(dns_array[index]);
}
HINSTANCE hNtdll = ::LoadLibrary(L"Ntdll.dll");
if (!hNtdll)
{
return false;
}
return true;
}
AddrArray
字段为C语言常见的可变数组用法,不明白的可以自行Google。
如果查询的目标域名有CDN服务器,那么DnsQuery
查询的结果就是一个链表,与实际CDN跳转顺序一致,所以需要遍历这个链表,找到Type为DNS_TYPE_A
的节点,才是最终目标。
经过上面的步骤,已经拿到了真实IP,但是应用程序在访问域名时,并不会主动使用这个IP,所以我们接下来需要让上层程序使用我们已经查询到的IP,以IE控件为例,当你调用Navigate进行URL导航时,浏览器会按照这样的顺序来查找IP地址
因此我们要做的就是把IP更新到本地缓存,这样就可以让应用程序无缝接入我们的防劫持逻辑,遗憾的是,暂时还没有找到方式,在不提权的情况下更新本地缓存,所以采用Hook的方式,最终Hook了GetAddrInfoW
函数,直接返回对应的IP,不在进行查询。
int WSAAPI MyGetAddrInfoExW(
_In_opt_ PCTSTR pName,
_In_opt_ PCTSTR pServiceName,
_In_ DWORD dwNameSpace,
_In_opt_ LPGUID lpNspId,
_In_opt_ const ADDRINFOEX *pHints,
_Out_ PADDRINFOEX *ppResult,
_In_opt_ struct timeval *timeout,
_In_opt_ LPOVERLAPPED lpOverlapped,
_In_opt_ LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine,
_Out_opt_ LPHANDLE lpNameHandle
)
{
int result = NO_ERROR;
std::map<std::wstring, std::pair<std::wstring, unsigned long> >::iterator itFind = gDnsCache_->find(pName);
if (itFind != gDnsCache_->end()
&& NS_DNS == dwNameSpace)
{
//const wchar_t* ai_canonname = L"optonly1st.webcache.ourwebpic.com";
PADDRINFOEX pResult = (PADDRINFOEX)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ADDRINFOEX));
pResult->ai_family = AF_INET;
pResult->ai_socktype = pHints->ai_socktype;
pResult->ai_protocol = pHints->ai_protocol;
pResult->ai_flags = 0; // 1 << 31;
const size_t canonname_len = itFind->second.first.size() + 1;
pResult->ai_canonname = (PWSTR)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, canonname_len * sizeof(wchar_t));
::wcscpy_s(pResult->ai_canonname, canonname_len, itFind->second.first.c_str());
pResult->ai_addrlen = sizeof(SOCKADDR_IN);
pResult->ai_addr = (sockaddr*)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, pResult->ai_addrlen);
reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_family = pResult->ai_family;
reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_port = 0;
reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_addr.S_un.S_addr = itFind->second.second;
*ppResult = pResult;
}
else
{
result = oldGetAddrInfoExW(pName, pServiceName, dwNameSpace, lpNspId, pHints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpNameHandle);
}
return result;
}
GetAddrInfoW
这个API就是获取ip地址的,被我们hook掉之后,如果应用程序查询的域名是已经缓存的,则填充完ADDRINFOEX这个结构体之后直接返回,否则调用原来的处理逻辑。
上面的代码并不能完全防止DNS劫持,还有两个待完成项:①使用自定义协议进行DNS查询,这就需要自建DNS服务器了;②更新系统的DNS缓存,而不使用hook的方式。