How can an independent DLL find out what Token file accesses will be made with? - c++

If the parent process has used LogonUser so that the access token being used for file access is different than than the token the process was started with, how can the DLL find out the NT User name that the file accesses will be processed under?
If I had a specific file location then I could use GetFileSecurity, however I don't know any guaranteed accessible paths in the context of the DLL.
If I used the following:
PSID ownedSID(NULL);
SECURITY_INFORMATION siRequested = OWNER_SECURITY_INFORMATION;
wSecInfoOK = GetSecurityInfo(GetCurrentProcess(), SE_KERNEL_OBJECT, siRequested, &ownedSID, NULL, NULL, NULL, NULL);
then the PSID returned references the Windows user of the logged on process rather than that under which any writes will be treated as!
New Question in light comment / answer from #arx
I am now using TokenUser with GetTokenInformation on the handle from OpenThreadToken, but again I am getting the launching user but not the impersonated user
HANDLE hThreadToken = NULL;
if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, TRUE, &hThreadToken))
{
// success
CHeapPtr<TOKEN_USER, CGlobalAllocator> pToken;
DWORD length = 0U, dwError(0UL);
if (!GetTokenInformation(hThreadToken, TokenUser, NULL, 0, &length) && ERROR_INSUFFICIENT_BUFFER == GetLastError())
{
pToken.AllocateBytes(length);
SetLastError(ERROR_SUCCESS);//Reset last error - we have now allocated the required memory so the buffer is now big enough i.e GetLastError() != ERROR_INSUFFICIENT_BUFFER
if (pToken && GetTokenInformation(hThreadToken, TokenUser, pToken, length, &length))
{
if (IsValidSid(pToken->User.Sid))
sFailedUser = WinSecurityInfo::GetAccountSID(pToken->User.Sid, dwError);
}
dwError = GetLastError();
if (dwError)
{
boost::system::error_code sidError = MakeSysError(dwError);
TRACE("Error text for GetLastError() = '%s'\n", sidError.message().c_str());
}
}
}
P.S WinSecurityInfo::GetAccountSID is just a wrapper around LookupAccountSid
P.P.S Tried both FALSE and TRUE in OpenThreadToken, no change

You are looking at the wrong information in the thread token retrieved with OpenThreadToken. To get the identity of the user being impersonated you need to look at the TokenUser, not the TokenOwner.
Use GetTokenInformation to retrieve the user.
However, rather than going to great lengths to work in the face of impersonation, it is more usual to specify as part of your API contract that you don't. And then ignore the problem.

Related

C++ Win32 - Getting App Name using PID and Executable Path

