Page 1 of 1

Reading the registry

Posted: Sun Mar 11, 2012 3:09 pm
by SFSxOI
I still see some code around that uses the old API 'RegQueryValueEx' for reading the registry. In the lab here at work recently, while profiling some newer varients on bot net infected machines we ran across some registry read calls using 'RegQueryValueEx' and saw one of that API's weakensses exposed by the bot net infection vector causing buffer overwrites reading back intentionally placed malformed REG_MULTI_SZ strings. Microsoft alludes to this weakness on the MSDN page for the 'RegQueryValueEx' APi at > http://msdn.microsoft.com/en-us/library ... s.85).aspx > where it states:

"If the data has the REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with the proper terminating null characters. Therefore, even if the function returns ERROR_SUCCESS, the application should ensure that the string is properly terminated before using it; otherwise, it may overwrite a buffer. (Note that REG_MULTI_SZ strings should have two terminating null characters.) One way an application can ensure that the string is properly terminated is to use RegGetValue, which adds terminating null characters if needed."

There are actually several issues with 'RegQueryValueEx', and the more older 'RegQueryValue' is for 16 bit compatability and just as bad (anyone still writing 16 bit code? I hope not). I'd recommend that folks update their registry read routines to use the newer API 'RegGetValue' instead, especially if deploying software with reading registry access capability.

Minimum supported client : Windows Vista, Windows XP Professional x64 Edition
Minimum supported server : Windows Server 2008, Windows Server 2003 with SP1

You will need to load this from the .dll

See example code below, of course you can change it to suit your needs, this is just example code and i threw in a lot. Tested on Windows 7

Code: Select all

#BIGBYTEINCREMENT = 4096
#SMALLBYTEINCREMENT = 1024
#RRF_RT_ANY = $ffff
#RRF_RT_DWORD = $18
#RRF_RT_QWORD = $48 ;Restrict type To 64-bit #RRF_RT_REG_BINARY | #RRF_RT_REG_DWORD.
#RRF_RT_REG_BINARY = $8
#RRF_RT_REG_DWORD = $10
#RRF_RT_REG_EXPAND_SZ = $4
#RRF_RT_REG_MULTI_SZ = $20
#RRF_RT_REG_NONE = $1
#RRF_RT_REG_QWORD = $40
#RRF_RT_REG_SZ = $2
#RRF_NOEXPAND = $10000000 ;Do Not automatically expand environment strings If the value is of type REG_EXPAND_SZ.
#RRF_ZEROONFAILURE = $20000000 ; RegGetValue - If pvData is not NULL, set the contents of the buffer to zeroes on failure

Prototype PRegGetValue(hkey, lpSubKey, lpValue, dwFlags, pdwType, pvData, pcbData)
Global RegGetValue.PRegGetValue

