Successfully reported this slideshow.
Your SlideShare is downloading. ×

Abusing Symlinks on Windows

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 99 Ad
Advertisement

More Related Content

Similar to Abusing Symlinks on Windows (20)

Advertisement

More from OWASP Delhi (20)

Recently uploaded (20)

Advertisement

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

×