I'd like to get the name of an application on Windows.
Currently I'm using EnumProcesses() to enumerate all processes and receive a list of PIDs.
Then I'm looping through all PIDs, each iteration looks like this, when aProcess[i] is the current PID:
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, aProcesses[i]);
std::string processName = get_process_name(proc);
My get_process_name(proc) function uses GetModuleFileNameEx to get the executable path and GetProcessImageFileName in order to retrieve the name of the executable file.
What I want to retrieve is basically the App Name, as it is displayed in the Windows Task Manager.
I've looked throughout Win32 API's documentation and could not find a clue on how to achieve this.
I've tried looking for other ways such as Windows Shell tasklist but it outputs different things, for example- Google Chrome:
Image Name: chrome.exe PID: 84 Session Name: Console
I'd really appreciate any thought on the matter, whether it be the Win32 API or some other way I can implement through C++ code.
You can do this with GetFileVersionInfoA and VerQueryValueA.
You just need to follow the example given in the VerQueryValueA document.
Here is my sample:
struct LANGANDCODEPAGE {
WORD wLanguage;
WORD wCodePage;
} *lpTranslate;
int main()
{
HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION , FALSE, 2140); //Modify pid to the pid of your application
if (!handle) return 0;
wchar_t pszFile[MAX_PATH] = L"";
DWORD len = MAX_PATH;
QueryFullProcessImageName(handle, 0, pszFile, &len);
UINT dwBytes, cbTranslate;
DWORD dwSize = GetFileVersionInfoSize(pszFile, (DWORD*)&dwBytes);
if (dwSize == 0) return 0;
LPVOID lpData = (LPVOID)malloc(dwSize);
ZeroMemory(lpData, dwSize);
if (GetFileVersionInfo(pszFile, 0, dwSize, lpData))
{
VerQueryValue(lpData,
L"\\VarFileInfo\\Translation",
(LPVOID*)&lpTranslate,
&cbTranslate);
wchar_t strSubBlock[MAX_PATH] = { 0 };
wchar_t* lpBuffer;
for (int i = 0; i < (cbTranslate / sizeof(struct LANGANDCODEPAGE)); i++)
{
StringCchPrintf(strSubBlock,50,
L"\\StringFileInfo\\%04x%04x\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage);
VerQueryValue(lpData,
strSubBlock,
(void**)&lpBuffer,
&dwBytes);
std::wcout << lpBuffer << std::endl;
}
}
if(lpData) free(lpData);
if (handle) CloseHandle(handle);
return 0;
}
And it works for me:
I think what you want are the "version" resources embedded in the PE file (the executables.)
You seem to be familiar with using Win32 API, so I'm just going to give you some hints.
You have to use LoadLibraryEx to load the EXE file (the Ex suffix is to enable passing the LOAD_LIBRARY_AS_DATAFILE flag,) and then call EnumResourceTypes (also see EnumResourceNames) to enumerate all the resource types/resources in the file, and find what you are looking for and then extract the data with LoadResource. The resource type you want is RT_VERSION.
I'm sure I'm omitting a lot of details (as per usual for Win32 programming,) and there might not be a need for enumeration at all; in which case you may want to call FindResource or FindResourceEx directly (if there is a fixed name for this particular resource.)
As further clarification, this gives you the date you see if you right-click on the EXE file (not the shortcut) in Windows Explorer and select "Properties", then go to the "Details" tab. If that information is indeed what you want (e.g. the "File description" field) then the above method should give you the data.

Setting up a chromium-like sandbox (error 0xc00000a5)