Lib_Advapi32 = OpenLibrary(#PB_Any,"Advapi32.dll")
If IsLibrary(Lib_Advapi32)
  CompilerIf #PB_Compiler_Unicode ; If compiled in unicode
    RegGetValue.PRegGetValue=GetFunction(Lib_Advapi32,"RegGetValueW")
  CompilerElse ; if not compiled in unicode
    RegGetValue.PRegGetValue=GetFunction(Lib_Advapi32,"RegGetValueA")
  CompilerEndIf
EndIf

Procedure.s ShowAPIError(CheckReturnValue) 
  Buffer.s = Space(4096) 
  NumberOfChars = FormatMessage_(#FORMAT_MESSAGE_FROM_SYSTEM, #Null, CheckReturnValue, #Null, Buffer.s, Len(Buffer.s), #Null) 
  ProcedureReturn Left(Buffer.s, NumberOfChars-2) 
EndProcedure

Procedure.q TopHiveKey(HiveKeyConvert$)
  Protected HiveKeyx.q
  
  HiveKeya$=StringField(HiveKeyConvert$,1,"\")
  HiveKeyTop$=UCase(HiveKeya$)
  
  Select HiveKeyTop$
    Case "HKEY_CLASSES_ROOT"
      HiveKeyx = #HKEY_CLASSES_ROOT 
    Case "HKEY_CURRENT_USER"
      HiveKeyx = #HKEY_CURRENT_USER
    Case "HKEY_LOCAL_MACHINE"
      HiveKeyx = #HKEY_LOCAL_MACHINE
    Case "HKEY_USERS"
      HiveKeyx = #HKEY_USERS
    Case "HKEY_CURRENT_CONFIG"
      HiveKeyx = #HKEY_CURRENT_CONFIG
  EndSelect
  
  ProcedureReturn HiveKeyx
EndProcedure


Procedure.s KeyConvert(KeyToConvert$)
  GetBackSlash=FindString(KeyToConvert$,"\",1)
  KeyNameX$=Right(KeyToConvert$,(Len(KeyToConvert$)-GetBackSlash))
  If Left(KeyNameX$, 1) = "\" 
    KeyNameX$ = Right(KeyNameX$, Len(KeyNameX$) - 1) 
  EndIf
  ProcedureReturn KeyNameX$
EndProcedure

; gets value data for REG_BINARY, REG_QWORD, REG_SZ, REG_EXPAND_SZ, REG_BINARY, and REG_MULTI_SZ types
Procedure.s Reg_GetValue(Key.s, ValueName.s)
  ; using newer API RegGetValue:
  ; Older RegQueryValueEx API suffered from many issues and was possible security vunlerability. 
  ; The biggest issue was that it didn't adequately type check the data being returned.
  ; If the data for REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with the proper null-terminating characters and
  ; this could allow buffer overwrite
  ; In addition REG_MULTI_SZ strings should have two null-terminating characters, but the older RegQueryValueEx function only attempted to add one
  ; causing more code being needed to account for this.
  ; The newer API RegGetValue doesn't have these issues
  ; RegGetValue takes care of opening and closing the key thus no more RegOpenKeyEx needed
  ; RegGetValue automatically handles type REG_EXPAND_SZ.
  ; RegGetValue ensures proper null termination of registry string
  ; RegGetValue validates the length of the registry string was a multiple of 2.
  Protected pdwType.i, pcbData.i, pcbDatb.i, hKey.i, HiveKey.q, FuncRet.i, Type.i, pDWORD.i, REG_SBuf.s    
  
  pcbData = #SMALLBYTEINCREMENT; initial buffer size
  
  HiveKey = TopHiveKey(Key)
  KeyName$ = KeyConvert(Key)
  
  GetValue$ = ""
  *RegBinary = #Null
  *qResult = #Null
  REG_SBuf = ""
  REG_SBuf = Space(pcbData)
  
  FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_ANY, @Type, @REG_SBuf, @pcbData);; get type and buffer size
  ; regardless of actual buffer contents or type, this still gets the correct buffer size, so if we need more get it before selecting
  If FuncRet = #ERROR_MORE_DATA
    While FuncRet = #ERROR_MORE_DATA 
      pcbData = pcbData + #SMALLBYTEINCREMENT
      REG_SBuf = Space(pcbData)
      FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_ANY, #Null, @REG_SBuf, @pcbData) ; and uses the function again to test for enough buffer size
    Wend
  EndIf
  If FuncRet = #ERROR_FILE_NOT_FOUND
    ProcedureReturn "Reg_GetValue - The requested registry key path - " + Key + " - or requested value - " + ValueName + " - not found."
  EndIf
  
  If FuncRet = #ERROR_SUCCESS
    Select Type
      Case #REG_SZ
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_SZ, #Null, @REG_SBuf, @pcbData)
        GetValue$ = REG_SBuf
        REG_SBuf = ""
      Case #REG_MULTI_SZ
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_MULTI_SZ, #Null, @REG_SBuf, @pcbData)
        For MzCount.i = @REG_SBuf To @REG_SBuf + pcbData -2
          If PeekB(MzCount)=0
            GetValue$ + Chr(13) + "  "
          Else
            GetValue$ + Chr(PeekB(MzCount))
          EndIf
        Next
        REG_SBuf = ""
      Case #REG_EXPAND_SZ
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_EXPAND_SZ, #Null, @REG_SBuf, @pcbData)
        GetValue$ = REG_SBuf
        REG_SBuf = ""
      Case #REG_BINARY ; some REG_BINARY data can be really big and take a longgg time to read out - for example "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache" - the "AppCompatCache" value can be really big and take a few minutes to get
        If pcbData > 0 ; special case - need to check binary data not zero-length or *RegBinary will crash with a zero buffer size
          *RegBinary=AllocateMemory(pcbData)
          FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_BINARY, #Null, *RegBinary, @pcbData)
          For BinCount.i = 0 To (pcbData-1)
            BinVar=PeekB(*RegBinary+BinCount)&$000000FF
            If BinVar<16
              GetValue$+"0"
            EndIf
            GetValue$ = GetValue$+ Hex(BinVar) + " "
          Next
          FreeMemory(*RegBinary)
          *RegBinary = #Null
        Else
          ProcedureReturn "Warning!!!! value - " + ValueName + " - at " + Key + " is zero-length binary data."
        EndIf
      Case #REG_QWORD
        *qResult=AllocateMemory(pcbData)
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_QWORD, #Null, *qResult, @pcbData)
        GetValue$ = "0x" + Hex(PeekQ(*qResult)) + " (" + Str(PeekQ(*qResult)) + ")"
        FreeMemory(*qResult)
        *qResult = #Null
      Case #REG_DWORD
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_DWORD, #Null, @pDWORD, @pcbData)
        GetValue$ = Str(pDWORD)
    EndSelect
    ProcedureReturn GetValue$
  Else
    ProcedureReturn "Unable to get data for value - " + ValueName + " - at " + Key + " due to : " + ShowAPIError(FuncRet)
  EndIf
  
EndProcedure

;some examples of usage below
REG_QWORD, REG_SZ, REG_EXPAND_SZ, REG_BINARY, And REG_MULTI_SZ types   
; get a REG_QWORD
;Debug Reg_GetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winsat", "TimeLastFormalAssessment") ; Winsat on Vista and above
; get a REG_QWORD
Debug Reg_GetValue("HKEY_CURRENT_USER\Control Panel\Appearance\New Schemes\0\Sizes\0", "Size #0")
;get a REG_SZ
Debug Reg_GetValue("HKEY_CURRENT_USER\Control Panel\Colors", "ActiveBorder")
; get a REG_EXPAND_SZ
Debug Reg_GetValue("HKEY_CURRENT_USER\Control Panel\Cursors", "AppStarting")
; get a REG_DWORD
Debug Reg_GetValue("HKEY_CURRENT_USER\Control Panel\Cursors", "Scheme Source")
; get a #REG_BINARY
Debug Reg_GetValue("HKEY_CURRENT_USER\Control Panel\Appearance", "SchemeLangID")
Oh, BTW, if you have the Droopy library installed then you will get an error telling you the parameters are wrong. This is because the Droopy library uses the 'RegGetValue' name for its own procedure in which it uses the 'RegQueryValueEx' API and not the 'RegGetValue' API. So if you keep the Drooppy Library installed then you will need to do a little renaming in the above to use it, like this for example:

Code: Select all

;note the 'x' added to RegGetValue to make it RegGetValuex - just rename left of the dot period side, not right of the dot period
Prototype PRegGetValue(hkey, lpSubKey, lpValue, dwFlags, pdwType, pvData, pcbData)
Global RegGetValuex.PRegGetValue

Lib_Advapi32 = OpenLibrary(#PB_Any,"Advapi32.dll")
If IsLibrary(Lib_Advapi32)
  CompilerIf #PB_Compiler_Unicode ; If compiled in unicode
    RegGetValuex.PRegGetValue=GetFunction(Lib_Advapi32,"RegGetValueW")
  CompilerElse ; if not compiled in unicode
    RegGetValuex.PRegGetValue=GetFunction(Lib_Advapi32,"RegGetValueA")
  CompilerEndIf
EndIf

then in your code you would call RegGetValuex and not RegGetValue

Re: Reading the registry

Posted: Sun Mar 11, 2012 4:00 pm
by SFSxOI
Note on the above:

I give a little special treatment to the REG_BINARY type because its about the only reg type that can not be zero length. A REG_BINARY value should not be zero-length - if it is then indicates a possible issue, if no data for it then at least be set to all 0's, or removed, but don't remove without investigation (unless its not supposed to be there) as some software packages may check for their software specific use presence even if all 0's and use it for various things. If your going to write to a REG_BINARY value make sure at least set to 0's if nothing else. Zero-length suddenly could indicate malicious software activity (especially if value was known to be good previously), could also indicate an issue with users system, lack of proper install permissions, poorly coded software, or registry corruption issue.

The security vunlerability with RegQueryValueEx was that if the value in the registry is exactly 512 bytes long the buffer isn't null terminated.

The buffer overwrite mentioned above can actually be a buffer over run, anyway, when it overwrites "a buffer" it doesn't necessarily mean its your own applications registry read routine buffer so you can see where the problem lies with that. :)

RegGetValue fixes all this.

For XP SP2, if RegGetValue is not available then use SHRegGetValue in shlwapi.dll in place of RegGetValue.

Re: Reading the registry

Posted: Sat Dec 07, 2013 6:07 am
by IdeasVacuum
[PB5.21LTS]

Works great on Win8.1 x64 (32bit app).

....but it fails on WinXP SP3 x86:

Code: Select all

FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_ANY, @Type, @REG_SBuf, @pcbData)
PB Compiler: [ERROR] Invalid memory access. (read error at address 0)

Re: Reading the registry

Posted: Sat Dec 07, 2013 11:31 am
by luis
IdeasVacuum wrote: ....but it fails on WinXP SP3 x86:
You should always test the returned address when importing an api.

Debug RegGetValue

Is it zero ?

SFSxOI wrote: Minimum supported client : Windows Vista, Windows XP Professional x64 Edition
Minimum supported server : Windows Server 2008, Windows Server 2003 with SP1

According to the above, probably it is.

Re: Reading the registry

Posted: Sat Dec 07, 2013 5:16 pm
by IdeasVacuum
Minimum supported client : Windows Vista, Windows XP Professional x64 Edition
Minimum supported server : Windows Server 2008, Windows Server 2003 with SP1
aha - if I learnt how to read, that would help!

Re: Reading the registry

Posted: Sat Dec 07, 2013 5:36 pm
by rsts
Nice addition to the toolkit. XP users are a pretty small subset of Windows users these days and getting smaller all the time.

Thanks for sharing.

Re: Reading the registry

Posted: Sun Dec 08, 2013 5:25 am
by IdeasVacuum
XP users are a pretty small subset of Windows users these days
.... That depends on whose statistics you believe! :mrgreen: (nearly 60% of my customers are still using WinXP)

Code: Select all

      Case #REG_MULTI_SZ
        FuncRet = RegGetValue(HiveKey, @KeyName$, @ValueName, #RRF_RT_REG_MULTI_SZ, #Null, @REG_SBuf, @pcbData)
        For MzCount.i = @REG_SBuf To @REG_SBuf + pcbData -2
          If PeekB(MzCount)=0
            GetValue$ + Chr(13) + "  "
          Else
            GetValue$ + Chr(PeekB(MzCount))
          EndIf
        Next
        REG_SBuf = ""
In my test on Win8.1, compiled as Unicode, the above code for multi-string data worked. Not sure that is solid though, since a UTF-8 Unicode Char can be up to 4bytes, in which case Chr(PeekB(MzCount)) would fail?
Edit: Just noticed that I had changed PeekB to PeekC, but even so, chars above 2bytes in size could be an issue.....
In fact I made my own issue too :oops: I changed:
If PeekB(MzCount) = 0
to
If PeekC(MzCount) = 0
That works well, if the test used PeekB then GetValue$ + Chr(13) + " " inserts a space in-between chars. However, if I also change:
GetValue$ + Chr(PeekB(MzCount))
to
GetValue$ + Chr(PeekC(MzCount))
Some chars in the range 1-127 are returned with incorrect glyphs.

Re: Reading the registry

Posted: Sun Dec 08, 2013 11:22 am
by PB
> XP users are a pretty small subset of Windows users these days

:shock: It's still Number One from 2008 to today! (According to StatCounter).

[Edit] Image removed. I shouldn't have hijacked the thread. Sorry!

Re: Reading the registry

Posted: Sun Dec 08, 2013 2:26 pm
by rsts
PB wrote:> XP users are a pretty small subset of Windows users these days

:shock:
It's still Number One from 2008 to today!
2008 was five years ago. Check a reliable source for 2013 and you'll get a smaller number.
But have it your way :D

http://en.wikipedia.org/wiki/Usage_shar ... ng_systems
http://news.cnet.com/8301-10805_3-57614 ... -os-users/

Regardless, it's a great tip.

Re: Reading the registry

Posted: Sun Dec 08, 2013 4:18 pm
by IdeasVacuum
ah, but those figures are completely flawed rsts - they are gathered from internet usage, and business PCs often do not have any access to the internet! I know of one manufacturing industry company alone that has thousands of PCs and none of them are connected to the Internet. This is one of the reasons why Microsoft should not be so confident that Win8 sales will shoot upwards when their support of WinXP ends in 2014 - those machines standalone, they are not going to catch a virus from the internet and only the IT department can install apps on them (typically, the app list is minuscule). Those machines still have years of service in them.

Re: Reading the registry

Posted: Mon Dec 09, 2013 2:13 am
by IdeasVacuum

Code: Select all

While FuncRet = #ERROR_MORE_DATA 
That loop needs a finite cap to stop it failing the app when the registry value has a fault.