[关闭]
@lniwn 2017-09-08T20:43:04.000000Z 字数 5049 阅读 1083

防运营商DNS劫持(一)

Windows DNS


关于DNS劫持的概念,网上很多介绍,这里就不细说了。最近正好在做这方面的预研,把过程记录下来。

1. 通过可信DNS查询服务器IP

DNS查询协议走的53端口,默认是UDP协议,可以自己按照MSDN描述来构造协议包,而Windows系统已经提供了相关的API,用于进行DNS查询。

  1. std::pair<std::wstring, std::string> CDNSHelper::QueryIpAndCanonname(const wchar_t* cname)
  2. {
  3. std::pair<std::wstring, std::string> result;
  4. std::wstring host_name = cname;
  5. PDNS_RECORD pDnsRecord = NULL;
  6. DNS_STATUS dnsStatus = ::DnsQuery(
  7. host_name.c_str(),
  8. DNS_TYPE_A,
  9. DNS_QUERY_NO_HOSTS_FILE|DNS_QUERY_BYPASS_CACHE|DNS_QUERY_TREAT_AS_FQDN,
  10. pSrvList_,
  11. &pDnsRecord,
  12. NULL);
  13. //if (DNS_ERROR_RCODE_NAME_ERROR == dnsStatus)
  14. //{
  15. // // #TODO
  16. // // DNS name does not exist.
  17. //}
  18. if (ERROR_SUCCESS != dnsStatus)
  19. {
  20. return result;
  21. }
  22. bool needBreak = false;
  23. for (PDNS_RECORD cursor = pDnsRecord; cursor != NULL && !needBreak; cursor = cursor->pNext)
  24. {
  25. if (cursor->Flags.S.Section != DnsSectionAnswer)
  26. {
  27. continue;
  28. }
  29. if (::DnsNameCompare(cursor->pName, host_name.c_str()))
  30. {
  31. switch(cursor->wType)
  32. {
  33. case DNS_TYPE_CNAME:
  34. {
  35. host_name = cursor->Data.CNAME.pNameHost;
  36. }
  37. break;
  38. case DNS_TYPE_A:
  39. {
  40. IN_ADDR ipaddr;
  41. ipaddr.S_un.S_addr = cursor->Data.A.IpAddress;
  42. result = std::make_pair(host_name, ::inet_ntoa(ipaddr));
  43. needBreak = true;
  44. }
  45. break;
  46. default:
  47. break;
  48. }
  49. }
  50. }
  51. ::DnsRecordListFree(pDnsRecord, DnsFreeRecordListDeep);
  52. return result;
  53. }

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数组,可以通过下面方式初始化

  1. bool CDNSHelper::Init(const char** dns_array, unsigned int length)
  2. {
  3. if (length < 1)
  4. {
  5. return false;
  6. }
  7. pSrvList_ = static_cast<PIP4_ARRAY>(::LocalAlloc(LPTR, sizeof(IP4_ARRAY) + sizeof(IP4_ADDRESS) * (length-1)));
  8. pSrvList_->AddrCount = length;
  9. for (unsigned int index = 0; index < length; ++index)
  10. {
  11. pSrvList_->AddrArray[index] = ::inet_addr(dns_array[index]);
  12. }
  13. HINSTANCE hNtdll = ::LoadLibrary(L"Ntdll.dll");
  14. if (!hNtdll)
  15. {
  16. return false;
  17. }
  18. return true;
  19. }

AddrArray字段为C语言常见的可变数组用法,不明白的可以自行Google。
如果查询的目标域名有CDN服务器,那么DnsQuery查询的结果就是一个链表,与实际CDN跳转顺序一致,所以需要遍历这个链表,找到Type为DNS_TYPE_A的节点,才是最终目标。

2. 更新IP到本地应用

经过上面的步骤,已经拿到了真实IP,但是应用程序在访问域名时,并不会主动使用这个IP,所以我们接下来需要让上层程序使用我们已经查询到的IP,以IE控件为例,当你调用Navigate进行URL导航时,浏览器会按照这样的顺序来查找IP地址

本地Host文件系统缓存DNS服务器

因此我们要做的就是把IP更新到本地缓存,这样就可以让应用程序无缝接入我们的防劫持逻辑,遗憾的是,暂时还没有找到方式,在不提权的情况下更新本地缓存,所以采用Hook的方式,最终Hook了GetAddrInfoW函数,直接返回对应的IP,不在进行查询。

  1. int WSAAPI MyGetAddrInfoExW(
  2. _In_opt_ PCTSTR pName,
  3. _In_opt_ PCTSTR pServiceName,
  4. _In_ DWORD dwNameSpace,
  5. _In_opt_ LPGUID lpNspId,
  6. _In_opt_ const ADDRINFOEX *pHints,
  7. _Out_ PADDRINFOEX *ppResult,
  8. _In_opt_ struct timeval *timeout,
  9. _In_opt_ LPOVERLAPPED lpOverlapped,
  10. _In_opt_ LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine,
  11. _Out_opt_ LPHANDLE lpNameHandle
  12. )
  13. {
  14. int result = NO_ERROR;
  15. std::map<std::wstring, std::pair<std::wstring, unsigned long> >::iterator itFind = gDnsCache_->find(pName);
  16. if (itFind != gDnsCache_->end()
  17. && NS_DNS == dwNameSpace)
  18. {
  19. //const wchar_t* ai_canonname = L"optonly1st.webcache.ourwebpic.com";
  20. PADDRINFOEX pResult = (PADDRINFOEX)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ADDRINFOEX));
  21. pResult->ai_family = AF_INET;
  22. pResult->ai_socktype = pHints->ai_socktype;
  23. pResult->ai_protocol = pHints->ai_protocol;
  24. pResult->ai_flags = 0; // 1 << 31;
  25. const size_t canonname_len = itFind->second.first.size() + 1;
  26. pResult->ai_canonname = (PWSTR)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, canonname_len * sizeof(wchar_t));
  27. ::wcscpy_s(pResult->ai_canonname, canonname_len, itFind->second.first.c_str());
  28. pResult->ai_addrlen = sizeof(SOCKADDR_IN);
  29. pResult->ai_addr = (sockaddr*)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, pResult->ai_addrlen);
  30. reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_family = pResult->ai_family;
  31. reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_port = 0;
  32. reinterpret_cast<PSOCKADDR_IN>(pResult->ai_addr)->sin_addr.S_un.S_addr = itFind->second.second;
  33. *ppResult = pResult;
  34. }
  35. else
  36. {
  37. result = oldGetAddrInfoExW(pName, pServiceName, dwNameSpace, lpNspId, pHints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpNameHandle);
  38. }
  39. return result;
  40. }

GetAddrInfoW这个API就是获取ip地址的,被我们hook掉之后,如果应用程序查询的域名是已经缓存的,则填充完ADDRINFOEX这个结构体之后直接返回,否则调用原来的处理逻辑。

后记

上面的代码并不能完全防止DNS劫持,还有两个待完成项:①使用自定义协议进行DNS查询,这就需要自建DNS服务器了;②更新系统的DNS缓存,而不使用hook的方式。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注