上一节介绍了ARP协议之后,群里面有朋友说ARP欺骗不是很简单么?实际上,实现ARP欺骗是很简单的,难点在于数据转发的速度。2004年我们做隔山打牛的时候,曾经去过一家网吧测试,刚开始是开了ARP欺骗几分钟就大量主机掉线,客人骂声一片(幸好是朋友的网吧,否则估计会被老板丢出去)。当然,那时候网上还没有ARP欺骗的介绍,更加没有什么ARP防火墙,ARP欺骗的防范是几年后的事情了。后来我们改进了算法,经过实际测试,250台电脑,原来的ICMP速度是多少(ping),开启ARP欺骗后就是多少,没有任何延迟,机器更加不会掉线。因为我们的代码仅出于演示目的,所以捕获线程和分析线程共用了一个List,然后加锁,如果你直接拿去网吧用,百分之百是被网吧老板抓住狂揍一顿的。
群里还有朋友说cain一开,整个局域网会掉线,这个实际上是cain处理的不好:它启动ARP欺骗后,并没有对现有连接的seq和ack等做转换,另外还有一种解决方法是被动欺骗。不过实际上,数据修改并不一定要上ARP欺骗这种中间人方式,不使用ARP欺骗而达到数据修改,至少有三种方法,后面我们会介绍一两种,至于第三种,因为命中率百分之百,为了造成不良后果,以后有机会再说。
上一节我们说了,网络通信的第一步是通过ARP协议获取目标电脑的物理地址—局域网内通信是获取目标电脑的,互联网是获取网关的,那第二步呢?答案当然是IP地址。所有基于IP的网络协议(例如icmp、tcp、udp)都必须拥有这个,但是因为ip难以记忆,而且有可能改变,所有实际应用中一般使用域名协议。例如,我们打开cmd,然后ping 一下我们的网站www.138soft.com,结果如下图:
从上图可以看到,ICMP协议会先获取www.138soft.com对应的ip地址,然后再跟它收发数据,用过浏览器的朋友们,实际上当你们输入域名的时候,浏览器也会有这个过程。写过网络程序的朋友就清楚的多,因为这个就是gethostbyname函数。这个过程,实质上就是DNS协议。
DNS协议的规范定义这里不作介绍了,我们只从程序的角度来说一下。DNS协议一般使用UDP,而且端口一般是53。DNS数据包分为查询包和应答包,其中查询包包含了这个包的ID(用于区别其它的查询包)、包的类型(这里为查询)、查询的类型(比如说:IP地址是A记录、MX发信地址是MX记录),DNS服务器的反馈包结构,前面是这个请求包(但包类型修改为反馈包),后面紧跟着具体的应答包。下面我们通过自己写的一个程序来重现这个过程(实际上,这个就是系统的gethostbyname函数的实现)。首先,我们使用iphlp库来获取本机的DNS服务器IP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function
GetDNSServerIP:
AnsiString
;
var
dwBuffSize:
Cardinal
;
pFinxedInfo: PFIXED_INFO;
pIPAddr: PIP_ADDR_STRING;
begin
Result :=
''
;
if
(GetNetworkParams(
nil
, dwBuffSize) <> ERROR_BUFFER_OVERFLOW)
then
Exit;
GetMem(pFinxedInfo, dwBuffSize);
if
pFinxedInfo =
nil
then
Exit;
if
(GetNetworkParams(pFinxedInfo, dwBuffSize) <> ERROR_SUCCESS)
then
begin
FreeMem(pFinxedInfo);
Exit;
end
;
pIPAddr := @pFinxedInfo^.DnsServerList;
while
(pIPAddr <>
nil
)
do
begin
if
Result =
''
then
Result := Format(
'%s'
, [pIPAddr^.IpAddress
.
S]);
pIPAddr := pIPAddr^.Next;
end
;
FreeMem(pFinxedInfo);
end
;
|
有个DNS地址,就可以构造请求包发送和接收了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
procedure
TForm1
.
Button1Click(Sender: TObject);
//这里省略部分内部函数,具体看代码,函数来源于Indy
var
AUDPSocket: TSocket;
SockAddrIn: TSockAddrIn;
strHostName:
AnsiString
;
nDNSPort:
Integer
;
QueryID:
Word
;
var
nLen, nRet:
Integer
;
begin
(Sender
as
TButton).Enabled :=
False
;
try
if
Trim(Edit_Host
.
Text) =
''
then
begin
ShowMessage(
'请输入域名!'
);
Edit_Host
.
SetFocus;
Exit;
end
;
if
Trim(Edit_DNSServer
.
Text) =
''
then
begin
ShowMessage(
'请输入DNS服务器IP!'
);
Edit_DNSServer
.
SetFocus;
Exit;
end
;
if
Trim(Edit_DNSPort
.
Text) =
''
then
begin
ShowMessage(
'请输入DNS服务器端口!'
);
Edit_DNSPort
.
SetFocus;
Exit;
end
;
strHostName :=
AnsiString
(Trim(Edit_Host
.
Text));
nDNSPort := StrToIntDef(Trim(Edit_DNSPort
.
Text),
53
);
AUDPSocket := socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if
AUDPSocket = INVALID_SOCKET
then
Exit;
SockAddrIn
.
sin_family := AF_INET;
SockAddrIn
.
sin_port := htons(nDNSPort);
SockAddrIn
.
sin_addr
.
s_addr := inet_addr(
PAnsiChar
(
AnsiString
(Trim(Edit_DNSServer
.
Text))));
Randomize;
QueryID := Random(
65535
);
Buffer
.
Count :=
0
;
Buffer
.
ID := Swap(QueryID);
//查询的ID
Move(Header_Option[
1
], Buffer
.
Option,
10
);
StringToLabel(strHostName, SubLabel);
Move(SubLabel[
1
], Buffer
.
Data[
0
], Length(SubLabel));
Buffer
.
Count := Length(SubLabel);
QType := #
0
#
1
;
//A记录
Move(QType[
1
], Buffer
.
Data[Buffer
.
Count],
2
);
Buffer
.
Count := Buffer
.
Count +
2
;
Move(QClass_Internet, Buffer
.
Data[Buffer
.
Count],
2
);
Buffer
.
Count := Buffer
.
Count +
14
;
// 2 + 12
if
sendto(AUDPSocket,
PAnsiChar
(@Buffer
.
ID)^, Buffer
.
Count,
0
, SockAddrIn, sizeof(SockAddrIn)) = SOCKET_ERROR
then
begin
CloseSocket(AUDPSocket);
ShowMessage(
'数据发送失败!'
);
Exit;
end
;
nLen := sizeof(SockAddrIn);
nRet := recvfrom(AUDPSocket,
PAnsiChar
(@Buffer
.
ID)^, sizeof(Buffer),
0
, SockAddrIn, nLen);
if
nRet = SOCKET_ERROR
then
begin
CloseSocket(AUDPSocket);
ShowMessage(
'数据接收失败!'
);
Exit;
end
;
CloseSocket(AUDPSocket);
//==========================================================================
if
Buffer
.
ID = Swap(QueryID)
then
begin
i :=
0
;
while
Buffer
.
Data[i] <> #
0
do
Inc(i);
i := i +
5
;
while
(Buffer
.
Data[i] <> #
32
)
and
(i < nRet)
do
begin
LabelToString(@Buffer
.
Data, i, RRDomain);
Inc(i);
RRDataType := Ord(Buffer
.
Data[i]);
i := i +
9
;
case
RRDataType
of
QType_CNAME: Memo1
.
Lines
.
Add(GetCNAME(Buffer, i));
QType_NS: Memo1
.
Lines
.
Add(GetNS(Buffer, i));
QType_A: Memo1
.
Lines
.
Add(GetA(Buffer, i));
QType_SOA: Memo1
.
Lines
.
Add(GetSOA(Buffer, i));
QType_PTR: Memo1
.
Lines
.
Add(GetPTR(Buffer, i));
QType_MX: Memo1
.
Lines
.
Add(GetMX(Buffer, i));
else
begin
uShort
.
B[
1
] := Buffer
.
nData[i -
2
];
uShort
.
B[
0
] := Buffer
.
nData[i -
1
];
i := i + uShort
.
Value;
end
;
end
;
//end for "case"
end
;
//end for "while"
end
;
//end for "if"
finally
(Sender
as
TButton).Enabled :=
True
;
end
;
end
;
|
程序运行界面如下:
对应的,我们再来实现一个DNS服务器,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
const
HOSTIP=
'127.0.0.1'
;
//需要返回的IP地址
procedure
TForm1
.
ReadData(
var
Message: TMessage);
var
client_addr: TSockAddrIn;
szBuffer:
array
[
0..4095
]
of
AnsiChar
;
len:
integer
;
flen:
integer
;
Event:
word
;
var
strRecvData:
AnsiString
;
ID, Code, QDCount:
Word
;
BitCode: TDNSBitCode;
i:
integer
;
APos:
Integer
;
DomainName:
AnsiString
;
QueryClass:
Word
;
QueryType:
Word
;
strSend:
AnsiString
;
begin
flen := sizeof(client_addr);
Event := WSAGetSelectEvent(Message
.
LParam);
if
Event = FD_READ
then
begin
len := recvfrom(m_UDPSocket, szBuffer, sizeof(szBuffer),
0
, client_addr, flen);
if
len <=
0
then
Exit;
if
len >
512
then
Exit;
SetLength(strRecvData, len);
Move(szBuffer[
0
], strRecvData[
1
], len);
if
Length(strRecvData) < sizeof(TDNSHeader)
then
Exit;
ID := TwoCharToWord(strRecvData[
1
], strRecvData[
2
]);
Code := TwoCharToWord(strRecvData[
3
], strRecvData[
4
]);
BitCode := GetDNSBitCode(Code);
if
BitCode
.
QR <>
0
then
Exit;
//0表示查询,1表示反馈
QDCount := TwoCharToWord(strRecvData[
5
], strRecvData[
6
]);
if
QDCount <=
0
then
Exit;
APos :=
13
;
// DNS头恒为12字节,我们从下一字节开始分析
for
i :=
1
to
QDCount
do
begin
DomainName := DNSStrToDomain(strRecvData, APos);
if
DomainName =
''
then
Exit;
QueryType := TwoCharToWord(strRecvData[APos], strRecvData[APos +
1
]);
Inc(APos,
2
);
QueryClass := TwoCharToWord(strRecvData[APos], strRecvData[APos +
1
]);
Inc(APos,
2
);
Memo1
.
Lines
.
Add(DomainName);
(*
m_strInfo := 'DNS查询包' + #$D#$A;
m_strInfo := m_strInfo + SourceIP + ':' + IntToStr(SourcePort) + '--->' + DestIP + ':' + IntToStr(DestPort) + #$D#$A;
m_strInfo := m_strInfo + 'DNSID:' + IntToStr(ID) + #$D#$A;
m_strInfo := m_strInfo + 'Domain:' + DomainName + #$D#$A;
m_strInfo := m_strInfo + 'QueryType:' + GetDNSTypeStr(QueryType) + #$D#$A;
m_strInfo := m_strInfo + 'QueryClass:' + GetDNSClassStr(QueryClass) + #$D#$A;
m_strInfo := m_strInfo + '===================================================';
Synchronize(ShowInfo);
*)
end
;
strSend := BuilderDNSResponse(szBuffer, len, HOSTIP);
//构造DNS应答包
sendto(m_UDPSocket, strSend[
1
], Length(strSend),
0
, client_addr, flen);
end
;
end
;
|
程序写的很简单,就是根据查询包,分析出客户端需要查询的域名,在Memo1显示出来,然后构造一个应答包返回。这里出于演示目的,我们简单的全部返回127.0.0.1,对于真正的DNS服务器,这里会使用gethostbyname之类往上一级查询,或直接在数据库和缓存里面查找,根据不同的域名返回对应的IP。我们先运行程序,然后把另外一台机器的DNS服务器指向这个IP:
然后随便Ping 一下www.qq.com:
可以看到,返回的IP地址是127.0.0.1。同时我们的DNS服务器的界面如下:
最后,我们把DNS服务端的代码移植到我们的Demo代码里面,就实现了DNS欺骗。需要注意的地方是,基于IP的数据包都有一个校验和的,所以如果你修改了数据包,一定要重新计算一次校验和,具体过程见CheckSum函数。程序运行效果如下:
DNS欺骗利用的原理是:DNS客户端发送请求包后,只要返回包里面的ID跟发送的一致,它就认为合法。如果有多个返回包,则它使用最前面的,这个是跟ARP刚好相反的(ARP会使用最新的覆盖原来的)。几年前,360云查杀的时候,我做过一个试验,就是在应用层开启RAWSOCKET来捕获数据,发现是请求360云服务器的话,则直接创建一个UDP SOCKET然后往请求地址发送一个回复,360就无法连接真正的云服务器了。实际上,不只是DNS协议如此,对于TCP协议,也是一样的。只要你捕获到它的数据包,就可以实现网页插代码、下载内容替换等等。这里再回头前面的话题:不用ARP欺骗就修改数据。方法之一就是在路由的镜像口接一台电脑,比如说,想欺骗TCP,只要你回复的数据包的SEQ和ACK按照顺序,客户端就会老老实实的被欺骗。因为你的程序工作在路由旁边,比真正的服务器回复的速度是更快的,甚至你可以再伪造回复的同时,顺便发送一个RST包给真正的服务器,让它断开连接。如果你无法接触路由,又不想使用ARP怎么办?那就用方法二或方法三了。
附件下载:
本节代码