Archive: System Plugin and netapi32


System Plugin and netapi32
I have created a small tool that provides a GUI for gathering information in order to create a user account in Windows. I figured it would be a good idea to make direct API calls and get to understand how the system plugin works ... After going over the source of the UserManager plugin as well as several MSDN pages I am having trouble getting this to work.

In the first part of my code I am using InstallOptionsEx in order to create the GUI then I gather all the info into variables (working part, not shown here). The second part however that is handling the user creation is not working:


; I am using some default definitions straight out of Lmaccess.h
!define UF_SCRIPT 0x000001
!define UF_NORMAL_ACCOUNT 0x000200
!define UF_DONT_EXPIRE_PASSWD 0x010000
!define USER_PRIV_USER 0x000001

; First I get the computername as ComputerNamePhysicalNetBIOS:
System::Call 'kernel32.dll::GetComputerNameExW(i 4, w .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2'
StrCpy $TargetServerName "\\$0"

; Then I am constructing the USER_INFO_1 structure
StrCpy $1 "$TargetServerName"
StrCpy $2 1
StrCpy $R1 "$UserName"
StrCpy $R2 "$Password"
StrCpy $R3 ${USER_PRIV_USER}
StrCpy $R4 "$UserDescription"
System::Int64Op ${UF_SCRIPT} + ${UF_NORMAL_ACCOUNT}
Pop $R5
System::Int64Op $R5 + ${UF_DONT_EXPIRE_PASSWD}
Pop $R5
System::Call '*(w R1, w R2, i 0, i R3, n, w R4, i R5, n)i.s'
Pop $R6

; Then I call the NetAddUser function from netapi32.dll
System::Call 'netapi32.dll::NetUserAdd(w r1, i 1, i R6, i.r3) i.r4 ?e'

The above gives me error 997 (ERROR_IO_PENDING) and $4 gives me error 87 (INVALID_PARAMETER)
If I make the call like this:

System::Call 'netapi32.dll::NetUserAdd(w r1, i 1, *i R6, i.r3) i.r4 ?e'

then I get error 997 (ERROR_IO_PENDING) and $4 gives me error 2202 (ERROR_BAD_USERNAME)

Note that all the variables used have been declared. Also I am using some error checking to make sure that the username and password have correct lengths.

Any help will be much appreciated ;)

CF

More info about the USER_INFO_1 structure can be found here
The NetUserAdd function is described here

Right ...
Although I am allocating space for the structure I am not putting any data into it, silly me. However I am still getting the same error (997 - ERROR_IO_PENDING) when I use this code:


!define UF_SCRIPT 0x000001
!define UF_NORMAL_ACCOUNT 0x000200
!define UF_DONT_EXPIRE_PASSWD 0x010000
!define USER_PRIV_USER 0x000001
System::Call 'kernel32.dll::GetComputerNameExW(i 4, w .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2'
StrCpy $TargetServerName "\\$0"
StrCpy $1 "$TargetServerName"
StrCpy $2 1
StrCpy $R1 "$UserName"
StrCpy $R2 "$Password"
StrCpy $R3 ${USER_PRIV_USER}
StrCpy $R4 "$UserDescription"
System::Int64Op ${UF_SCRIPT} + ${UF_NORMAL_ACCOUNT}
Pop $R5
System::Int64Op $R5 + ${UF_DONT_EXPIRE_PASSWD}
Pop $R5
System::Call '*(w R1, w R2, n, i R3, n, w R4, i R5, n) i.s'
Pop $R6
System::Call '*$R6(w R1, w R2, n, i R3, n, w R4, i R5, n)'

System::Call 'netapi32.dll::NetUserAdd(w r1, i 1, *i R6, i.r3) i.r4 ?e'

As for the string values in the structure, they have to be unicode and I am not sure if setting them as 'w Rx' is the way to go ...

Any ideas?
Thanks in advance

CF

$R6 already contains the pointer to your buffer. You shouldn't pass it as a pointer or you'll get a pointer to the pointer. Use just `i` instead of `*i`. On the same issue, in the fourth parameter, you should use `*i` instead of `i`.


Thanks for the info kichik :)

Is it sufficient to pass the LPWSTR values to the structure as 'w Rx' since they should be unicode?

StrCpy R1 "$UserName"
; ... and so on for R2,3,...
!define strUSERINFO '(w, w, n, i, n, w, i, n) i'
System::Call '*${strUSERINFO}(R1,R2,,R3,,R4,R5,).s'
or should I create a buffer for each parameter, then feed the value in and then point to that buffer from within the structure?
StrLen $9 "$UserName"
IntOp $9 $9 + 1
System::Call '*(&w$9 "$UserName")i.R1'
;... and so on for R2,3,...
!define strUSERINFO '(i, i, n, i, n, i, i, n) i'
System::Call '*${strUSERINFO}(R1,R2,,R3,,R4,R5,).s'

Thanks in advance

CF

'w' should be good enough.


Hmmm ... Still error 997.
Here is the part of the code that I am running:

SetCompressor /SOLID lzma
!include "MUI.nsh"
!include "LogicLib.nsh"

