conundrum
尊榮會員
發表:893 回覆:1272 積分:643 註冊:2004-01-06
發送簡訊給我
|
ktop下載此文件
http://delphi.ktop.com.tw/topic.php?TOPIC_ID=81522 Sniffer實現之用Raw Socket實現Sniffer(1) http://www.chinaitlab.com/www/news/article_show.asp?id=31400 一. 摘要 Raw Socket: 原始套接字
可以用它來發送和接收 IP 層以上的原始資料包, 如 ICMP, TCP, UDP... int sockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); 這樣我們就創建了一個 Raw Socket Sniffer: 嗅探器
關於嗅探器的原理我想大多數人可能都知道
1. 把網卡置於混雜模式;
2. 捕獲數據包;
3. 分析資料包. 但具體的實現知道的人恐怕就不是那麼多了. 好, 現在讓我們用 Raw Socket 的做一個自已的 Sniffer. 二. 把網卡置於混雜模式 在正常的情況下,一個網路介面應該只回應兩種資料幀:
一種是與自己硬體位址相匹配的資料幀
一種是發向所有機器的廣播資料幀
如果要網卡接收所有通過它的資料, 而不管是不是發給它的, 那麼必須把網卡置於混雜模式. 也就是說讓它的思維混亂, 不按正常的方式工作. 用 Raw Socket 實現代碼如下: setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag); //設置 IP 頭操作選項
bind(sockRaw, (PSOCKADDR)&addrLocal, sizeof(addrLocal); //把 sockRaw 綁定到本地網卡上
ioctlsocket(sockRaw, SIO_RCVALL, &dwValue); //讓 sockRaw 接受所有的資料 flag 標誌是用來設置 IP 頭操作的, 也就是說要親自處理 IP 頭: bool flag = ture;
addrLocal 為本地地址: SOCKADDR_IN addrLocal;
dwValue 為輸入輸出參數, 為 1 時執行, 0 時取消: DWORD dwValue = 1;
沒想到這麼簡單吧? 三. 捕獲數據包 你的 sockRaw 現在已經在工作了, 可以在局域網內其他的電腦上用 Sniffer 檢測工具檢測一下, 看你的網卡是否處於混雜模式(比如 DigitalBrain 的 ARPKiller). 不能讓他白白的浪費資源啊, 抓包! recv(sockRaw, RecvBuf, BUFFER_SIZE, 0); //接受任意數據包 #define BUFFER_SIZE 65535
char RecvBuf[BUFFER_SIZE];
越來越發現 Sniffer 原來如此的簡單了, 這麼一個函數就已經完成抓取資料包的任務了. 四. 分析資料包 這回抓來的包和平常用 Socket 接受陌刪筒皇且換厥露? 裏面包含 IP, TCP 等原始資訊. 要分析它首先得知道這些結構.
資料包的總體結構: ----------------------------------------------
| ip header | tcp header(or x header) | data |
---------------------------------------------- IP header structure:
4 8 16 32 bit
|--------|--------|----------------|--------------------------------|
| Ver | IHL |Type of service | Total length |
|--------|--------|----------------|--------------------------------|
| Identification | Flags | Fragment offset |
|--------|--------|----------------|--------------------------------|
| Time to live | Protocol | Header checksum |
|--------|--------|----------------|--------------------------------|
| Source address |
|--------|--------|----------------|--------------------------------|
| Destination address |
|--------|--------|----------------|--------------------------------|
| Option + Padding |
|--------|--------|----------------|--------------------------------|
| Data |
|--------|--------|----------------|--------------------------------| TCP header structure: 16 32 bit
|--------------------------------|--------------------------------|
| Source port | Destination port |
|--------------------------------|--------------------------------|
| Sequence number |
|--------------------------------|--------------------------------|
| Acknowledgement number |
|--------------------------------|--------------------------------|
| Offset | Resrvd |U|A|P|R|S|F| Window |
|--------------------------------|--------------------------------|
| Checksum | Urgent pointer |
|--------------------------------|--------------------------------|
| Option + Padding |
|--------------------------------|--------------------------------|
| Data |
|--------------------------------|--------------------------------| 五. 實現 Sniffer OK!
現在都清楚了, 還等什麼.
下麵是我用 BCB6 寫的一個 Simple Sniffer 的代碼, 僅供參考.
(需要在工程檔裏加入WS2_32.LIB這個檔)
//*************************************************************************//
//* CPP File: WMain.cpp
//* Simple Sniffer by shadowstar
//* http://shadowstar.126.com/
//*************************************************************************//
#include
#pragma hdrstop #include
#include
#include
#include
#include "WMain.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------------------------------------
__fastcall TMainForm::TMainForm(TComponent* Owner)
: TForm(Owner)
{
WSADATA WSAData;
BOOL flag = true;
int nTimeout = 1000;
char LocalName[16];
struct hostent *pHost; //檢查 Winsock 版本號
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
throw Exception("WSAStartup error!"); //初始化 Raw Socket
if ((sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == INVALID_SOCKET)
throw Exception("socket setup error!"); //設置IP頭操作選項
if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag)) == SOCKET_ERROR)
throw Exception("setsockopt IP_HDRINCL error!"); //獲取本機名
if (gethostname((char*)LocalName, sizeof(LocalName)-1) == SOCKET_ERROR)
throw Exception("gethostname error!"); //獲取本地 IP 地址
if ((pHost = gethostbyname((char*)LocalName)) == NULL)
throw Exception("gethostbyname error!"); addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP
addr_in.sin_family = AF_INET;
addr_in.sin_port = htons(57274); //把 sock 綁定到本地地址上
if (bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)) == SOCKET_ERROR)
throw Exception("bind error!"); iSortDirection = 1;
}
//---------------------------------------------------------------------------
__fastcall TMainForm::~TMainForm()
{
WSACleanup();
}
//--------------------------------------------------------------------------- void __fastcall TMainForm::btnCtrlClick(TObject *Sender)
{
TListItem *Item;
DWORD dwValue;
int nIndex = 0; if (btnCtrl->Caption == "&Start")
{
dwValue = 1;
//設置 SOCK_RAW 為SIO_RCVALL,以便接收所有的IP包
if (ioctlsocket(sock, SIO_RCVALL, &dwValue) != 0)
throw Exception("ioctlsocket SIO_RCVALL error!");
bStop = false;
btnCtrl->Caption = "&Stop";
lsvPacket->Items->Clear();
}
else
{
dwValue = 0;
bStop = true;
btnCtrl->Caption = "&Start";
//設置SOCK_RAW為SIO_RCVALL,停止接收
if (ioctlsocket(sock, SIO_RCVALL, &dwValue) != 0)
throw Exception("WSAIoctl SIO_RCVALL error!");
} while (!bStop)
{
if (recv(sock, RecvBuf, BUFFER_SIZE, 0) > 0)
{
nIndex ; ip = *(IP*)RecvBuf;
tcp = *(TCP*)(RecvBuf (ip.HdrLen & IP_HDRLEN_MASK)); Item = lsvPacket->Items->Add();
Item->Caption = nIndex;
Item->SubItems->Add(GetProtocolTxt(ip.Protocol));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip.SrcAddr));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip.DstAddr));
Item->SubItems->Add(tcp.SrcPort);
Item->SubItems->Add(tcp.DstPort);
Item->SubItems->Add(ntohs(ip.TotalLen));
}
Application->ProcessMessages();
}
}
//--------------------------------------------------------------------------- AnsiString __fastcall TMainForm::GetProtocolTxt(int Protocol)
{
switch (Protocol)
{
case IPPROTO_ICMP : //1 /* control message protocol */
return PROTOCOL_STRING_ICMP_TXT;
case IPPROTO_TCP : //6 /* tcp */
return PROTOCOL_STRING_TCP_TXT;
case IPPROTO_UDP : //17 /* user datagram protocol */
return PROTOCOL_STRING_UDP_TXT;
default :
return PROTOCOL_STRING_UNKNOWN_TXT;
}
}
//--------------------------------------------------------------------------- //*************************************************************************//
//* Header File: WMain.h for WMain.cpp class TMainForm
//*************************************************************************//
//--------------------------------------------------------------------------- #ifndef WMainH
#define WMainH
//---------------------------------------------------------------------------
#define BUFFER_SIZE 65535 #include
#include
#include
#include <Forms.hpp>
#include
#include
#include
#include "netmon.h" //---------------------------------------------------------------------------
class TMainForm : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
TButton *btnCtrl;
TListView *lsvPacket;
TLabel *Label1;
void __fastcall btnCtrlClick(TObject *Sender);
void __fastcall lsvPacketColumnClick(TObject *Sender,
TListColumn *Column);
void __fastcall lsvPacketCompare(TObject *Sender, TListItem *Item1,
TListItem *Item2, int Data, int &Compare);
void __fastcall Label1Click(TObject *Sender);
private: // User declarations
AnsiString __fastcall GetProtocolTxt(int Protocol);
public: // User declarations
SOCKET sock;
SOCKADDR_IN addr_in;
IP ip;
TCP tcp;
PSUHDR psdHeader;
char RecvBuf[BUFFER_SIZE];
bool bStop; int iSortDirection;
int iColumnToSort; __fastcall TMainForm(TComponent* Owner);
__fastcall ~TMainForm();
};
//---------------------------------------------------------------------------
extern PACKAGE TMainForm *MainForm;
//---------------------------------------------------------------------------
#endif 偷了個懶, IP, TCP 頭及一些巨集定義用了 netmon.h 的頭, 這個檔在 BCB6 的 include 目錄下可以找得到, 其中與本程式相關內容如下: //*************************************************************************//
//* Header File: netmon.h
//*************************************************************************//
//
// IP Packet Structure
//
typedef struct _IP
{
union
{
BYTE Version;
BYTE HdrLen;
};
BYTE ServiceType;
WORD TotalLen;
WORD ID;
union
{
WORD Flags;
WORD FragOff;
};
BYTE TimeToLive;
BYTE Protocol;
WORD HdrChksum;
DWORD SrcAddr;
DWORD DstAddr;
BYTE Options[0];
} IP; typedef IP * LPIP;
typedef IP UNALIGNED * ULPIP; //
// TCP Packet Structure
//
typedef struct _TCP
{
WORD SrcPort;
WORD DstPort;
DWORD SeqNum;
DWORD AckNum;
BYTE DataOff;
BYTE Flags;
WORD Window;
WORD Chksum;
WORD UrgPtr;
} TCP; typedef TCP *LPTCP;
typedef TCP UNALIGNED * ULPTCP; // upper protocols
#define PROTOCOL_STRING_ICMP_TXT "ICMP"
#define PROTOCOL_STRING_TCP_TXT "TCP"
#define PROTOCOL_STRING_UDP_TXT "UDP"
#define PROTOCOL_STRING_SPX_TXT "SPX"
#define PROTOCOL_STRING_NCP_TXT "NCP" #define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW" 這個檔也有人聲稱沒有.
//*************************************************************************//
//* Header File: mstcpip.h
//*************************************************************************//
// Copyright (c) Microsoft Corporation. All rights reserved.
#if _MSC_VER > 1000
#pragma once
#endif /* Argument structure for SIO_KEEPALIVE_VALS */ struct tcp_keepalive {
u_long onoff;
u_long keepalivetime;
u_long keepaliveinterval;
}; // New WSAIoctl Options #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
#define SIO_RCVALL_MCAST _WSAIOW(IOC_VENDOR,2)
#define SIO_RCVALL_IGMPMCAST _WSAIOW(IOC_VENDOR,3)
#define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR,4)
#define SIO_ABSORB_RTRALERT _WSAIOW(IOC_VENDOR,5)
#define SIO_UCAST_IF _WSAIOW(IOC_VENDOR,6)
#define SIO_LIMIT_BROADCASTS _WSAIOW(IOC_VENDOR,7)
#define SIO_INDEX_BIND _WSAIOW(IOC_VENDOR,8)
#define SIO_INDEX_MCASTIF _WSAIOW(IOC_VENDOR,9)
#define SIO_INDEX_ADD_MCAST _WSAIOW(IOC_VENDOR,10)
#define SIO_INDEX_DEL_MCAST _WSAIOW(IOC_VENDOR,11) // Values for use with SIO_RCVALL* options
#define RCVALL_OFF 0
#define RCVALL_ON 1
#define RCVALL_SOCKETLEVELONLY 2 現在我們自已的 Sniffer 就做好了, Run, Start......哇, 這麼多資料包, 都是從這一台機器上發出的, 它在幹什麼? 原來 Adminstrator 密碼為空, 中了尼姆達病毒! 六. 小結 優點: 實現簡單, 不需要做驅動程式就可實現抓包.
缺點: 資料包頭不含幀資訊, 不能接收到與 IP 同層的其他資料包, 如 ARP, RARP...
這裏提供的程式僅僅是一個 Sniffer 的例子, 沒有對資料包進行進一步的分析. 寫此文的目的在於熟悉Raw Socket 編程方法, 瞭解 TCP/IP 協定結構原理以及各協定之間的關係 Sniffer實現之用Raw Socket實現Sniffer(2) http://www.chinaitlab.com/www/news/article_show.asp?id=31401 一、引言
上一次介紹了用 Raw Socket 實現 Sniffer 的方法,實現起來比較簡單,但有個缺點就是只能截獲 IP 層以上的包,資料包頭不含幀資訊。對一些特殊的要求就不能滿足了,其中很重要的一條就是不能對 ARP 包進行處理。用 NDIS 驅動程式可以實現對整個乙太網包的截獲,但複雜的驅動程式讓好多人望而卻步。沒關係,有現成的東西幹嘛不好好利用呢?在微軟的 DDK 裏提供了一個 Packet 的例子,Packet.sys 可以對網卡進行任意的操作,Packete32.dll 提供給應用程式一個方便的介面,而與驅動程式通訊相關的複雜的內部操作由 DLL 完成,面向應用層的程式師不需要瞭解這些細節。可惜我按 Packet32.dll 的提供的介面一步一步的做下去,卻總也得不到想要的結果,一抓包就死在那兒不動了。看來它是不想給我幹活了:(還是不想自已寫驅動程式……
幸運的是有一套 WinPcap 的東東,專門用來在 Win32 平臺下抓包的,可以在 http://winpcap.polito.it 上下載到。而且介面基本上和微軟的 Packet 是一樣的。哈哈,這下好了,原來的代碼還可以用,一試就靈!下面就介紹怎樣利用 WinPcap 直接對網卡進行操作及對接收到的資料進行分析。
二、Windows 系統中的網路通信結構
1.Windows 系統中的網路通信結構
圖1的上層應用程式包括 IE、Outlook 等各種基於網路的軟體,網路驅動協定包括 TCP/IP、NETBEUI 等各種 Windows 支援的網路層、傳輸層協定,NDIS 是 Windows 作業系統網路功能驅動的關鍵部分,下面對 NDIS 進行介紹。
2.NDIS及其特點
NDIS(Network Driver Interface Specification) 是 Microsoft 和 3Com 公司聯合制定的網路驅動規範,並提供了大量的操作函數。它為上層的協定驅動提供服務,遮罩了下層各種網卡的差別。
NDIS 向上支援多種網路協定,比如 TCP/IP、NWLink IPX/SPX、NETBEUI 等,向下支持不同廠家生產的多種網卡。NDIS 還支援多種工作模式,支援多處理器,提供一個完備的 NDIS 庫(Library)。 但庫中所提供的各個函數都是工作在核心模式下的,用戶不宜直接操作,這就需要尋找另外的介面。
三、WinPcap 簡介
1. WinPcap結構圖
2. WinPcap 包括三個部分
第一個模組NPF(Netgroup Packet Filter),是一個虛擬設備驅動程式檔。它的功能是過濾資料包,並把這些資料包原封不動地傳給用戶態模組,這個過程中包括了一些作業系統特有的代碼。
第二個模組packet.dll為win32平臺提供了一個公共的介面。不同版本的Windows系統都有自己的內核模組和用戶層模組。Packet.dll用於解決這些不同。調用Packet.dll的程式可以運行在不同版本的Windows平臺上,而無需重新編譯。
第三個模組 Wpcap.dll是不依賴於作業系統的。它提供了更加高層、抽象的函數。
3. packet.dll和Wpcap.dll
packet.dll直接映射了內核的調用。
Wpcap.dll提供了更加友好、功能更加強大的函數調用。
4. WinPcap的優勢
提供了一套標準的抓包介面,與libpcap相容,可使得原來許多UNIX平臺下的網路分析工具快速移植過來
便於開發各種網路分析工具
充分考慮了各種性能和效率的優化,包括對於NPF內核層次上的篩檢程式支援
支援內核態的統計模式
提供了發送資料包的能力
四、Packet.dll 的使用
WinPcap的主頁:http://winpcap.polito.it/你可以到這裏下載它的驅動、DLLs和開發包。這裏只是對WinPcap實現Sniffer做一個簡單的介紹,不做深入研究。你只需要把下載回來的驅動安裝到你的電腦上,用你的程式調用Packet.dll就可以了。Packet.dll在安裝的時候會被拷貝到你的系統目錄下,也可以用WinRAR打開安裝包,可以看到裏面的檔,直接提取你想要的Packet.dll。
Packet.dll提供了一套完整的、功能強大的API,其介面形式與Microsoft DDK提供的Packet32.dll基本一致。開發過Windows應用程式的人,對調用DLL一定不會莫生,如果你還不知道怎麼使用DLL請參考相關書籍,這裏不多講了。新建一個DLL工程命名為sniffer2,保存到硬碟。把開發包裏的include、lib目錄拷貝到工程目錄中。如果你用的是Visual C ,可以直接使用lib裏面的引入庫。shadowstar用的是C Builder,需要用C Builder提供的implib工具為Packet.dll生成一個lib檔,命令行如下:
implib -a packet.lib packet.dll
五、簡單實現
shadowstar用C Builder寫了一個簡單的演示程式,這裏只給出主要部分的代碼,完整的代碼可以到http://shadowstar.126.com/下載。
void __fastcall TMainForm::btnCtrlClick(TObject *Sender)
{
//define a pointer to an ADAPTER structure
LPADAPTER lpAdapter = 0;
//define a pointer to a PACKET structure
LPPACKET lpPacket;
int i;
DWORD dwErrorCode;
DWORD dwVersion;
DWORD dwWindowsMajorVersion;
//unicode strings (winnt)
WCHAR AdapterName[8192]; // string that contains a list of the network adapters
WCHAR *temp,*temp1;
//ascii strings (win95)
char AdapterNamea[8192]; // string that contains a list of the network adapters
char *tempa,*temp1a;
int AdapterNum=0,Open;
ULONG AdapterLength;
char buffer[256000]; // buffer to hold the data coming from the driver
struct bpf_stat stat;
// obtain the name of the adapters installed on this machine
AdapterLength=4096;
ShowMessage(AnsiString("Packet.dll test application. Library version: ") PacketGetVersion());
ShowMessage("Adapters installed:");
i=0;
// the data returned by PacketGetAdapterNames is different in Win95 and in WinNT.
// We have to check the os on which we are running
dwVersion=GetVersion();
dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
if (!(dwVersion >= 0x80000000 && dwWindowsMajorVersion >= 4))
{ // Windows NT
if(PacketGetAdapterNames((PTSTR)AdapterName,&AdapterLength) == FALSE)
{
ShowMessage("Unable to retrieve the list of the adapters!\n");
return;
}
temp=AdapterName;
temp1=AdapterName;
while ((*temp!='\0')||(*(temp-1)!='\0'))
{
if (*temp=='\0')
{
memcpy(AdapterList[i],temp1,(temp-temp1)*2);
temp1=temp 1;
i ;
}
temp ;
}
AdapterNum=i;
for (i=0;ihFile == INVALID_HANDLE_VALUE))
{
dwErrorCode=GetLastError();
ShowMessage(Format("Unable to open the adapter, Error Code : %lx\n",
ARRAYOFCONST(((int)dwErrorCode))));
return;
}
// set the network adapter in promiscuous mode
if(PacketSetHwFilter(lpAdapter,NDIS_PACKET_TYPE_PROMISCUOUS)==FALSE)
{
ShowMessage("Warning: unable to set promiscuous mode!\n");
}
// set a 512K buffer in the driver
if(PacketSetBuff(lpAdapter,512000) == FALSE)
{
ShowMessage("Unable to set the kernel buffer!\n");
return;
}
// set a 1 second read timeout
if(PacketSetReadTimeout(lpAdapter,1000)==FALSE)
{
ShowMessage("Warning: unable to set the read tiemout!\n");
}
//allocate and initialize a packet structure that will be used to
//receive the packets.
if((lpPacket = PacketAllocatePacket()) == NULL)
{
ShowMessage("\nError: failed to allocate the LPPACKET structure.");
return;
}
PacketInitPacket(lpPacket,(char*)buffer,256000);
if (btnCtrl->Caption == "&Start")
{
bStop = false;
btnCtrl->Caption = "&Stop";
}
else
{
bStop = true;
btnCtrl->Caption = "&Start";
}
int nIndex = 0;
LPIP ip;
LPTCP tcp;
TListItem *Item;
struct bpf_hdr *hdr;
int off;
BYTE* buf;
//main capture loop
while(!bStop)
{
// capture the packets
if(PacketReceivePacket(lpAdapter,lpPacket,TRUE)==FALSE)
ShowMessage("Error: PacketReceivePacket failed");
off = 0;
buf = (BYTE*)lpPacket->Buffer;
while(offulBytesReceived & !bStop)
{
nIndex ;
hdr = (struct bpf_hdr *)(buf off);
off = hdr->bh_hdrlen;
ip = (IP*)(buf off ETHERNET_HEADER_LENGTH);
tcp = (TCP*)((BYTE*)ip (ip->HdrLen & IP_HDRLEN_MASK));
off = Packet_WORDALIGN(off hdr->bh_caplen);
Item = lsvPacket->Items->Add();
Item->Caption = nIndex;
Item->SubItems->Add(GetProtocolTxt(ip->Protocol));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip->SrcAddr));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip->DstAddr));
Item->SubItems->Add(tcp->SrcPort);
Item->SubItems->Add(tcp->DstPort);
Item->SubItems->Add(hdr->bh_datalen);
Application->ProcessMessages();
}
}
//print the capture statistics
if(PacketGetStats(lpAdapter,&stat)==FALSE)
ShowMessage("Warning: unable to get stats from the kernel!\n");
else
ShowMessage(Format("\n\n%d packets received.\n%d Packets lost",
ARRAYOFCONST(((int)stat.bs_recv,(int)stat.bs_drop))));
PacketFreePacket(lpPacket);
// close the adapter and exit
PacketCloseAdapter(lpAdapter);
return;
}
六、結束語
如果在一個繁忙的網路上進行截獲,而且不設置任何過濾,那得到的資料包是非常多的,可能在一秒鐘內得到上千的資料包。如果應用程式不進行必要的性能優化,那麼將會大量的丟失資料包,下面就是我對性能的一個優化方案。
這個方案使用了多線程來處理資料包。在程式中建立一個公共的資料包緩衝池,這個緩衝池是一個LILO的佇列。程式中使用三個線程進行操作:一個線程只進行捕獲操作,它將從驅動程式獲得的資料包添加到資料包佇列的頭部;另一個線程只進行過濾操作,它檢查新到的隊尾的資料包,檢查其是否滿足過濾條件,如果不滿足則將其刪除出佇列;最後一個線程進行資料包處理操作,象根據接收的資料包發送新資料包這樣的工作都由它來進行。上面三個線程中,考慮盡可能少丟失資料包的條件,應該是進行捕獲操作的線程的優先順序最高,當然具體問題具體分析,看應用的側重點是什麼了。 台灣災難都是事後算帳
無人飛行載具(Unmanned Aerial Vehicle,UAV)為什麼沒大量應用於救災行列 絲絲有2種
.net有很多種 一種治眼睛是MS 另一種治腦筋是Borland
|