根据公开资料的记载,默认情况下,SAP GUI(客户端)和SAP服务端之间的通信是压缩通信,而非加密通信。[1]
决定SAP GUI(客户端)是否采用网络数据包压缩功能的环境变量是TDW_NOCOMPRESS。若把这个变量设置为1,那么就可以关闭客户端的网络数据压缩功能。不过,如果当真进行了这种设置,客户端程序就会弹出如图80.1所示的提示窗口,而且这个弹出窗口根本关不掉。
图80.1 截屏
本节的任务就是去除这个提示窗口。
在此之前,我们可以确定的事实是:
① 在SAP GUI客户端程序里,环境变量TDW_ NOCOMPRESS被设为1。
② 程序的某个文件里应当存在字符串“data compression switched off”。
借助文件管理器FAR程序[2],我们可在SAPguilib.dll里找到这个字符串。
然后,我们用IDA程序打开文件SAPguilib.dll,在文件里搜索字符串“TDW_NOCOMPRESS”。所幸的是,我们可以找到这个字符串,而且整个文件只有一处指令调用了这个字符串。
该文件的相关指令为:[3]
.text:6440D51B lea eax, [ebp+2108h+var_211C]
.text:6440D51E push eax ; int
.text:6440D51F push offset aTdw_nocompress ; "TDW_NOCOMPRESS"
.text:6440D524 mov byte ptr [edi+15h], 0
.text:6440D528 call chk_env
.text:6440D52D pop ecx
.text:6440D52E pop ecx
.text:6440D52F push offset byte_64443AF8
.text:6440D534 lea ecx, [ebp+2108h+var_211C]
; demangled name: int ATL::CStringT::Compare(char const *)const
.text:6440D537 call ds:mfc90_1603
.text:6440D53D test eax, eax
.text:6440D53F jz short loc_6440D55A
.text:6440D541 lea ecx, [ebp+2108h+var_211C]
; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:6440D544 call ds:mfc90_910
.text:6440D54A push eax ; Str
.text:6440D54B call ds:atoi
.text:6440D551 test eax, eax
.text:6440D553 setnz al
.text:6440D556 pop ecx
.text:6440D557 mov [edi+15h], al
据此可知,chk_env()函数通过第二个参数获取环境变量字符串,然后MFC字符串处理函数会继续处理这个字符串,接下来标准C函数atoi()(从字符串转换为数字)再从这个字符串里提取数值。最终,程序会把解析出来的环境变量的变量值存储在地址edi+15h里。
接下来,我们一起分析chk_env()函数[4]:
.text:64413F20 ; int __cdecl chk_env(char *VarName, int)
.text:64413F20 chk_env proc near
.text:64413F20
.text:64413F20 DstSize = dword ptr -0Ch
.text:64413F20 var_8 = dword ptr -8
.text:64413F20 DstBuf = dword ptr -4
.text:64413F20 VarName = dword ptr 8
.text:64413F20 arg_4 = dword ptr 0Ch
.text:64413F20
.text:64413F20 push ebp
.text:64413F21 mov ebp, esp
.text:64413F23 sub esp, 0Ch
.text:64413F26 mov [ebp+DstSize], 0
.text:64413F2D mov [ebp+DstBuf], 0
.text:64413F34 push offset unk_6444C88C
.text:64413F39 mov ecx, [ebp+arg_4]
; (demangled name) ATL::CStringT::operator=(char const *)
.text:64413F3C call ds:mfc90_820
.text:64413F42 mov eax, [ebp+VarName]
.text:64413F45 push eax ; VarName
.text:64413F46 mov ecx, [ebp+DstSize]
.text:64413F49 push ecx ; DstSize
.text:64413F4A mov edx, [ebp+DstBuf]
.text:64413F4D push edx ; DstBuf
.text:64413F4E lea eax, [ebp+DstSize]
.text:64413F51 push eax ; ReturnSize
.text:64413F52 call ds:getenv_s
.text:64413F58 add esp, 10h
.text:64413F5B mov [ebp+var_8], eax
.text:64413F5E cmp [ebp+var_8], 0
.text:64413F62 jz short loc_64413F68
.text:64413F64 xor eax, eax
.text:64413F66 jmp short loc_64413FBC
.text:64413F68
.text:64413F68 loc_64413F68:
.text:64413F68 cmp [ebp+DstSize], 0
.text:64413F6C jnz short loc_64413F72
.text:64413F6E xor eax, eax
.text:64413F70 jmp short loc_64413FBC
.text:64413F72
.text:64413F72 loc_64413F72:
.text:64413F72 jmp short loc_64413FBC
.text:64413F75 push ecx
.text:64413F76 mov ecx, [ebp+arg_4]
; demangled name: ATL::CSimpleStringT<char, 1>::Preallocate(int)
.text:64413F79 call ds:mfc90_2691
.text:64413F7F mov [ebp+DstBuf], eax
.text:64413F82 mov edx, [ebp+VarName]
.text:64413F85 push edx ; VarName
.text:64413F86 mov eax, [ebp+DstSize]
.text:64413F89 push eax ; DstSize
.text:64413F8A mov ecx, [ebp+DstBuf]
.text:64413F8D push ecx ; DstBuf
.text:64413F8E lea edx, [ebp+DstSize]
.text:64413F91 push edx ; ReturnSize
.text:64413F92 call ds:getenv_s
.text:64413F98 add esp, 10h
.text:64413F9B mov [ebp+var_8], eax
.text:64413F9E push 0FFFFFFFFh
.text:64413FA0 mov ecx, [ebp+arg_4]
; demangled name: ATL::CSimpleStringT::ReleaseBuffer(int)
.text:64413FA3 call ds:mfc90_5835
.text:64413FA9 cmp [ebp+var_8], 0
.text:64413FAD jz short loc_64413FB3
.text:64413FAF xor eax, eax
.text:64413FB1 jmp short loc_64413FBC
.text:64413FB3
.text:64413FB3 loc_64413FB3:
.text:64413FB3 mov ecx, [ebp+arg_4]
; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64413FB6 call ds:mfc90_910
.text:64413FBC
.text:64413FBC loc_64413FBC:
.text:64413FBC
.text:64413FBC mov esp, ebp
.text:64413FBE mov esp, ebp
.text:64413FBF retn
.text:64413FBF chk_env endp
其中,getenv_s()函数[5]是微软退出的改进版getenv()函数[6],它提升了原有函数的安全特性。
这个函数多处调用了MFC字符串处理函数。
不仅如此,它还检测了其他的环境变量。如果打开SAP GUI(客户端)程序的日志记录功能,就会在trace log里看到它检测的环境变量,如下表所示。
DPTRACE | “GUI-OPTION: Trace set to %d” |
TDW_HEXDUMP | “GUI-OPTION: Hexdump enabled” |
TDW_WORKDIR | “GUI-OPTION: working directory ‘%s’ ” |
TDW_SPLASHSRCEENOFF | “GUI-OPTION: Splash Screen Off” / “GUI-OPTION: Splash Screen On” |
TDW_REPLYTIMEOUT | “GUI-OPTION: reply timeout %d milliseconds” |
TDW_PLAYBACKTIMEOUT | “GUI-OPTION: PlaybackTimeout set to %d milliseconds” |
TDW_NOCOMPRESS | “GUI-OPTION: no compression read” |
TDW_EXPERT | “GUI-OPTION: expert mode” |
TDW_PLAYBACKPROGRESS | “GUI-OPTION: PlaybackProgress” |
TDW_PLAYBACKNETTRAFFIC | “GUI-OPTION: PlaybackNetTraffic” |
TDW_PLAYLOG | “GUI-OPTION: /PlayLog is YES, file %s” |
TDW_PLAYTIME | “GUI-OPTION: /PlayTime set to %d milliseconds” |
TDW_LOGFILE | “GUI-OPTION: TDW_LOGFILE ‘%s’ ” |
TDW_WAN | “GUI-OPTION: WAN - low speed connection enabled” |
TDW_FULLMENU | “GUI-OPTION: FullMenu enabled” |
SAP_CP / SAP_CODEPAGE | “GUI-OPTION: SAP_CODEPAGE ‘%d’ ” |
UPDOWNLOAD_CP | “GUI-OPTION: UPDOWNLOAD_CP ‘%d’ ” |
SNC_PARTNERNAME | “GUI-OPTION: SNC name ‘%s’ ” |
SNC_QOP | “GUI-OPTION: SNC_QOP ‘%s’ ” |
SNC_LIB | “GUI-OPTION: SNC is set to: %s” |
SAPGUI_INPLACE | “GUI-OPTION: environment variable SAPGUI_INPLACE is on” |
函数把这些变量都存储在数组里,而且把EDI寄存器当作这个数组的指针。在调用chk_evn()函数之前,程序首先设置了EDI的值:
.text:6440EE00 lea edi, [ebp+2884h+var_2884] ; options here like +0x15...
.text:6440EE03 lea ecx, [esi+24h]
.text:6440EE06 call load_command_line
.text:6440EE0B mov edi, eax
.text:6440EE0D xor ebx, ebx
.text:6440EE0F cmp edi, ebx
.text:6440EE11 jz short loc_6440EE42
.text:6440EE13 push edi
.text:6440EE14 push offset aSapguiStoppedA ; "Sapgui stopped after ↙
↘ commandline interp"...
.text:6440EE19 push dword_644F93E8
.text:6440EE1F call FEWTraceError
那么,我们关注的“data record mode switched on”字符串在这个文件里吗?整个文件里,只有CDwsGui::PrepareInfoWindow()构造函数调用了这个字符串。我们可通过日志文件的调试调用(debugging calls)信息了解各个class/method的名字。
例如,下述调试调用信息就透露了构造函数的函数名称:
.text:64405160 push dword ptr [esi+2854h]
.text:64405166 push offset aCdwsguiPrepare ; "\nCDwsGui::PrepareInfoWindow: ↙
↘ sapgui env"...
.text:6440516B push dword ptr [esi+2848h]
.text:64405171 call dbg
.text:64405176 add esp, 0Ch
以及:
.text:6440237A push eax
.text:6440237B push offset aCclientStart_6 ; "CClient::Start: set shortcut ↙
↘ user to '\%"...
.text:64402380 push dword ptr [edi+4]
.text:64402383 call dbg
.text:64402388 add esp, 0Ch
这些信息的作用很大。
接下来,我们直奔那个令人恼火的弹出窗口:
.text:64404F4F CDwsGui__PrepareInfoWindow proc near
.text:64404F4F
.text:64404F4F pvParam = byte ptr -3Ch
.text:64404F4F var_38 = dword ptr -38h
.text:64404F4F var_34 = dword ptr -34h
.text:64404F4F rc = tagRECT ptr -2Ch
.text:64404F4F cy = dword ptr -1Ch
.text:64404F4F h = dword ptr -18h
.text:64404F4F var_14 = dword ptr -14h
.text:64404F4F var_10 = dword ptr -10h
.text:64404F4F var_4 = dword ptr -4
.text:64404F4F
.text:64404F4F push 30h
.text:64404F51 mov eax, offset loc_64438E00
.text:64404F56 call __EH_prolog3
.text:64404F5B mov esi, ecx ; ECX is pointer to object
.text:64404F5D xor ebx, ebx
.text:64404F5F lea ecx, [ebp+var_14]
.text:64404F62 mov [ebp+var_10], ebx
; demangled name: ATL::CStringT(void)
.text:64404F65 call ds:mfc90_316
.text:64404F6B mov [ebp+var_4], ebx
.text:64404F6E lea edi, [esi+2854h]
.text:64404F74 push offset aEnvironmentInf ; "Environment information:\n"
.text:64404F79 mov ecx, edi
; demangled name: ATL::CStringT::operator=(char const *)
.text:64404F7B call ds:mfc90_820
.text:64404F81 cmp [esi+38h], ebx
.text:64404F84 mov ebx, ds:mfc90_2539
.text:64404F8A jbe short loc_64404FA9
.text:64404F8C push dword ptr [esi+34h]
.text:64404F8F lea eax, [ebp+var_14]
.text:64404F92 push offset aWorkingDirecto ; "working directory: '\%s'\n"
.text:64404F97 push eax
; demangled name: ATL::CStringT::Format(char const *,...)
.text:64404F98 call ebx ; mfc90_2539
.text:64404F9A add esp, 0Ch
.text:64404F9D lea eax, [ebp+var_14]
.text:64404FA0 push eax
.text:64404FA1 mov ecx, edi
; demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text:64404FA3 call ds:mfc90_941
.text:64404FA9
.text:64404FA9 loc_64404FA9:
.text:64404FA9 mov eax, [esi+38h]
.text:64404FAC test eax, eax
.text:64404FAE jbe short loc_64404FD3
.text:64404FB0 push eax
.text:64404FB1 lea eax, [ebp+var_14]
.text:64404FB4 push offset aTraceLevelDAct ; "trace level \%d activated\n"
.text:64404FB9 push eax
; demangled name: ATL::CStringT::Format(char const *,...)
.text:64404FBA call ebx ; mfc90_2539
.text:64404FBC add esp, 0Ch
.text:64404FBF lea eax, [ebp+var_14]
.text:64404FC2 push eax
.text:64404FC3 mov ecx, edi
; demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text:64404FC5 call ds:mfc90_941
.text:64404FCB xor ebx, ebx
.text:64404FCD inc ebx
.text:64404FCE mov [ebp+var_10], ebx
.text:64404FD1 jmp short loc_64404FD6
.text:64404FD3
.text:64404FD3 loc_64404FD3:
.text:64404FD3 xor ebx, ebx
.text:64404FD5 inc ebx
.text:64404FD6
.text:64404FD6 loc_64404FD6:
.text:64404FD6 cmp [esi+38h], ebx
.text:64404FD9 jbe short loc_64404FF1
.text:64404FDB cmp dword ptr [esi+2978h], 0
.text:64404FE2 jz short loc_64404FF1
.text:64404FE4 push offset aHexdumpInTrace ; "hexdump in trace activated\n"
.text:64404FE9 mov ecx, edi
; demangled name: ATL::CStringT::operator+=(char const *)
.text:64404FEB call ds:mfc90_945
.text:64404FF1
.text:64404FF1 loc_64404FF1:
.text:64404FF1
.text:64404FF1 cmp byte ptr [esi+78h], 0
.text:64404FF5 jz short loc_64405007
.text:64404FF7 push offset aLoggingActivat ; "logging activated\n"
.text:64404FFC mov ecx, edi
; demangled name: ATL::CStringT::operator+=(char const *)
.text:64404FFE call ds:mfc90_945
.text:64405004 mov [ebp+var_10], ebx
.text:64405007
.text:64405007 loc_64405007:
.text:64405007 cmp byte ptr [esi+3Dh], 0
.text:6440500B jz short bypass
.text:6440500D push offset aDataCompressio ;"data compression switched off\n"
.text:64405012 mov ecx, edi
; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405014 call ds:mfc90_945
.text:6440501A mov [ebp+var_10], ebx
.text:6440501D
.text:6440501D bypass:
.text:6440501D mov eax, [esi+20h]
.text:64405020 test eax, eax
.text:64405022 jz short loc_6440503A
.text:64405024 cmp dword ptr [eax+28h], 0
.text:64405028 jz short loc_6440503A
.text:6440502A push offset aDataRecordMode ; "data record mode switched on\n"
.text:6440502F mov ecx, edi
; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405031 call ds:mfc90_945
.text:64405037 mov [ebp+var_10], ebx
.text:6440503A
.text:6440503A loc_6440503A:
.text:6440503A
.text:6440503A mov ecx, edi
.text:6440503C cmp [ebp+var_10], ebx
.text:6440503F jnz loc_64405142
.text:64405045 push offset aForMaximumData ; "\nFor maximum data security ↙
↘ delete\nthe s"
; demangled name: ATL::CStringT::operator+=(char const *)
.text:6440504A call ds:mfc90_945
.text:64405050 xor edi, edi
.text:64405052 push edi ; fWinIni
.text:64405053 lea eax, [ebp+pvParam]
.text:64405056 push eax ; PvParam
.text:64405057 push edi ; uiParam
.text:64405058 push 30h ; uiAction
.text:6440505A call ds:SystemParametersInfoA
.text:64405060 mov eax, [ebp+var_34]
.text:64405063 cmp eax, 1600
.text:64405068 jle short loc_64405072
.text:6440506A cdq
.text:6440506B sub eax, edx
.text:6440506D sar eax, 1
.text:6440506F mov [ebp+var_34], eax
.text:64405072
.text:64405072 loc_64405072:
.text:64405072 push edi ; hWnd
.text:64405073 mov [ebp+cy], 0A0h
.text:6440507A call ds:GetDC
.text:64405080 mov [ebp+var_10], eax
.text:64405083 mov ebx, 12Ch
.text:64405088 cmp eax, edi
.text:6440508A jz loc_64405113
.text:64405090 push 11h ; i
.text:64405092 call ds:GetStockObject
.text:64405098 mov edi, ds:SelectObject
.text:6440509E push eax ;h
.text:6440509F push [ebp+var_10] ; hdc
.text:644050A2 call edi ; SelectObject
.text:644050A4 and [ebp+rc.left], 0
.text:644050A8 and [ebp+rc.top], 0
.text:644050AC mov [ebp+h], eax
.text:644050AF push 401h ; format
.text:644050B4 lea eax, [ebp+rc]
.text:644050B7 push eax ; lprc
.text:644050B8 lea ecx, [esi+2854h]
.text:644050BE mov [ebp+rc.right], ebx
.text:644050C1 mov [ebp+rc.bottom], 0B4h
; demangled name: ATL::CSimpleStringT::GetLength(void)
.text:644050C8 call ds:mfc90_3178
.text:644050CE push eax ; cchText
.text:644050CF lea ecx, [esi+2854h]
; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:644050D5 call ds:mfc90_910
.text:644050DB push eax ; lpchText
.text:644050DC push [ebp+var_10] ; hdc
.text:644050DF call ds:DrawTextA
.text:644050E5 push 4 ; nIndex
.text:644050E7 call ds:GetSystemMetrics
.text:644050ED mov ecx, [ebp+rc.bottom]
.text:644050F0 sub ecx, [ebp+rc.top]
.text:644050F3 cmp [ebp+h], 0
.text:644050F7 lea eax, [eax+ecx+28h]
.text:644050FB mov [ebp+cy], eax
.text:644050FE jz short loc_64405108
.text:64405100 push [ebp+h] ; h
.text:64405103 push [ebp+var_10] ; hdc
.text:64405106 call edi ; SelectObject
.text:64405108
.text:64405108 loc_64405108:
.text:64405108 push [ebp+var_10] ; hDC
.text:6440510B push 0 ; hWnd
.text:6440510D call ds:ReleaseDC
.text:64405113
.text:64405113 loc_64405113:
.text:64405113 mov eax, [ebp+var_38]
.text:64405116 push 80h ; uFlags
.text:6440511B push [ebp+cy] ; cy
.text:6440511E inc eax
.text:6440511F push ebx ; cx
.text:64405120 push eax ; Y
.text:64405121 mov eax, [ebp+var_34]
.text:64405124 add eax, 0FFFFFED4h
.text:64405129 cdq
.text:6440512A sub eax, edx
.text:6440512C sar eax, 1
.text:6440512E push eax ; X
.text:6440512F push 0 ; hWndInsertAfter
.text:64405131 push dword ptr [esi+285Ch] ; hWnd
.text:64405137 call ds:SetWindowPos
.text:6440513D xor ebx, ebx
.text:6440513F inc ebx
.text:64405140 jmp short loc_6440514D
.text:64405142
.text:64405142 loc_64405142:
.text:64405142 push offset byte_64443AF8
; demangled name: ATL::CStringT::operator=(char const *)
.text:64405147 call ds:mfc90_820
.text:6440514D
.text:6440514D loc_6440514D:
.text:6440514D cmp dword_6450B970, ebx
.text:64405153 jl short loc_64405188
.text:64405155 call sub_6441C910
.text:6440515A mov dword_644F858C, ebx
.text:64405160 push dword ptr [esi+2854h]
.text:64405166 push offset aCdwsguiPrepare ; "\nCDwsGui::PrepareInfoWindow: ↙
↘ sapgui env"...
.text:6440516B push dword ptr [esi+2848h]
.text:64405171 call dbg
.text:64405176 add esp, 0Ch
.text:64405179 mov dword_644F858C, 2
.text:64405183 call sub_6441C920
.text:64405188
.text:64405188 loc_64405188:
.text:64405188 or [ebp+var_4], 0FFFFFFFFh
.text:6440518C lea ecx, [ebp+var_14]
; demangled name: ATL::CStringT::~CStringT()
.text:6440518F call ds:mfc90_601
.text:64405195 call __EH_epilog3
.text:6440519A retn
.text:6440519A CDwsGui__PrepareInfoWindow endp
在执行上述函数的最初几个指令时,数据对象(thiscall)的指针存储于ECX寄存器。[7]本例中,对象明显使用了CDwsGui类。它所记录的选项开关,直接决定着函数窗口的提示信息。
如果地址this+0x3D的值不是0,那么整个程序就会关闭网络数据的压缩功能:
.text:64405007 loc_64405007:
.text:64405007 cmp byte ptr [esi+3Dh], 0
.text:6440500B jz short bypass
.text:6440500D push offset aDataCompressio ; "data compression switched off\n"
.text:64405012 mov ecx, edi
; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405014 call ds:mfc90_945
.text:6440501A mov [ebp+var_10], ebx
.text:6440501D
.text:6440501D bypass:
更有意思的是,决定程序是否显示提示窗口的关键因素是变量var_10:
.text:6440503C cmp [ebp+var_10], ebx
.text:6440503F jnz exit ; bypass drawing
; add strings "For maximum data security delete" / "the setting(s) as soon as possible !":
.text:64405045 push offset aForMaximumData ; "\nFor maximum data security ↙
↘ delete\nthe s"...
.text:6440504A call ds:mfc90_945 ; ATL::CStringT::operator+=(char const *)
.text:64405050 xor edi, edi
.text:64405052 push edi ; fWinIni
.text:64405053 lea eax, [ebp+pvParam]
.text:64405056 push eax ; pvParam
.text:64405057 push edi ; uiParam
.text:64405058 push 30h ; uiAction
.text:6440505A call ds:SystemParametersInfoA
.text:64405060 mov eax, [ebp+var_34]
.text:64405063 cmp eax, 1600
.text:64405068 jle short loc_64405072
.text:6440506A cdq
.text:6440506B sub eax, edx
.text:6440506D sar eax, 1
.text:6440506F mov [ebp+var_34], eax
.text:64405072
.text:64405072 loc_64405072:
start drawing:
.text:64405072 push edi ; hWnd
.text:64405073 mov [ebp+cy], 0A0h
.text:6440507A call ds:GetDC
那么,我们通过实践来验证刚才这些推测吧。
首先找到这个JNZ指令:
.text:6440503F jnz exit ; bypass drawing
把它改为JMP之后,SAPGUI程序就再也不会显示恼人的提示窗口了!
下一步,我们找到load_command_line()函数(函数名称是笔者命名的名字)里偏移量为0x15的数据,以及CDwsGui::PrepareInfoWindow里的变量this+0x3D。这两个值是相等的值么?
为了验证这一猜测,笔者在程序里搜索与偏移量0x15有关的全部指令。在SAPGUI这样的小型程序里,某个规定变量一般只会被同一个文件调用;换而言之,我们不必检索其他文件。
在当前文件里,第一处赋值的指令如下:
.text:64404C19 sub_64404C19 proc near
.text:64404C19
.text:64404C19 arg_0 = dword ptr 4
.text:64404C19
.text:64404C19 push ebx
.text:64404C1A push ebp
.text:64404C1B push esi
.text:64404C1C push edi
.text:64404C1D mov edi, [esp+10h+arg_0]
.text:64404C21 mov eax, [edi]
.text:64404C23 mov esi, ecx ; ESI/ECX are pointers to some unknown object.
.text:64404C25 mov [esi], eax
.text:64404C27 mov eax, [edi+4]
.text:64404C2A mov [esi+4], eax
.text:64404C2D mov eax, [edi+8]
.text:64404C30 mov [esi+8], eax
.text:64404C33 lea eax, [edi+0Ch]
.text:64404C36 push eax
.text:64404C37 lea ecx, [esi+0Ch]
; demangled name: ATL::CStringT::operator=(class ATL::CStringT ... &)
.text:64404C3A call ds:mfc90_817
.text:64404C40 mov eax, [edi+10h]
.text:64404C43 mov [esi+10h], eax
.text:64404C46 mov al, [edi+14h]
.text:64404C49 mov [esi+14h], al
.text:64404C4C mov al, [edi+15h] ; copy byte from 0x15 offset
.text:64404C4F mov [esi+15h], al ; to 0x15 offset in CDwsGui object
上述函数的调用方函数是CDwsGui::CopyOptions。这些名字都是参照调试信息命名的。
但是在整理程序流程之后,我们会发现在“时间上”第一次调用上述函数的调用方函数是CDWsGui::Init():
.text:6440B0BF loc_6440B0BF:
.text:6440B0BF mov eax, [ebp+arg_0]
.text:6440B0C2 push [ebp+arg_4]
.text:6440B0C5 mov [esi+2844h], eax
.text:6440B0CB lea eax, [esi+28h] ; ESI is pointer to CDwsGui object
.text:6440B0CE push eax
.text:6440B0CF call CDwsGui__CopyOptions
综合上述分析可知:由load_command_line()函数填充的数组,是CDwsGui class(类)的一部分。这个数组的地址是this+0x28。0x15+0x28=0x3D。这应该就是数据被传递的终点站。
接下来,我们在程序里查找“使用偏移量0x3D的指令”。我们发现CDwsGui::SapguiRun函数(根据调试信息进行命名)就使用了这个偏移量:
.text:64409D58 cmp [esi+3Dh], bl ; ESI is pointer to CDwsGui object
.text:64409D5B lea ecx, [esi+2B8h]
.text:64409D61 setz al
.text:64409D64 push eax ; arg_10 of CConnectionContext:: ↙
↘ CreateNetwork
.text:64409D65 push dword ptr [esi+64h]
; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64409D68 call ds:mfc90_910
.text:64409D68 ; no arguments
.text:64409D6E push eax
.text:64409D6F lea ecx, [esi+2BCh]
; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64409D75 call ds:mfc90_910
.text:64409D75 ; no arguments
.text:64409D7B push eax
.text:64409D7C push esi
.text:64409D7D lea ecx, [esi+8]
.text:64409D80 call CConnectionContext__CreateNetwork
而后,验证我们的推测:把“setz al”换为“xor eax, eax / nop”指令,清除环境变量TDW_NOCOMPRESS,然后再次运行SAPGUI。此后,令人不快的提示窗口果然不见了,而且Wireshark显示网络包不再压缩了!显然,这种修改可以对CConnectionContext对象的压缩标识进行直接操作。
可见,压缩标识传递到了CConnectionContext::CreateNetwork的第五个参数。不过这个构造函数还调用了其他函数:
...
.text:64403476 push [ebp+compression]
.text:64403479 push [ebp+arg_C]
.text:6440347C push [ebp+arg_8]
.text:6440347F push [ebp+arg_4]
.text:64403482 push [ebp+arg_0]
.text:64403485 call CNetwork__CNetwork
压缩标识接着被传递到构造函数CNetwork::CNetwork的第五个参数。根据这个参数,构造函数CNetwork在对象体CNetwork设置标识、并设置另一个与压缩传输可能有关的变量。这个构造函数的有关操作如下:
.text:64411DF1 cmp [ebp+compression], esi
.text:64411DF7 jz short set_EAX_to_0
.text:64411DF9 mov al, [ebx+78h] ; another value may affect compression?
.text:64411DFC cmp al, '3'
.text:64411DFE jz short set_EAX_to_1
.text:64411E00 cmp al, '4'
.text:64411E02 jnz short set_EAX_to_0
.text:64411E04
.text:64411E04 set_EAX_to_1:
.text:64411E04 xor eax, eax
.text:64411E06 inc eax ; EAX -> 1
.text:64411E07 jmp short loc_64411E0B
.text:64411E09
.text:64411E09 set_EAX_to_0:
.text:64411E09
.text:64411E09 xor eax, eax ; EAX -> 0
.text:64411E0B
.text:64411E0B loc_64411E0B:
.text:64411E0B mov [ebx+3A4h], eax ; EBX is pointer to CNetwork object
综上,我们可以确定CNetwork class存储压缩标识的相对地址是this+0x3A4。
然后我们以0x3A4为着手点,继续分析SAPguilib.dll。这个偏移量再次出现在CDwsGui::OnClientMessageWrite里。当然,笔者还是通过调试信息才能确定构造函数的准确名称:
.text:64406F76 loc_64406F76:
.text:64406F76 mov ecx, [ebp+7728h+var_7794]
.text:64406F79 cmp dword ptr [ecx+3A4h], 1
.text:64406F80 jnz compression_flag_is_zero
.text:64406F86 mov byte ptr [ebx+7], 1
.text:64406F8A mov eax, [esi+18h]
.text:64406F8D mov ecx, eax
.text:64406F8F test eax, eax
.text:64406F91 ja short loc_64406FFF
.text:64406F93 mov ecx, [esi+14h]
.text:64406F96 mov eax, [esi+20h]
.text:64406F99
.text:64406F99 loc_64406F99:
.text:64406F99 push dword ptr [edi+2868h] ; int
.text:64406F9F lea edx, [ebp+7728h+var_77A4]
.text:64406FA2 push edx ; int
.text:64406FA3 push 30000 ; int
.text:64406FA8 lea edx, [ebp+7728h+Dst]
.text:64406FAB push edx ; Dst
.text:64406FAC push ecx ; int
.text:64406FAD push eax ; Src
.text:64406FAE push dword ptr [edi+28C0h] ; int
.text:64406FB4 call sub_644055C5 ; actual compression routine
.text:64406FB9 add esp, 1Ch
.text:64406FBC cmp eax, 0FFFFFFF6h
.text:64406FBF jz short loc_64407004
.text:64406FC1 cmp eax, 1
.text:64406FC4 jz loc_6440708C
.text:64406FCA cmp eax, 2
.text:64406FCD jz short loc_64407004
.text:64406FCF push eax
.text:64406FD0 push offset aCompressionErr ; "compression error [rc = \%d]- program wi" ...
.text:64406FD5 push offset aGui_err_compre ; "GUI_ERR_COMPRESS"
.text:64406FDA push dword ptr [edi+28D0h]
.text:64406FE0 call SapPcTxtRead
由此可见,压缩网络数据的关键函数是 sub_644055C5。这个函数分别调用了 memcpy()函数,以及函数sub_64417440(IDA显示的函数名)。而sub_64417440的指令是:
.text:6441747C push offset aErrorCsrcompre ; "\nERROR: CsRCompress: invalid handle"
.text:64417481 call eax ; dword_644F94C8
.text:64417483 add esp, 4
到此为止,我们完整地分析了压缩网络数据包的函数。参照笔者先前的分析[8]可知,这个网络包压缩函数是SAP和开源项目MaxDB的公用函数(上述两个产品都是SAP开发的)。因此,实际上我们可以找到它的源代码。
最后要分析的是:
.text:64406F79 cmp dword ptr [ecx+3A4h], 1
.text:64406F80 jnz compression_flag_is_zero
把此处的JNZ替换为无条件转移指令JMP,删除环境变量TDW_NOCOMPRESS。瞧!再用WireShark分析网络数据包时,我们就会发现SAPGUI不再压缩网络数据了。
综上,在找到环境变量与数据压缩功能的切合点之后,我们可以强行启用这个功能,也可以强制程序关闭这项功能。
某天,在VMware平台上打开SAP 6.0 IDES时,笔者发现自己忘记SAP账户名了。几经周折找到了账户名之后,我尝试着用常用密码进行登录。结果可想而知,笔者最终遇到了提示信息“Password logon no longer possible-too many failed attempts”,再也无法登录。
好消息是SAP官方提供了完整的disp+work.pdb文件。这个PDB文件涵盖的信息还十分全面:函数名、结构体、类型、局部变量及参数名等等,简直是应有尽有。
为了便于挖掘信息,笔者使用TYPEINFODUMP程序[9],把PDB文件转换为了人类可读的文本文件。
转换后的文本文件含有函数名称、函数参数、局部变量等信息:
FUNCTION ThVmcSysEvent
Address: 10143190 Size: 675 bytes Index: 60483 TypeIndex: 60484
Type: int NEAR_C ThVmcSysEvent (unsigned int, unsigned char, unsigned short*)
Flags: 0
PARAMETER events
Address: Reg335+288 Size: 4 bytes Index: 60488 TypeIndex: 60489
Type: unsigned int
Flags: d0
PARAMETER opcode
Address: Reg335+296 Size: 1 bytes Index: 60490 TypeIndex: 60491
Type: unsigned char
Flags: d0
PARAMETER serverName
Address: Reg335+304 Size: 8 bytes Index: 60492 TypeIndex: 60493
Type: unsigned short*
Flags: d0
STATIC_LOCAL_VAR func
Address: 12274af0 Size: 8 bytes Index: 60495 TypeIndex: 60496
Type: wchar_t*
Flags: 80
LOCAL_VAR admhead
Address: Reg335+304 Size: 8 bytes Index: 60498 TypeIndex: 60499
Type: unsigned char*
Flags: 90
LOCAL_VAR record
Address: Reg335+64 Size: 204 bytes Index: 60501 TypeIndex: 60502
Type: AD_RECORD
Flags: 90
LOCAL_VAR adlen
Address: Reg335+296 Size: 4 bytes Index: 60508 TypeIndex: 60509
Type: int
Flags: 90
不仅如此,它还解释了结构体的有关信息:
STRUCT DBSL_STMTID
Size: 120 Variables: 4 Functions: 0 Base classes: 0
MEMBER moduletype
Type: DBSL_MODULETYPE
Offset: 0 Index: 3 TypeIndex: 38653
MEMBER module
Type: wchar_t module[40]
Offset: 4 Index: 3 TypeIndex: 831
MEMBER stmtnum
Type: long
Offset: 84 Index: 3 TypeIndex: 440
MEMBER timestamp
Type: wchar_t timestamp[15]
Offset: 88 Index: 3 TypeIndex: 6612
此外,调试呼叫(debugging calls)也可提供大量信息。
不久,笔者就注意到设置日志详细程度的全局变量ct_level。SAP官方对这个变量有详细的解释: http://help.sap.com/saphelp_nwpi71/helpdata/en/46/962416a5a613e8e10000000a155369/ content.htm
。
disp+work.exe文件保留了大量的调试信息:
cmp cs:ct_level, 1
jl short loc_1400375DA
call DpLock
lea rcx, aDpxxtool4_c ; "dpxxtool4.c"
mov edx, 4Eh ; line
call CTrcSaveLocation
mov r8, cs:func_48
mov rcx, cs:hdl ; hdl
lea rdx, aSDpreadmemvalu ; "%s: DpReadMemValue (%d)"
mov r9d, ebx
call DpTrcErr
call DpUnlock
如果ctl_level的值大于或等于程序预设的某个阈值,那么程序将会按照相应的详细程度记录dev_wo、 dev_disp等dev-日志文件。
在使用TYPEINFODUMP程序把PDB文件转换为文本文件之后,我们使用grep指令搜索与密码有关的函数名称:
cat "disp+work.pdb.d" | grep FUNCTION | grep -i password
上述指令的返回结果是:
FUNCTION rcui::AgiPassword::DiagISelection
FUNCTION ssf_password_encrypt
FUNCTION ssf_password_decrypt
FUNCTION password_logon_disabled
FUNCTION dySignSkipUserPassword
FUNCTION migrate_password_history
FUNCTION password_is_initial
FUNCTION rcui::AgiPassword::IsVisible
FUNCTION password_distance_ok
FUNCTION get_password_downwards_compatibility
FUNCTION dySignUnSkipUserPassword
FUNCTION rcui::AgiPassword::GetTypeName
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$2
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$0
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$1
FUNCTION usm_set_password
FUNCTION rcui::AgiPassword::TraceTo
FUNCTION days_since_last_password_change
FUNCTION rsecgrp_generate_random_password
FUNCTION rcui::AgiPassword::`scalar deleting destructor'
FUNCTION password_attempt_limit_exceeded
FUNCTION handle_incorrect_password
FUNCTION 'rcui::AgiPassword::`scalar deleting destructor''::'1'::dtor$1
FUNCTION calculate_new_password_hash
FUNCTION shift_password_to_history
FUNCTION rcui::AgiPassword::GetType
FUNCTION found_password_in_history
FUNCTION `rcui::AgiPassword::`scalar deleting destructor''::'1'::dtor$0
FUNCTION rcui::AgiObj::IsaPassword
FUNCTION password_idle_check
FUNCTION SlicHwPasswordForDay
FUNCTION rcui::AgiPassword::IsaPassword
FUNCTION rcui::AgiPassword::AgiPassword
FUNCTION delete_user_password
FUNCTION usm_set_user_password
FUNCTION Password_API
FUNCTION get_password_change_for_SSO
FUNCTION password_in_USR40
FUNCTION rsec_agrp_abap_generate_random_password
根据提示信息,接下来我们在调试信息里搜索关键词“password”和“locked”。略加分析之后,笔者发现password_attempt_limit_exceeded()函数会调用关键字符串“user was locked by subsequently failed password logon attempts”。
这个函数还会在日志文件里记录“password logon attempt will be rejected immediately (preventing dictionary attacks)”“failed-logon lock: expired (but not removed due to ‘read-only’ operation)”以及“failed-logon lock: expired => removed”。
进一步的研究表明,这个函数就是登录保护函数。它会被密码验证函数——chckpass()函数调用。
首先要验证上述推测是否正确。使用笔者开发的tracer程序进行分析:
tracer64.exe -a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode
PID=2236|TID=2248|(0) disp+work.exe!chckpass (0x202c770, L"Brewered1 ", 0x41) (called from ↙
↘ 0x1402f1060 (disp+work.exe!usrexist+0x3c0))
PID=2236|TID=2248|(0) disp+work.exe!chckpass -> 0x35
调用逻辑是syssigni()→DyISigni()→dychkusr()→usrexist()→chckpass()。
数字0x35是chckpass()函数返回的错误信息编号:
.text:00000001402ED567 loc_1402ED567: ; CODE XREF: chckpass+B4
.text:00000001402ED567 mov rcx, rbx ; usr02
.text:00000001402ED56A call password_idle_check
.text:00000001402ED56F cmp eax, 33h
.text:00000001402ED572 jz loc_1402EDB4E
.text:00000001402ED578 cmp eax, 36h
.text:00000001402ED57B jz loc_1402EDB3D
.text:00000001402ED581 xor edx, edx ; usr02_readonly
.text:00000001402ED583 mov rcx, rbx ; usr02
.text:00000001402ED586 call password_attempt_limit_exceeded
.text:00000001402ED58B test al, al
.text:00000001402ED58D jz short loc_1402ED5A0
.text:00000001402ED58F mov eax, 35h
.text:00000001402ED594 add rsp, 60h
.text:00000001402ED598 pop r14
.text:00000001402ED59A pop r12
.text:00000001402ED59C pop rdi
.text:00000001402ED59D pop rsi
.text:00000001402ED59E pop rbx
.text:00000001402ED59F retn
然后进行试验:
tracer64.exe -a:disp+work.exe bpf=disp+work.exe!password_attempt_limit_exceeded,args:4,unicode, rt:0
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0x202c770, 0, 0x257758, 0) ↙
↘ (called from 0x1402ed58b (disp+work.exe!chckpass+0xeb))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded -> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0x202c770, 0, 0, 0) (called ↙
↘ from 0x1402e9794 (disp+work.exe!chngpass+0xe4))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded -> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0
此后我们就可以进行登录了。
顺便提一下,如果忘记密码的话,可以把chckpass()函数的返回值强制改为0,那样它就不会进行密码验证了:
tracer64.exe -a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode,rt:0
PID=2744|TID=360|(0) disp+work.exe!chckpass (0x202c770, L"bogus ", 0x41) (called from 0x1402f1060 ↙
↘ (disp+work.exe!usrexist+0x3c0))
PID=2744|TID=360|(0) disp+work.exe!chckpass -> 0x35
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0
在分析password_attemp_limit_exceeded()函数时,我们可以看到函数的前几行指令是:
lea rcx, aLoginFailed_us ; "login/failed_user_auto_unlock"
call sapgparam
test rax, rax
jz short loc_1402E19DE
movzx eax, word ptr [rax]
cmp ax, 'N'
jz short loc_1402E19D4
cmp ax, 'n'
jz short loc_1402E19D4
cmp ax, '0'
jnz short loc_1402E19DE
很显然,sapgparam()函数的作用是获取配置参数。整个程序有1768处指令调用这个函数。据此推测,只要追踪这个函数的调用关系,就可以分析特定参数对整个程序的影响。
不得不说,SAP要比Oracle RDBMS亲切得多。前者提供的函数名等信息远比后者清晰。不过disp+work程序具有C++程序的特征,莫非官方最近重新编写了它的源程序?
[1] 关于SAP采用的密码传输和指令传输技术,请参见笔者的博客:http://blog.yurichev.com/ node/44http://blog.yurichev.com/node/47。
[2] http://www.farmanager.com/。
[3] 本章演示的程序是SAP GUI v720 for Win32,其版本号为7200,1,0,9009。如果版本不同,那么相关偏移量应当会是不同值。
[4] 这个函数名称是笔者命名的。
[5] https://msdn.microsoft.com/en-us/library/tb2sfw2z(VS.80).aspx。
[6] 返回环境变量的标准C函数。
[7] 这属于thiscall类型的函数。有关thiscall类型的函数,可参见本书51.1.1节。
[8] http://conus.info/utils/SAP_pkt_decompr.txt。
[9] http://www.debuginfo.com/tools/typeinfodump.html。