OutFile "Test.exe"
InstallDir "$TEMP"

Var "UserFirstName"
Var "UserLastName"
Var "UserDescription"
Var "UserName"
Var "Password"
Var "UnlimitedPass"
Var "TargetServerName"

!define UF_SCRIPT 0x000001
!define UF_NORMAL_ACCOUNT 0x000200
!define UF_DONT_EXPIRE_PASSWD 0x010000

!define USER_PRIV_GUEST 0x000000
!define USER_PRIV_USER 0x000001
!define USER_PRIV_ADMIN 0x000002

Section -Boo
SectionEnd

Function .onInit
StrCpy $UserFirstName "First Name"
StrCpy $UserLastName "Last Name"
StrCpy $UserDescription "Long user description"
StrCpy $UserName "TemplateUser"
StrCpy $Password "123tgfdds"
StrCpy $UnlimitedPass 1
Call GetServerName
Call CreateNewUser
MessageBox MB_OK|MB_ICONINFORMATION 'The Error Code from the NetAPI command was "$0" and got "$3" and "$4"'
Quit
FunctionEnd

Function GetServerName
System::Call 'kernel32.dll::GetComputerNameExW(i 4, w .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2'
${If} $2 = 1
StrCpy $TargetServerName "\\$0"
${Else}
MessageBox MB_OK|MB_ICONSTOP 'Could not determine the computer name!$\nAborting...'
Quit
${EndIf}
System::Free $0
FunctionEnd

Function CreateNewUser
StrCpy $1 "$TargetServerName"
StrCpy $2 1
!define strUSERINFO '(w, w, n, i, n, w, i, n) i'
StrCpy $R1 "$UserName"
StrCpy $R2 "$Password"
StrCpy $R3 ${USER_PRIV_USER}
StrCpy $R4 "$UserDescription"
System::Int64Op ${UF_SCRIPT} + ${UF_NORMAL_ACCOUNT}
Pop $R5
${If} $UnlimitedPass = 1
System::Int64Op $R5 + ${UF_DONT_EXPIRE_PASSWD}
Pop $R5
${EndIf}
System::Call '*${strUSERINFO}(R1,R2,,R3,,R4,R5,) i.s'
Pop $R6
System::Call '*$R6${strUSERINFO}(R1,R2,,R3,,R4,R5,)'
System::Call 'netapi32.dll::NetUserAdd(w r1, i 1, i R6, *i.r3) i.r4 ?e'
Pop $0
FunctionEnd


? :igor:
CF

According to MSDN, NetUserAdd doesn't set the last error. The documentation doesn't mention GetLastError. 997 means nothing. The real error is returned by the function itself and set to $4 in your script. 87, which is the real error code, is ERROR_INVALID_PARAMETER.

The real problems is with the `n` in the structure definition. `n` is not a valid type, it's only a valid value. Replace it with `i` and it'll work.


:) It works!
Thanks a lot kichik!
Please add me to the (long) list of people that you made happy in this forum :D

CF


Hmmm ...
Adding the user and changing some data works but I am stuck when I try to add the user to a group.
Here is the part that works:

SetCompressor /SOLID lzma
!include "MUI.nsh"
!include "LogicLib.nsh"

OutFile "Test.exe"
InstallDir "$TEMP"

Var "UserFirstName"
Var "UserLastName"
Var "UserDescription"
Var "UserName"
Var "Password"
Var "UnlimitedPass"
Var "TargetServerName"

!define UF_SCRIPT 0x000001
!define UF_NORMAL_ACCOUNT 0x000200
!define UF_DONT_EXPIRE_PASSWD 0x010000
!define USER_PRIV_USER 0x000001
!define USER_TYPE_1 1
!define USER_INFO_23 23
!define USER_TYPE_1011 1011

!define strUSER_TYPE_1 '(w,w,i,i,i,w,i,i)i'
!define strUSER_TYPE_1011 '(w)i'

Section -Boo
SectionEnd

Function .onInit
StrCpy $UserFirstName "First Name"
StrCpy $UserLastName "Last Name"
StrCpy $UserDescription "Long user description"
StrCpy $UserName "TemplateUser"
StrCpy $Password "123tgfdds"
StrCpy $UnlimitedPass 1
Call GetServerName
Call CreateNewUser
Call SetUserFullName
Quit
FunctionEnd

Function GetServerName
System::Call 'kernel32.dll::GetComputerNameExW(i 4, w .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2'
${If} $2 = 1
StrCpy $TargetServerName "\\$0"
${Else}
MessageBox MB_OK|MB_ICONSTOP 'Could not determine the computer name!$\nAborting...'
Quit
${EndIf}
System::Free $0
FunctionEnd

