|
|
1.1 ! root 1: /****************************************************************************\ ! 2: * ! 3: * MODULE: sidclean.c ! 4: * ! 5: * NT never deletes SIDs, so the name of this sample is most ! 6: * accurately be intrepreted as "Clean up SID ownership and ! 7: * ACE's that relate to SIDs that still (and always will) ! 8: * exist, but for which the corresponding user account has been ! 9: * deleted" ! 10: * ! 11: * ! 12: * PURPOSE: Demonstrate some of the Win32 security api(s), and provide a ! 13: * sample of how a utility could be written that recovers ! 14: * on-disk resources remaining allocated to deleted user ! 15: * accounts. The on-disk resources recovered are 1) Files that ! 16: * are still owned by accounts that have been deleted are ! 17: * assigned ownership to the account logged on when this sample ! 18: * is run, and 2) ACE's for deleted accounts are edited ! 19: * (deleted) out of the ACLs of files to which the deleted ! 20: * accounts had been granted authorizations (eg., Read access) ! 21: * ! 22: * It may be that running this sample as a utility has no ! 23: * practical value in many environments, as the number of files ! 24: * belonging to deleted user accounts will often be quite ! 25: * small, and the number of bytes recovered on disk by editing ! 26: * out ACEs for deleted accounts may well not be worth the time ! 27: * it takes to run this sample. The time it takes to run this ! 28: * sample may be quite significant when processing an entire ! 29: * hard disk or partition ! 30: * ! 31: * This sample is not a supported utility ! 32: * ! 33: * ! 34: * TO RUN: You must log on using an account, such as Administrator, that ! 35: * has the priviledges to take file ownership and edit ACls ! 36: * ! 37: * The ACL editing part of this sample can only be excercised for ! 38: * files on a partition that has ACLs NT processes: NTFS ! 39: * ! 40: * Typical test scenario: Create a user account or two, log on ! 41: * as each of these accounts in turn, while logged on for each ! 42: * account, go to an NTFS partition, create a couple of files ! 43: * so the test accounts each own a few files, use the file ! 44: * manager to edit permissions for those files so that each ! 45: * test user has some authorities (e.g., Read) explicitly ! 46: * granted for those files. Logon as Administrator, authorize ! 47: * each test user to a few Administrator-owned files. Delete ! 48: * the test accounts. Run the sample in the directories where ! 49: * you put the files the test accounts owned or were authorized ! 50: * to ! 51: * ! 52: * ! 53: * OVERALL APPROACH: The command line interface is kept inflexible to simplify ! 54: * it's parsing in this sample. The user must pass in a switch ! 55: * argument, a directory spec, and a file search pattern ! 56: * ! 57: * The sample positions the current directory (of the process the ! 58: * sample runs in) to the dir spec, and uses FindFirstFile and ! 59: * FindNextFile to walk through the directory specified looking ! 60: * for files that match the file pattern specified ! 61: * ! 62: * The switch argument can cause subdirectories to be searched ! 63: * recursively ! 64: * ! 65: * The switch argument lets the user choose only to take ! 66: * ownerships, only to edit ACLs, do both, or do neither, in ! 67: * which case the sample merely reports on what ownerships ! 68: * would have been taken, and what ACE's would have been ! 69: * deleted ! 70: * ! 71: * As the directories are walked, each file that matches the ! 72: * file pattern is processed right then ! 73: * ! 74: * Note that we process files in a directory, and we also process ! 75: * the directory itself that contains the files. We process ! 76: * directories because they can also be owned by deleted ! 77: * accounts, or could have ACEs that will no longer be used ! 78: * ! 79: * Note also that we process all directories that we check for ! 80: * files, regardless of the spelling of the directory name ! 81: * ! 82: * Counters are kept of file ownerships taken, ACEs deleted and ! 83: * total files checked, to print a summary line at the end of ! 84: * the run ! 85: * ! 86: * The sample considers it perfectly acceptable if 0 files match ! 87: * the file pattern for the entire run ! 88: * ! 89: * ! 90: * FUNCTIONS: DoMatchingFilesInOneDir ! 91: * ! 92: * Look in one dir or sub-dir for files that match the file ! 93: * pattern. For each match call DoOneFileOrDir ! 94: * ! 95: * DoAllDirsInOneDir ! 96: * ! 97: * For all the sub-dirs in a dir, set the current directory to be ! 98: * that directory, check for files that match the file pattern, ! 99: * and if any match, call DoMatchingFilesInOneDir to process ! 100: * those. Then reset the current directory ! 101: * ! 102: * GetFullFileOrDirName ! 103: * ! 104: * Get the full name of the file or dir for simplified processing ! 105: * (and for display on the console) ! 106: * ! 107: * DoOneFileOrDir ! 108: * ! 109: * Get the file's SD (Security Descriptor), and call ! 110: * TakeOwnershipIfAppropriate and/or DeleteACEsAsAppropriate as ! 111: * needed ! 112: * ! 113: * TakeOwnershipIfAppropriate ! 114: * ! 115: * Get the owning SID of the file from the file's SD that ! 116: * DoOneFileOrDir passed in, check that SID to see if the ! 117: * account is deleted. If so, edit into the file's SD a new ! 118: * owning SID (the SID of the process running the sample). ! 119: * Then write the modified file SD to disk ! 120: * ! 121: * DeleteACEsAsAppropriate ! 122: * ! 123: * Get the DACL of the file from the file's SD that ! 124: * DoOneFileOrDir passed in. Walk through the ACE list for ! 125: * that DACL, checking each ACE to see what SID the ACE refers ! 126: * to. For the SID referred to, check to see if the account is ! 127: * deleted. If so, delete that ACE from the DACL. When all ! 128: * ACE's have been examined, write the new DACL into the file's ! 129: * SD. Then write the modified file SD to disk ! 130: * ! 131: * GetProcessSid ! 132: * ! 133: * Retrieve into a global variable the SID of the user account ! 134: * logged on when this sample is run. This is the SID used by ! 135: * TakeOwnershipIfAppropriate ! 136: * ! 137: * CrackArgs ! 138: * ! 139: * Process the command line, cracking (parsing/decoding) the ! 140: * switch argument into boolean global variables (see below). ! 141: * Call DisplayHelp if anything illegal is found in the command ! 142: * line, or if the user asked for help ! 143: * ! 144: * DisplayHelp ! 145: * ! 146: * Display help text on the console ! 147: * ! 148: * ! 149: * GLOBAL VARS: ! 150: * BOOL bTakeOwnership ! 151: * BOOL bEditACLs ! 152: * BOOL bRecurse ! 153: * BOOL bJustCount ! 154: * ! 155: * These store the values the user specified on the command ! 156: * line's first argument (the switches argument). ! 157: * Respectively, they record whether we are to do the ! 158: * processing to Take Ownerships, Edit ACLs, whether we are to ! 159: * recurse into all subdirectories, and whether we are just ! 160: * counting up what would be processed (in which case we take ! 161: * no ownerships and edit no ACLs) ! 162: * ! 163: * DWORD dwFilesChecked ! 164: * DWORD dwFilesOwned ! 165: * DWORD dwACEsDeleted ! 166: * ! 167: * These count, respecively, the total files we checked, the ! 168: * number of files we took ownership of (or would have if we ! 169: * had not been told only to count), and the number of ACEs we ! 170: * deleted (or would have if we had not been told only to ! 171: * count). Note that the total number of files checked does ! 172: * not include files in the directories we process that do not ! 173: * match the file pattern ! 174: * ! 175: * Note, however, that we process directories regardless of ! 176: * whether they match the file pattern ! 177: * ! 178: * UCHAR ucProcessSIDBuf ! 179: * PSID psidProcessOwnerSID ! 180: * ! 181: * These store the SID of the account logged on as this sample ! 182: * runs, and a pointer to that SID ! 183: * ! 184: \****************************************************************************/ ! 185: ! 186: ! 187: /****************************************************************************\ ! 188: * INCLUDES, DEFINES, TYPEDEFS ! 189: \****************************************************************************/ ! 190: #include <windows.h> ! 191: #include <stdio.h> ! 192: #include <string.h> ! 193: ! 194: #define PERR(api) printf("%s: Error %d from %s on line %d\n", \ ! 195: __FILE__, GetLastError(), api, __LINE__); ! 196: #define PMSG(msg) printf("%s line %d: %s\n", \ ! 197: __FILE__, __LINE__, msg); ! 198: ! 199: #define PrintAppStyleAPIError(ApiTxt,MsgTxt) { \ ! 200: DWORD dwLastError; \ ! 201: dwLastError = GetLastError(); \ ! 202: switch (dwLastError) \ ! 203: { case ERROR_FILE_NOT_FOUND : \ ! 204: printf("\nFile not found (%s) line %d",MsgTxt,__LINE__); \ ! 205: break; \ ! 206: case ERROR_INVALID_NAME : \ ! 207: printf("\nInvalid name (%s) line %d",MsgTxt,__LINE__); \ ! 208: break; \ ! 209: case ERROR_PATH_NOT_FOUND : \ ! 210: printf("\nError path not found (%s) line %d",MsgTxt,__LINE__); \ ! 211: break; \ ! 212: case ERROR_SHARING_VIOLATION : \ ! 213: printf("\nSharing violation - shut down net and/or stop other sessions (%s) line %d",MsgTxt,__LINE__); \ ! 214: break; \ ! 215: case ERROR_ACCESS_DENIED : \ ! 216: printf("\nAccess denied (%s) line %d",MsgTxt,__LINE__); \ ! 217: break; \ ! 218: default : \ ! 219: printf("\n" #ApiTxt " - unexpected return code=%d (%s) line %d",dwLastError,MsgTxt,__LINE__); \ ! 220: break; \ ! 221: } \ ! 222: } ! 223: ! 224: /****************************************************************************\ ! 225: * GLOBAL VARIABLES ! 226: \****************************************************************************/ ! 227: ! 228: BOOL bTakeOwnership = FALSE; ! 229: BOOL bEditACLs = FALSE; ! 230: BOOL bRecurse = FALSE; ! 231: BOOL bJustCount = FALSE; ! 232: ! 233: DWORD dwFilesChecked = 0; ! 234: DWORD dwFilesOwned = 0; ! 235: DWORD dwACEsDeleted = 0; ! 236: ! 237: #define SZ_PROCESS_SID_BUF 16 ! 238: UCHAR ucProcessSIDBuf[SZ_PROCESS_SID_BUF]; ! 239: PSID psidProcessOwnerSID = &ucProcessSIDBuf; ! 240: // Why we allocate 16 bytes is explained in GetProcessSid ! 241: ! 242: ! 243: /****************************************************************************\ ! 244: * FUNCTION PROTOTYPES ! 245: \****************************************************************************/ ! 246: ! 247: BOOL DoMatchingFilesInOneDir(HANDLE hFound, ! 248: WIN32_FIND_DATA ffdFoundData); ! 249: BOOL DoAllDirsInOneDir(char *FilePattern); ! 250: BOOL GetFullFileOrDirName(LPTSTR lpszFileName); ! 251: BOOL DoOneFileOrDir(LPTSTR lpszFullName); ! 252: BOOL TakeOwnershipIfAppropriate(PSECURITY_DESCRIPTOR psdFileSD, ! 253: LPTSTR lpszFullName); ! 254: BOOL DeleteACEsAsAppropriate (PSECURITY_DESCRIPTOR psdFileSD, ! 255: LPTSTR lpszFullName); ! 256: BOOL GetProcessSid(VOID); ! 257: BOOL CrackArgs(UINT argc, char *argv[]); ! 258: VOID DisplayHelp(VOID); ! 259: ! 260: /****************************************************************************\ ! 261: * ! 262: * FUNCTION: Main ! 263: * ! 264: \****************************************************************************/ ! 265: ! 266: UINT main(UINT argc, char *argv[]) ! 267: { ! 268: WIN32_FIND_DATA ffdFoundData; ! 269: HANDLE hFound; ! 270: #define SZ_NAME_BUF MAX_PATH ! 271: UCHAR ucPathBuf[SZ_NAME_BUF]; ! 272: LPTSTR lpszFullName = (LPTSTR)&ucPathBuf; ! 273: ! 274: /**************************************************************************\ ! 275: * ! 276: * Store the process's SID in a global variable for later use (in taking ! 277: * ownership). ! 278: * ! 279: \**************************************************************************/ ! 280: ! 281: if (!GetProcessSid()) ! 282: { PERR("Can't proceed without process SID - see earlier error messages"); ! 283: return(1); ! 284: } ! 285: ! 286: if (!CrackArgs(argc,argv)) ! 287: return(1); ! 288: ! 289: /**************************************************************************\ ! 290: * ! 291: * CrackArgs has set our global processing switches, and proven argv[2] and ! 292: * argv[3] are our non-blank dir-spec and file-pattern strings. Now we ! 293: * must see that the file-spec is acceptable to the Win32 api's. Argv[2] ! 294: * is the file-spec to pass to SetCurrentDirectory, and argv[3] is the ! 295: * file-pattern to pass to FindFirstFile ! 296: * ! 297: * First we have to expand the dir-spec in argv[2], because if we set the ! 298: * current directory to it before expansion,the expansion will have a ! 299: * different result if argv[2] is something like ..\.. ! 300: * ! 301: \**************************************************************************/ ! 302: ! 303: strcpy(lpszFullName,argv[2]); ! 304: ! 305: if (!GetFullFileOrDirName(lpszFullName)) ! 306: { PMSG("Failed to expand to full name the 2nd argument (directory specification)"); ! 307: return(1); ! 308: } ! 309: ! 310: /**************************************************************************\ ! 311: * ! 312: * Now we pass the un-expanded argv[2] to SetCurrentDirectory for validity ! 313: * checking. GetFullPathName (called by GetFullFileOrDirName) does not ! 314: * validity check ! 315: * ! 316: \**************************************************************************/ ! 317: ! 318: if (!SetCurrentDirectory(argv[2])) ! 319: { PrintAppStyleAPIError(SetCurrentDirectory,"2nd argument (directory specification)"); ! 320: return(1); ! 321: } ! 322: ! 323: /**************************************************************************\ ! 324: * ! 325: * We begin processing with the current directory, using the expanded form we ! 326: * got before. We have to use the expanded form, because if we set to ! 327: * ..\.. and then try to process the string ..\.. as a dir name, instead of ! 328: * processing the dir two levels up from where we are we'll process the dir ! 329: * four levels up ! 330: * ! 331: \**************************************************************************/ ! 332: ! 333: if (!DoOneFileOrDir(lpszFullName)) ! 334: return(1); ! 335: ! 336: /**************************************************************************\ ! 337: * ! 338: * It's OK to get no hits. The files-checked counter will show how many ! 339: * files we looked at, and it's OK to look at 0 ! 340: * ! 341: * On the else branch, Argv[3] has been verified, and we have a good handle. ! 342: * We now pass to DoMatchingFilesInOneDir for processing the handle and ! 343: * found data we just got from FindFirstFile ! 344: * ! 345: \**************************************************************************/ ! 346: ! 347: hFound = FindFirstFile(argv[3], ! 348: (LPWIN32_FIND_DATA)&ffdFoundData); ! 349: if ((HANDLE)(-1) == hFound) ! 350: { if (GetLastError() != ERROR_FILE_NOT_FOUND) ! 351: { PrintAppStyleAPIError(FindFirstFile,"3rd argument"); ! 352: return(1); ! 353: } ! 354: } ! 355: else if (!DoMatchingFilesInOneDir(hFound,ffdFoundData)) ! 356: return(1); ! 357: ! 358: /**************************************************************************\ ! 359: * ! 360: * Pass the original file pattern for recursive calling to DoAllDirsInOneDir ! 361: * ! 362: \**************************************************************************/ ! 363: ! 364: if (!DoAllDirsInOneDir(argv[3])) ! 365: return(1); ! 366: ! 367: if (bJustCount) ! 368: printf("\nChecked %d files, would have taken ownership of %d files, would have deleted %d ACEs\n", ! 369: dwFilesChecked,dwFilesOwned,dwACEsDeleted); ! 370: else ! 371: printf("\nChecked %d files, took ownership of %d files, deleted %d ACEs\n", ! 372: dwFilesChecked,dwFilesOwned,dwACEsDeleted); ! 373: ! 374: return(0); ! 375: } ! 376: ! 377: /****************************************************************************\ ! 378: * ! 379: * FUNCTION: DoMatchingFilesInOneDir ! 380: * ! 381: \****************************************************************************/ ! 382: ! 383: BOOL DoMatchingFilesInOneDir(HANDLE hFound, ! 384: WIN32_FIND_DATA ffdFoundData) ! 385: { ! 386: BOOL bDoneWithHandle = FALSE; ! 387: ! 388: /**************************************************************************\ ! 389: * ! 390: * Process all files referred to by the handle, but not including ! 391: * directories, because directories are handled with separate calls to ! 392: * DoOneFileOrDir. Such separate calls are made as we are setting the ! 393: * current directory to be the directory to be processed ! 394: * ! 395: \**************************************************************************/ ! 396: ! 397: while (!bDoneWithHandle) ! 398: { ! 399: if (!(FILE_ATTRIBUTE_DIRECTORY & ffdFoundData.dwFileAttributes)) ! 400: { ! 401: if (!DoOneFileOrDir(ffdFoundData.cFileName)) ! 402: return(FALSE); ! 403: } ! 404: ! 405: if (!FindNextFile(hFound, ! 406: (LPWIN32_FIND_DATA)&ffdFoundData)) ! 407: if (GetLastError() == ERROR_NO_MORE_FILES) ! 408: bDoneWithHandle = TRUE; ! 409: else ! 410: { PrintAppStyleAPIError(FindNextFile,"on FindNext"); ! 411: return(FALSE); ! 412: } ! 413: } ! 414: } ! 415: ! 416: /****************************************************************************\ ! 417: * ! 418: * FUNCTION: DoAllDirsInOneDir ! 419: * ! 420: \****************************************************************************/ ! 421: ! 422: BOOL DoAllDirsInOneDir(char *FilePattern) ! 423: { ! 424: HANDLE hFound; ! 425: WIN32_FIND_DATA ffdFoundData; ! 426: BOOL bDoneWithHandle = FALSE; ! 427: ! 428: /**************************************************************************\ ! 429: * ! 430: * If not recursing into dirs, simply return ! 431: * ! 432: \**************************************************************************/ ! 433: ! 434: if (!bRecurse) ! 435: return TRUE; ! 436: ! 437: /**************************************************************************\ ! 438: * ! 439: * Since we are recursing, get a handle that points to entire directory, and ! 440: * walk the handle picking off only directories to recurse into ! 441: * ! 442: \**************************************************************************/ ! 443: ! 444: hFound = FindFirstFile("*.*", ! 445: (LPWIN32_FIND_DATA)&ffdFoundData); ! 446: if ((HANDLE)(-1) == hFound) ! 447: { PrintAppStyleAPIError(FindFirstFile,"on dir *.* FindFirst"); ! 448: return(FALSE); ! 449: } ! 450: ! 451: while (!bDoneWithHandle) ! 452: { ! 453: /************************************************************************\ ! 454: * ! 455: * We only do dirs here, and we only do directories with textual names ! 456: * (i.e., not "." and not "..") ! 457: * ! 458: \************************************************************************/ ! 459: ! 460: if ( (FILE_ATTRIBUTE_DIRECTORY & ffdFoundData.dwFileAttributes) ! 461: && (0 != strcmp("." ,ffdFoundData.cFileName)) ! 462: && (0 != strcmp("..",ffdFoundData.cFileName))) ! 463: { ! 464: HANDLE hFile2; ! 465: WIN32_FIND_DATA ffdFound2; ! 466: ! 467: /**********************************************************************\ ! 468: * ! 469: * We begin processing the new current directory by processing it itself, ! 470: * then setting the current dir to be the dir itself ! 471: * ! 472: \**********************************************************************/ ! 473: ! 474: if (!DoOneFileOrDir(ffdFoundData.cFileName)) ! 475: return(FALSE); ! 476: ! 477: if (!SetCurrentDirectory(ffdFoundData.cFileName)) ! 478: { PrintAppStyleAPIError(SetCurrentDirectory,"recursive set"); ! 479: return(FALSE); ! 480: } ! 481: ! 482: /**********************************************************************\ ! 483: * ! 484: * It's OK to get no hits. The files-checked counter will show how many ! 485: * files we looked at, and it's OK to look at 0 ! 486: * ! 487: \**********************************************************************/ ! 488: ! 489: hFile2 = FindFirstFile(FilePattern, ! 490: (LPWIN32_FIND_DATA)&ffdFound2); ! 491: if ((HANDLE)(-1) == hFile2) ! 492: { if (GetLastError() != ERROR_FILE_NOT_FOUND) ! 493: { PrintAppStyleAPIError(FindFirstFile,"during recursion"); ! 494: return(FALSE); ! 495: } ! 496: } ! 497: else if (!DoMatchingFilesInOneDir(hFile2,ffdFound2)) ! 498: return(FALSE); ! 499: ! 500: if (!DoAllDirsInOneDir(FilePattern)) ! 501: return(FALSE); ! 502: ! 503: if (!SetCurrentDirectory("..")) ! 504: { PrintAppStyleAPIError(SetCurrentDirectory,"un-recursive set"); ! 505: return(FALSE); ! 506: } ! 507: } ! 508: ! 509: /************************************************************************\ ! 510: * ! 511: * Get next recursion candidate (file or dir at this point, however at loop ! 512: * top files are screened out) ! 513: * ! 514: \************************************************************************/ ! 515: ! 516: if (!FindNextFile(hFound, ! 517: (LPWIN32_FIND_DATA)&ffdFoundData)) ! 518: if (GetLastError() == ERROR_NO_MORE_FILES) ! 519: bDoneWithHandle = TRUE; ! 520: else ! 521: { PrintAppStyleAPIError(FindNextFile,"on dir *.* FindNext"); ! 522: return(FALSE); ! 523: } ! 524: } ! 525: return(TRUE); ! 526: } ! 527: ! 528: /****************************************************************************\ ! 529: * ! 530: * FUNCTION: GetFullFileOrDirName ! 531: * ! 532: \****************************************************************************/ ! 533: ! 534: BOOL GetFullFileOrDirName(LPTSTR lpszFileName) ! 535: { ! 536: UCHAR ucPathBuf[SZ_NAME_BUF]; ! 537: DWORD dwSzReturned; ! 538: LPTSTR lpszLastNamePart; ! 539: LPTSTR lpszFullName; ! 540: ! 541: dwSzReturned = GetFullPathName ! 542: (lpszFileName, ! 543: (DWORD)SZ_NAME_BUF, ! 544: (LPTSTR)&ucPathBuf, ! 545: (LPTSTR *)&lpszLastNamePart); ! 546: if (0 == dwSzReturned) ! 547: switch (GetLastError()) ! 548: { case ERROR_INVALID_NAME : ! 549: printf("\nError invalid file full-name (on GetFullPathName)"); ! 550: return(FALSE); ! 551: default : ! 552: PERR("GetFullPathName - unexpected return code"); ! 553: return(FALSE); ! 554: } ! 555: ! 556: if (dwSzReturned > SZ_NAME_BUF) ! 557: { PERR("GetFullPathName - buffer too small"); ! 558: return(FALSE); ! 559: } ! 560: ! 561: lpszFullName = CharLower((LPTSTR)&ucPathBuf); ! 562: ! 563: if (!lpszFullName) ! 564: { PERR("CharLower failure"); ! 565: return(FALSE); ! 566: } ! 567: ! 568: /**************************************************************************\ ! 569: * ! 570: * Copy the expanded and upper-case-shifted name to the buffer pointed to by ! 571: * the input argument ! 572: * ! 573: \**************************************************************************/ ! 574: ! 575: strcpy(lpszFileName,lpszFullName); ! 576: } ! 577: ! 578: /****************************************************************************\ ! 579: * ! 580: * FUNCTION: DoOneFileOrDir ! 581: * ! 582: \****************************************************************************/ ! 583: ! 584: BOOL DoOneFileOrDir(LPTSTR lpszFullName) ! 585: { ! 586: #define SZ_SD_BUF 1000 ! 587: UCHAR ucBuf [SZ_SD_BUF]; ! 588: DWORD dwSDLength = SZ_SD_BUF; ! 589: DWORD dwSDLengthNeeded; ! 590: PSECURITY_DESCRIPTOR psdFileSD; ! 591: ! 592: if (!GetFullFileOrDirName(lpszFullName)) ! 593: return(FALSE); ! 594: ! 595: /**************************************************************************\ ! 596: * ! 597: * Now the input argument's name is accurate: it is expanded and lower-case ! 598: * ! 599: \**************************************************************************/ ! 600: ! 601: printf("\nChecking %s",lpszFullName); ! 602: ! 603: dwFilesChecked++; ! 604: ! 605: psdFileSD = (PSECURITY_DESCRIPTOR)&ucBuf; ! 606: ! 607: if (!GetFileSecurity ! 608: (lpszFullName, ! 609: (SECURITY_INFORMATION)(OWNER_SECURITY_INFORMATION ! 610: | DACL_SECURITY_INFORMATION), ! 611: psdFileSD, ! 612: dwSDLength, ! 613: (LPDWORD)&dwSDLengthNeeded)) ! 614: { PERR("GetFileSecurity"); ! 615: return(FALSE); ! 616: } ! 617: ! 618: /**************************************************************************\ ! 619: * ! 620: * This validity check is here for demonstration pruposes. It's not likely a ! 621: * real app would need to check the validity of this returned SD. The ! 622: * validity check APIs are more intended to check validity after app code ! 623: * has manipulated the structure and is about to hand it back to the system ! 624: * ! 625: \**************************************************************************/ ! 626: ! 627: if (!IsValidSecurityDescriptor(psdFileSD)) ! 628: { PERR("IsValidSecurityDescriptor said bad SD"); ! 629: return(FALSE); ! 630: } ! 631: ! 632: if (bTakeOwnership) ! 633: if (!TakeOwnershipIfAppropriate(psdFileSD,lpszFullName)) ! 634: return(FALSE); ! 635: ! 636: if (bEditACLs) ! 637: if (!DeleteACEsAsAppropriate (psdFileSD,lpszFullName)) ! 638: return(FALSE); ! 639: ! 640: return(TRUE); ! 641: } ! 642: ! 643: /****************************************************************************\ ! 644: * ! 645: * FUNCTION: TakeOwnershipIfAppropriate ! 646: * ! 647: \****************************************************************************/ ! 648: ! 649: BOOL TakeOwnershipIfAppropriate(PSECURITY_DESCRIPTOR psdFileSD, ! 650: LPTSTR lpszFullName) ! 651: { ! 652: PSID psidFileOwnerSID; ! 653: { ! 654: BOOL bOwnerDefaulted; ! 655: ! 656: if (!GetSecurityDescriptorOwner ! 657: (psdFileSD, ! 658: (PSID *)&psidFileOwnerSID, ! 659: (LPBOOL)&bOwnerDefaulted)) ! 660: { PERR("GetSecurityDescriptorOwner"); ! 661: return(FALSE); ! 662: } ! 663: ! 664: /************************************************************************\ ! 665: * ! 666: * This validity check is here for demonstration pruposes. It's not likely ! 667: * a real app would need to check the validity of this returned SID. The ! 668: * validity check APIs are more intended to check validity after app code ! 669: * has manipulated the structure and is about to hand it back to the ! 670: * system ! 671: * ! 672: \************************************************************************/ ! 673: ! 674: if (!IsValidSid(psidFileOwnerSID)) ! 675: { PERR("IsValidSid said bad SID!"); ! 676: return(FALSE); ! 677: } ! 678: } ! 679: ! 680: { ! 681: DWORD dwLastError = NO_ERROR; ! 682: #define SZ_ACCT_NAME_BUF 1000 ! 683: UCHAR ucNameBuf [SZ_ACCT_NAME_BUF]; ! 684: DWORD dwNameLength = SZ_ACCT_NAME_BUF; ! 685: #define SZ_DMN_NAME_BUF 1000 ! 686: UCHAR ucDomainNmBuf [SZ_DMN_NAME_BUF ]; ! 687: DWORD dwDNameLength = SZ_DMN_NAME_BUF ; ! 688: SID_NAME_USE peAcctNameUse; ! 689: ! 690: if (!LookupAccountSid ! 691: ((LPTSTR)"", // Look on local machine ! 692: psidFileOwnerSID, ! 693: (LPTSTR)&ucNameBuf, ! 694: (LPDWORD)&dwNameLength, ! 695: (LPTSTR)&ucDomainNmBuf, ! 696: (LPDWORD)&dwDNameLength, ! 697: (PSID_NAME_USE)&peAcctNameUse)) ! 698: { dwLastError = GetLastError(); ! 699: if (ERROR_NONE_MAPPED != dwLastError) ! 700: { PERR("LookupAccountSID"); ! 701: return(FALSE); ! 702: } ! 703: } ! 704: ! 705: /************************************************************************\ ! 706: * ! 707: * If deleted account, take ownership. This routine's caller checked the ! 708: * global switches that said we are in take ownership mode ! 709: * ! 710: * In some cases, the account lookup will fail with ERROR_NONE_MAPPED, ! 711: * meaning there is no deleted account mapped to the SID, in which case ! 712: * we also take ownership ! 713: * ! 714: * We check that first to avoid referencing peAcctNameUse, which in that ! 715: * case is not set ! 716: * ! 717: \************************************************************************/ ! 718: ! 719: if ( (ERROR_NONE_MAPPED == dwLastError) ! 720: || (SidTypeDeletedAccount == peAcctNameUse)) ! 721: { ! 722: dwFilesOwned++; ! 723: ! 724: if (bJustCount) ! 725: { printf(" - would have taken ownership"); ! 726: return(TRUE); ! 727: } ! 728: else ! 729: { ! 730: /********************************************************************\ ! 731: * ! 732: * Modify the SD in virtual memory. No check on the new owning SID ! 733: * here, because we validity checked it when we fetched it in ! 734: * GetProcessSid ! 735: * ! 736: \********************************************************************/ ! 737: ! 738: if (!SetSecurityDescriptorOwner ! 739: (psdFileSD, ! 740: psidProcessOwnerSID, ! 741: FALSE)) // New owner explicitly specified ! 742: { PERR("SetSecurityDescriptorOwner"); ! 743: return(FALSE); ! 744: } ! 745: ! 746: /********************************************************************\ ! 747: * ! 748: * This validity check is something a real app might actually like to ! 749: * do. We manupulated the SD, so before we write it back out to the ! 750: * file system, a check is worth considering. ! 751: * ! 752: \********************************************************************/ ! 753: ! 754: if (!IsValidSecurityDescriptor(psdFileSD)) ! 755: { PERR("IsValidSecurityDescriptor said bad SD"); ! 756: return(FALSE); ! 757: } ! 758: ! 759: /********************************************************************\ ! 760: * ! 761: * Modify the SD on the hard disk ! 762: * ! 763: \********************************************************************/ ! 764: ! 765: if (!SetFileSecurity ! 766: (lpszFullName, ! 767: (SECURITY_INFORMATION)OWNER_SECURITY_INFORMATION, ! 768: psdFileSD)) ! 769: { PERR("SetFileSecurity"); ! 770: return(FALSE); ! 771: } ! 772: ! 773: printf(" - took ownership"); ! 774: } ! 775: } ! 776: } ! 777: ! 778: return(TRUE); ! 779: ! 780: } ! 781: ! 782: /****************************************************************************\ ! 783: * ! 784: * FUNCTION: DeleteACEsAsAppropriate ! 785: * ! 786: \****************************************************************************/ ! 787: ! 788: BOOL DeleteACEsAsAppropriate(PSECURITY_DESCRIPTOR psdFileSD, ! 789: LPTSTR lpszFullName) ! 790: { ! 791: PACL paclFile; ! 792: BOOL bHasACL; ! 793: BOOL bOwnerDefaulted; ! 794: DWORD dwAcl_i; ! 795: DWORD dwACEsDeletedBeforeNow; ! 796: ! 797: ACL_SIZE_INFORMATION asiAclSize; ! 798: DWORD dwBufLength = sizeof(asiAclSize); ! 799: ACCESS_ALLOWED_ACE *paaAllowedAce; ! 800: ! 801: if (!GetSecurityDescriptorDacl(psdFileSD, ! 802: (LPBOOL)&bHasACL, ! 803: (PACL *)&paclFile, ! 804: (LPBOOL)&bOwnerDefaulted)) ! 805: { PERR("GetSecurityDescriptorDacl"); ! 806: return(FALSE); ! 807: } ! 808: ! 809: if (!bHasACL) // No ACL to process, so OK, return ! 810: return(TRUE); ! 811: ! 812: /**************************************************************************\ ! 813: * ! 814: * This validity check is here for demonstration pruposes. It's not likely a ! 815: * real app would need to check the validity of this returned ACL. The ! 816: * validity check APIs are more intended to check validity after app code ! 817: * has manipulated the structure and is about to hand it back to the system ! 818: * ! 819: \**************************************************************************/ ! 820: ! 821: if (!IsValidAcl(paclFile)) ! 822: { PERR("IsValidAcl said bad ACL!"); ! 823: return(FALSE); ! 824: } ! 825: ! 826: if (!GetAclInformation(paclFile, ! 827: (LPVOID)&asiAclSize, ! 828: (DWORD)dwBufLength, ! 829: (ACL_INFORMATION_CLASS)AclSizeInformation)) ! 830: { PERR("GetAclInformation"); ! 831: return(FALSE); ! 832: } ! 833: ! 834: dwACEsDeletedBeforeNow = dwACEsDeleted; ! 835: ! 836: /**************************************************************************\ ! 837: * ! 838: * We loop through in reverse order, because that's simpler, given that we ! 839: * potentially delete ACEs as we loop through. If started at 0 and went ! 840: * up, if we deleted the 0th ACE, then the 1th ACE would become the 0th, ! 841: * and we'd have to check the 0th ACE again ! 842: * ! 843: \**************************************************************************/ ! 844: ! 845: for (dwAcl_i = asiAclSize.AceCount-1; ((int)dwAcl_i) >= 0; dwAcl_i--) ! 846: { ! 847: /************************************************************************\ ! 848: * ! 849: * It doesn't matter for this sample that we don't yet know the ACE type, ! 850: * because they all start with the header field and that's what we need ! 851: * ! 852: \************************************************************************/ ! 853: ! 854: if (!GetAce(paclFile, ! 855: dwAcl_i, ! 856: (LPVOID *)&paaAllowedAce)) ! 857: { PERR("GetAce"); ! 858: return(FALSE); ! 859: } ! 860: ! 861: /************************************************************************\ ! 862: * ! 863: * There are only four Ace Types pre-defined, so this next check is ! 864: * redundant in a real app, but useful as a sanity check and a ! 865: * demonstration in a sample ! 866: * ! 867: \************************************************************************/ ! 868: ! 869: if (!( (paaAllowedAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE) ! 870: ||(paaAllowedAce->Header.AceType == ACCESS_DENIED_ACE_TYPE ) ! 871: ||(paaAllowedAce->Header.AceType == SYSTEM_AUDIT_ACE_TYPE ) ! 872: ||(paaAllowedAce->Header.AceType == SYSTEM_ALARM_ACE_TYPE ))) ! 873: { PERR("Invalid AceType"); ! 874: return(FALSE); ! 875: } ! 876: ! 877: { // Find SID of ACE, check if acct deleted ! 878: ! 879: UCHAR ucNameBuf [SZ_ACCT_NAME_BUF]; ! 880: DWORD dwNameLength = SZ_ACCT_NAME_BUF; ! 881: UCHAR ucDomainNmBuf [SZ_DMN_NAME_BUF]; ! 882: DWORD dwDNameLength = SZ_DMN_NAME_BUF; ! 883: SID_NAME_USE peAcctNameUse; ! 884: DWORD dwLastError = NO_ERROR; ! 885: ! 886: /**********************************************************************\ ! 887: * ! 888: * This validity check is here for demonstration pruposes. It's not ! 889: * likely a real app would need to check the validity of the SID ! 890: * contained in the returned ACL. The validity check APIs are more ! 891: * intended to check validity after app code has manipulated the ! 892: * structure and is about to hand it back to the system ! 893: * ! 894: \**********************************************************************/ ! 895: ! 896: if (!IsValidSid((PSID)&(paaAllowedAce->SidStart))) ! 897: { PERR("IsValidSid said bad SID!"); ! 898: return(FALSE); ! 899: } ! 900: ! 901: if (!LookupAccountSid ! 902: ((LPTSTR)"", // Look on local machine ! 903: (PSID)&(paaAllowedAce->SidStart), ! 904: (LPTSTR)&ucNameBuf, ! 905: (LPDWORD)&dwNameLength, ! 906: (LPTSTR)&ucDomainNmBuf, ! 907: (LPDWORD)&dwDNameLength, ! 908: (PSID_NAME_USE)&peAcctNameUse)) ! 909: { dwLastError = GetLastError(); ! 910: if (ERROR_NONE_MAPPED != dwLastError) ! 911: { PERR("LookupAccountSID"); ! 912: return(FALSE); ! 913: } ! 914: } ! 915: ! 916: if ( (ERROR_NONE_MAPPED == dwLastError) ! 917: || (SidTypeDeletedAccount == peAcctNameUse)) ! 918: { ! 919: dwACEsDeleted++; ! 920: ! 921: if (bJustCount) ! 922: { printf(" - would have edited ACL"); ! 923: return(TRUE); ! 924: } ! 925: ! 926: if (!DeleteAce(paclFile,dwAcl_i)) ! 927: { PERR("DeleteAce"); ! 928: return(FALSE); ! 929: } ! 930: } ! 931: } ! 932: } ! 933: ! 934: if (dwACEsDeletedBeforeNow < dwACEsDeleted) ! 935: { ! 936: /************************************************************************\ ! 937: * ! 938: * This validity check is something a real app might actually like to do. ! 939: * We manupulated the ACL, so before we write it back into an SD, a check ! 940: * is worth considering ! 941: * ! 942: \************************************************************************/ ! 943: ! 944: if (!IsValidAcl(paclFile)) ! 945: { PERR("IsValidAcl said bad ACL!"); ! 946: return(FALSE); ! 947: } ! 948: ! 949: /************************************************************************\ ! 950: * ! 951: * Modify the SD in virtual memory ! 952: * ! 953: \************************************************************************/ ! 954: ! 955: if (!SetSecurityDescriptorDacl ! 956: (psdFileSD, ! 957: TRUE, // Yes, set the DACL ! 958: paclFile, ! 959: FALSE)) // New DACL explicitly specified ! 960: { PERR("SetSecurityDescriptorDacl"); ! 961: return(FALSE); ! 962: } ! 963: ! 964: /************************************************************************\ ! 965: * ! 966: * This validity check is something a real app might actually like to do. ! 967: * We manupulated the SD, so before we write it back out to the file ! 968: * system, a check is worth considering ! 969: * ! 970: \************************************************************************/ ! 971: ! 972: if (!IsValidSecurityDescriptor(psdFileSD)) ! 973: { PERR("IsValidSecurityDescriptor said bad SD"); ! 974: return(FALSE); ! 975: } ! 976: ! 977: /************************************************************************\ ! 978: * ! 979: * Modify the SD on the hard disk ! 980: * ! 981: \************************************************************************/ ! 982: ! 983: if (!SetFileSecurity ! 984: (lpszFullName, ! 985: (SECURITY_INFORMATION)DACL_SECURITY_INFORMATION, ! 986: psdFileSD)) ! 987: { PERR("SetFileSecurity"); ! 988: return(FALSE); ! 989: } ! 990: ! 991: printf(" - edited ACL"); ! 992: } ! 993: ! 994: return(TRUE); ! 995: ! 996: } ! 997: ! 998: /****************************************************************************\ ! 999: * ! 1000: * FUNCTION: GetProcessSid ! 1001: * ! 1002: \****************************************************************************/ ! 1003: ! 1004: BOOL GetProcessSid(VOID) ! 1005: { ! 1006: HANDLE hProcess; ! 1007: PSECURITY_DESCRIPTOR psdProcessSD; ! 1008: PSID psidProcessOwnerSIDTemp; ! 1009: ! 1010: UCHAR ucBuf [SZ_SD_BUF]; ! 1011: DWORD dwSDLength = SZ_SD_BUF; ! 1012: DWORD dwSDLengthNeeded; ! 1013: BOOL bOwnerDefaulted; ! 1014: ! 1015: hProcess = GetCurrentProcess(); ! 1016: ! 1017: if (!hProcess) ! 1018: { PERR("GetCurrentProcess"); ! 1019: return(FALSE); ! 1020: } ! 1021: ! 1022: psdProcessSD = (PSECURITY_DESCRIPTOR)ucBuf; ! 1023: ! 1024: if (!GetKernelObjectSecurity ! 1025: (hProcess, ! 1026: (SECURITY_INFORMATION)(OWNER_SECURITY_INFORMATION), ! 1027: psdProcessSD, ! 1028: dwSDLength, ! 1029: (LPDWORD)&dwSDLengthNeeded)) ! 1030: { PERR("GetKernelObjectSecurity on current process handle"); ! 1031: return(FALSE); ! 1032: } ! 1033: ! 1034: /**************************************************************************\ ! 1035: * ! 1036: * This validity check is here for demonstration purposes. It's not likely a ! 1037: * real app would need to check the validity of this returned SD. The ! 1038: * validity check APIs are more intended to check validity after app code ! 1039: * has manipulated the structure and is about to hand it back to the system ! 1040: * ! 1041: \**************************************************************************/ ! 1042: ! 1043: if (!IsValidSecurityDescriptor(psdProcessSD)) ! 1044: { PERR("IsValidSecurityDescriptor said bad SD"); ! 1045: return(FALSE); ! 1046: } ! 1047: ! 1048: if (!GetSecurityDescriptorOwner ! 1049: (psdProcessSD, ! 1050: (PSID *)&psidProcessOwnerSIDTemp, ! 1051: (LPBOOL)&bOwnerDefaulted)) ! 1052: { PERR("GetSecurityDescriptorOwner of current process"); ! 1053: return(FALSE); ! 1054: } ! 1055: ! 1056: /**************************************************************************\ ! 1057: * ! 1058: * This validity check is here for demonstration pruposes. It's not likely a ! 1059: * real app would need to check the validity of this returned SID. The ! 1060: * validity check APIs are more intended to check validity after app code ! 1061: * has manipulated the structure and is about to hand it back to the system ! 1062: * ! 1063: \**************************************************************************/ ! 1064: ! 1065: if (!IsValidSid(psidProcessOwnerSIDTemp)) ! 1066: { PERR("IsValidSid said bad process SID!"); ! 1067: return(FALSE); ! 1068: } ! 1069: ! 1070: /**************************************************************************\ ! 1071: * ! 1072: * On the other hand, we are about to call GetLengthSid on the returned SID, ! 1073: * and calling GetLengthSid with an invalid SID is a bad idea, since then ! 1074: * GetLengthSid's result is undefined, and an undefined result is hard to ! 1075: * handle cleanly ! 1076: * ! 1077: * An app that handles a lot of SIDs would probably call GetLengthSid on the ! 1078: * SIDs it retrieves, then VirtualAlloc or malloc storage to hold copies of ! 1079: * the SIDs (when holding copies is necessary). Since this sample only ! 1080: * needs to hold one retrieved SID for any length of time, it's simpler to ! 1081: * statically allocate the size needed, and only use the call to ! 1082: * GetLengthSid to verify that the static area is large enough. The static ! 1083: * area will always be large enough unless perhaps something in the SID ! 1084: * data structures changes in a future build of NT. So, SZ_PROCESS_SID_BUF ! 1085: * is defined to be 16 simply because that's the value we know GetLengthSid ! 1086: * returns in this case (we know it's 16 by examining the value using the ! 1087: * debugger during app development). ! 1088: * ! 1089: \**************************************************************************/ ! 1090: ! 1091: if (SZ_PROCESS_SID_BUF < GetLengthSid(psidProcessOwnerSIDTemp)) ! 1092: { PERR("Can't do CopySid because buffer too small"); ! 1093: return(FALSE); ! 1094: } ! 1095: ! 1096: if (!CopySid((DWORD)SZ_PROCESS_SID_BUF, ! 1097: psidProcessOwnerSID, ! 1098: psidProcessOwnerSIDTemp)) ! 1099: { PERR("CopySid"); ! 1100: return(FALSE); ! 1101: } ! 1102: ! 1103: /**************************************************************************\ ! 1104: * ! 1105: * This validity check is here for demonstration pruposes only (see above). ! 1106: * ! 1107: \**************************************************************************/ ! 1108: ! 1109: if (!IsValidSid(psidProcessOwnerSID)) ! 1110: { PERR("IsValidSid said bad process SID!"); ! 1111: return(FALSE); ! 1112: } ! 1113: } ! 1114: ! 1115: /****************************************************************************\ ! 1116: * ! 1117: * FUNCTION: CrackArgs ! 1118: * ! 1119: \****************************************************************************/ ! 1120: ! 1121: BOOL CrackArgs(UINT argc, char *argv[]) ! 1122: { ! 1123: char *p; ! 1124: ! 1125: /**************************************************************************\ ! 1126: * ! 1127: * There must be three arguments ! 1128: * ! 1129: \**************************************************************************/ ! 1130: ! 1131: if (argc != 4) ! 1132: { DisplayHelp(); ! 1133: return(FALSE); ! 1134: } ! 1135: ! 1136: p=argv[1]; ! 1137: ! 1138: /**************************************************************************\ ! 1139: * ! 1140: * The switch argument must be 2-5 chars long ! 1141: * ! 1142: \**************************************************************************/ ! 1143: ! 1144: if ((strlen(p) < 2) || (strlen(p) > 5)) ! 1145: { DisplayHelp(); ! 1146: return(FALSE); ! 1147: } ! 1148: ! 1149: /**************************************************************************\ ! 1150: * ! 1151: * The first char in the switch argument must be / ! 1152: * ! 1153: \**************************************************************************/ ! 1154: ! 1155: if ('/' != *p) ! 1156: { DisplayHelp(); ! 1157: return(FALSE); ! 1158: } ! 1159: ! 1160: /**************************************************************************\ ! 1161: * ! 1162: * Chars 2-5 of the switch argument must be O or A or R or C ! 1163: * ! 1164: \**************************************************************************/ ! 1165: ! 1166: for (p=p+1; *p; p++) ! 1167: switch (*p) ! 1168: { case 'o': ! 1169: case 'O': ! 1170: bTakeOwnership = TRUE; ! 1171: break; ! 1172: case 'a': ! 1173: case 'A': ! 1174: bEditACLs = TRUE; ! 1175: break; ! 1176: case 'r': ! 1177: case 'R': ! 1178: bRecurse = TRUE; ! 1179: break; ! 1180: case 'c': ! 1181: case 'C': ! 1182: bJustCount = TRUE; ! 1183: break; ! 1184: default : ! 1185: DisplayHelp(); ! 1186: return(FALSE); ! 1187: } ! 1188: ! 1189: /**************************************************************************\ ! 1190: * ! 1191: * Have to say one of O or A ! 1192: * ! 1193: \**************************************************************************/ ! 1194: ! 1195: if (!(bTakeOwnership || bEditACLs)) ! 1196: { DisplayHelp(); ! 1197: return(FALSE); ! 1198: } ! 1199: ! 1200: return(TRUE); ! 1201: } ! 1202: ! 1203: /****************************************************************************\ ! 1204: * ! 1205: * FUNCTION: DisplayHelp ! 1206: * ! 1207: \****************************************************************************/ ! 1208: ! 1209: VOID DisplayHelp(VOID) ! 1210: { ! 1211: printf("\nTo run type SIDCLEAN and 3 parameters. Syntax:"); ! 1212: printf("\n SIDCLEAN /roah dirspec filepattern"); ! 1213: printf("\n /r Recursively process subdirectories"); ! 1214: printf("\n /o For any files matching filepattern: Take ownership if"); ! 1215: printf("\n file currently owned by any deleted SID"); ! 1216: printf("\n /a For any files matching filepattern: Edit ACL, deleting"); ! 1217: printf("\n ACEs associated with any deleted SID"); ! 1218: printf("\n /c Overrides /o and /a, causes counts of /a or /o actions that"); ! 1219: printf("\n would take place if /c not used. Counts always displayed"); ! 1220: printf("\n /h Override other switch values, just display this message\n"); ! 1221: printf("\n . and .. syntax allowed in dirspec"); ! 1222: printf("\n * and ? wildcards allowed in filepattern"); ! 1223: printf("\n Switch letters can be in any order, upper or lower case"); ! 1224: printf("\nExamples:"); ! 1225: printf("\n SIDCLEAN /o . *.* Take ownership of all files (but not subdirs) in "); ! 1226: printf("\n current dir that are owned by any deleted SID"); ! 1227: printf("\n SIDCLEAN /a . *.* For any file in current dir (but not subdirs), delete"); ! 1228: printf("\n any ACL info that is associated with any deleted SID"); ! 1229: printf("\n SIDCLEAN /ro . *.* Same as first example, but also recursively process"); ! 1230: printf("\n subdirectories"); ! 1231: printf("\n SIDCLEAN /ar . *.* Same as second example, but also recursively process"); ! 1232: printf("\n subdirectories"); ! 1233: printf("\n SIDCLEAN /O \\ *.* Same as first example, but process files in root"); ! 1234: printf("\n of current drive"); ! 1235: printf("\n SIDCLEAN /oC .. *.* Same as first example, but looks at files in dir"); ! 1236: printf("\n containing current dir, processes nothing, just counts"); ! 1237: printf("\n SIDCLEAN /A d:\\ *.* Same as second example, but process files in root"); ! 1238: printf("\n of D: drive"); ! 1239: printf("\n SIDCLEAN Displays this message"); ! 1240: printf("\n SIDCLEAN /h Displays this message (so do ? -? /? -h -H /H)\n"); ! 1241: printf("\nThis utility must be run while logged on as Administrator\n"); ! 1242: ! 1243: return; ! 1244: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.