I'm trying to setup a sandbox akin to chromium. In particular, I'm trying to replicate their trick of creating a sleeping process with a low-privilege token, then setting a high-privilege token temporarily before running it. The idea is to let the process do all its initialization in high-privilege mode, then reverting to the low-privilege token right before running any unsafe code.
So far, I'm struggling just to get a basic test up and running. Here's my code:
#include "stdafx.h"
#include <atlbase.h>
#include <iostream>
#include <cassert>
#include <vector>
#include <string>
#include <AccCtrl.h>
#include <aclapi.h>
#define VERIFY(x) { bool r = x; assert(r); }
uint8_t* GetTokenInfo(const HANDLE& token, TOKEN_INFORMATION_CLASS info_class, DWORD* error)
{
// Get the required buffer size.
DWORD size = 0;
::GetTokenInformation(token, info_class, NULL, 0, &size);
if (!size)
{
*error = ::GetLastError();
return nullptr;
}
uint8_t* buffer = new uint8_t[size];
if (!::GetTokenInformation(token, info_class, buffer, size, &size))
{
*error = ::GetLastError();
return nullptr;
}
*error = ERROR_SUCCESS;
return buffer;
}
int main()
{
// Open the current token
CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(), TOKEN_ALL_ACCESS, &processToken.m_h));
// Create an impersonation token without restrictions
HANDLE impersonationToken;
VERIFY(DuplicateToken(processToken, SecurityImpersonation, &impersonationToken));
// Build the list of the deny only group SIDs
DWORD error;
uint8_t* buffer = GetTokenInfo(processToken, TokenGroups, &error);
if (!buffer) return error;
TOKEN_GROUPS* token_groups = reinterpret_cast<TOKEN_GROUPS*>(buffer);
std::vector<SID*> sids_for_deny_only;
for (unsigned int i = 0; i < token_groups->GroupCount; ++i)
{
if ((token_groups->Groups[i].Attributes & SE_GROUP_INTEGRITY) == 0 &&
(token_groups->Groups[i].Attributes & SE_GROUP_LOGON_ID) == 0)
{
sids_for_deny_only.push_back(reinterpret_cast<SID*>(token_groups->Groups[i].Sid));
}
}
{
DWORD size = sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE;
uint8_t* buffer = new uint8_t[size];
TOKEN_USER* token_user = reinterpret_cast<TOKEN_USER*>(buffer);
BOOL result = ::GetTokenInformation(processToken, TokenUser, token_user, size, &size);
if (!result) return ::GetLastError();
sids_for_deny_only.push_back(reinterpret_cast<SID*>(token_user->User.Sid));
}
size_t deny_size = sids_for_deny_only.size();
SID_AND_ATTRIBUTES *deny_only_array = NULL;
if (deny_size)
{
deny_only_array = new SID_AND_ATTRIBUTES[deny_size];
for (unsigned int i = 0; i < sids_for_deny_only.size(); ++i)
{
deny_only_array[i].Attributes = SE_GROUP_USE_FOR_DENY_ONLY;
deny_only_array[i].Sid = const_cast<SID*>(sids_for_deny_only[i]);
}
}
// Create restricted sids
DWORD size_sid = SECURITY_MAX_SID_SIZE;
BYTE sid_[SECURITY_MAX_SID_SIZE];
VERIFY(::CreateWellKnownSid(WinNullSid, NULL, sid_, &size_sid));
SID_AND_ATTRIBUTES sidsToRestrict[] =
{
reinterpret_cast<SID*>(const_cast<BYTE*>(sid_)),
0
};
// Create the restricted token
HANDLE restrictedToken;
VERIFY(::CreateRestrictedToken(processToken,
0, // flags
deny_size,
deny_only_array,
0,
0,
_countof(sidsToRestrict), // number of SIDs to restrict,
sidsToRestrict, // no SIDs to restrict,
&restrictedToken));
VERIFY(::IsTokenRestricted(restrictedToken));
// Create a process using the restricted token (but keep it suspended)
STARTUPINFO startupInfo = { 0 };
PROCESS_INFORMATION processInfo;
VERIFY(::CreateProcessAsUser(restrictedToken,
L"C:\\Dev\\Projects\\SandboxTest\\Debug\\Naughty.exe",
0, // cmd line
0, // process attributes
0, // thread attributes
FALSE, // don't inherit handles
CREATE_SUSPENDED | DETACHED_PROCESS, // flags
0, // inherit environment
0, // inherit current directory
&startupInfo,
&processInfo));
// Set impersonation token with more rights
{
HANDLE temp_thread = processInfo.hThread;
if (!::SetThreadToken(&temp_thread, impersonationToken))
{
return 1;
}
}
// Run the process
if (!::ResumeThread(processInfo.hThread)) // Other process crashes immediately when this is run
{
return 1;
}
std::cout << "Done!" << std::endl;
return 0;
}
Not quite sure about deny list and restrict list yet, but if I understand this correctly it should be irrelevant. I'm calling SetThreadToken with my unrestricted token before running the thread, so I figure it should not matter what settings I use for restrictedToken. However, this is not the case; the new process crashes with the error code 0xc00000a5. If I use processToken instead of restrictedToken in CreateProcessAsUser, the code runs just fine. It's like SetThreadToken isn't doing anything.
I'm not doing much in naughty.exe right now, just starting an infinite loop.
Anyone know what I'm doing wrong here?
Edit 1:
According to this page, 0xc00000a5 means "STATUS_BAD_IMPERSONATION_LEVEL". Not sure on this, but I think I'm missing SeImpersonatePrivilege, causing stuff to fail. Still investigating options...
Edit 2:
Okay, seems like I had to reduce the privilege of the impersonation token to be able to use it with the other process. Not sure why, but not I can run the program without admin rights.
Still getting an error though :/ Now it's "STATUS_DLL_NOT_FOUND". Best lead from examining Process Monitor logs is an ACCESS DENIED on "C:\Windows\SysWOW64\ucrtbased.dll". The weird part is that it seems to be working once in a while (i.e. the spawned process sometimes runs just fine). Back to digging...
The problem is caused by the startup code trying to load the C runtime DLL from a new thread (which doesn't have access to the high-privilege token). What worked for me is to statically link the CRT into the sandbox process (i.e. /MTd in Debug builds, /MT in Release builds).
the new process crashes with the error code 0xc00000a5 / STATUS_BAD_IMPERSONATION_LEVEL
I've encountered this when:
the restricted token specifies SidsToRestrict that the permissive token does not.
the restricted token specifies a lower integrity level than the permissive token.
"restricted": The more restrictive token passed to CreateProcessAsUser
"permissive": The less restrictive token passed to SetThreadToken, and used until that thread calls RevertToSelf
It appears you cannot unrestrict sids or raise the integrity level with SetThreadToken, even if the parent process is unrestricted in these regards - you can only undeny sids, or unremove privileges.
I had to reduce the privilege of the impersonation token to be able to use it with the other process
This was a red herring for me. I tried every combination of keeping/removing privilige LUIDs for the permissive and restrictive tokens with little effect.
0xC0000135 / STATUS_DLL_NOT_FOUND
Best lead from examining Process Monitor logs is an ACCESS DENIED on "C:\Windows\SysWOW64\ucrtbased.dll". The weird part is that it seems to be working once in a while (i.e. the spawned process sometimes runs just fine).
The DLL loading/initializing code appears to be multithreaded. Speculating a bit here, my theory is that the new threads don't inherit the permissive token specified by SetThreadToken, and that ucrtbased.dll only loads successfully if the initial/main thread happened to be the thread that loaded ucrtbased.dll, and will fail if it's instead loaded by any of the worker threads - hence why it works sometimes, but typically not.
Workaround options:
Statically link the CRT. This is my current preference.
Don't pass Everyone or Users to SidsToDisable. Defeats the point of denying sids for sandboxing purposes in the first place, so I'd recommend against this, especially since I see no way to disable them later.
Have the parent process listen for CREATE_THREAD_DEBUG_EVENT and SetThreadToken for them too? I haven't tested this, but I suspect it might work. Would only want to do this during startup, lest you break open the sandbox after the child process has called RevertToSelf() etc.
0xC0000142 / ERROR_DLL_INIT_FAILED
Okay, this one's just me: I encountered this when trying to spawn a process at Untrusted integrity when initializing bcrypt.dll for Rust's stdlib. Spawn at Low instead, and have the child process lower itself to Untrusted post-init IMO.
How the heck do you use SidsToRestrict at all then?
You can't go from nullptr restrictions on a permissive token to real restrictions on a restricted token without causing 0xc00000a5 / STATUS_BAD_IMPERSONATION_LEVEL.
However, you can go from one restriction list to another, and neither necessairly needs to contain all the exact same SIDs as the other.
With a restrictive SidsToRestrict of only S-1-0-0 "NULL SID", I can use a permissive SidsToRestrict containing only:
S-1-1-0 "Everyone" (otherwise child dies w/ STATUS_ACCESS_DENIED)
S-1-5-5-x-yyyyyyy "LogonSessionId_..." (otherwise dies w/ STATUS_DLL_INIT_FAILED?)
Perhaps S-1-0-0 is considered a subset of Everyone, or perhaps the restricted sids can be outright disjoint?
Using all group SIDs marked SE_GROUP_ENABLED | SE_GROUP_LOGON_ID for your permissive token might be more appropriate.
Note that the child can't lower it's integrity level unless it can OpenProcessToken(.., ADJUST_DEFAULT, ..) based on the current access token.
The only overlap between the permissive token's restriction sids, and the restricted default TokenDefaultDacl, is the logon session, which doesn't grant write access by default:
ACCESS_ALLOWED_ACE { Mask: GENERIC_ALL, Sid: S-1-5-21-xxxx-yyyy-zzzz "%USERNAME%", .. }
ACCESS_ALLOWED_ACE { Mask: GENERIC_ALL, Sid: S-1-5-18 "SYSTEM", .. }
ACCESS_ALLOWED_ACE { Mask: GENERIC_READ | GENERIC_EXECUTE, Sid: S-1-5-5-x-yyyyyyyyy "LogonSessionId_x_yyyyyyyyy, .. }
So you may want to create a new default dacl for the restricted token with:
InitializeAcl(...);
AddAccessAllowedAce(acl, ACL_REVISION, TOKEN_ADJUST_DEFAULT | ..., logon_session_sid);
TOKEN_DEFAULT_DACL default_dacl = { acl };
SetTokenInformation(restriced, TokenDefaultDacl, &default_dacl, sizeof(default_dacl));
And ensure you adjust your child process's process token integrity level before calling RevertToSelf.

OpenProcess function returns invalid handles

I'm working on an application that creates multiple desktops and gives the user the ability to start whatever applications he desires under the desktop he is currently using.
When that desktop is closed (using a combo key) I want close all applications opened under that desktop. In order to do this I enumarate all processes using the EnumProcesses function and retrive a handle based on every process identifier returned by EnumProcesses using OpenProcess function. Using GetThreadId I retrieve the thread identifier which is used as the parameter for GetThreadDesktop function and the returned handle is compared with the one from my desktop, so I can find out in which desktop the process runs.
At least in theory, this works, because for every process identifier, OpenProcess function returns an invalid handle for GetThreadId (error code 6). I'm running the application as administrator and I enable the SeDebugPrivilege privilege.
I don't understand why the returned handle is always invalid, here is the code that I use:
void iterateProcesses(HDESK threadDesktop)
{
EnableDebugPriv(); // functions enables the SeDebugPrivilege privilege
int found = 0;
int wanted = 0;
DWORD aProcesses[1024], cbNeeded, cProcesses;
unsigned int i;
EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded);
cProcesses = cbNeeded / sizeof(DWORD);
for (i = 0; i < cProcesses; i++)
{
if (aProcesses[i] != 0)
{
found++;
if (GetThreadDesktop(checkProcess(aProcesses[i])) == threadDesktop)
{
wanted++;
}
}
}
}
DWORD checkProcess(DWORD processID)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processID);
GetLastError(); // if in the manifest file under 'UAC execution level'
// the application does not requests for administrator rights
// GetLastError() will return code 5 (access denied)
DWORD dwThreadId = GetThreadId(hProcess);
GetLastError(); // return code 6 (ERROR_INVALID_HANDLE)
// dwThreadId returned is always 0 because the handle is not valid
CloseHandle(hProcess);
return dwThreadId;
}
Your error checking is wrong. Please read the documentation again. Only call GetLastError if the function failed.
It's reasonable that you will only be able to get all access to a process handle if you are executing elevated. But you do need to check the value returned by OpenProcess, as described in the documentation. Only proceed if that value indicates success. Otherwise, call GetLastError to find out why.
You are expected to pass a thread handle to GetThreadId. hProcess is a process handle. Hence the ERROR_INVALID_HANDLE error code. But again, you are not checking for errors properly. You must first check the return value, as stated in the documentation. Only if that indicates failure do you call GetLastError.
I'm not sure how you can expect to get a single thread from a process. Processes can, and do, have many threads. Indeed threads can be created and destroyed so perhaps the thread you are looking for is not there anymore. All the same, here's how to enumerate threads in a process: https://msdn.microsoft.com/en-us/library/windows/desktop/ms686852.aspx