Function CreateNewUser
StrCpy $1 "$TargetServerName"
StrCpy $2 ${USER_TYPE_1}
StrCpy $R1 "$UserName"
StrCpy $R2 "$Password"
StrCpy $R3 ${USER_PRIV_USER}
StrCpy $R4 "$UserDescription"
System::Int64Op ${UF_SCRIPT} + ${UF_NORMAL_ACCOUNT}
Pop $R5
${If} $UnlimitedPass = 1
System::Int64Op $R5 + ${UF_DONT_EXPIRE_PASSWD}
Pop $R5
${EndIf}
System::Call '*${strUSER_TYPE_1}(R1,R2,n,R3,n,R4,R5,n) i.s'
Pop $R6
System::Call '*$R6${strUSER_TYPE_1}(R1,R2,,R3,,R4,R5,)'
System::Call 'netapi32.dll::NetUserAdd(w r1, i r2, i R6, *i.r3) i.r4'
FunctionEnd

Function SetUserFullName
StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"
StrCpy $3 ${USER_TYPE_1011}
StrCpy $R1 "$UserFirstName $UserLastName"
System::Call '*${strUSER_INFO_1011}(R1)i.s'
Pop $R2
System::Call '*$R2${strUSER_INFO_1011}(R1)'
System::Call 'netapi32.dll::NetUserSetInfo(w r1, w r2, i r3, i R2, *i.r4) i.r5'
FunctionEnd

I tried using the NetGroupAddUser function I get error 2220 on $4 (Group Name Cannot be found) and I suspect that this function is only for domain groups (although you would expect a fancier name like AddUserToDomainGroup or something :D )
Function AddUserToGroup
StrCpy $1 "$TargetServerName"
StrCpy $2 "USERS"
StrCpy $3 "$UserName"
System::Call 'netapi32.dll::NetGroupAddUser(w r1, w r2, w r3)i.r4'
FunctionEnd

Then I tried to use the NetLocalGroupAddMembers function. If I understand correctly, I need to allocate a buffer that will contain the LOCALGROUP_MEMBERS_INFO_0 structure, which in turn should contain the SID structure of the user-account ... To get the SID I would have to call the NetUserGetInfo function and allocate a buffer for the User_Info_23 structure which in turn contains a pointer to the SID structure (and this should have been pre-allocated) :hang:
Something like this:
System::Call 'netapi32.dll::NetApiBufferAllocate(&w${NSIS_MAX_STRLEN}, *i.R9)'
System::Call '*(&w${NSIS_MAX_STRLEN},&w${NSIS_MAX_STRLEN},\
&w${NSIS_MAX_STRLEN},&i${NSIS_MAX_STRLEN},&i${NSIS_MAX_STRLEN})i.R8'
StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"
StrCpy $3 ${USER_INFO_23}
System::Call 'netapi32.dll::NetUserGetInfo(w r1, w r2, i r3, i R8)i.r4'
System::Call '*$R8(w.R1,w.R2,w.R3,i.R4,i.R5)'
System::Call '*$R9(w.R6)'
MessageBox MB_OK|MB_ICONINFORMATION 'Got the following Information:$\n\
Username: "$R1"$\nFull Name: "$R2"$\n\
User Comment: "$R3"$\nUser Flags: "$R4"$\n\
User SID: "$R5"$\nStored SID "$R6"'
System::Call 'netapi32.dll::NetApiBufferFree(i R9)'

However R1-6 are filled with gibberish after running the above code...
:igor:

Any ideas?
CF

Your call to NetApiBufferAllocate is invalid. The ampersand prefix is only available on structures and should it be an integer anyway and not a wide string. The allocation of the USER_INFO_23 structure is redundant. NetUserGetInfo allocates it for you. For this, its last parameter must be a pointer and not just a simple integer pointing to your allocated structure.

StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"

System::Call 'netapi32::NetUserGetInfo(w r1, w r2, i 23, *i .R8) i.r4'
System::Call '*$R8(w.R1,w.R2,w.R3,i.R4,i.R5)'

System::Call 'netapi32::NetApiBufferFree(i R8)'

DetailPrint $R1
DetailPrint $R2
DetailPrint $R3
DetailPrint $R4
DetailPrint $R5
BTW, according to MSDN, USER_INFO_23 is only available on XP and above. You might want to use LOCALGROUP_MEMBERS_INFO_3 instead.

Makes sense ;)
However I am still a bit confused.
The reason I am using the USER_INFO_23 structure for the NetUserGetInfo is that this one points to an SID. The other option would be the USER_INFO_20 structure which points to an RID. In both cases your code gives me in R5 the buffer where the SID/RID is stored so I have to call

System::Call '*$R5(i .R7)'
to get the actual SID/RID out to $R7. In the case of the RIDs, I get the 4 last digits of a user's SID, and this number is different for different users.
In the case of the SIDs, if I make the call as an 'i' the output is always the same for any user (1281). If I make the call as an 'l' then the output is different for different users. Which brings me to the real question (which explains in part why I was trying to allocate buffers with NetApiBufferAllocate as w) what is the format of the SID that I should expect? Does it matter, since I will feed it to the NetLocalGroupAddMembers function or should I first transform it somehow to a readable quantity?

Isn't LOCAL_GROUPMEMBERS_INFO_3 supposed to be available only to XP etc clients? The only reason I was not going to use it is that it contains the information in the format
<DomainName>\<AccountName>
and I assumed that it is used only in a domain and not on a single workstation ...

