Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Abusing Symlinks on Windows

3,268 views

Published on

Abusing symlinks on Windows to elevate privileges

By James Forshaw (Researcher at Google's Project Zero)

Published in: Technology

Abusing Symlinks on Windows

  1. 1. A Link to the Past Abusing Symbolic Links on Windows James Forshaw @tiraniddo 1
  2. 2. James Forshaw @tiraniddo Obligatory Background Slide ● Researcher in Google’s Project Zero team ● Specialize in Windows ○ Especially local privilege escalation ● Never met a logical vulnerability I didn’t like https://www.flickr.com/photos/barretthall/2478623520/ 2
  3. 3. James Forshaw @tiraniddo What I’m Going to Talk About ● Implementation of Symbolic Links on Windows ● Exploitable Bug Classes ● Example vulnerabilities ● Offensive exploitation tricks 3
  4. 4. James Forshaw @tiraniddo Symbolic Links 4
  5. 5. James Forshaw @tiraniddo Dangers of Symbolic Links 5
  6. 6. James Forshaw @tiraniddo Resource Creation or Overwrite 6 Privileged Application pathtoresource Write to resource sensitivepath Symbolic Link
  7. 7. James Forshaw @tiraniddo Information Disclosure 7 Privileged Application pathtoresource Read Resource sensitivepath Symbolic Link Disclosure Unprivileged Application
  8. 8. James Forshaw @tiraniddo Time of Check/Time of Use 8 Privileged Application pathtoresource Check and use Resource validfile Check Symbolic Link maliciousfile Use Symbolic Link
  9. 9. James Forshaw @tiraniddo History of Windows Symbolic Links Windows NT 3.1 - July 27 1993 Object Manager Symbolic Links Registry Key Symbolic Links 9
  10. 10. James Forshaw @tiraniddo History of Windows Symbolic Links Windows NT 3.1 - July 27 1993 Object Manager Symbolic Links Registry Key Symbolic Links Windows 2000 - Feb 17 2000 NTFS Mount Points and Directory Junctions 10
  11. 11. James Forshaw @tiraniddo History of Windows Symbolic Links Windows NT 3.1 - July 27 1993 Object Manager Symbolic Links Registry Key Symbolic Links Windows 2000 - Feb 17 2000 NTFS Mount Points and Directory Junctions Windows Vista - Nov 30 2006 NTFS Symbolic Links 11
  12. 12. James Forshaw @tiraniddo Object Manager Symbolic Links 12
  13. 13. James Forshaw @tiraniddo Named Objects 13 IO/File ??C:Windowsnotepad.exe DeviceNamedPipemypipe Registry RegistryMachineSoftware Semaphore BaseNamedObjectsMySema
  14. 14. James Forshaw @tiraniddo Creating Object Manager Symbolic Links HANDLE CreateSymlink(LPCWSTR linkname, LPCWSTR targetname) { OBJECT_ATTRIBUTES obj_attr; UNICODE_STRING name, target; HANDLE hLink; RtlInitUnicodeString(&name, linkname); RtlInitUnicodeString(&target, targetname); InitializeObjectAttributes(&objAttr, &name, OBJ_CASE_INSENSITIVE, nullptr, nullptr); NtCreateSymbolicLinkObject(&hLink, SYMBOLIC_LINK_ALL_ACCESS, &obj_attr, &target); return hLink; } 14
  15. 15. James Forshaw @tiraniddo Parsing Name Object Manager Reparsing NtOpenSemaphore 15 MyObjectsGlobalMySema
  16. 16. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName 16 Parsing Name MyObjectsGlobalMySema
  17. 17. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName ObpLookupObjectName 17 Parsing Name MyObjectsGlobalMySema Current Component
  18. 18. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName ObpLookupObjectName 18 Parsing Name MyObjectsGlobalMySema Current Component
  19. 19. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName ObpLookupObjectName ObpParseSymbolicLink 19 Parsing Name MyObjectsGlobalMySema Current Component Global → BaseNamedObjects
  20. 20. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName ObpLookupObjectName ObpParseSymbolicLink 20 Parsing Name MyObjectsGlobalMySema BaseNamedObjectsMySema Global → BaseNamedObjects
  21. 21. James Forshaw @tiraniddo Object Manager Reparsing NtOpenSemaphore ObOpenObjectByName ObpLookupObjectName ObpParseSymbolicLink STATUS_REPARSE 21 Parsing Name BaseNamedObjectsMySema
  22. 22. James Forshaw @tiraniddo Abusing Object Manager Symbolic Links ● Most obvious attack is object squatting ○ Redirect privileged object creation to another name ○ Open named pipes for attacking impersonation ○ Shadowing ALPC ports ● File symlink attacks perhaps more interesting! 22
  23. 23. James Forshaw @tiraniddo Example Vulnerability IE EPM MOTWCreateFile Information Disclosure 23
  24. 24. James Forshaw @tiraniddo IE Shell Broker MOTWCreateFile 24 HANDLE MOTWCreateFile(PCWSTR FileName, ...) { if (FileHasMOTW(FileName) || IsURLFile(FileName)) { return CreateFile(FileName, GENERIC_READ, ...); } } BOOL IsURLFile(PCWSTR FileName) { PCWSTR extension = PathFindExtension(FileName); return wcsicmp(extension, L".url") == 0; }
  25. 25. James Forshaw @tiraniddo Win32 Path Support Path Description somepath Relative path to current directory c:somepath Absolute directory .c:somepath Device path, canonicalized ?c:somepath Device path, non- canonicalized Interesting!
  26. 26. James Forshaw @tiraniddo Win32 to Native NT File Paths 26 .c:somepathWin32 Path
  27. 27. James Forshaw @tiraniddo Win32 to Native NT File Paths 27 .c:somepath ??c:somepath Win32 Path Native Path RtlDosPathNameToRelativeNtPathName
  28. 28. James Forshaw @tiraniddo Win32 to Native NT File Paths 28 .c:somepath ??c:somepath Win32 Path Native Path RtlDosPathNameToRelativeNtPathName DeviceHarddiskVolume4somepath ObpLookupObjectName After Lookup
  29. 29. James Forshaw @tiraniddo Global Root Symlink 29 .GLOBALROOTsomepathWin32 Path Empty Symlink Path
  30. 30. James Forshaw @tiraniddo Global Root Symlink 30 .GLOBALROOTsomepath ??GLOBALROOTsomepath Win32 Path Native Path RtlDosPathNameToRelativeNtPathName Empty Symlink Path
  31. 31. James Forshaw @tiraniddo Global Root Symlink 31 .GLOBALROOTsomepath ??GLOBALROOTsomepath Win32 Path Native Path RtlDosPathNameToRelativeNtPathName somepath ObpLookupObjectName After Lookup Empty Symlink Path
  32. 32. James Forshaw @tiraniddo Writeable Object Directories from IE Sandbox 32 Path Sandbox RPC Control PM SessionsXBaseNamedObjects PM SessionsXAppContainerNamedObjectsSID... EPM
  33. 33. James Forshaw @tiraniddo Exploiting IShDocVwBroker* broker; CreateSymlink(L"RPC Controlfake.url", L"??C:somefile"); broker->MOTWCreateFile( L".GLOBALROOTRPC Controlfake.url", ...); // Read File 33
  34. 34. James Forshaw @tiraniddo Registry Key Symbolic Links 34
  35. 35. James Forshaw @tiraniddo Under the hood 35 NtOpenKey ObOpenObjectByName ObpLookupObjectName Parsing Name RegistryMachineMylink
  36. 36. James Forshaw @tiraniddo Under the hood 36 NtOpenKey ObOpenObjectByName ObpLookupObjectName CmpParseKey Parsing Name RegistryMachineMylink CmpGetSymbolicLink Current Component
  37. 37. James Forshaw @tiraniddo Under the hood 37 NtOpenKey ObOpenObjectByName ObpLookupObjectName CmpParseKeySTATUS_REPARSE CmpGetSymbolicLink Parsing Name RegistryMachineMylink RegistryMachineNewKey
  38. 38. James Forshaw @tiraniddo Serious Limitations ● Windows 7 fixed numerous issues with registry symbolic links ○ Blocked symlinks between untrusted (user) and trusted (local machine) hives ○ Symbolic link must be a valid registry path ● MS10-021 ensured it was also available downstream ● Still can exploit user to user vulnerabilities such as in IE EPM ○ CVE-2013-5054 ○ CVE-2014-6322 ● Mitigation (pass flag to RegCreateKeyEx) still undocumented 38
  39. 39. James Forshaw @tiraniddo NTFS Mount Points / Directory Junctions 39
  40. 40. James Forshaw @tiraniddo Under the hood 40 NtOpenFile ObOpenObjectByName ObpLookupObjectName IopParseDevice Parsing Name ??C:tempmylinkfile NTFS Driver
  41. 41. James Forshaw @tiraniddo Under the hood 41 NtOpenFile ObOpenObjectByName ObpLookupObjectName IopParseDevice Parsing Name ??C:tempmylinkfile ??C:Windows NTFS Driver
  42. 42. James Forshaw @tiraniddo Under the hood 42 NtOpenFile ObOpenObjectByName ObpLookupObjectName IopParseDevice NTFS Driver STATUS_REPARSE Parsing Name ??C:Windowsfile
  43. 43. James Forshaw @tiraniddo Structure of a Mount Point typedef struct MOUNT_POINT_REPARSE_BUFFER { ULONG ReparseTag; USHORT ReparseDataLength; USHORT Reserved; USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; WCHAR PathBuffer[1]; }; 43 Set to 0xA0000003 for Mount Point Substitute NT Name Print Name? String Data Header Reparse Data
  44. 44. James Forshaw @tiraniddo Create a Mount Point PREPARSE_DATA_BUFFER reparse_buffer = BuildMountPoint(target); CreateDirectory(dir); HANDLE handle = CreateFile(dir, ..., FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, ...); DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparse_buffer, reparse_buffer.size(), ...); 44
  45. 45. James Forshaw @tiraniddo Mount Point Limitations ● Directory must be empty to set the reparse data ● Target device must be an IO device (no opening registry keys for example) ● Target device heavily restricted in IopParseDevice: 45 IO_PARSE_CONTEXT *ctx; if (ctx->LastReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { switch(TargetDeviceType) { case FILE_DEVICE_DISK: case FILE_DEVICE_CD_ROM: case FILE_DEVICE_VIRTUAL_DISK: case FILE_DEVICE_TAPE: break; default: return STATUS_IO_REPARSE_DATA_INVALID; } } Limited Device Subset
  46. 46. James Forshaw @tiraniddo Example Vulnerability Windows Task Scheduler TOCTOU Arbitrary File Creation 46
  47. 47. James Forshaw @tiraniddo Running a Scheduled Task 47 void Load_Task_File(string task_name, string orig_hash) { string task_path = "c:windowssystem32tasks" + task_name; string file_hash = Hash_File(task_path); if (file_hash != orig_hash) { Rewrite_Task_File(task_path); } } Hash task file contents Rewrite Task without Impersonation
  48. 48. James Forshaw @tiraniddo System Task Folder Writable from normal user privilege, therefore can create a mount point directory 48
  49. 49. James Forshaw @tiraniddo Winning the Race Condition 49 Hash File Rewrite Task File ???? Profit?
  50. 50. James Forshaw @tiraniddo Is that an OPLOCK in your Pocket? void SetOplock(HANDLE hFile) { REQUEST_OPLOCK_INPUT_BUFFER inputBuffer; REQUEST_OPLOCK_OUTPUT_BUFFER outputBuffer; OVERLAPPED overlapped; overlapped.hEvent = CreateEvent(...); DeviceIoControl(hFile, FSCTL_REQUEST_OPLOCK, &inputBuffer, sizeof(inputBuffer), &outputBuffer, sizeof(outputBuffer), nullptr, &overlapped); WaitForSingleObject(overlapped.hEvent, ...); } 50
  51. 51. James Forshaw @tiraniddo User Application Exploitation Task Scheduler Service 51 IRegisteredTask::Run Current Mount Point: MyTaskFolder → C:dummy Open File for Reading C:DummyMyTask
  52. 52. James Forshaw @tiraniddo User Application Exploitation Task Scheduler Service 52 IRegisteredTask::Run Current Mount Point: MyTaskFolder → C:windows Open File for Reading C:DummyMyTask Change Mount Point Location Event Set
  53. 53. James Forshaw @tiraniddo User Application Exploitation Task Scheduler Service 53 IRegisteredTask::Run Current Mount Point: MyTaskFolder → C:windows Open File for Reading C:DummyMyTask Release Oplock Change Mount Point Location Event Set
  54. 54. James Forshaw @tiraniddo User Application Exploitation Task Scheduler Service 54 IRegisteredTask::Run Current Mount Point: MyTaskFolder → C:windows Open File for Reading C:DummyMyTask Generate and Verify Hash of File C:DummyMyTask Release Oplock Change Mount Point Location Event Set
  55. 55. James Forshaw @tiraniddo User Application Exploitation Task Scheduler Service 55 IRegisteredTask::Run Current Mount Point: MyTaskFolder → C:windows Open File for Reading C:DummyMyTask Rewrite Task File C:WindowsMyTask Generate and Verify Hash of File C:DummyMyTask Release Oplock Change Mount Point Location Event Set
  56. 56. James Forshaw @tiraniddo OPLOCK Limitations ● Can’t block on access to standard attributes or FILE_READ_ATTRIBUTES ● One-shot, need to be quick to reestablish if opened multiple times ● Can get around attribute reading in certain circumstances by oplocking a directory. ● For example these scenarios opens directories for read access ○ Shell SHParseDisplayName accesses each directory in path ○ GetLongPathName or GetShortPathName ○ FindFirstFile/FindNextFile 56
  57. 57. James Forshaw @tiraniddo DEMO OPLOCKs in Action 57
  58. 58. James Forshaw @tiraniddo NTFS Symbolic Links 58
  59. 59. James Forshaw @tiraniddo Structure of a Symbolic Link typedef struct SYMLINK_REPARSE_BUFFER { ULONG ReparseTag; USHORT ReparseDataLength; USHORT Reserved; USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; USHORT Flags; WCHAR PathBuffer[1]; }; 59 Set to 0xA000000C for SymlinkHeader Reparse Data Flags: 0 - Absolute path 1 - Relative path
  60. 60. James Forshaw @tiraniddo Create Symlink Privilege Admin user - Yay! Normal user - Boo :-( 60
  61. 61. James Forshaw @tiraniddo Create Symbolic Link Privilege NTSTATUS NtfsSetReparsePoint(NTFS_CREATE_CONTEXT* ctx) { // Validation ... PREPARSE_DATA_BUFFER* reparse_buf; if ((reparse_buf->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) && (ctx->Type != FILE_DIRECTORY)) { return STATUS_NOT_A_DIRECTORY; } if ((reparse_buf->ReparseTag == IO_REPARSE_SYMLINK) && ((ctx->Flags & 0x400) == 0)) { return STATUS_ACCESS_DENIED } // ... } 61
  62. 62. James Forshaw @tiraniddo Create Symbolic Link Privilege NTSTATUS NtfsSetReparsePoint(NTFS_CREATE_CONTEXT* ctx) { // Validation ... PREPARSE_DATA_BUFFER* reparse_buf; if ((reparse_buf->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) && (ctx->Type != FILE_DIRECTORY)) { return STATUS_NOT_A_DIRECTORY; } if ((reparse_buf->ReparseTag == IO_REPARSE_SYMLINK) && ((ctx->Flags & 0x400) == 0)) { return STATUS_ACCESS_DENIED } // ... } Context must contain 0x400 flag 62
  63. 63. James Forshaw @tiraniddo Flags Setting NTSTATUS NtfsSetCcbAccessFlags(NTFS_FILE_CONTEXT* ctx) { ACCESS_MODE AccessMode = NtfsEffectiveMode(); if (ctx->HasRestorePrivilege) { ctx->Flags |= 0x400; } if (AccessMode == KernelMode || SeSinglePrivilegeCheck(&SeCreateSymbolicLinkPrivilege, &security_ctx, UserMode)) { ctx->Flags |= 0x400; } // ... } 63
  64. 64. James Forshaw @tiraniddo Hypothetical Scenario NTSTATUS Handle_OpenLog(PIRP Irp) { OBJECT_ATTRIBUTES objattr; UNICODE_STRING name; RtlInitUnicodeString(&name, L"SystemRootLogFilesuser.log"); InitObjectAttributes(&objattr, &name, 0, 0, 0, 0); PHANDLE Handle = Irp->AssociatedIrp->SystemBuffer; return ZwCreateFile(Handle, &objattr, ...); } 64 Returns handle to user mode process
  65. 65. James Forshaw @tiraniddo 65 SMBv2 Symbolic Links https://msdn.microsoft.com/en-us/library/cc246542.aspx
  66. 66. James Forshaw @tiraniddo SMBv2 Symbolic Link Restrictions 66 ● Remote to Local would be useful ● Disabled by default in local security policy
  67. 67. James Forshaw @tiraniddo Back to IopParseDevice enum SymlinkDeviceType { Local, Network }; if (ctx->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { // ... } else { SymlinkDeviceType target_type = GetSymlinkDeviceType(TargetDeviceType); if (target_type == Local || target_type == Network) { if (!NT_SUCCESS(IopSymlinkEnforceEnabledTypes( target_type, ctx->last_target_type))) { return STATUS_IO_REPARSE_DATA_INVALID; } } } 67 Enforces Symlink Traversal based on device types
  68. 68. James Forshaw @tiraniddo MRXSMB20 NTSTATUS Smb2Create_Finalize(SMB_CONTEXT* ctx) { // Make request and get response if (RequestResult == STATUS_STOPPED_ON_SYMLINK) { result = FsRtlValidateReparsePointBuffer( ctx->ErrorData, ctx->ErrorDataLength); if (!NT_SUCCESS(result)) { return result; } } // ... } 68 No check on ReparseTag
  69. 69. James Forshaw @tiraniddo SMBv2 Device Type Bypass 69 NtOpenFile ObpLookupObjectName IopParseDevice SMB2 Driver Parsing Name serversharefile Current Component Server Create sharefile
  70. 70. James Forshaw @tiraniddo SMBv2 Device Type Bypass 70 NtOpenFile ObpLookupObjectName IopParseDevice SMB2 Driver STATUS_REPARSE Parsing Name serversharefile Current Component Server STATUS_STOPPED_ON_SYMLINK with IO_REPARSE_TAG_MOUNT_POINT
  71. 71. James Forshaw @tiraniddo SMBv2 Device Type Bypass 71 NtOpenFile ObpLookupObjectName IopParseDevice SMB2 Driver Parsing Name serversharefile ??C:hello.txt Server NTFS Driver
  72. 72. James Forshaw @tiraniddo DEMO 72 SMBv2 Local File Disclosure in IE
  73. 73. James Forshaw @tiraniddo File Symbolic Links - Without Permissions 73
  74. 74. James Forshaw @tiraniddo First Try Default CreateFile call won’t open the file. Returns Access Denied 74
  75. 75. James Forshaw @tiraniddo Success FILE_FLAG_BACKUP_SEMANTICS allows us to open the file 75
  76. 76. James Forshaw @tiraniddo The NtCreateFile Paradox FILE_DIRECTORY_FILE Flag FILE_NON_DIRECTORY_FILE Flag 76 Neither FILE_DIRECTORY_FILE or FILE_NON_DIRECTORY_FILE
  77. 77. James Forshaw @tiraniddo The Old ADS Directory Trick Using $INDEX_ALLOCATION stream will bypass initial directory failure 77
  78. 78. James Forshaw @tiraniddo Let Our Powers Combine 78
  79. 79. James Forshaw @tiraniddo Let Our Powers Combine 79 NtOpenFile ObpLookupObjectName IopParseDevice Parsing Name ??C:tempmylink RPC Controlmylink NTFS Driver STATUS_REPARSE
  80. 80. James Forshaw @tiraniddo 80 NtOpenFile ObpLookupObjectName IopParseDevice NTFS Driver ObpParseSymbolicLink STATUS_REPARSE Parsing Name RPC Controlmylink ??C:hello.txt Let Our Powers Combine
  81. 81. James Forshaw @tiraniddo Persisting the Symlink ● Might be useful to persist the symlink between login sessions ● Can’t pass OBJ_PERMANENT directly ○ Needs SeCreatePermanentPrivilege ● Get CSRSS to do it for us :-) 81 DefineDosDeviceW( DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH, L"GLOBALROOTRPC Controlmylink", L"TargetPath" );
  82. 82. James Forshaw @tiraniddo Combined Symbolic Link Limitations ● All existing limitations of Mount Points apply ● Vulnerable application can’t try to list or inspect the mount point itself ○ Listing the directory ○ Open for GetFileAttributes or similar ● Can mitigate somewhat by clever tricks with oplocks on directory hierarchy 82
  83. 83. James Forshaw @tiraniddo DEMO One More Thing! 83
  84. 84. James Forshaw @tiraniddo DEMO One More Thing! 84
  85. 85. James Forshaw @tiraniddo CVE-2015-1644 85
  86. 86. James Forshaw @tiraniddo DosDevice Prefix 86 ??c:somepath THE PREFIX IS A LIE
  87. 87. James Forshaw @tiraniddo DosDevice Prefix 87 Sessions0DosDevicesX-Yc:somepath ??c:somepath
  88. 88. James Forshaw @tiraniddo DosDevice Prefix 88 Sessions0DosDevicesX-Yc:somepath ??c:somepath GLOBAL??c:somepath
  89. 89. James Forshaw @tiraniddo New C: Drive 89
  90. 90. James Forshaw @tiraniddo Windows User Impersonation 90
  91. 91. James Forshaw @tiraniddo Very Exploitable Behaviour 91 void ExploitableFunction() { ImpersonateLoggedOnUser(hToken); LoadLibrary("c:secure.dll"); RevertToSelf(); } c:secure.dll
  92. 92. James Forshaw @tiraniddo Very Exploitable Behaviour 92 void ExploitableFunction() { ImpersonateLoggedOnUser(hToken); LoadLibrary("c:secure.dll"); RevertToSelf(); } c:secure.dll ??c:secure.dll
  93. 93. James Forshaw @tiraniddo Very Exploitable Behaviour 93 void ExploitableFunction() { ImpersonateLoggedOnUser(hToken); LoadLibrary("c:secure.dll"); RevertToSelf(); } c:secure.dll ??c:secure.dll Sessions0DosDevicesX-Yc:secure.dll
  94. 94. James Forshaw @tiraniddo Very Exploitable Behaviour 94 void ExploitableFunction() { ImpersonateLoggedOnUser(hToken); LoadLibrary("c:secure.dll"); RevertToSelf(); } c:secure.dll ??c:secure.dll Sessions0DosDevicesX-Yc:secure.dll c:somearbitrary.dll
  95. 95. James Forshaw @tiraniddo Very Exploitable Behaviour 95 void ExploitableFunction() { ImpersonateLoggedOnUser(hToken); LoadLibrary("secure.dll"); RevertToSelf(); } void COMExploitableFunction() { ImpersonateLoggedOnUser(hToken); CoCreateInstance(CLSID_SecureObject, ...); RevertToSelf(); }
  96. 96. James Forshaw @tiraniddo Finding an Ideal Service 96 Requirement Spooler Service Runs as NT AUTHORITYSYSTEM Yup Uses impersonation Definitely Accessible by normal user Kind of the point Has a habit of loading DLLs Think of all the printer drivers
  97. 97. James Forshaw @tiraniddo DEMO REALLY One More Thing! 97
  98. 98. James Forshaw @tiraniddo Links and References ● Symlink Testing Tools https://github.com/google/symboliclink-testing-tools ● File Test Application https://github.com/ladislav-zezula/FileTest 98
  99. 99. James Forshaw @tiraniddo Questions? 99

×