CreateFileMapping and OpenFileMapping not cooperating in different processes

I'm trying to use CreateFileMapping and OpenFileMapping to share memory between processes. This isn't working as I want it to - OpenFileMapping returns null and GetLastError is 5 - access denied. Any ideas what I am doing wrong? Name is something like MemoryTest.
Edit:
using CreateFileMapping both times I can read the data written in the other process. The reason this is a problem is that I get Error 183 - memory area already exists. However, it still returns a handle to the existing memory.
var map_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(int), name.c_str());
....
var handle = MapViewOfFile(map_handle, FILE_MAP_ALL_ACCESS , 0, 0, 0)
*handle = 10;
UnMapViewOfFile(map_handle);
getchar();
Other process:
var map_handle = OpenFileMapping(PAGE_READWRITE, false, name.c_str())
....
var handle = MapViewOfFile(map_handle, FILE_MAP_ALL_ACCESS , 0, 0, 0) //returns null
var out = *handle;
getchar();
This works for the second process though:
var map_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(int), name.c_str());
....
var handle = MapViewOfFile(map_handle, FILE_MAP_ALL_ACCESS , 0, 0, 0) //returns null
var out = *handle;
getchar();
Simple things to be aware of from the very start:
Error code 5: ERROR_ACCESS_DENIED "Access is denied."
Error code 183: ERROR_ALREADY_EXISTS "Cannot create a file when that file already exists."
ERROR_ALREADY_EXISTS is a documented behavior and is an indication of scenario that you do receive handle, but it is a handle to already existing object, not created.
The problem with not working OpenFileMapping is around its first argument: the API function expects values/flags from another enumeration, it takes FILE_MAP_* values and not PAGE_*. Incorrect argument results in failure to open you the mapping you want.
In case someone else needed, in my case the error has nothing to do with the access to the file, it's with the size provided to the CreateFileMapping, after spending hours with a similar error I'd to use a working sample posted somewhere else and line by line compare what was the difference.
If you don't know the size of the file when executing the CreateFileMapping you need to use 0, this will tell the API to use the file size of the mapped file. Most of the answers in SO around this are wrong and people is not bothering testing what is the problem about, I wasted hours reading other posts with similar suggestions.
To solve the problem the code should look like this:
var map_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 0, name.c_str());
Hope this saves hours to other fellow developers.