Thanks for all the help by the way, without your comments I would have spent way much more time in this, probably to no good end ;)

CF

$R5 is a pointer to the SID, it's what you need. You don't want to dereference it. LOCALGROUP_MEMBERS_INFO_0 has only one member which is a pointer to a SID. You don't need to mess with the internals of the SID. All you need is the pointer.

LOCAL_GROUPMEMBERS_INFO_3 is available on NT and 2000 as well as XP. It does seem to require a domain name, so it's probably not good enough. You find another way to get the user's SID. You can get the RID using USER_INFO_20 and assemble the SID on your own with GetSidIdentifierAuthority, AllocateAndInitializeSid, GetSidSubAuthority and GetSidSubAuthorityCount. Count the sub-authorities using GetSidSubAuthorityCount, get all of them using GetSidSubAuthority, get the authority identifier using GetSidIdentifierAuthority and allocate your new SID with the those authorities and the RID using AllocateAndInitializeSid.


I just saw your reply while I was going to write that I finally figured it out :)
I'll get the code shaped up a bit and I'll post it here.

kichik I appreciate all your help :)
Thank you very very very much :D
CF


After a lot of messing around with those netapi32 functions I managed to get them to work.
Needless to say that without kichik's comments I would probably be still trying ...

So here is the full story if anyone wants to use those in the future.

Some background:
I wanted to be able to create a user and specify parameters that were not used in the UserMgr plugin like making the password to not expire or prevent the user from changing it. In order to achieve this I had to use netapi32.dll and its functions (You can get all the info needed for the netapi32 functions through this MSDN page).

Step 0 - define constants and variables
Here are the constants that are used by the NetAPI functions that I am calling, along with some variables that you may or may not want to use:


;Possible user flags
!define UF_SCRIPT 0x000001
!define UF_ACCOUNTDISABLE 0x000002
!define UF_HOMEDIR_REQUIRED 0x000008
!define UF_LOCKOUT 0x000010
!define UF_PASSWD_NOTREQD 0x000020
!define UF_PASSWD_CANT_CHANGE 0x000040
!define UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED 0x000080
!define UF_TEMP_DUPLICATE_ACCOUNT 0x000100
!define UF_NORMAL_ACCOUNT 0x000200
!define UF_INTERDOMAIN_TRUST_ACCOUNT 0x000800
!define UF_WORKSTATION_TRUST_ACCOUNT 0x001000
!define UF_SERVER_TRUST_ACCOUNT 0x002000
!define UF_DONT_EXPIRE_PASSWD 0x010000
!define UF_MNS_LOGON_ACCOUNT 0x020000
!define UF_SMARTCARD_REQUIRED 0x040000
!define UF_TRUSTED_FOR_DELEGATION 0x080000
!define UF_NOT_DELEGATED 0x100000
!define UF_USE_DES_KEY_ONLY 0x200000
!define UF_DONT_REQUIRE_PREAUTH 0x400000
!define UF_PASSWORD_EXPIRED 0x800000

; User Types
!define USER_TYPE_1 1
!define USER_TYPE_1011 1011

; USER_INFO Types
!define USER_INFO_23 23

; Local Group levels
!define LOCALGROUP_MEMBERS_INFO_0 0

; User level of privilege
!define USER_PRIV_GUEST 0x000000
!define USER_PRIV_USER 0x000001
!define USER_PRIV_ADMIN 0x000002

; Structure Definitions
!define strUSER_INFO_1 '(w,w,i,i,w,w,i,w)i'
!define strUSER_INFO_1011 '(w)i'

;Variables
Var "TargetServerName"
Var "UserName"
Var "UserPassword"
Var "UserFirstName"
Var "UserLastName"
Var "UserComment"
Var "MakeAdmin"
Var "UnlimitedPass"

Step 1 - Get the computer name
This step may be redundant since you can use a null name and then the local machine will be the target system. Here is a function that will get the computer name and will place it on the $TargetServerName variable (you need to include LogicLib.nsh for this):
Function GetComputerNameAPI
System::Call 'kernel32.dll::GetComputerNameExW(i 4, w .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2'
${If} $2 = 1
StrCpy $TargetServerName "\\$0"
${Else}
System::Call "kernel32.dll::GetComputerNameW(t .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2"
${If} $2 = 1
StrCpy $TargetServerName "\\$0"
${Else}
MessageBox MB_OK|MB_ICONSTOP 'Could not determine the computer name!$\nAborting...'
Quit
${EndIf}
${EndIf}
System::Free $0
FunctionEnd

Step 2 - Create the user
In order to create the user we need to call the NetUserAdd function which requires to generate at least a USER_INFO_1 structure which will hold the user's information (Note that we could use the USER_INFO_3 structure instead which contains a lot more information for the user). Here is the relevant code:
Function CreateUserAPI
StrCpy $1 "$TargetServerName"
StrCpy $2 ${USER_TYPE_1}
StrCpy $R1 "$UserName"
StrCpy $R2 "$UserPassword"
StrCpy $R3 ${USER_PRIV_USER}
StrCpy $R4 "$UserComment"
System::Int64Op ${UF_SCRIPT} + ${UF_NORMAL_ACCOUNT}
Pop $R5
${If} $UnlimitedPass = 1
System::Int64Op $R5 + ${UF_DONT_EXPIRE_PASSWD}
Pop $R5
${EndIf}
System::Call '*${strUSER_INFO_1}(R1,R2,n,R3,n,R4,R5,n) i.s'
Pop $R6
System::Call '*$R6${strUSER_INFO_1}(R1,R2,n,R3,n,R4,R5,n)'
System::Call 'netapi32.dll::NetUserAdd(w r1, i r2, i R6, *i.r3) i.r4'
System::Free $R6
FunctionEnd

Note that we can add more user flags (see Step 0) by adding them incrementally to $R5

Step 3 - Set the user's first and last name
To do this I will be using the NetUserSetInfo function. There are quite a few things we can change with this function, but I will only alter the first and last name of the user (structure USER_TYPE_1011). Here is the function:
Function SetUserInfoAPI
StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"
StrCpy $3 ${USER_TYPE_1011}
StrCpy $R1 "$UserFirstName $UserLastName"
System::Call '*${strUSER_INFO_1011}(R1)i.s'
Pop $R2
System::Call '*$R2${strUSER_INFO_1011}(R1)'
System::Call 'netapi32.dll::NetUserSetInfo(w r1, w r2, i r3, i R2, *i.r4) i.r5'
System::Free $R2
FunctionEnd

Step 4 - Get the user's SID
We need this in order to use it in Step 5 (see bellow)
I will use the NetUserGetInfo function to get the SID of the user that was just created in Step 3. The data will be fed to a USER_INFO_23 structure. The SID will be kept at a buffer pointed to by R9 in the next function:
Function GetUserInfoAPI
StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"
StrCpy $3 ${USER_INFO_23}
System::Call 'netapi32::NetUserGetInfo(w r1, w r2, i r3, *i .R8) i.r4'
System::Call '*$R8(w .R1, w .R2, w .R3, i .R4, i .R9)'
System::Free $R8
FunctionEnd

Step 5 - Add the user to a group
This will be done using the NetLocalGroupAddMembers function using the SID from the previous step. I am using the $MakeAdmin variable (you have to set it to 1 in order to add the user to the admin group) to determine the group I will be adding the user to.
Function AddUserToGroupAPI
StrCpy $1 "$TargetServerName"
${If} $MakeAdmin = 1
StrCpy $2 "Administrators"
${Else}
StrCpy $2 "Users"
${EndIf}
StrCpy $3 ${LOCALGROUP_MEMBERS_INFO_0}
System::Int64Op 1 * 0x000001
Pop $4
System::Call 'netapi32.dll::NetLocalGroupAddMembers(w r1, w r2, i r3, *i R9, i r4) i.r6'
FunctionEnd

That's it!
You can call the functions from inside your code or make a continuous block of all the commands and get the user created.
Also you can add some error checking for each function. In general if the NetAPIs succeed they return 0 to the relevant variable.

I have to say this was a an excellent chance for me to dive into the system plugin and I really enjoyed the process :D

Again a big 'thank you' to kichik for the directions ...
CF

This is very handy info, would be great if you added a page in the wiki.
One question; is it ok to use the strings "Administrators" and "Users" on non english systems?


I'll gladly post a page in the wiki :)
I am pretty sure that the strings "Administrators" and "Users" apply only to english systems. Where can I find their definitions for other languages?
CF

[Edit]
I guess we could use NetLocalGroupEnum to get all the groups, place them in an array then use NetGroupGetInfo for each member of the array to get the name of the group along with the RID. Then based on the RID decide which is the Admin and which is the guest group ...


Right ... Stuck again.
I am trying to enumerate the groups on the local machine using the NetLocalGroupEnum function:

Function NetLocalGroupEnumAPI
!define LOCAL_GROUP_INFO_0 0
StrCpy $1 "$TargetServerName"
StrCpy $2 ${LOCAL_GROUP_INFO_0}
System::Int64Op 1 * 0x000000
Pop $R4
System::Call 'netapi32.dll::NetLocalGroupEnum(w r1, i r2, *i .R1, i ${NSIS_MAX_STRLEN}, *i .R2, *i .R3, i R4)i .R5'
System::Call '*$R1(w .R9)'
DetailPrint $R1 ; the buffer holding the LOCAL_GROUP_INFO_0 structure
DetailPrint $R2 ; number of entries that have been parsed
DetailPrint $R3 ; total number of entries
DetailPrint $R4 ; the resume handle
DetailPrint $R9 ; the group name from $R1
FunctionEnd