Why WNetAddConnection2 still returns 1219 after successfully calling WNetCancelConnection2?

I wrote some code to connect with some share on a remote server. If WNetAddConnection2 returns ERROR_SESSION_CREDENTIAL_CONFLICT (1219), I will first cancel the connection by WNetCancelConnection2 (return NO_ERROR). And then reconnect. But WNetAddConnection2 still returns 1219.
Why this and how to fix it?
Here's my code
BOOL ADDirectorySearch::IPCConnect(CString strServerName, CString strDomainName, CString strUserName, CString strPassWord)
{
CString strServerNameWithSlash = _T("\\\\") + strServerName; //actually is \\klbnt
CString strFullUserName = strDomainName + _T("\\") + strUserName; //is domaintest\administrator
_bstr_t bstrServerNameWithSlash = strServerNameWithSlash;
_bstr_t bstrFullUserName = strFullUserName;
_bstr_t bstrPassWord = strPassWord;
DWORD dwResult;
NETRESOURCEW netResource;
memset(&netResource, 0, sizeof(netResource));
netResource.dwScope = RESOURCE_GLOBALNET;
netResource.dwType = RESOURCETYPE_DISK;
netResource.dwDisplayType = RESOURCEDISPLAYTYPE_GENERIC;
netResource.dwUsage = RESOURCEUSAGE_CONNECTABLE;
netResource.lpProvider = L"";
netResource.lpRemoteName = bstrServerNameWithSlash;//Remote IP like:\\192.168.1.11
dwResult = WNetAddConnection2W(&netResource, bstrPassWord, bstrFullUserName, CONNECT_INTERACTIVE);
if (dwResult == ERROR_SESSION_CREDENTIAL_CONFLICT)
{
dwResult = WNetCancelConnection2W(bstrServerNameWithSlash, CONNECT_UPDATE_PROFILE, TRUE);
if (dwResult == NO_ERROR)
{
dwResult = WNetAddConnection2W(&netResource, bstrPassWord, bstrFullUserName, CONNECT_INTERACTIVE);
}
else
{
//MyMessageBox_Error(_T("IPCConnect Error."), _T("Error"));
return FALSE;
}
}
if (dwResult == NO_ERROR)
{
return TRUE;
}
else
{
//MyMessageBox_Error(_T("IPCConnect Error."), _T("Error"));
return FALSE;
}
}
FYI: After typing "net use" in cmd, I got this, I feel there's something with error:
Status Local Remote Network
-------------------------------------------------------------------------------
OK \\klbnt\NRDC1001 Microsoft Windows Network
The command completed successfully.
I was just having this problem now, and basically it seemed that it was due to another process still having file open, even though I specified "true" as the last parameter of WNetCancelConnection2() to force close the connection. Once I shut-down that other process, I was able to use successfully switch between credentials connecting and re-connecting to the same share. This is on Windows 2012 (64-bit), and the share was local (referenced by the machinename).
BUT...it's still a problem if you want to connect to different shares on the same machine. If I try to connect to \\mymachine\share1 as user1 then to \\mymachine\share2 as user2, I get the 1219 error (even if it's in a completely different process). I have to explicitly call WNetCancelConnnection on \\mymachine\share1 before I can connect to share2, which means at the point you connect to a share on a particular machine, you may have to first enumerate existing connections and close each one.
Rather frustrating, and I can't understand the design principle here. It seems the flags to create temporary connections etc. have no effect on this behaviour either. Really what I want to be able to do is say "for this thread, connect to this share on this machine and as this user, such that all attempts to access files on the share are done with that user's credentials". That way what other processes/threads are doing can't cause issues with the current one.