According to MSDN the handle should be 0 when we first call the function and then remain unchanged (?).
Calling a second time the function gives the same group name every time (since the handle doesn't change?)... And both $R2 and $R3 are the same, as if the function goes directly to the last member of the allocated array.

:mad:

CF

[Edit]
Or, if R1 points to an array of LOCAL_GROUP_INFO_0 structures, then would it be logical to get the first value out, count its length, advance to the next memory buffer ($R1 + lenght of value in $R1) then get out the second value etc?

It's better to use a well-known SID. There may be a lot of groups with the administrator in them.


True.
Which brings me back to my previous question: How do I supply the SID? What format do I have to use? Basically I am looking at WinBuiltinUsersSid, WinBuiltinGuestsSid, WinBuiltinAdministratorsSid etc but I am not sure how to tell the netapi32 which one it is ...
CF

[Edit]
What puzzles me is that NetLocalGroupAddMembers accepts the group name as LPCWSTR so I am not sure how to pass an SID instead or how to get a groupname if I know its SID


You find another way to get the user's SID. You can get the RID using USER_INFO_20 and assemble the SID on your own with GetSidIdentifierAuthority, AllocateAndInitializeSid, GetSidSubAuthority and GetSidSubAuthorityCount. Count the sub-authorities using GetSidSubAuthorityCount, get all of them using GetSidSubAuthority, get the authority identifier using GetSidIdentifierAuthority and allocate your new SID with the those authorities and the RID using AllocateAndInitializeSid.
There must be an easier way!
I can't believe that pre-XP software devellopers had to go through ALL that just to figure out a user's SID!


Anyway here are some more netapi32 functions:

Use the NetUserDel API function to delete a user. More info here
Function NetUserDeleteAPI
StrCpy $1 "$TargetServerName"
StrCpy $2 "$UserName"
System::Call 'netapi32.dll::NetUserDel(w r1, w r2)i.r3'
FunctionEnd

Use the NetLocalGroupDelMembers API function to delete a user from a group. More info here
Function NetLocalGroupDelMembersAPI
!define LOCALGROUP_MEMBERS_INFO_0 0
StrCpy $1 "$TargetServerName"
StrCpy $2 "$GroupName"
StrCpy $3 ${LOCALGROUP_MEMBERS_INFO_0}
; Need to call GetUserInfo first to get the SID of the user
StrCpy $4 $UserSID
System::Int64Op 1 * 0x000001
Pop $5
System::Call 'netapi32.dll::NetLocalGroupDelMembers(w r1, w r2, i r3, *i r4, i r5) i.r6'
System::Free $4
FunctionEnd

Use the NetLocalGroupAdd function to add a new group. More info here
Function NetLocalGroupAddAPI
!define LOCALGROUP_INFO_1 1
!define strLOCALGROUP_INFO_1 '(w,w)i)'
StrCpy $1 "$TargetServerName"
StrCpy $2 ${LOCALGROUP_INFO_1}
StrCpy $R1 "$GroupName"
StrCpy $R2 "$GroupDescription"
System::Call '*${strLOCALGROUP_INFO_1}(R1,R2)i.s'
Pop $R3
System::Call '*$R3${strLOCALGROUP_INFO_1}(R1,R2)'
System::Call 'netapi32.dll::NetLocalGroupAdd(w r1, i r2, i R3, *i r4)i.r5'
System::Free $R3
FunctionEnd

Use the NetLocalGroupDel API function to delete a group. More info here
Function NetLocalGroupDelAPI
StrCpy $1 "$TargetServerName"
StrCpy $2 "$GroupName"
System::Call 'netapi32.dll::NetLocalGroupDel(w r1, w r2)i.r3'
FunctionEnd

Use the NetLocalGroupGetInfo API function to get info for a group. More infohere
Function NetLocalGroupGetInfoAPI
!define LOCALGROUP_MEMBERS_INFO_1 1
StrCpy $1 "$TargetServerName"
StrCpy $2 "$GroupName"
StrCpy $3 ${LOCALGROUP_MEMBERS_INFO_1}
System::Call 'netapi32.dll::NetLocalGroupGetInfo(w r1, w r2, i r3, *i.R3)i.r4'
System::Call '*$R3(w .R1, w .R2)'
StrCpy $GroupName $R1
StrCpy $GroupComment $R2
System::Free $R3
FunctionEnd

Use the NetLocalGroupSetInfo function to set the info for a group. More info here
Function NetLocalGroupSetInfoAPI
!define LOCALGROUP_INFO_1 1
!define strLOCALGROUP_INFO_1 '(w,w)i)'
StrCpy $1 "$TargetServerName"
StrCpy $2 "$GroupName"
StrCpy $3 ${LOCALGROUP_INFO_1}
StrCpy $R1 "$GroupName"
StrCpy $R2 "$GroupDescription"
System::Call '*${strLOCALGROUP_INFO_1}(R1,R2)i.s'
Pop $R3
System::Call '*$R3${strLOCALGROUP_INFO_1}(R1,R2)'
System::Call 'netapi32.dll::NetLocalGroupSetInfo(w r1, w r2, i r3, i R3, *i r4)i.r5'
System::Free $R3
FunctionEnd


This IS addictive :D
CF

Right ...
I am trying to add the user that I created to a group without providing the actual group name, in order to avoid the localization of the name (assuming for example that a German version of windows will not have 'Administrators' but something else), I thought about enumerating the groups on the local machine and then try to get their SID. Once I have the SID I can compare it to the well-known ones and decide which group to use. In this way I can also avoid the problem of unicode characters as the group name will always be stored in a memory buffer and I can feed it directly to a netapi32 function. However I hit (yet) another obstacle trying to get the SIDs:

!define LOCALGROUP_INFO_0 0
!define POLICY_LOOKUP_NAMES 800
!define strLSA_OBJECT_ATTRIBUTES '(i,i,w,i,i,i)i'
!define strLSA_UNICODE_STRING '(i,i,w)i'

System::Int64Op 1 * 0x000000
Pop $R4
; R1 buffer containing the array of group names
; R2 number of entries that have been read
; R3 total number of entries
; R4 resume handle
System::Call 'netapi32.dll::NetLocalGroupEnum(n,i${LOCALGROUP_INFO_0},*i.R1,i${NSIS_MAX_STRLEN},*i.R2, *i .R3,*i.R4)'
; Pop one entry out to $R9
System::Call '*$R1(wR9)'
; open an LSA policy object in order to lookup the SID
System::Call '*${strLSA_OBJECT_ATTRIBUTES}(0,n,n,0,n,n).s'
Pop $R0
System::Call '*$R0${strLSA_OBJECT_ATTRIBUTES}(0,n,n,0,n,n)'
; get the Lsa handle to $R2
System::Call 'advapi32::LsaOpenPolicy(n,iR0,i${POLICY_LOOKUP_NAMES},*i.R2) ?e'
Pop $0
StrCmp $0 0 +1 Error
; Create an LSA_UNICODE_STRING array with the name to lookup its SID
StrLen $1 $R9 ; get its length
IntOp $2 $1 + 1
IntOp $2 $2 * 2 ; max length
System::Call '*${strLSA_UNICODE_STRING}(r1,r2,R9).s'
Pop $R4
System::Call '*$R4${strLSA_UNICODE_STRING}(r1,r2,R9)'
; Call LsaLookupNames
System::Call 'advapi32::LsaLookupNames(iR2,i1,iR4,*i.R5,*i.R6)?e'
Pop $0
System::Call 'advapi32::LsaNtStatusToWinError(i r0)i.R1' ; $0 is Lsa NTSTATUS error, $1 is Windows error code
StrCmp $0 0 +1 Error
...
Error:
The above code is not working. The Lsa handle is allocated but the LsaLookupNames call returns error 126 (NTSTATUS) and 317 (Windows) both of which don't make sense.
I suspect that the problem is in the way I define the LSA_UNICODE_STRING array but playing around with that didn't get me anywhere ...

Any ideas?

CF

You can use LookupAccountSid to get the name from a SID. Here's an example. Don't forget to allocate a buffer, call the wide-string (Unicode) version and pass that buffer instead of putting the result in $0.

# allocate SID_IDENTIFIER_AUTHORITY
System::Call "*(&i1 0, &i4 0, &i1 5) i.r0"

# allocate SID
System::Call "advapi32::AllocateAndInitializeSid(i r0, i 2, i 32, i 545, i 0, i 0, i 0, i 0, i 0, i 0, *i .r1)"

# free SID_IDENTIFIER_AUTHORITY
System::Free $0

# debug, see that the right SID was created
System::Call advapi32::ConvertSidToStringSid(ir1,*t.R0)
DetailPrint $R0

# use SID to look-up account name
StrCpy $R0 ${NSIS_MAX_STRLEN}
System::Call "advapi32::LookupAccountSid(i 0, i r1, t .r0, *i R0, t .r2, *i R0, *i .r3)"

# free SID
System::Call "advapi32::FreeSid(i r1)"

# print result
DetailPrint $0

Much faster/smarter than my approach, as usual :)
Thanks kichik!

I am still interested however in understanding why the previous code does not work ...
Any clues?

CF


I can't tell right now why the previous code wasn't working. Although I can give you a hint and say that 126 is not the error you're looking for. First off, it's not an NTSTATUS, it's a simple Windows error which means a module can't be found. The System plug-in always causes it while looking for the right module to load. You probably got 317, which means a message can't be found, because you tried converting the invalid NTSTATUS code.


Allright, I'll play around a bit more and try to figure it out Thanks again kichik :)

CF


Here are some more netapi32 functions:

Group enumeration using NetLocalGroup Enum

!define LOCALGROUP_INFO_0 0
StrCpy $1 "$TargetServerName" ; should be \\COMPUTERNAME
StrCpy $2 ${LOCALGROUP_INFO_0}
System::Int64Op 1 * 0x000000
Pop $R3
; R0 - Buffer holding group names
; R1 - number of items enumerated
; R2 - total number of groups
; R3 - resume handle
System::Call 'netapi32.dll::NetLocalGroupEnum(w r1, i r2, *i .R0, i ${NSIS_MAX_STRLEN}, *i .R1, *i .R2, *i .R3)'
NSISArray::New /NOUNLOAD LocalGroups
StrCpy $Counter 0
FeedArray:
${If} $Counter < $R2
System::Call '*$R0(w.R4)'
NSISArray::Write /NOUNLOAD LocalGroups $Counter "$R4"
IntOp $R0 $R0 + 4
IntOp $Counter $Counter + 1
Goto FeedArray
${EndIf}
NSISArray::SizeOf /NOUNLOAD LocalGroups
Pop $0 ; compare it to $R2 (total number of elements in the memory)
System::Call 'netapi32.dll::NetApiBufferFree($R0)'
${EndIf}

User enumeration using NetUser Enum
StrCpy $1 "$TargetServerName"
StrCpy $2 0
StrCpy $3 ${FILTER_NORMAL_ACCOUNT}
System::Int64Op 1 * 0x000000
Pop $R3
System::Call 'netapi32::NetUserEnum(w r1, i r2, i r3, *i .R0, i ${NSIS_MAX_STRLEN}, *i .R1, *i .R2, *i .R3)'
StrCpy $Counter 0
${If} $Counter < $R2
System::Call '*$R0(w.R4)'
NSISArray::Write /NOUNLOAD LocalUsers $Counter "$R4"
IntOp $R0 $R0 + 4
IntOp $Counter $Counter + 1
Goto FeedArray
${EndIf}
NSISArray::SizeOf /NOUNLOAD LocalUsers
Pop $0
System::Call 'netapi32.dll::NetApiBufferFree($R0)'
${EndIf}

Following the previous suggestions by kichik here is a better way to add users to groups: instead of adding them to 'GroupName' which may not be localized, we can first grab the group's SID using a well-known SID and then add the user to that group. So 'Step 5' of the above instructions would become:

Step 5 - Add user to a group using NetLocalGroupAddMembers
!define LOCALGROUP_MEMBERS_INFO_0 0
; First get the group's SID
; Well-known SIDs
; Administrators S-1-5-32-544
; Users S-1-5-32-545
; Guests S-1-5-32-546
; Power Users S-1-5-32-547
; Define a variable $GroupID and set it to 544 for administrators, 454 for users etc
System::Call "*(&i1 0, &i4 0, &i1 5) i.r0"
StrCpy $1 $GroupID
System::Call "advapi32::AllocateAndInitializeSid(i r0, i 2, i 32, i r1, i 0, i 0, i 0, i 0, i 0, i 0, *i .r2)"
System::Free $0
System::Call advapi32::ConvertSidToStringSid(ir2,*t.R0)
${If} $R0 != "S-1-5-32-$GroupID"
; set some error flag here
${EndIf}
System::Call '*(&w${NSIS_MAX_STRLEN})i.R4'
StrCpy $R0 ${NSIS_MAX_STRLEN}
System::Call "advapi32::LookupAccountSidW(i 0, i r2, i R4, *i R0, t .r3, *i R0, *i .r4)"
; the memory buffer with the SID is at $R4
System::Call "advapi32::FreeSid(i r2)"
; Now add the user to a group
StrCpy $1 "$TargetServerName"
StrCpy $3 ${LOCALGROUP_MEMBERS_INFO_0}
StrCpy $4 $UserSID ; taken from 'Step 4 - Get the user's SID'
System::Int64Op 1 * 0x000001
Pop $5
System::Call 'netapi32.dll::NetLocalGroupAddMembers(w r1, i R4, i r3, *i r4, i r5) i.r6' ; if the function is successful $6 should be 0

Note that since we are manipulating users on the local machine, $1 in all the above can be set to null (n) and then the localhost will be the target.

:)

CF

Added a (long) WIKI page
:)

CF


I'm trying to use the EnumerateUsers macro, but keep getting the following error:

"Could not place all the user accounts into an array!"

On closer inspection the macro does succeed in enumerating users, but NSISArray::SizeOf returns an empty string.

What do you think could be wrong?

This is how I use it (this is the full installer script):

Name "EnumUsrs"
OutFile "EnumUsrs.exe"

!include NSISArray.nsh
!include EnumerateUsers.nsh

ShowInstDetails show
Page instfiles

Section ""
!insertmacro EnumerateUsers "" UserArray
SectionEnd

It looks like the problem is caused by a couple of bugs in the EnumerateUsers code.

First the array is created without specifying any sizes. This is wrong.
I propose the following instead:

NSISArray::New /NOUNLOAD ${USER_ARRAY_NAME} 5 ${NSIS_MAX_STRLEN}


Secondly, when getting the size of the array it's important to call 'pop' three times to get the number of elements in the array.
Like this:
NSISArray::SizeOf /NOUNLOAD ${USER_ARRAY_NAME}
Pop $0
Pop $0
Pop $0
StrCmp $0 $R2 +2 +1

Hi TobbeSweden,
The API calls and the array code on the Wiki are quite old (2+ years now!).
AfroUK has updated his plugin and shuffled things around, so the options that I was using probably don't work on the latest version. I haven't really changed anything since then so feel free to alter the code on the Wiki as you see fit, as long as it works :)
CF


Heh. I don't know if my changes work with the latest version of NSIS or the plugins either... But it works with the versions I had installed when I wrote that :)