├── .current_state.txt ├── .gitignore ├── Dockerfile ├── README.md ├── antivirus_debugger.py ├── config.json ├── config.py ├── find.py ├── find_bad_strings.py ├── known_bad_strings.txt ├── output.txt ├── pe_utils.py ├── pytest.ini ├── requirements.txt ├── scan.py ├── scanner.py ├── slides └── 2022_03_Insomnihack_Antivirus_vmeier.pdf ├── string_encryptor.py └── test_find_bad_strings.py /.current_state.txt: -------------------------------------------------------------------------------- 1 | StringRef(index=404, paddr=654016, vaddr=6443108032, length=21, size=44, section='(.rdata)', encoding='utf16le', content='Pass-the-ccache [NT6]', is_replaced=False, is_bad=False)StringRef(index=993, paddr=694520, vaddr=6443148536, length=10, size=22, section='(.rdata)', encoding='utf16le', content='lsasrv.dll', is_replaced=False, is_bad=False)StringRef(index=1153, paddr=703360, vaddr=6443157376, length=31, size=32, section='(.rdata)', encoding='ascii', content='SamIFree_SAMPR_USER_INFO_BUFFER', is_replaced=False, is_bad=False)StringRef(index=1338, paddr=717832, vaddr=6443171848, length=20, size=42, section='(.rdata)', encoding='utf16le', content='KiwiAndRegistryTools', is_replaced=False, is_bad=False)StringRef(index=1743, paddr=747560, vaddr=6443201576, length=11, size=24, section='(.rdata)', encoding='utf16le', content='wdigest.dll', is_replaced=False, is_bad=False)StringRef(index=1854, paddr=755440, vaddr=6443209456, length=8, size=18, section='(.rdata)', encoding='utf16le', content='multirdp', is_replaced=False, is_bad=False)StringRef(index=2318, paddr=787008, vaddr=6443241024, length=14, size=30, section='(.rdata)', encoding='utf16le', content='logonPasswords', is_replaced=False, is_bad=False)StringRef(index=2334, paddr=787760, vaddr=6443241776, length=7, size=16, section='(.rdata)', encoding='utf16le', content='credman', is_replaced=False, is_bad=False)StringRef(index=2478, paddr=798944, vaddr=6443252960, length=34, size=70, section='(.rdata)', encoding='utf16le', content='[%x;%x]-%1u-%u-%08x-%wZ@%wZ-%wZ.%s', is_replaced=False, is_bad=False)StringRef(index=2489, paddr=799696, vaddr=6443253712, length=33, size=68, section='(.rdata)', encoding='utf16le', content='n.e. (KIWI_MSV1_0_CREDENTIALS KO)', is_replaced=False, is_bad=False)StringRef(index=2936, paddr=832360, vaddr=6443286376, length=11, size=24, section='(.rdata)', encoding='utf16le', content='\\\\.\\mimidrv', is_replaced=False, is_bad=False)StringRef(index=2937, paddr=832384, vaddr=6443286400, length=18, size=38, section='(.rdata)', encoding='utf16le', content='%*s**KEY (capi)**\\n', is_replaced=False, is_bad=False)StringRef(index=2938, paddr=832432, vaddr=6443286448, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwUniqueNameLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2939, paddr=832512, vaddr=6443286528, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwSiPublicKeyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2940, paddr=832592, vaddr=6443286608, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwSiPrivateKeyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2941, paddr=832672, vaddr=6443286688, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwExPublicKeyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2942, paddr=832752, vaddr=6443286768, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwExPrivateKeyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2943, paddr=832832, vaddr=6443286848, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwHashLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2944, paddr=832912, vaddr=6443286928, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwSiExportFlagLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2945, paddr=832992, vaddr=6443287008, length=36, size=74, section='(.rdata)', encoding='utf16le', content='%*s dwExExportFlagLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2946, paddr=833072, vaddr=6443287088, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pUniqueName :', is_replaced=False, is_bad=False)StringRef(index=2947, paddr=833128, vaddr=6443287144, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pHash :', is_replaced=False, is_bad=False)StringRef(index=2948, paddr=833184, vaddr=6443287200, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pSiPublicKey :', is_replaced=False, is_bad=False)StringRef(index=2949, paddr=833240, vaddr=6443287256, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pSiPrivateKey :\\n', is_replaced=False, is_bad=False)StringRef(index=2950, paddr=833296, vaddr=6443287312, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pSiExportFlag :\\n', is_replaced=False, is_bad=False)StringRef(index=2951, paddr=833352, vaddr=6443287368, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pExPublicKey :', is_replaced=False, is_bad=False)StringRef(index=2952, paddr=833408, vaddr=6443287424, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pExPrivateKey :\\n', is_replaced=False, is_bad=False)StringRef(index=2953, paddr=833464, vaddr=6443287480, length=26, size=54, section='(.rdata)', encoding='utf16le', content='%*s pExExportFlag :\\n', is_replaced=False, is_bad=False)StringRef(index=2954, paddr=833520, vaddr=6443287536, length=72, size=146, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_key_cng_create ; kull_m_key_cng_properties_create (public)\\n', is_replaced=False, is_bad=False)StringRef(index=2955, paddr=833672, vaddr=6443287688, length=17, size=36, section='(.rdata)', encoding='utf16le', content='%*s**KEY (cng)**\\n', is_replaced=False, is_bad=False)StringRef(index=2956, paddr=833712, vaddr=6443287728, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s dwVersion : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2957, paddr=833792, vaddr=6443287808, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s unk : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2958, paddr=833872, vaddr=6443287888, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s dwNameLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2959, paddr=833952, vaddr=6443287968, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s type : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2960, paddr=834032, vaddr=6443288048, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s dwPublicPropertiesLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2961, paddr=834112, vaddr=6443288128, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s dwPrivatePropertiesLen: %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2962, paddr=834192, vaddr=6443288208, length=39, size=80, section='(.rdata)', encoding='utf16le', content='%*s dwPrivateKeyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2963, paddr=834272, vaddr=6443288288, length=29, size=60, section='(.rdata)', encoding='utf16le', content='%*s unkArray[16] :', is_replaced=False, is_bad=False)StringRef(index=2964, paddr=834336, vaddr=6443288352, length=29, size=60, section='(.rdata)', encoding='utf16le', content='%*s pName :', is_replaced=False, is_bad=False)StringRef(index=2965, paddr=834400, vaddr=6443288416, length=5, size=12, section='(.rdata)', encoding='utf16le', content='%.*s\\n', is_replaced=False, is_bad=False)StringRef(index=2966, paddr=834416, vaddr=6443288432, length=29, size=60, section='(.rdata)', encoding='utf16le', content='%*s pPublicProperties :', is_replaced=False, is_bad=False)StringRef(index=2967, paddr=834480, vaddr=6443288496, length=29, size=60, section='(.rdata)', encoding='utf16le', content='%*s pPrivateProperties :\\n', is_replaced=False, is_bad=False)StringRef(index=2968, paddr=834544, vaddr=6443288560, length=29, size=60, section='(.rdata)', encoding='utf16le', content='%*s pPrivateKey :\\n', is_replaced=False, is_bad=False)StringRef(index=2969, paddr=834608, vaddr=6443288624, length=24, size=50, section='(.rdata)', encoding='utf16le', content='%*s**KEY CNG PROPERTY**\\n', is_replaced=False, is_bad=False)StringRef(index=2970, paddr=834672, vaddr=6443288688, length=33, size=68, section='(.rdata)', encoding='utf16le', content='%*s dwStructLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2971, paddr=834752, vaddr=6443288768, length=33, size=68, section='(.rdata)', encoding='utf16le', content='%*s type : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2972, paddr=834832, vaddr=6443288848, length=33, size=68, section='(.rdata)', encoding='utf16le', content='%*s unk : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2973, paddr=834912, vaddr=6443288928, length=33, size=68, section='(.rdata)', encoding='utf16le', content='%*s dwNameLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2974, paddr=834992, vaddr=6443289008, length=33, size=68, section='(.rdata)', encoding='utf16le', content='%*s dwPropertyLen : %08x - %u\\n', is_replaced=False, is_bad=False)StringRef(index=2975, paddr=835064, vaddr=6443289080, length=23, size=48, section='(.rdata)', encoding='utf16le', content='%*s pName :', is_replaced=False, is_bad=False)StringRef(index=2976, paddr=835112, vaddr=6443289128, length=23, size=48, section='(.rdata)', encoding='utf16le', content='%*s pProperty :', is_replaced=False, is_bad=False)StringRef(index=2977, paddr=835160, vaddr=6443289176, length=12, size=26, section='(.rdata)', encoding='utf16le', content='%u field(s)\\n', is_replaced=False, is_bad=False)StringRef(index=2978, paddr=835200, vaddr=6443289216, length=41, size=84, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_net_getDC ; DsGetDcName: %u\\n', is_replaced=False, is_bad=False)StringRef(index=2979, paddr=835296, vaddr=6443289312, length=68, size=138, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_net_getComputerName ; GetComputerNameEx(data) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2980, paddr=835440, vaddr=6443289456, length=68, size=138, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_net_getComputerName ; GetComputerNameEx(init) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2981, paddr=835584, vaddr=6443289600, length=22, size=46, section='(.rdata)', encoding='utf16le', content='Erreur LocalAlloc: %u\\n', is_replaced=False, is_bad=False)StringRef(index=2982, paddr=835640, vaddr=6443289656, length=21, size=44, section='(.rdata)', encoding='utf16le', content='"%s" service patched\\n', is_replaced=False, is_bad=False)StringRef(index=2983, paddr=835696, vaddr=6443289712, length=76, size=154, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; kull_m_patch (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2984, paddr=835856, vaddr=6443289872, length=116, size=234, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; kull_m_process_getVeryBasicModuleInformationsForName (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2985, paddr=836096, vaddr=6443290112, length=75, size=152, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; OpenProcess (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2986, paddr=836256, vaddr=6443290272, length=77, size=156, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; Service is not running\\n', is_replaced=False, is_bad=False)StringRef(index=2987, paddr=836416, vaddr=6443290432, length=95, size=192, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; kull_m_service_getUniqueForName (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=2988, paddr=836608, vaddr=6443290624, length=86, size=174, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_patch_genericProcessOrServiceFromBuild ; Incorrect version in references\\n', is_replaced=False, is_bad=False)StringRef(index=2989, paddr=836784, vaddr=6443290800, length=4, size=10, section='(.rdata)', encoding='utf16le', content='NONE', is_replaced=False, is_bad=False)StringRef(index=2990, paddr=836808, vaddr=6443290824, length=9, size=20, section='(.rdata)', encoding='utf16le', content='EXPAND_SZ', is_replaced=False, is_bad=False)StringRef(index=2991, paddr=836832, vaddr=6443290848, length=6, size=14, section='(.rdata)', encoding='utf16le', content='BINARY', is_replaced=False, is_bad=False)StringRef(index=2992, paddr=836848, vaddr=6443290864, length=5, size=12, section='(.rdata)', encoding='utf16le', content='DWORD', is_replaced=False, is_bad=False)StringRef(index=2993, paddr=836864, vaddr=6443290880, length=16, size=34, section='(.rdata)', encoding='utf16le', content='DWORD_BIG_ENDIAN', is_replaced=False, is_bad=False)StringRef(index=2994, paddr=836904, vaddr=6443290920, length=4, size=10, section='(.rdata)', encoding='utf16le', content='LINK', is_replaced=False, is_bad=False)StringRef(index=2995, paddr=836920, vaddr=6443290936, length=8, size=18, section='(.rdata)', encoding='utf16le', content='MULTI_SZ', is_replaced=False, is_bad=False)StringRef(index=2996, paddr=836944, vaddr=6443290960, length=13, size=28, section='(.rdata)', encoding='utf16le', content='RESOURCE_LIST', is_replaced=False, is_bad=False)StringRef(index=2997, paddr=836976, vaddr=6443290992, length=24, size=50, section='(.rdata)', encoding='utf16le', content='FULL_RESOURCE_DESCRIPTOR', is_replaced=False, is_bad=False)StringRef(index=2998, paddr=837032, vaddr=6443291048, length=26, size=54, section='(.rdata)', encoding='utf16le', content='RESOURCE_REQUIREMENTS_LIST', is_replaced=False, is_bad=False)StringRef(index=2999, paddr=837088, vaddr=6443291104, length=5, size=12, section='(.rdata)', encoding='utf16le', content='QWORD', is_replaced=False, is_bad=False)StringRef(index=3000, paddr=837104, vaddr=6443291120, length=78, size=158, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_registry_OpenAndQueryWithAlloc ; kull_m_registry_RegOpenKeyEx KO\\n', is_replaced=False, is_bad=False)StringRef(index=3001, paddr=837264, vaddr=6443291280, length=74, size=150, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_registry_QueryWithAlloc ; kull_m_registry_RegQueryValueEx KO\\n', is_replaced=False, is_bad=False)StringRef(index=3002, paddr=837424, vaddr=6443291440, length=80, size=162, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_registry_QueryWithAlloc ; pre - kull_m_registry_RegQueryValueEx KO\\n', is_replaced=False, is_bad=False)StringRef(index=3003, paddr=837600, vaddr=6443291616, length=61, size=124, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_create ; RtlCreateUserThread (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3004, paddr=837728, vaddr=6443291744, length=60, size=122, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_create ; CreateRemoteThread (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3005, paddr=837856, vaddr=6443291872, length=16, size=34, section='(.rdata)', encoding='utf16le', content='Th @ %p\\nDa @ %p\\n', is_replaced=False, is_bad=False)StringRef(index=3006, paddr=837904, vaddr=6443291920, length=68, size=138, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_create ; kull_m_kernel_ioctl_handle (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3007, paddr=838048, vaddr=6443292064, length=89, size=180, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_CreateRemoteCodeWitthPatternReplace ; kull_m_memory_copy (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3008, paddr=838240, vaddr=6443292256, length=109, size=220, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_CreateRemoteCodeWitthPatternReplace ; kull_m_memory_alloc / VirtualAlloc(Ex) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3009, paddr=838464, vaddr=6443292480, length=73, size=148, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_remotelib_CreateRemoteCodeWitthPatternReplace ; No buffer ?\\n', is_replaced=False, is_bad=False)StringRef(index=3010, paddr=838616, vaddr=6443292632, length=14, size=30, section='(.rdata)', encoding='utf16le', content='ServicesActive', is_replaced=False, is_bad=False)StringRef(index=3011, paddr=838656, vaddr=6443292672, length=36, size=74, section='(.rdata)', encoding='utf16le', content="[+] '%s' service already registered\\n", is_replaced=False, is_bad=False)StringRef(index=3012, paddr=838736, vaddr=6443292752, length=29, size=60, section='(.rdata)', encoding='utf16le', content="[*] '%s' service not present\\n", is_replaced=False, is_bad=False)StringRef(index=3013, paddr=838800, vaddr=6443292816, length=41, size=84, section='(.rdata)', encoding='utf16le', content="[+] '%s' service successfully registered\\n", is_replaced=False, is_bad=False)StringRef(index=3014, paddr=838896, vaddr=6443292912, length=33, size=68, section='(.rdata)', encoding='utf16le', content="[+] '%s' service ACL to everyone\\n", is_replaced=False, is_bad=False)StringRef(index=3015, paddr=838976, vaddr=6443292992, length=68, size=138, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_install ; kull_m_service_addWorldToSD (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3016, paddr=839120, vaddr=6443293136, length=54, size=110, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_install ; CreateService (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3017, paddr=839232, vaddr=6443293248, length=52, size=106, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_install ; OpenService (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3018, paddr=839344, vaddr=6443293360, length=25, size=52, section='(.rdata)', encoding='utf16le', content="[+] '%s' service started\\n", is_replaced=False, is_bad=False)StringRef(index=3019, paddr=839408, vaddr=6443293424, length=33, size=68, section='(.rdata)', encoding='utf16le', content="[*] '%s' service already started\\n", is_replaced=False, is_bad=False)StringRef(index=3020, paddr=839488, vaddr=6443293504, length=53, size=108, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_install ; StartService (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3021, paddr=839600, vaddr=6443293616, length=62, size=126, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_install ; OpenSCManager(create) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3022, paddr=839728, vaddr=6443293744, length=25, size=52, section='(.rdata)', encoding='utf16le', content="[+] '%s' service stopped\\n", is_replaced=False, is_bad=False)StringRef(index=3023, paddr=839784, vaddr=6443293800, length=29, size=60, section='(.rdata)', encoding='utf16le', content="[*] '%s' service not running\\n", is_replaced=False, is_bad=False)StringRef(index=3024, paddr=839856, vaddr=6443293872, length=62, size=126, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_uninstall ; kull_m_service_stop (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3025, paddr=839984, vaddr=6443294000, length=25, size=52, section='(.rdata)', encoding='utf16le', content="[+] '%s' service removed\\n", is_replaced=False, is_bad=False)StringRef(index=3026, paddr=840048, vaddr=6443294064, length=64, size=130, section='(.rdata)', encoding='utf16le', content='ERROR kull_m_service_uninstall ; kull_m_service_remove (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3027, paddr=840192, vaddr=6443294208, length=64, size=130, section='(.rdata)', encoding='utf16le', content='ERROR sr98_test_device ; Received data is not origin+1 (0x%04x)\\n', is_replaced=False, is_bad=False)StringRef(index=3028, paddr=840336, vaddr=6443294352, length=57, size=116, section='(.rdata)', encoding='utf16le', content='ERROR sr98_test_device ; Received size is not 2 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3029, paddr=840464, vaddr=6443294480, length=55, size=112, section='(.rdata)', encoding='utf16le', content='ERROR sr98_read_emid ; Received size is not 6 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3030, paddr=840576, vaddr=6443294592, length=68, size=138, section='(.rdata)', encoding='utf16le', content='ERROR sr98_t5577_write_block ; Received data size is not 4 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3031, paddr=840720, vaddr=6443294736, length=63, size=128, section='(.rdata)', encoding='utf16le', content='ERROR sr98_t5577_write_block ; Received size is not 1 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3032, paddr=840848, vaddr=6443294864, length=53, size=108, section='(.rdata)', encoding='utf16le', content='ERROR sr98_t5577_reset ; Data size is not 0 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3033, paddr=840960, vaddr=6443294976, length=57, size=116, section='(.rdata)', encoding='utf16le', content='ERROR sr98_t5577_reset ; Received size is not 1 (0x%02x)\\n', is_replaced=False, is_bad=False)StringRef(index=3034, paddr=841088, vaddr=6443295104, length=39, size=80, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; Bad CRC/data\\n', is_replaced=False, is_bad=False)StringRef(index=3035, paddr=841168, vaddr=6443295184, length=49, size=100, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; Bad data size/ctl code\\n', is_replaced=False, is_bad=False)StringRef(index=3036, paddr=841280, vaddr=6443295296, length=37, size=76, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; Bad header\\n', is_replaced=False, is_bad=False)StringRef(index=3037, paddr=841360, vaddr=6443295376, length=41, size=84, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; Read size = %u\\n', is_replaced=False, is_bad=False)StringRef(index=3038, paddr=841456, vaddr=6443295472, length=44, size=90, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; ReadFile (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3039, paddr=841552, vaddr=6443295568, length=45, size=92, section='(.rdata)', encoding='utf16le', content='ERROR sr98_send_receive ; WriteFile (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3040, paddr=841648, vaddr=6443295664, length=45, size=92, section='(.rdata)', encoding='utf16le', content='ERROR sr98_devices_get ; HidP_GetCaps (%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3041, paddr=841744, vaddr=6443295760, length=55, size=112, section='(.rdata)', encoding='utf16le', content='ERROR sr98_devices_get ; CreateFile (hDevice) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3042, paddr=841856, vaddr=6443295872, length=60, size=122, section='(.rdata)', encoding='utf16le', content='ERROR sr98_devices_get ; CreateFile (deviceHandle) (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3043, paddr=841984, vaddr=6443296000, length=54, size=110, section='(.rdata)', encoding='utf16le', content='ERROR sr98_devices_get ; SetupDiGetClassDevs (0x%08x)\\n', is_replaced=False, is_bad=False)StringRef(index=3044, paddr=842096, vaddr=6443296112, length=4, size=10, section='(.rdata)', encoding='utf16le', content='%02x', is_replaced=False, is_bad=False)StringRef(index=3045, paddr=842112, vaddr=6443296128, length=5, size=12, section='(.rdata)', encoding='utf16le', content='%02x', is_replaced=False, is_bad=False)StringRef(index=3046, paddr=842128, vaddr=6443296144, length=8, size=18, section='(.rdata)', encoding='utf16le', content='0x%02x,', is_replaced=False, is_bad=False)StringRef(index=3047, paddr=842152, vaddr=6443296168, length=6, size=14, section='(.rdata)', encoding='utf16le', content='\\x%02x', is_replaced=False, is_bad=False) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | __pycache__/* 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux/base 2 | RUN echo "root:root" | chpasswd 3 | RUN useradd -m -G wheel -s /bin/bash toto \ 4 | && echo "toto:toto" | chpasswd 5 | RUN pacman -Syu --noconfirm && pacman -Sy --noconfirm git sudo vim base-devel cabextract python3 cmake lib32-glibc lib32-gcc-libs gcc-multilib python-pip radare2 6 | RUN echo -e "%wheel ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/99_wheel 7 | RUN python3 -m pip install tqdm r2pipe 8 | 9 | #RUN cd /tmp \ 10 | # && git clone https://aur.archlinux.org/yay.git \ 11 | # && cd yay \ 12 | # && chown -R toto. /tmp/yay/ \ 13 | # && sudo -u toto makepkg -s \ 14 | # && pacman --noconfirm -U /tmp/yay/yay*.pkg.tar.xz 15 | # 16 | #RUN sudo -u toto yay -Sy --noconfirm cmake 17 | 18 | USER toto 19 | WORKDIR /home/toto 20 | 21 | #RUN git clone https://github.com/taviso/loadlibrary# && cd loadlibrary && make 22 | #COPY engine/* /home/toto/loadlibrary/engine 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Most antivirus engines rely on strings or other bytes sequences, function exports and big integers to recognize malware. 4 | This project helps to automatically recover these signatures. 5 | 6 | # Project status 7 | 8 | Able to automatically find and remove the strings that have the most impact on the AV's verdict. 9 | 10 | # Setup and usage 11 | 12 | Here are the instructions to use this tool. 13 | 14 | ## Dependencies (Python 3) 15 | 16 | * python-tqdm 17 | * python-hexdump 18 | * pytest 19 | 20 | ``` 21 | python3 -m pip install -r requirements.txt 22 | ``` 23 | 24 | ## Dependencies (other) 25 | 26 | * rabin2 (from radare2) 27 | * loadlibrary: Windows Defender scanner ported to Linux by taviso (3 minutes setup, instructions at https://github.com/taviso/loadlibrary) 28 | 29 | ## Configuration 30 | 31 | Fix all the values in `config.json`. 32 | 33 | ## Usage 34 | 35 | ``` 36 | python3 antivirus_debugger.py -h 37 | usage: antivirus_debugger.py [-h] [-s] [-z] [-f FILE] [-e] [-l LENGTH] [-c SECTION] [-g] [-V] [-H HIDE_SECTION] [-S SCANNER] 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | -s, --skip-strings Skip strings analysis 42 | -z, --skip-sections Skip sections analysis 43 | -f FILE, --file FILE path to file 44 | -e, --extensive search strings in all sections 45 | -l LENGTH, --length LENGTH 46 | minimum length of strings 47 | -c SECTION, --section SECTION 48 | Analyze provided section 49 | -g, --globals Analyze global variables in .data section 50 | -V, --virus Virus scan 51 | -H HIDE_SECTION, --hide-section HIDE_SECTION 52 | Hide a section 53 | -S SCANNER, --scanner SCANNER 54 | Antivirus engine. Default = DockerWindowsDefender 55 | ``` 56 | -------------------------------------------------------------------------------- /antivirus_debugger.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from tempfile import NamedTemporaryFile 4 | 5 | from find import bytes_detection 6 | from find_bad_strings import bissect 7 | from pe_utils import * 8 | from scanner import DockerWindowsDefender 9 | from scanner import g_scanner 10 | 11 | log_format = '[%(levelname)-8s][%(asctime)s][%(filename)s:%(lineno)3d] %(funcName)s() :: %(message)s' 12 | logging.basicConfig(filename='debug.log', 13 | filemode='a', 14 | format=format, 15 | datefmt='%Y/%m/%d %H:%M', 16 | level=logging.DEBUG 17 | ) 18 | 19 | 20 | rootLogger = logging.getLogger() 21 | logFormatter = logging.Formatter(log_format) 22 | 23 | consoleHandler = logging.StreamHandler() 24 | consoleHandler.setFormatter(logFormatter) 25 | rootLogger.addHandler(consoleHandler) 26 | 27 | BINARY = "" 28 | 29 | g_args = None 30 | 31 | 32 | 33 | """ 34 | attempts to locate the part in a PE file that causes the antivirus detection 35 | """ 36 | def locate_signature(pe): 37 | 38 | nb_section_detected = 0 39 | detected_sections = [] 40 | 41 | for section in pe.sections: 42 | 43 | # copy the binary 44 | new_name = NamedTemporaryFile().name 45 | shutil.copyfile(pe.filename, new_name) 46 | 47 | # hide the section 48 | new_pe = deepcopy(pe) 49 | new_pe.filename = new_name 50 | hide_section(new_pe, section.name) 51 | new_pe.md5 = md5(new_name) 52 | 53 | logging.debug(f"Scanning {new_name} md5 = {new_pe.md5}") 54 | # scan it 55 | status = not g_scanner.scan(new_pe.filename) 56 | 57 | # record the result 58 | section.detected = not status 59 | 60 | if status: 61 | logging.info(f"Section {section.name} triggers the antivirus") 62 | nb_section_detected += 1 63 | detected_sections += [section] 64 | 65 | logging.info(f"{nb_section_detected} section(s) trigger the antivirus") 66 | return nb_section_detected, detected_sections 67 | 68 | 69 | def bytes_analysis(pe, start_address, end_address): 70 | 71 | # check that masking these bytes is sufficient to evade the antivirus 72 | # copy the binary 73 | new_name = NamedTemporaryFile().name 74 | shutil.copyfile(pe.filename, new_name) 75 | 76 | # hide the byzes 77 | new_pe = deepcopy(pe) 78 | new_pe.filename = new_name 79 | hide_bytes(new_pe, start_address, end_address-start_address) 80 | status = g_scanner.scan(new_pe.filename) 81 | 82 | if status: 83 | logging.warning("No idea. Your binary is indeed detected but hiding everything but the PE header results in detection anyways. Check PE header.") 84 | logging.debug(new_pe.filename) 85 | spwn_dbg() 86 | raise Exception("") 87 | 88 | 89 | # a signature is present in these bystes, let's binary search 90 | 91 | bytes_detection(pe.filename, start_address, end_address) 92 | 93 | 94 | def strings_analysis(pe): 95 | 96 | 97 | # no point in continuing if the binary is not detected as malicious already. 98 | #assert(scan(sample_file) is True) 99 | 100 | str_refs = pe.strings 101 | logging.debug(f"Got {len(str_refs)} string objects") 102 | 103 | # mask all strings 104 | logging.debug("Patching all the strings in the binary") 105 | 106 | # patch the binary (mask the string) 107 | for str_ref in str_refs: 108 | # if str_ref.length > 500: 109 | str_ref.should_mask = True 110 | 111 | # copy the binary 112 | new_name = NamedTemporaryFile().name 113 | shutil.copyfile(pe.filename, new_name) 114 | logging.info(new_name) 115 | 116 | # hide the byzes 117 | new_pe = deepcopy(pe) 118 | new_pe.filename = new_name 119 | 120 | pipe = r2pipe.open(new_name, flags=["-w"]) 121 | patch_binary_mass(new_name, str_refs, pipe) 122 | 123 | logging.debug("Binary patched") 124 | detection_result = not g_scanner.scan(new_name) 125 | 126 | # sometimes there are signatures in the .txt sections 127 | if detection_result: 128 | logging.info(f"{g_scanner.scanner_name} seems to only detect strings in this binary.") 129 | else: 130 | logging.warning(f"Patching all the strings does not evade detection.") 131 | 132 | return detection_result 133 | 134 | 135 | 136 | def multi_signatures_analysis(pe): 137 | 138 | 139 | 140 | for section in pe.sections: 141 | new_pe = backup_pe(pe) 142 | 143 | hide_all_sections_except(new_pe, section.name) 144 | 145 | status = g_scanner.scan(new_pe.filename) 146 | 147 | if status: 148 | logging.info(f"Section {section.name} has a signature") 149 | 150 | return True # TODO 151 | 152 | 153 | def global_vars_analysis(pe): 154 | 155 | logging.info("Applying patches") 156 | new_pe1 = backup_pe(pe) 157 | for patch in new_pe1.patches: 158 | hide_bytes(new_pe1, patch.addr, patch.size) 159 | 160 | 161 | logging.info(f"Simple check: maybe a single global variable is detected") 162 | vars = detect_data(new_pe1) 163 | print_global_variables(new_pe1, vars) 164 | 165 | status, base_threat_name = g_scanner.scan(new_pe1.filename, with_name=True) 166 | assert(status) 167 | 168 | 169 | sig_found = False 170 | 171 | for var in vars: 172 | 173 | new_pe = backup_pe(new_pe1) 174 | hide_bytes(new_pe, var.paddr, var.size) 175 | status, threat_name = g_scanner.scan(new_pe.filename, with_name=True) 176 | logging.debug(f"{status} - {threat_name}") 177 | if not status or threat_name != base_threat_name: 178 | logging.info(f"{g_scanner.scanner_name} detects this global variable:") 179 | var.display(pe) 180 | pe.patches += [Patch(var.paddr, var.size)] 181 | sig_found = True 182 | 183 | if not status: 184 | logging.info(f"Done ! You should patch these bytes:") 185 | for patch in pe.patches: 186 | patch.display(pe) 187 | return 188 | else: 189 | logging.error("Patching and starting over, since we've found something that may decrease the detection score.") 190 | 191 | break 192 | 193 | if sig_found: 194 | global_vars_analysis(pe) 195 | 196 | def investigate(pe): 197 | 198 | status = g_scanner.scan(pe.filename) 199 | 200 | if not status: 201 | 202 | logging.error(f"{pe.filename} is not detected by {g_scanner.scanner_name}") 203 | return 204 | 205 | if g_args.virus: 206 | logging.info(status) 207 | return 208 | 209 | if g_args.globals: 210 | 211 | global_vars_analysis(pe) 212 | 213 | if not g_args.skip_strings: 214 | strings_based_detection = strings_analysis(pe) 215 | else: 216 | strings_based_detection = False 217 | 218 | if not g_args.skip_sections and not g_args.section: 219 | nb_sections, detected_sections = locate_signature(pe) 220 | else: 221 | nb_sections = 0 222 | 223 | 224 | if not strings_based_detection and nb_sections == 0 and not g_args.section: 225 | 226 | if multi_signatures_analysis(pe): 227 | 228 | logging.info(f"Signature in .text section, but also elsewhere") 229 | else: 230 | 231 | logging.info(f"{g_scanner.scanner_name} seems to have a dumb bytes-based detection engine") 232 | sections_addr = [section.addr for section in pe.sections] 233 | start_address = min(sections_addr) 234 | end_address = max(sections_addr) 235 | assert(end_address > start_address) 236 | bytes_analysis(pe, start_address, end_address) 237 | 238 | elif nb_sections == 0 and not g_args.section: 239 | logging.info("Finding the high score strings will be sufficient to evade the engine") 240 | bissect(pe.filename) 241 | 242 | elif nb_sections == 1: 243 | 244 | if detected_sections[0].name == ".data": 245 | global_vars_analysis(pe) 246 | else: 247 | logging.info(f"Launching bytes analysis on section {detected_sections[0].name}") 248 | bytes_analysis(pe, detected_sections[0].addr, detected_sections[0].addr+detected_sections[0].size) 249 | elif g_args.section: 250 | logging.info(f"Launching bytes analysis on section {g_args.section}") 251 | 252 | section = next((sec for sec in pe.sections if sec.name == g_args.section), None) 253 | bytes_analysis(pe, section.addr, section.addr+section.size) 254 | 255 | 256 | 257 | def parse_pe(sample_file): 258 | 259 | pe = PE() 260 | pe.filename = sample_file 261 | pe.sections = get_sections(pe) 262 | pe.strings = parse_strings(sample_file, g_args.extensive, g_args.length) 263 | pe.md5 = md5(sample_file) 264 | return pe 265 | 266 | 267 | if __name__ == "__main__": 268 | 269 | parser = argparse.ArgumentParser() 270 | 271 | parser.add_argument("-s", '--skip-strings', help="Skip strings analysis", action="store_true") 272 | parser.add_argument("-z", "--skip-sections", help="Skip sections analysis", action="store_true") 273 | parser.add_argument("-f", "--file", help="path to file") 274 | parser.add_argument("-e", "--extensive", help="search strings in all sections", action="store_true") 275 | parser.add_argument("-l", "--length", help="minimum length of strings", type=int, default=5) 276 | parser.add_argument('-c', '--section', help="Analyze provided section") 277 | parser.add_argument('-g', '--globals', help="Analyze global variables in .data section", action="store_true") 278 | parser.add_argument('-V', '--virus', help="Virus scan", action="store_true") 279 | parser.add_argument('-H', '--hide-section', help="Hide a section", type=str) 280 | parser.add_argument('-S', "--scanner", help="Antivirus engine", default="DockerWindowsDefender") 281 | g_args = parser.parse_args() 282 | 283 | if g_args.scanner == "DockerWindowsDefender": 284 | g_scanner = DockerWindowsDefender() 285 | 286 | pe = parse_pe(g_args.file) 287 | 288 | if g_args.hide_section: 289 | copy_file = NamedTemporaryFile(delete=False) 290 | shutil.copy(pe.filename, copy_file.name) 291 | pe.filename = copy_file.name 292 | hide_section(pe, g_args.hide_section) 293 | logging.info(f"Dumped patched file @ {copy_file.name}") 294 | sys.exit(0) 295 | 296 | 297 | investigate(pe) 298 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loadlibrary_path": "/home/toto/loadlibrary/mpclient", 3 | "loadlibrary_path_dir": "/home/toto/loadlibrary", 4 | "docker_loadlibrary_name": "loadlibrary-working", 5 | "avast_vmx": "/Users/vladimir/Virtual Machines.localized/Windows_10_Avast.vmwarevm/Windows_10_Avast.vmx", 6 | "kaspersky_vmx": "../../kasp.vmx", 7 | "deepinstinct_vmx": "../../Virtual Machines.localized/Windows_10_AV_DeepInstinct.vmwarevm/Windows_10_AV_DeepInstinct.vmx", 8 | "vmware_user": "toto", 9 | "vmware_passwd": "wow, such strong hardcoded password" 10 | } 11 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | CONFIG_FILE = os.path.join(os.path.dirname(__file__), "config.json") 5 | 6 | 7 | def get_value(value): 8 | 9 | try: 10 | with open(CONFIG_FILE) as jsonfile: 11 | 12 | data = json.load(jsonfile) 13 | 14 | if value in data: 15 | return data[value] 16 | except Exception as e: 17 | import traceback 18 | traceback.print_exc() 19 | import os 20 | print(os.getcwd()) 21 | raise Exception(e) 22 | 23 | raise Exception(f"Unknown field: {value}. Available fields are: {data.keys()}") 24 | -------------------------------------------------------------------------------- /find.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import logging 4 | import string 5 | import sys 6 | from collections import deque 7 | from concurrent import futures 8 | 9 | import hexdump 10 | from capstone import * 11 | from intervaltree import Interval, IntervalTree 12 | from tqdm import tqdm 13 | 14 | import config 15 | import pe_utils 16 | from scanner import g_scanner 17 | 18 | logging.basicConfig(filename='debug.log', 19 | filemode='a', 20 | format='[%(levelname)-8s][%(asctime)s][%(filename)s:%(lineno)3d] %(funcName)s() :: %(message)s', 21 | datefmt='%Y/%m/%d %H:%M', 22 | level=logging.DEBUG) 23 | 24 | """ 25 | dependecies: hexdump, intervaltree 26 | todo: 27 | * pefile -> locate code section 28 | * sliding window algorithm in order to find a smaller section 29 | 30 | """ 31 | 32 | ResultQueue = deque() 33 | interval_tree = IntervalTree() 34 | START_LEAP = 2048 35 | MIN_LEAP = 100 36 | IGNORE_START = 0 37 | IGNORE_END = 0x256 #todo use pefile to find the start of code 38 | GOAT_FILE = '/tmp/metsrv.x64.dll' 39 | WDEFENDER_INSTALL_PATH = config.get_value("loadlibrary_path_dir") 40 | MAX_THREADS = 10 41 | DEBUG_LEVEL = 1 42 | buffer = [] 43 | goat = [] 44 | res = {} 45 | has_lead = False 46 | progress = 0 47 | cs = Cs(CS_ARCH_X86, CS_MODE_64) 48 | cs.detail = True 49 | cs.skipdata = True 50 | 51 | 52 | 53 | def print_disass(base, code_size, raw_code): 54 | 55 | code_base = base 56 | if code_base == 0: 57 | code_base = 0x1000 58 | 59 | for i in cs.disasm(raw_code, code_base): 60 | 61 | if i > 256: 62 | break 63 | logging.info("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str)) 64 | 65 | 66 | """ 67 | remove the intervals for which we have a more precise one 68 | for instance, if we have [10:20] and [0:500], the last one is useless. 69 | Problem: if [0:5][6:10], it is not correct to delete interval [0:10] 70 | """ 71 | def filter_matches(good_res): 72 | filtered = IntervalTree() # good_res.copy() 73 | 74 | for match in good_res: 75 | 76 | # if not filtered.containsi(*match): 77 | # continue 78 | 79 | try: 80 | # filtered.remove_overlap(match) 81 | if len(good_res.envelop(match.begin, match.end)) <= 1: 82 | filtered.add(match) 83 | except Exception as e: 84 | logging.warning(f"Exception encountered ({e.args}). Spawning IPython shell to debug...") 85 | import traceback 86 | traceback.format_exc() 87 | if not "IPython" in sys.modules: 88 | import IPython 89 | IPython.embed() 90 | 91 | 92 | return filtered 93 | 94 | """ 95 | get strings from binary blob 96 | """ 97 | def strings(binary, min=4): 98 | result = "" 99 | for c in binary: 100 | c = chr(c) 101 | if c in string.printable[:-5]: 102 | result += c 103 | continue 104 | if len(result) >= min: 105 | yield result 106 | result = "" 107 | if len(result) >= min: # catch result at EOF 108 | yield result 109 | 110 | """ 111 | Scans a file with Windows Defender and returns True if the file 112 | is detected as a threat. 113 | """ 114 | def scan(path): 115 | 116 | return g_scanner.scan(path) 117 | 118 | 119 | """ 120 | replace each half of a file with null bytes and check if it impacts 121 | the detection verdict. If it does, the half is added to a queue in order 122 | to improve the precision. 123 | Problem if each half is detected for now 124 | """ 125 | def sigseek(buffer, current_offset, end, counter): 126 | 127 | global progress 128 | leap = (end - current_offset) // 2 129 | patch = bytes(chr(0),'ascii')*int(leap) 130 | nb_chunk = (end - current_offset) // leap if not leap == 0 else 0 131 | detected_chunks = 0 132 | bufs = [] 133 | 134 | logging.info(f"\t\t[+] {nb_chunk} chunks to process") 135 | 136 | while current_offset < end and leap >= MIN_LEAP: 137 | progress.set_postfix(current_offset=current_offset+leap, refresh=True) 138 | logging.info(f"\t\t[+] Patching buffer of size = {len(buffer)}, offset = {current_offset}, leap = {leap}") 139 | goat = buffer[:current_offset] + patch + buffer[current_offset+leap:] 140 | bufs += [goat] 141 | filepath = GOAT_FILE + "_"+str(counter) 142 | 143 | with open(filepath,'wb') as fw: 144 | fw.write(goat) 145 | 146 | if not scan(filepath): 147 | has_lead = True 148 | logging.info(f"[+] Found signature between {current_offset} and {current_offset+leap}") 149 | ResultQueue.append(Interval(int(current_offset), current_offset+leap)) 150 | progress.update(1) 151 | 152 | else: 153 | logging.info(f"[-] Current offset = {current_offset}") 154 | detected_chunks += 1 155 | 156 | current_offset += leap 157 | 158 | if detected_chunks == nb_chunk and detected_chunks > 0: 159 | 160 | logging.info(f"\t[!] File appears to be detected with several patterns ({detected_chunks})") 161 | sigseek(bufs[0], current_offset, end, counter+1000) 162 | sigseek(bufs[1], current_offset-leap, end, counter+5000) 163 | elif detected_chunks == 1: 164 | branch_skipped = count_max_chunks(leap) 165 | progress.update(branch_skipped) 166 | 167 | 168 | def locate_found_signatures(filename, interval_tree): 169 | 170 | pe = pe_utils.PE() 171 | pe.filename=filename 172 | sections = pe_utils.get_sections(pe) 173 | 174 | for interval in interval_tree: 175 | for section in sections: 176 | if interval.begin >= section.addr and interval.end < section.addr + section.size: 177 | logging.info(f"Signature in {section.name} section ({interval.begin} to {interval.end})") 178 | else: 179 | 180 | section_begin = next(section for section in sections if 181 | section.addr <= interval.begin < (section.addr + section.size)) 182 | 183 | section_end = next(section for section in sections if 184 | section.addr <= interval.end < (section.addr + section.size)) 185 | logging.info(f"Signature crossing two sections: {section_begin.name} and {section_end.name} ({interval.begin} to {interval.end})") 186 | 187 | """ 188 | pretty print the results with hexdumps 189 | """ 190 | def clean_results(filename): 191 | global START_LEAP 192 | global buffer 193 | 194 | logging.info(f"[*] Got {len(interval_tree)} signatures, filtering...") 195 | good_res = filter_matches(interval_tree) 196 | logging.info(f"[*] Filtered {len(interval_tree) - len(good_res)} signatures...") 197 | locate_found_signatures(filename, good_res) 198 | 199 | logging.info(f"[*] Got {len(good_res)} signatures...") 200 | 201 | logging.info("[+] Here are the potential findings:") 202 | 203 | for i in sorted(good_res): 204 | 205 | leap = i.end - i.begin 206 | 207 | if not leap < len(buffer) // 2: 208 | logging.info(f"\t\t[-] Skipping because leap ({leap}) is bigger than initial value") 209 | continue 210 | 211 | dump_path = "/tmp/goat_"+str(i.begin)+"-"+str(i.end)+".bin" 212 | sig = buffer[i.begin:i.end] 213 | 214 | patch = leap * bytes(chr(0),'ascii') 215 | goat = buffer[:i.begin] + patch + buffer[i.end:] 216 | 217 | with open(dump_path, "wb") as fd: 218 | fd.write(goat) 219 | 220 | logging.info(f"[*] Signature between {i.begin} and {i.end} dumped at {dump_path}:") 221 | logging.info(hexdump.hexdump(sig, result='return')) 222 | 223 | logging.info("[*] Strings:") 224 | for s in strings(sig): 225 | logging.info(f"> {s}") 226 | 227 | try: 228 | logging.info("[*] Disassembly (x64):") 229 | print_disass(i.begin, leap, sig) 230 | except: 231 | logging.error("Disassembly failed") 232 | 233 | logging.info("[*] Done") 234 | 235 | """ 236 | used for the progress bar and helps 237 | estimate the work to be done 238 | """ 239 | def count_max_chunks(size): 240 | 241 | if size <= MIN_LEAP: 242 | return 0 243 | 244 | return 1 + 2 * count_max_chunks(size//2) 245 | 246 | 247 | """ 248 | main function 249 | """ 250 | def process_file(sample_file, start = 0, end=-1): 251 | 252 | global interval_tree 253 | global ResultQueue 254 | global buffer 255 | global progress 256 | 257 | with open(sample_file, 'rb') as f: 258 | buffer = f.read() 259 | 260 | max(len(buffer), end) 261 | total = count_max_chunks(end) 262 | logging.info(f"[*] Number of chunks to process: {total}") 263 | progress = tqdm(total=total, leave=False) 264 | ResultQueue.append(Interval(start, end)) 265 | counter = 0 266 | 267 | with futures.ThreadPoolExecutor(max_workers=MAX_THREADS) as exec: 268 | 269 | to_do = [] 270 | last_size = 0 271 | 272 | while len(ResultQueue) > 0 or len(to_do) > 0: 273 | 274 | if len(ResultQueue) == 0: 275 | logging.debug("\t\t[-] Waiting on results...") 276 | next(futures.as_completed(to_do)).result() 277 | 278 | counter += 1 279 | 280 | logging.debug(f"\t\t[*] {len(ResultQueue)} elements in queue...") 281 | 282 | for i in ResultQueue: 283 | interval_tree.add(i) 284 | 285 | if len(ResultQueue) > 0: 286 | match = ResultQueue.pop() 287 | else: 288 | return 289 | 290 | last_size = match.end - match.begin 291 | futur = exec.submit(sigseek, buffer, match.begin, match.end, counter) 292 | to_do = [futur] 293 | progress.clear() 294 | progress.close() 295 | 296 | 297 | def bytes_detection(filename, start=0, end=-1): 298 | 299 | # g_scanner = VMWareAvast() 300 | sample_file = filename 301 | 302 | try: 303 | pe = pe_utils.PE() 304 | pe.filename = sample_file 305 | new_pe = pe_utils.backup_pe(pe) 306 | process_file(new_pe.filename, start, end) 307 | clean_results(filename) 308 | logging.info("[*] Done ! Press any to exit...") 309 | 310 | except KeyboardInterrupt: 311 | logging.info("[*] Not done, but here is what I've found so far:") 312 | clean_results(filename) 313 | 314 | if __name__ == "__main__": 315 | 316 | 317 | if len(sys.argv) > 1: 318 | sample_file = sys.argv[1] 319 | 320 | else: 321 | exit(-1) 322 | 323 | bytes_detection(sample_file) 324 | 325 | 326 | 327 | -------------------------------------------------------------------------------- /find_bad_strings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import dataclasses 3 | import logging 4 | import re 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | from tqdm import tqdm 10 | 11 | from scanner import g_scanner 12 | 13 | logging.basicConfig(filename='debug.log', 14 | filemode='a', 15 | format='[%(levelname)-8s][%(asctime)s][%(filename)s:%(lineno)3d] %(funcName)s() :: %(message)s', 16 | datefmt='%Y/%m/%d %H:%M', 17 | level=logging.DEBUG) 18 | 19 | BINARY = "test_cases/ext_server_kiwi.x64.dll" 20 | ORIGINAL_BINARY = "" 21 | DEBUG_LEVEL = 2 # setting supporting levels 0-3, incrementing the verbosity of log msgs 22 | LVL_ALL_DETAILS = 3 # everything 23 | LVL_DETAILS = 2 # only important details 24 | LVL_RES_ONLY = 1 # only results 25 | LVL_SILENT = 0 # quiet 26 | 27 | 28 | @dataclasses.dataclass 29 | class StringRef: 30 | index : int = 0 # index of the string 31 | paddr : int = 0 # offset from the beginning of the file 32 | vaddr : int = 0 # virtual address in the binary 33 | length : int = 0 # number of characters of the string 34 | size : int = 0 # size of the memory taken by the string 35 | section : str = "" # segment where the string is located 36 | encoding : str = "" # encoding of the string (utf-8, utf-16, utf-32, etc) 37 | content : str = "" # actual string 38 | is_replaced: bool = False # has this string already been patched? 39 | is_bad : bool = False # does this string has a signifcant impact on the AV's verdict? 40 | 41 | 42 | """ 43 | Wrapper to print text to stdout, either for concurrent access to 44 | the file descriptor, or because we need to enrich the text before. 45 | """ 46 | def print_dbg(msg, level=3, decorate=True): 47 | 48 | toprint = msg 49 | 50 | if decorate: 51 | toprint = "[*] " + toprint 52 | 53 | if level <= DEBUG_LEVEL: 54 | tqdm.write(toprint) 55 | 56 | """ 57 | Loads an entire binary to memory. 58 | Warning: don't use on huge files. 59 | """ 60 | def get_binary(path): 61 | 62 | data = [] 63 | 64 | with open(path, "rb") as f: 65 | data = f.read() 66 | 67 | return data 68 | 69 | 70 | """ 71 | Executes rabin2 to get all the strings from a binary. 72 | @param filepath: the path to the file to be analyzed. 73 | @return: the raw output from rabin2 74 | """ 75 | def get_all_strings(file_path, extensive=False): 76 | 77 | command = ['rabin2', "-z", file_path] 78 | if extensive: 79 | command = ['rabin2', "-zz", file_path] 80 | 81 | p = subprocess.Popen(command, stdout=subprocess.PIPE, 82 | stderr=subprocess.STDOUT) 83 | rout = "" 84 | iterations = 0 85 | while(True): 86 | 87 | retcode = p.poll() # returns None while subprocess is running 88 | out = p.stdout.readline().decode('utf-8', errors="ignore") 89 | iterations += 1 90 | rout += out 91 | if(retcode is not None): 92 | break 93 | 94 | return rout 95 | 96 | 97 | """ 98 | Executes rabin2 to enumerate the binary's sections information 99 | @param filepath: the path to the file to be analyzed. 100 | @return: the raw output from rabin2 101 | """ 102 | def get_sections(file_path): 103 | 104 | command = ['rabin2', "-S", file_path] 105 | 106 | p = subprocess.Popen(command, stdout=subprocess.PIPE, 107 | stderr=subprocess.STDOUT) 108 | rout = "" 109 | iterations = 0 110 | while(True): 111 | 112 | retcode = p.poll() # returns None while subprocess is running 113 | out = p.stdout.readline().decode('utf-8', errors="ignore") 114 | iterations += 1 115 | rout += out 116 | if(retcode is not None): 117 | break 118 | 119 | return rout 120 | 121 | 122 | """ 123 | Hides an entire section of a binary 124 | rabin2 output: 125 | [Sections] 126 | Nm Paddr Size Vaddr Memsz Perms Name 127 | 00 0x00000400 619008 0x180001000 622592 -r-x .text 128 | """ 129 | def hide_section(section, filepath, binary): 130 | 131 | section_size = 0 132 | section_addr = 0 133 | 134 | strings_data = get_sections(filepath) 135 | 136 | for string in strings_data.split('\n'): 137 | 138 | # to preserve some whitespaces 139 | data = string.split() 140 | 141 | if len(data) >= 4 and data[0].isnumeric(): 142 | 143 | if data[6] == section: 144 | print_dbg(f"Found {section} section, hiding it...", LVL_DETAILS, True) 145 | section_size = int(data[2],16) 146 | section_addr = int(data[1],16) 147 | break 148 | 149 | assert(section_size > 0) 150 | assert(section_addr > 0) 151 | 152 | patch = bytes('\x41' * section_size, 'ascii') 153 | new_bin = binary[:section_addr] + patch + binary[section_addr+section_size:] 154 | 155 | # binary's size is not expected to change. 156 | assert(len(new_bin) == len(binary)) 157 | 158 | return new_bin 159 | 160 | """ 161 | converts rabin2 encoding to python3 162 | @param encoding the requested encoding (string) 163 | @return the correct encoding as string 164 | """ 165 | def convert_encoding(encoding): 166 | 167 | table = { 168 | "ascii": "ascii", 169 | "utf16le": "utf_16_le", 170 | "utf32le": "utf_32_le", 171 | "utf8": "utf8" 172 | } 173 | 174 | assert(table.get(encoding) is not None) 175 | return table.get(encoding) 176 | 177 | 178 | """ 179 | Used to process the raw output of rabin2. 180 | Populates a collection of StringRefs objects from the collected data. 181 | TODO: parse output of -zz 182 | @param strings_data: the raw output of rabin2 183 | @return: a collection of StringRefs 184 | """ 185 | def parse_strings(strings_data): 186 | # columns: Num, Paddr, Vaddr, Len, Size, Section, Type, String 187 | string_refs = [] 188 | 189 | for string in strings_data.split('\n'): 190 | data = re.split(r'(\s+)', string) # to preserve some whitespaces 191 | if len(data) >= 7 and data[0].isnumeric(): 192 | str_ref = StringRef() 193 | str_ref.index = int(data[0]) 194 | str_ref.paddr = int(data[2], 16) 195 | str_ref.vaddr = int(data[4], 16) 196 | str_ref.length = int(data[6]) 197 | str_ref.size = int(data[8]) 198 | str_ref.section = data[10] 199 | str_ref.encoding = data[12] 200 | new_encoding = convert_encoding(str_ref.encoding) 201 | to_parse_len = str_ref.length+len("\x00".encode(new_encoding)) 202 | # skip first whitespace 203 | content = "".join(data[13:])[1:to_parse_len] 204 | str_ref.content = content 205 | string_refs += [str_ref] 206 | 207 | return string_refs 208 | 209 | 210 | """ 211 | Scans a file with Windows Defender and returns True if the file 212 | is detected as a threat. 213 | """ 214 | def scan(path): 215 | 216 | return g_scanner.scan(path) 217 | 218 | 219 | 220 | """ 221 | @description patch a binary blob at the location pointed by "str_ref" 222 | @param binary binary blob of data 223 | @param str_ref StringRef object, must hold size, length and content. 224 | @param filepath if non empty, the function will write the resulting binary to the specified location on disk. 225 | @param mask if true, patches with junk data, or else path with str_ref.content (revert to original content) 226 | """ 227 | def patch_binary(binary, str_ref, filepath, mask=True): 228 | 229 | encoding = convert_encoding(str_ref.encoding) 230 | patch = bytes('\x00' * str_ref.size, 'ascii') 231 | 232 | # tricky part, the original string must be put back in the binary. 233 | # however, several encodings and null bytes make that a pain to realize. 234 | # In case of failures, the original binary is used instead of str_ref.content 235 | if not mask: 236 | cnt = str_ref.content + '\x00' # why already ?? 237 | cnt = str_ref.content.replace("\\n", '\x0a') 238 | cnt = cnt.replace("\\t", '\x09') 239 | patch = bytes(cnt+chr(0), encoding) 240 | 241 | if len(patch) != str_ref.size or "\\" in str_ref.content: 242 | print_dbg( 243 | "Oops, parsing error, will recover bytes from the original file...", LVL_ALL_DETAILS) 244 | with open(BINARY, "rb") as tmp_fd: 245 | tmp_fd.seek(str_ref.paddr) 246 | patch = tmp_fd.read(str_ref.size) 247 | 248 | new_bin = binary[:str_ref.paddr] + patch + \ 249 | binary[str_ref.paddr+str_ref.size:] 250 | 251 | # binary's size is not expected to change. 252 | assert(len(new_bin) == len(binary)) 253 | 254 | # write the patched binary to disk 255 | if len(filepath) > 0: 256 | with open(filepath, "wb") as f: 257 | f.write(new_bin) 258 | 259 | return new_bin 260 | 261 | 262 | """ 263 | returns true if all string_refs are in the blacklist 264 | tested: true 265 | @param string_refs a collection of StringRef objects 266 | @param blacklist a collection of indexes that are known to the AV engine 267 | """ 268 | def is_all_blacklisted(string_refs, blacklist): 269 | return all(s.index in blacklist for s in string_refs) 270 | 271 | 272 | """ 273 | merges two lists without duplicates 274 | @param list1 some collection of type 'list' 275 | @param list2 somme collection of type 'list' 276 | return list1 and list2 merged together (type: list) 277 | """ 278 | def merge_unique(list1, list2): 279 | list3 = list1 + list2 280 | unique_set = set(list3) 281 | return list(unique_set) 282 | 283 | 284 | """ 285 | returns true if both lists are equal and order doesn't matter 286 | @param list1 some list 287 | @param list2 some list 288 | """ 289 | def is_equal_unordered(list1, list2): 290 | set1 = set(list1) 291 | set2 = set(list2) 292 | return set1 == set2 293 | 294 | 295 | """ 296 | Takes the original binary, patches the strings whose 297 | indexes are in "blacklist" and re-scan with the AV. 298 | """ 299 | def validate_results(sample_file, tmpfile, blacklist, all_strings): 300 | 301 | 302 | # read the binary. 303 | binary = get_binary(sample_file) 304 | 305 | for b in blacklist: 306 | string = next(filter(lambda x: x.index == b, all_strings)) 307 | print_dbg(f"Removing bad string {repr(string)}", LVL_DETAILS, True) 308 | binary = patch_binary(binary, string, "", True) 309 | 310 | with open(tmpfile, "wb") as fd: 311 | fd.write(binary) 312 | 313 | detection = scan(tmpfile) 314 | 315 | return detection 316 | 317 | 318 | """ 319 | TODO: update the progress bar. 320 | TODO: use a threadpool. 321 | @param binary binary blob currently edited, all strings hidden 322 | @param string_refs list of StringRefs objects. 323 | @param blacklist list of strings' index to never unmask. 324 | """ 325 | def rec_bissect(binary, string_refs, blacklist): 326 | 327 | if type(string_refs) is list and len(string_refs) < 2: 328 | if len(string_refs) > 0: 329 | i = string_refs[0] 330 | print_dbg(f"Found it: {repr(i)}", LVL_RES_ONLY, False) 331 | blacklist.append(i.index) 332 | return blacklist 333 | 334 | elif type(string_refs) is StringRef: 335 | print_dbg(f"Found it: f{repr(string_refs)}", LVL_RES_ONLY, False) 336 | blacklist.append(string_refs.index) 337 | return blacklist 338 | 339 | 340 | try: 341 | half_nb_strings = len(string_refs) // 2 342 | except: 343 | print_dbg(f"Found it: f{repr(string_refs)}", LVL_RES_ONLY, False) 344 | blacklist.append(string_refs.index) 345 | return blacklist 346 | half1 = string_refs[:half_nb_strings] 347 | half2 = string_refs[half_nb_strings:] 348 | binary1 = binary 349 | binary2 = binary 350 | 351 | for string in half1: 352 | 353 | # hide all upper half of binary2 354 | binary2 = patch_binary(binary2, string, "", mask=True) 355 | 356 | if string.index in blacklist: 357 | # hide the blacklisted string 358 | binary1 = patch_binary(binary1, string, "", mask=True) 359 | binary2 = patch_binary(binary2, string, "", mask=True) 360 | 361 | else: 362 | # put the string back 363 | binary1 = patch_binary(binary1, string, "", mask=False) 364 | 365 | for string in half2: 366 | 367 | #hide all lower half of binary1 368 | binary1 = patch_binary(binary1, string, "", mask=True) 369 | 370 | if string.index in blacklist: 371 | # hide blacklisted strings in both halves 372 | binary1 = patch_binary(binary1, string, "", mask=True) 373 | binary2 = patch_binary(binary2, string, "", mask=True) 374 | else: 375 | # unhide all lower half of binary2 376 | binary2 = patch_binary(binary2, string, "", mask=False) 377 | pass 378 | 379 | dump_path1 = tempfile.NamedTemporaryFile() 380 | dump_path2 = tempfile.NamedTemporaryFile() 381 | 382 | with open(dump_path1.name, "wb") as f: 383 | f.write(binary1) 384 | 385 | with open(dump_path2.name, "wb") as fd: 386 | fd.write(binary2) 387 | 388 | detection_result1 = scan(dump_path1.name) 389 | detection_result2 = scan(dump_path2.name) 390 | 391 | dump_path1.close() 392 | dump_path2.close() 393 | 394 | res = detection_result1 or detection_result2 395 | 396 | # the upper half triggers the detection 397 | if detection_result1: 398 | print_dbg(f"Signature between half1 {half1[0].index} and {half1[-1].index}", LVL_DETAILS) 399 | blacklist1 = rec_bissect(binary1, half1, blacklist) 400 | blacklist = merge_unique(blacklist, blacklist1) 401 | 402 | if detection_result2: 403 | print_dbg(f"Signature between half2 {half2[0].index} and {half2[-1].index}", LVL_DETAILS) 404 | blacklist2 = rec_bissect(binary2, half2, blacklist) 405 | blacklist = merge_unique(blacklist, blacklist2) 406 | 407 | if not res: 408 | print_dbg("Both halves are not detected", LVL_DETAILS) 409 | 410 | # TODO: rather hazardous, but works for mimikatz. In case of failures, fix this. 411 | half1 = string_refs[:len(string_refs)//4] 412 | half2 = string_refs[len(string_refs)//4] 413 | blacklist = merge_unique( 414 | blacklist, rec_bissect(binary, half1, blacklist)) 415 | blacklist = merge_unique( 416 | blacklist, rec_bissect(binary, half2, blacklist)) 417 | 418 | return blacklist 419 | 420 | """ 421 | Amorce function for the bissection algorithm. 422 | Expects a path to a binary detected by the AV engine. 423 | Returns a list of signatures or crashes. 424 | """ 425 | def bissect(sample_file, blacklist = []): 426 | 427 | global BINARY 428 | 429 | BINARY = sample_file 430 | 431 | # no point in continuing if the binary is not detected as malicious already. 432 | assert(scan(sample_file) is True) 433 | 434 | # use rabin2 from radare2 to extract all the strings from the binary 435 | #strings_data = get_all_strings(sample_file, extensive=True) 436 | 437 | # parse rabin2 output 438 | import pe_utils 439 | str_refs = pe_utils.parse_strings(sample_file) 440 | 441 | print_dbg(f"Got {len(str_refs)} string objects", LVL_DETAILS, True) 442 | 443 | # read the binary. 444 | binary = get_binary(sample_file) 445 | binary1 = binary 446 | # mask all strings 447 | for string in str_refs: 448 | # patch the binary (mask the string) 449 | binary = patch_binary(binary, string, "", True) 450 | 451 | dump_path = tempfile.NamedTemporaryFile() 452 | 453 | with open(dump_path.name, "wb") as f: 454 | f.write(binary) 455 | 456 | detection_result = scan(dump_path.name) 457 | dump_path.close() 458 | 459 | # sometimes there are signatures in the .txt sections 460 | if detection_result is True: 461 | print_dbg("Hiding all the strings doesn't seem to impact the AV's verdict.\ 462 | Retrying after masking the .text section", LVL_DETAILS, True) 463 | binary = hide_section(".text", sample_file, binary1) 464 | tmp = tempfile.NamedTemporaryFile() 465 | with open("/tmp/toto", "wb") as f: 466 | f.write(binary) 467 | bissect("/tmp/toto") 468 | exit(0) 469 | 470 | 471 | print_dbg("Good, masking all the strings has an impact on the AV's verdict", 0) 472 | #progress = tqdm(total=len(str_refs), leave=False) 473 | 474 | blacklist = rec_bissect(binary1, str_refs, blacklist) 475 | 476 | if len(blacklist) > 0: 477 | print_dbg(f"Found {len(blacklist)} signatures", LVL_DETAILS, True) 478 | print(blacklist) 479 | 480 | for b in blacklist: 481 | string = next(filter(lambda x: x.index == b, str_refs)) 482 | logging.info(f"String @ {hex(string.paddr)} should be patched: ") 483 | logging.info(string.content) 484 | tmpfile = "/tmp/newbin" 485 | if ORIGINAL_BINARY != "": 486 | if not validate_results(ORIGINAL_BINARY, tmpfile, blacklist, str_refs): 487 | print_dbg("Validation is ok !", LVL_DETAILS, True) 488 | else: 489 | print_dbg("Patched binary is still detected, retrying.", LVL_DETAILS, True) 490 | bissect("/tmp/newbin", blacklist) 491 | else: 492 | print_dbg("No signatures found...", LVL_DETAILS, True) 493 | return blacklist 494 | 495 | 496 | if __name__ == "__main__": 497 | 498 | #g_scanner = DockerWindowsDefender() 499 | 500 | 501 | sample_file = BINARY 502 | 503 | if len(sys.argv) > 1: 504 | sample_file = sys.argv[1] 505 | BINARY = sample_file 506 | 507 | ORIGINAL_BINARY = BINARY 508 | 509 | try: 510 | # explore(sample_file) 511 | bissect(sample_file) 512 | print_dbg("[*] Done ! Press any to exit...", 0) 513 | 514 | except KeyboardInterrupt: 515 | print_dbg("[*] Not done, but here is what I've found so far:", 0) 516 | -------------------------------------------------------------------------------- /known_bad_strings.txt: -------------------------------------------------------------------------------- 1 | Pass-the-ccache [NT6] 2 | ERROR kuhl_m_crypto_l_certificates ; CryptAcquireCertificatePrivateKey (0x%08x) 3 | ERROR kuhl_m_crypto_l_certificates ; CertGetCertificateContextProperty (0x%08x) 4 | ERROR kuhl_m_crypto_l_certificates ; CertGetNameString (0x%08x) 5 | lsasrv.dll 6 | ERROR kuhl_m_lsadump_sam ; CreateFile (SYSTEM hive) (0x%08x) 7 | SamIFree_SAMPR_USER_INFO_BUFFER 8 | KiwiAndRegistryTools 9 | wdigest.dll 10 | multirdp 11 | logonPasswords 12 | credman 13 | [%x;%x]-%1u-%u-%08x-%wZ@%wZ-%wZ.%s 14 | n.e. (KIWI_MSV1_0_CREDENTIALS KO) 15 | \\.\mimidrv 16 | -------------------------------------------------------------------------------- /output.txt: -------------------------------------------------------------------------------- 1 | vladimir@anon ~/dev/research/find_detected_strings % python3 find_bad_strings.py 2 | 3 | StringRef(index=449, paddr=658672, vaddr=6443112688, length=59, size=120, section='(.rdata)', encoding='utf16le', content="ERROR kuhl_m_kerberos_ask ; '%wZ' Kerberos name not found!\\n", is_replaced=False, is_bad=False) 4 | 5 | We got 5080 string objects 6 | Good, masking all the strings has an impact on the AV's verdict 7 | 8 | Found a bad string, re-masking it 9 | Pass-the-ccache [NT6] 10 | 11 | Found a bad string, re-masking it 12 | ERROR kuhl_m_crypto_l_certificates ; CryptAcquireCertificatePrivateKey (0x%08x)\n 13 | 14 | Found a bad string, re-masking it 15 | ERROR kuhl_m_crypto_l_certificates ; CertGetCertificateContextProperty (0x%08x)\n 16 | 17 | Found a bad string, re-masking it 18 | ERROR kuhl_m_crypto_l_certificates ; CertGetNameString (0x%08x)\n 19 | 20 | Found a bad string, re-masking it 21 | lsasrv.dll 22 | 23 | Found a bad string, re-masking it 24 | ERROR kuhl_m_lsadump_sam ; CreateFile (SYSTEM hive) (0x%08x)\n 25 | 26 | Found a bad string, re-masking it 27 | SamIFree_SAMPR_USER_INFO_BUFFER 28 | 29 | Found a bad string, re-masking it 30 | KiwiAndRegistryTools 31 | 32 | Found a bad string, re-masking it 33 | wdigest.dll 34 | 35 | Found a bad string, re-masking it 36 | multirdp 37 | 38 | Found a bad string, re-masking it 39 | logonPasswords 40 | 41 | Found a bad string, re-masking it 42 | credman 43 | 44 | Found a bad string, re-masking it 45 | [%x;%x]-%1u-%u-%08x-%wZ@%wZ-%wZ.%s 46 | 47 | Found a bad string, re-masking it 48 | n.e. (KIWI_MSV1_0_CREDENTIALS KO) 49 | 50 | Found a bad string, re-masking it 51 | \\.\mimidrv 52 | 53 | 54 | 74%|█████████████████████████████████████████████▎ | 3773/5080 [2:32:56<48:54, 2.25s/it]Traceback (most recent call last): 55 | File "find_bad_strings.py", line 253, in 56 | explore(sample_file) 57 | File "find_bad_strings.py", line 220, in explore 58 | binary = patch_binary(binary, string, dump_path, False) 59 | File "find_bad_strings.py", line 164, in patch_binary 60 | f.write(new_bin) 61 | OSError: [Errno 28] No space left on device 62 | python3 find_bad_strings.py 8411.79s user 766.15s system 99% cpu 2:33:04.12 total 63 | 1 vladimir@anon ~/dev/research/find_detected_strings % 64 | -------------------------------------------------------------------------------- /pe_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import random 4 | import re 5 | import string 6 | import hashlib 7 | import hexdump 8 | from copy import deepcopy 9 | import shutil 10 | from tempfile import NamedTemporaryFile 11 | 12 | from dataclasses import dataclass 13 | 14 | import r2pipe 15 | 16 | logging.basicConfig(filename='debug.log', 17 | filemode='a', 18 | format='[%(levelname)-8s][%(asctime)s][%(filename)s:%(lineno)3d] %(funcName)s() :: %(message)s', 19 | datefmt='%Y/%m/%d %H:%M', 20 | level=logging.DEBUG) 21 | 22 | 23 | @dataclass 24 | class Section: 25 | name: str 26 | size: int 27 | vsize: int 28 | addr: int 29 | vaddr: int 30 | detected: bool = False 31 | 32 | 33 | @dataclass 34 | class PE: 35 | filename = "" 36 | sections = [] 37 | strings = [] 38 | md5 = "" 39 | patches = [] 40 | 41 | 42 | @dataclass 43 | class StringRef: 44 | index: int = 0 # index of the string 45 | paddr: int = 0 # offset from the beginning of the file 46 | vaddr: int = 0 # virtual address in the binary 47 | length: int = 0 # number of characters of the string 48 | size: int = 0 # size of the memory taken by the string 49 | section: str = "" # segment where the string is located 50 | encoding: str = "" # encoding of the string (utf-8, utf-16, utf-32, etc) 51 | content: str = "" # actual string 52 | is_replaced: bool = False # has this string already been patched? 53 | is_bad: bool = False # does this string has a significant impact on the AV's verdict? 54 | should_mask: bool = True 55 | 56 | 57 | 58 | @dataclass(unsafe_hash=True, eq=True, order=True) 59 | class Variable: 60 | addr: int 61 | size: int 62 | paddr:int = 0 63 | 64 | 65 | def display(self, pe): 66 | 67 | with open(pe.filename, 'rb') as f: 68 | f.seek(self.paddr) 69 | bf = f.read(min(self.size,128)) 70 | logging.info("\n"+hexdump.hexdump(bf, result="return")) 71 | 72 | 73 | @dataclass 74 | class Patch: 75 | 76 | addr:int 77 | size:int 78 | 79 | def display(self, pe): 80 | 81 | logging.info(f"{self.size} bytes @ {self.addr}:") 82 | 83 | with open(pe.filename, 'rb') as f: 84 | f.seek(self.addr) 85 | bf = f.read(min(self.size,128)) 86 | logging.info("\n"+hexdump.hexdump(bf, result="return")) 87 | 88 | def spwn_dbg(): 89 | import sys 90 | if not "IPython" in sys.modules: 91 | logging.warning(f"Spawning IPython shell to debug...") 92 | import IPython 93 | IPython.embed() 94 | 95 | 96 | def md5(file): 97 | with open(file, 'rb') as f: 98 | data = f.read() 99 | 100 | return hashlib.md5(data).hexdigest() 101 | 102 | 103 | def backup_pe(pe): 104 | # copy the binary 105 | new_name = NamedTemporaryFile().name 106 | shutil.copyfile(pe.filename, new_name) 107 | 108 | # hide the byzes 109 | new_pe = deepcopy(pe) 110 | new_pe.filename = new_name 111 | 112 | return new_pe 113 | 114 | 115 | """ 116 | gets sections information about a PE 117 | """ 118 | 119 | 120 | def get_sections(pe): 121 | section_size = 0 122 | section_addr = 0 123 | 124 | pipe = r2pipe.open(pe.filename) 125 | 126 | # get the sections 127 | sections = pipe.cmdj("iSj") 128 | 129 | for section in sections: 130 | 131 | if section.get("size") != 0 and section.get("addr") != 0: 132 | pe.sections += [ 133 | Section(section.get("name"), section.get("size"), section.get("vsize"), section.get("paddr"), 134 | section.get("vaddr"))] 135 | logging.debug(f"Found section: {pe.sections[-1]}") 136 | 137 | return pe.sections 138 | 139 | 140 | def hide_section(pe, section_name): 141 | section = next((sec for sec in pe.sections if sec.name == section_name), None) 142 | logging.debug(f"Hiding section {section.name}") 143 | hide_bytes(pe, section.addr, section.size) 144 | 145 | 146 | """ 147 | Hide all sections but the one specified 148 | """ 149 | 150 | 151 | def hide_all_sections_except(pe, exception=".text"): 152 | for section in pe.sections: 153 | if section.name != exception: 154 | hide_section(pe, section.name) 155 | 156 | 157 | def hide_bytes(pe, start, length): 158 | logging.debug(f"Hiding {length} bytes @ {start}") 159 | """ 160 | pipe = r2pipe.open(pe.filename, flags=["-w"]) 161 | replacement = ''.join(random.choice(string.ascii_letters) for i in range(length)) 162 | replacement = base64.b64encode(bytes(replacement, "ascii")).decode() 163 | pipe.cmd(f"w6d {replacement} @ {start}") 164 | """ 165 | # for some reasons the code above is buggy with my radare2 version 166 | with open(pe.filename, 'r+b') as f: 167 | f.seek(start) 168 | f.write(bytes(''.join(random.choice(string.ascii_letters) for i in range(length)), encoding='ascii')) 169 | 170 | 171 | """ 172 | converts rabin2 encoding to python3 173 | @param encoding the requested encoding (string) 174 | @return the correct encoding as string 175 | """ 176 | 177 | 178 | def convert_encoding(encoding): 179 | table = { 180 | "ascii": "ascii", 181 | "utf16le": "utf_16_le", 182 | "utf32le": "utf_32_le", 183 | "utf8": "utf8" 184 | } 185 | 186 | assert (table.get(encoding) is not None) 187 | return table.get(encoding) 188 | 189 | 190 | """ 191 | Used to process the raw output of rabin2. 192 | Populates a collection of StringRefs objects from the collected data. 193 | TODO: parse output of -zz 194 | @param strings_data: the raw output of rabin2 195 | @return: a collection of StringRefs 196 | """ 197 | 198 | 199 | def parse_strings_old(strings_data): 200 | # columns: Num, Paddr, Vaddr, Len, Size, Section, Type, String 201 | string_refs = [] 202 | 203 | for string in strings_data.split('\n'): 204 | data = re.split(r'(\s+)', string) # to preserve some whitespaces 205 | if len(data) >= 7 and data[0].isnumeric(): 206 | str_ref = StringRef() 207 | str_ref.index = int(data[0]) 208 | str_ref.paddr = int(data[2], 16) 209 | str_ref.vaddr = int(data[4], 16) 210 | str_ref.length = int(data[6]) 211 | str_ref.size = int(data[8]) 212 | str_ref.section = data[10] 213 | str_ref.encoding = data[12] 214 | new_encoding = convert_encoding(str_ref.encoding) 215 | to_parse_len = str_ref.length + len("\x00".encode(new_encoding)) 216 | # skip first whitespace 217 | content = "".join(data[13:])[1:to_parse_len] 218 | str_ref.content = content 219 | string_refs += [str_ref] 220 | 221 | return string_refs 222 | 223 | 224 | def parse_strings(filename, all_sections=False, min_length=12): 225 | pipe = r2pipe.open(filename) 226 | pipe.cmd("aaa") 227 | # pipe.cmd("aaa") 228 | if all_sections: 229 | strings = pipe.cmdj("izzj") 230 | else: 231 | strings = pipe.cmdj("izj") 232 | 233 | string_refs = [] 234 | 235 | for string in strings: 236 | 237 | if string.get("length") < min_length: 238 | continue 239 | 240 | if not string.get("section").startswith("."): 241 | continue 242 | 243 | str_ref = StringRef() 244 | str_ref.index = string["ordinal"] 245 | str_ref.paddr = string.get("paddr") 246 | str_ref.vaddr = string.get("vaddr") 247 | str_ref.length = string.get("length") 248 | str_ref.size = string.get("size") 249 | str_ref.section = string.get("section") 250 | str_ref.encoding = string.get("type") 251 | new_encoding = convert_encoding(str_ref.encoding) 252 | # to_parse_len = str_ref.length + len("\x00".encode(new_encoding)) 253 | # skip first whitespace 254 | content = string.get("string").replace("\\\\", "\\") 255 | str_ref.content = content # .encode(convert_encoding(str_ref.encoding)) 256 | string_refs += [str_ref] 257 | return string_refs 258 | 259 | 260 | def patch_binary_mass(filename, str_refs, pipe=None, unmask_only=False): 261 | if pipe is None: 262 | pipe = r2pipe.open(filename, flags=["-w"]) 263 | 264 | #nstr = [x for x in str_refs if x.length==29 and x.size==30] 265 | #for str_ref in nstr[len(nstr)//4:-len(nstr)//4]: 266 | for str_ref in str_refs: 267 | #if 28 < str_ref.length <= 29: 268 | patch_string(filename, str_ref, pipe, unmask_only=unmask_only) 269 | 270 | 271 | def patch_string(filename, str_ref, pipe=None, unmask_only=False): 272 | if pipe is None: 273 | pipe = r2pipe.open(filename, flags=["-w"]) 274 | 275 | if not str_ref.should_mask: 276 | replacement = str_ref.content 277 | elif not unmask_only: 278 | replacement = ''.join(random.choice(['\x00']) for _ in range(str_ref.length)) 279 | replacement = replacement + '\0' 280 | else: 281 | return 282 | 283 | # replacement = base64.b64encode(bytes(replacement, convert_encoding(str_ref.encoding))).decode() 284 | # pipe.cmd(f"w6d {replacement} @ {str_ref.paddr}") 285 | logging.debug(f"Patching {str_ref.content} @ {str_ref.paddr} ({filename})") 286 | with open(filename, 'r+b') as f: 287 | f.seek(str_ref.paddr) 288 | f.write(bytes(replacement, encoding=convert_encoding(str_ref.encoding))) 289 | 290 | 291 | def detect_data(pe): 292 | pipe = r2pipe.open(pe.filename) 293 | pipe.cmd("aaa") 294 | xrefs = pipe.cmdj("axj") 295 | xrefs = [x for x in xrefs if x["type"] == "DATA"] 296 | xrefs = sorted(xrefs, key=lambda x: x["addr"]) 297 | vars = [] 298 | 299 | # guess var's size 300 | for index, xref in enumerate(xrefs): 301 | 302 | if index >= len(xrefs) - 1: 303 | size = 256 # TODO flemme 304 | else: 305 | size = xrefs[index + 1]["addr"] - xref["addr"] 306 | 307 | vars += [Variable(xref["addr"], size)] 308 | 309 | # fix vars with size 0 310 | for i, var in enumerate(vars): 311 | 312 | for j, var2 in enumerate(vars): 313 | if i == j: 314 | continue 315 | 316 | if var.addr == var2.addr: 317 | if var.size == 0: 318 | var.size = var2.size 319 | 320 | elif var2.size == 0: 321 | var2.size = var.size 322 | 323 | # uniq sort 324 | vars_filtered = sorted(list(set(vars)), key=lambda x: x.addr) 325 | 326 | # only vars in .data section 327 | section = next((sec for sec in pe.sections if sec.name == ".data"), None) 328 | vars_filtered = [x for x in vars_filtered if section.vaddr <= x.addr < section.vaddr + section.vsize] 329 | 330 | # guess file address with virtual address 331 | for var in vars_filtered: 332 | var.paddr = var.addr - section.vaddr + section.addr 333 | 334 | logging.debug(vars_filtered) 335 | return vars_filtered 336 | 337 | def print_global_variables(pe, vars): 338 | 339 | for var in vars: 340 | 341 | logging.debug(f"Found {var.size} bytes variable @ {hex(var.addr)}:") 342 | with open(pe.filename, 'rb') as f: 343 | f.seek(var.paddr) 344 | bf = f.read(min(var.size,128)) 345 | logging.debug("\n"+hexdump.hexdump(bf, result="return")) 346 | 347 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = true 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | hexdump 3 | pytest 4 | r2pipe 5 | -------------------------------------------------------------------------------- /scan.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import re 5 | import config 6 | 7 | WDEFENDER_INSTALL_PATH = config.get_value("loadlibrary_path") 8 | 9 | current_dir = os.getcwd() 10 | os.chdir(os.path.dirname(WDEFENDER_INSTALL_PATH)) 11 | command = [WDEFENDER_INSTALL_PATH, sys.argv[1]] 12 | p = subprocess.Popen(command, stdout=subprocess.PIPE, 13 | stderr=subprocess.STDOUT) 14 | 15 | while (True): 16 | 17 | retcode = p.poll() # returns None while subprocess is running 18 | out = p.stdout.readline().decode('utf-8', errors='ignore') 19 | print(out) 20 | m = re.search('Threat', out) 21 | 22 | if m: 23 | print("Threat found\n") 24 | exit(-1) 25 | 26 | if (retcode is not None): 27 | break 28 | 29 | os.chdir(current_dir) 30 | exit(0) 31 | -------------------------------------------------------------------------------- /scanner.py: -------------------------------------------------------------------------------- 1 | 2 | from dataclasses import dataclass 3 | import subprocess 4 | import logging 5 | import re 6 | import os 7 | import shutil 8 | import tempfile 9 | import config 10 | 11 | WDEFENDER_INSTALL_PATH = config.get_value("loadlibrary_path") 12 | WDEFENDER_INSTALL_PATH_DIR = config.get_value("loadlibrary_path_dir") 13 | 14 | logging.basicConfig(filename='debug.log', 15 | filemode='a', 16 | format='[%(levelname)-8s][%(asctime)s][%(filename)s:%(lineno)3d] %(funcName)s() :: %(message)s', 17 | datefmt='%Y/%m/%d %H:%M', 18 | level=logging.DEBUG) 19 | 20 | 21 | @dataclass 22 | class Scanner: 23 | 24 | scanner_path: str = "" 25 | scanner_name: str = "" 26 | 27 | def scan(self, path): 28 | return False 29 | 30 | 31 | class WindowsDefender(Scanner): 32 | 33 | def __init__(self): 34 | self.scanner_path = WDEFENDER_INSTALL_PATH 35 | self.scanner_name = "Windows Defender" 36 | """ 37 | Scans a file with Windows Defender and returns True if the file 38 | is detected as a threat. 39 | """ 40 | 41 | def scan(self, file_path): 42 | 43 | os.chdir(os.path.dirname(self.scanner_path)) 44 | command = [self.scanner_path, file_path] 45 | p = subprocess.Popen(command, stdout=subprocess.PIPE, 46 | stderr=subprocess.STDOUT) 47 | 48 | while(True): 49 | 50 | retcode = p.poll() # returns None while subprocess is running 51 | out = p.stdout.readline().decode('utf-8', errors='ignore') 52 | logging.debug(out) 53 | m = re.search('Threat', out) 54 | 55 | if m: 56 | return True 57 | 58 | if(retcode is not None): 59 | break 60 | 61 | return False 62 | 63 | 64 | class DockerWindowsDefender(Scanner): 65 | 66 | def __init__(self): 67 | self.scanner_path = WDEFENDER_INSTALL_PATH 68 | self.scanner_name = "Windows Defender" 69 | 70 | """ 71 | Scans a file with Windows Defender and returns True if the file 72 | is detected as a threat. 73 | """ 74 | 75 | def scan(self, file_path, with_name=False): 76 | tmp_file_name = tempfile.NamedTemporaryFile().name 77 | shutil.copyfile(file_path, tmp_file_name) 78 | file_path = tmp_file_name 79 | 80 | run_cmd = ["docker", "run", "--rm", "-v", f"{os.getcwd()}:/home/toto/av-signatures-finder", "-v", 81 | "/tmp:/tmp", "-v", "/var:/var", config.get_value("docker_loadlibrary_name"), "python3", "/home/toto/av-signatures-finder/scan.py", file_path] 82 | p = subprocess.Popen(run_cmd, stdout=subprocess.PIPE, 83 | stderr=subprocess.STDOUT) 84 | ret_value = False 85 | threat_name = "Nothing" 86 | while(True): 87 | 88 | retcode = p.poll() # returns None while subprocess is running 89 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 90 | # print(out) 91 | m = re.search('identified', out) 92 | 93 | if m: 94 | 95 | threat_name = out.split("Threat")[1].split("identified")[0] 96 | ret_value = True 97 | 98 | if len(out) > 0: 99 | logging.debug(out) 100 | 101 | if(retcode is not None): 102 | break 103 | 104 | os.unlink(tmp_file_name) 105 | 106 | if with_name: 107 | return ret_value, threat_name 108 | 109 | return ret_value 110 | 111 | 112 | class VMWareDeepInstinct(Scanner): 113 | 114 | def __init__(self): 115 | self.scanner_path = WDEFENDER_INSTALL_PATH 116 | self.scanner_name = "DeepInstinct" 117 | 118 | """ 119 | Scans a file with Windows Defender and returns True if the file 120 | is detected as a threat. 121 | """ 122 | 123 | def scan(self, file_path, with_name=False): 124 | tmp_file_name = tempfile.NamedTemporaryFile().name 125 | shutil.copyfile(file_path, tmp_file_name) 126 | file_path = tmp_file_name 127 | basename = os.path.basename(tmp_file_name) 128 | vmx_path = config.get_value("deepinstinct_vmx") 129 | passwd = config.get_value("vmware_passwd") 130 | username = config.get_value("vmware_user") 131 | copy_file_cmd = f"vmrun -T ws -gu {username} -gp {passwd} CopyFileFromHostToGuest {vmx_path} {tmp_file_name} C:\\Users\\toto\\Desktop\\{basename}.exe" 132 | file_exists_cmd = f"vmrun -T ws -gu {username} -gp {passwd} fileExistsInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 133 | exec_cmd = f"vmrun -T ws -gu {username} -gp {passwd} runProgramInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 134 | print(copy_file_cmd) 135 | p = subprocess.Popen(copy_file_cmd.split(" "), stdout=subprocess.PIPE, 136 | stderr=subprocess.STDOUT) 137 | print( 138 | f"Copy file result: {p.stdout.readline().decode('utf-8', errors='ignore').strip()}") 139 | 140 | p = subprocess.Popen(file_exists_cmd.split(" "), stdout=subprocess.PIPE, 141 | stderr=subprocess.STDOUT) 142 | 143 | print( 144 | f"File exists 1 result: {p.stdout.readline().decode('utf-8', errors='ignore').strip()}") 145 | 146 | p = subprocess.Popen(exec_cmd.split(" "), stdout=subprocess.PIPE, 147 | stderr=subprocess.STDOUT) 148 | 149 | p = subprocess.Popen(file_exists_cmd.split(" "), stdout=subprocess.PIPE, 150 | stderr=subprocess.STDOUT) 151 | 152 | ret_value = False 153 | threat_name = "Nothing" 154 | while(True): 155 | 156 | retcode = p.poll() # returns None while subprocess is running 157 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 158 | # print(out) 159 | m = re.search('does not exist', out) 160 | 161 | if m: 162 | 163 | threat_name = out 164 | ret_value = True 165 | 166 | if len(out) > 0: 167 | logging.debug(out) 168 | 169 | if(retcode is not None): 170 | break 171 | 172 | os.unlink(tmp_file_name) 173 | 174 | if with_name: 175 | return ret_value, threat_name 176 | 177 | return ret_value 178 | 179 | 180 | class VMWareKaspersky(Scanner): 181 | 182 | def __init__(self): 183 | self.scanner_path = WDEFENDER_INSTALL_PATH 184 | self.scanner_name = "Kaspersky" 185 | 186 | """ 187 | Scans a file with Kaspersky and returns True if the file 188 | is detected as a threat. 189 | """ 190 | 191 | def scan(self, file_path, with_name=False): 192 | tmp_file_name = tempfile.NamedTemporaryFile().name 193 | shutil.copyfile(file_path, tmp_file_name) 194 | file_path = tmp_file_name 195 | basename = os.path.basename(tmp_file_name) 196 | vmx_path = config.get_value("kaspersky_vmx") 197 | passwd = config.get_value("vmware_passwd") 198 | username = config.get_value("vmware_user") 199 | copy_file_cmd = f"vmrun -T ws -gu {username} -gp {passwd} CopyFileFromHostToGuest {vmx_path} {tmp_file_name} C:\\Users\\toto\\Desktop\\{basename}.exe" 200 | file_exists_cmd = f"vmrun -T ws -gu {username} -gp {passwd} fileExistsInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 201 | exec_cmd = f"vmrun -T ws -gu {username} -gp {passwd} runProgramInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 202 | scan_cmd = ["vmrun", "-T", "ws", "-gu", username, "-gp", passwd, "runProgramInGuest", 203 | f"{vmx_path}", "C:\\Program Files (x86)\\Kaspersky Lab\\Kaspersky Anti-Virus 21.3\\avp.exe", "SCAN", f"C:\\Users\\{username}\\Desktop\\{basename}.exe", "/i0"] 204 | # print(f"Copy file result: {subprocess.Popen(scan_cmd.split(' '), stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.readline().decode('utf-8', errors='ignore').strip()}") 205 | p = subprocess.Popen(copy_file_cmd.split(" "), stdout=subprocess.PIPE, 206 | stderr=subprocess.STDOUT) 207 | #print(f"Copy file result: {p.stdout.readline().decode('utf-8', errors='ignore').strip()}") 208 | 209 | p = subprocess.Popen(file_exists_cmd.split(" "), stdout=subprocess.PIPE, 210 | stderr=subprocess.STDOUT) 211 | 212 | #print(f"File exists 1 result: {p.stdout.readline().decode('utf-8', errors='ignore').strip()}") 213 | 214 | # execp = subprocess.Popen(exec_cmd.split(" "), stdout=subprocess.PIPE, 215 | # stderr=subprocess.STDOUT) 216 | #out2 = execp.stdout.readline().decode('utf-8', errors='ignore').strip() 217 | #print(f"File exec result: {out2}") 218 | 219 | # time.sleep(1) 220 | # print(scan_cmd) 221 | p = subprocess.Popen(scan_cmd, stdout=subprocess.PIPE, 222 | stderr=subprocess.STDOUT) 223 | 224 | ret_value = False 225 | threat_name = "Nothing" 226 | 227 | while(True): 228 | 229 | retcode = p.poll() # returns None while subprocess is running 230 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 231 | # out2 += execp.stdout.readline().decode('utf-8', errors='ignore').strip() 232 | # print(out) 233 | m = re.search('suspicion', out) 234 | # n = re.search("A program could not run", out2) 235 | if m: 236 | 237 | threat_name = out 238 | logging.debug("Detected") 239 | ret_value = True 240 | 241 | if retcode == 3: 242 | logging.debug("Detected") 243 | 244 | ret_value = True 245 | break 246 | 247 | """ 248 | elif False: 249 | ret_value = True 250 | threat_name = execp 251 | logging.debug("Detected") 252 | break 253 | else: 254 | logging.debug(out2) 255 | """ 256 | 257 | # logging.debug(f"Retcode:{retcode}") 258 | 259 | if retcode is not None: 260 | break 261 | 262 | os.unlink(tmp_file_name) 263 | 264 | if with_name: 265 | return ret_value, threat_name 266 | 267 | return ret_value 268 | 269 | 270 | class VMWareAvast(Scanner): 271 | 272 | def __init__(self): 273 | self.scanner_path = WDEFENDER_INSTALL_PATH 274 | self.scanner_name = "Avast" 275 | 276 | """ 277 | Scans a file with Kaspersky and returns True if the file 278 | is detected as a threat. 279 | """ 280 | 281 | def scan(self, file_path, with_name=False): 282 | tmp_file_name = tempfile.NamedTemporaryFile().name 283 | shutil.copyfile(file_path, tmp_file_name) 284 | file_path = tmp_file_name 285 | basename = os.path.basename(tmp_file_name) 286 | vmx_path = config.get_value("avast_vmx") 287 | passwd = config.get_value("vmware_passwd") 288 | username = config.get_value("vmware_user") 289 | copy_file_cmd = f"vmrun -T ws -gu {username} -gp {passwd} CopyFileFromHostToGuest {vmx_path} {tmp_file_name} C:\\Users\\toto\\Desktop\\{basename}.exe" 290 | file_exists_cmd = f"vmrun -T ws -gu {username} -gp {passwd} fileExistsInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 291 | exec_cmd = f"vmrun -T ws -gu {username} -gp {passwd} runProgramInGuest {vmx_path} C:\\Users\\toto\\Desktop\\{basename}.exe" 292 | p = subprocess.Popen(copy_file_cmd.split(" "), stdout=subprocess.PIPE, 293 | stderr=subprocess.STDOUT) 294 | 295 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 296 | logging.debug(out) 297 | 298 | p = subprocess.Popen(exec_cmd.split( 299 | " "), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 300 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 301 | logging.debug(out) 302 | if re.search("could not run", out): 303 | return True, "toto" 304 | logging.debug(file_exists_cmd) 305 | p = subprocess.Popen(file_exists_cmd.split(" "), stdout=subprocess.PIPE, 306 | stderr=subprocess.STDOUT) 307 | 308 | ret_value = False 309 | threat_name = "Nothing" 310 | while True: 311 | 312 | retcode = p.poll() # returns None while subprocess is running 313 | out = p.stdout.readline().decode('utf-8', errors='ignore').strip() 314 | # print(out) 315 | m = re.search('does not exist', out) 316 | 317 | if m: 318 | 319 | threat_name = out 320 | ret_value = True 321 | 322 | if len(out) > 0: 323 | logging.debug(out) 324 | 325 | if retcode is not None: 326 | break 327 | 328 | os.unlink(tmp_file_name) 329 | 330 | if with_name: 331 | return ret_value, threat_name 332 | 333 | return ret_value 334 | 335 | 336 | g_scanner = DockerWindowsDefender() 337 | -------------------------------------------------------------------------------- /slides/2022_03_Insomnihack_Antivirus_vmeier.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrt/avdebugger/63eea5d5eaebd19ff89e20d4fdec8c4067fe2939/slides/2022_03_Insomnihack_Antivirus_vmeier.pdf -------------------------------------------------------------------------------- /string_encryptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import r2pipe # add dependency to list 4 | import base64 5 | import lief 6 | import keystone 7 | import logging 8 | import sys 9 | import stackprinter 10 | from itertools import cycle 11 | 12 | 13 | """ 14 | Steps: 15 | 16 | 1. List XREFS to all strings (done) 17 | 2. Insert 3 instructions, with the decrypt_string function already present in the binary. 18 | 3. Insert the decrypt_string function in a new section. (done) 19 | 20 | 21 | OR 22 | 23 | 2. Replace instruction with a JMP 24 | 3. To a new code section with the 3 aforementioned instructions. 25 | 4. JMP to return address. 26 | 27 | Needs to add a new section statically, at the end of the binary. Either LIEF or radare2. 28 | Needs to compute the JMP destination (+ check 2 MB restrictions :O). 29 | Needs to a PIE code able to decrypt a function O.o 30 | 1. Find strings, replace with Vigenere :D 31 | 2. Write other PIE elf bin that de-Vignere strings (done) 32 | 3. Steal the code section and patch it in the binary in a new code section (done). 33 | 34 | Add new section (".toto"): (done) 35 | 36 | 1. JMP instruction lands here 37 | 2. Need to remember the EIP address 38 | 3. Copy original instruction in that section. (done) 39 | 4. Replace original instruction by JMP NEAR + offset to new section + offset to instruction 40 | 5. JMP to section ".test" O.o absolute jmp is best. (done) 41 | 6. Add JMP to ".toto", after function call. (done) 42 | 7. JMP to original EIP + 1 (address of step 2). (done) 43 | 44 | 45 | TODO: 46 | 47 | 1. Encrypt string in .data section (with vigenere for the PoC). (done) 48 | 2. Implement add_jump_table_section (done) 49 | 50 | 1. Chiffrer automatiquement les strings dans le binaire (easy) (P1) (done) 51 | 4. Améliorer le hook pour se rappeler automatiquement l'adresse de retour. (P2) (done) 52 | 2. Appliquer l'algo à toutes les strings (done) 53 | 3. Check pour pas déchiffrer deux fois la même string dans le cas où elle est employée plusieurs fois. (LP) 54 | 55 | 56 | TODO: 57 | 1. Do not encrypt strings if patch_xref won't handle it.l (done) 58 | """ 59 | 60 | # config init 61 | stackprinter.set_excepthook(style='darkbg2') # stacktraces with variables' values 62 | logging.basicConfig(level=logging.INFO) 63 | 64 | # constants 65 | BINARY = "bin/simple_test.bin" 66 | STUB = "bin/simple_vigenere.bin" 67 | TRAMPOLINE_SECTION = ".switch" 68 | DECRYPT_SECTION = ".test" 69 | KEY = "MUSIQUE" 70 | SZ_BLK_PER_STRING = 29 # space required to handle 1 string in the switch section 71 | 72 | # global variables 73 | g_is_pe = False 74 | ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64) 75 | 76 | """ 77 | takes a position-independant function in a given binary 78 | and copy it. 79 | @name: function name 80 | @binary: a binary already parsed with LIEF. 81 | """ 82 | def strip_function(name: str, binary: lief.ELF.Binary): 83 | 84 | address = 0 # offset of the function within the binary 85 | size = 0 # size of the function 86 | 87 | if binary.format == lief.EXE_FORMATS.ELF: 88 | symbol = binary.get_static_symbol(name) 89 | 90 | address = symbol.value 91 | size = symbol.size 92 | 93 | # lief does not appear to be able to locate function by name in PE files. 94 | elif binary.format == lief.EXE_FORMATS.PE: 95 | 96 | r2 = r2pipe.open(STUB) 97 | r2.cmd("aaa") 98 | all_functions = r2.cmdj("aflj") 99 | matching_functions = [] 100 | 101 | for fn in all_functions: 102 | 103 | if name in fn['name']: 104 | logging.info(f"Found function matching '{name}': {fn}") 105 | matching_functions += [fn] 106 | 107 | if len(matching_functions) > 1: 108 | logging.warning(f"More than 1 function found with name {name}. Bug incoming.") 109 | 110 | address = matching_functions[0]['offset'] 111 | size = matching_functions[0]['size'] 112 | 113 | else: 114 | raise Exception("Unsupported file format") 115 | 116 | function_bytes = binary.get_content_from_virtual_address(address, size) 117 | return function_bytes, address, size 118 | 119 | """ 120 | TODO: bad function name 121 | TODO: document 122 | TODO: cleanup 123 | """ 124 | def add_section(original_binary): 125 | 126 | r2 = r2pipe.open(BINARY) 127 | strings = get_strings(r2) 128 | nb_strings = len(strings) 129 | 130 | # :( 131 | if g_is_pe: 132 | 133 | section = original_binary.get_section(".rdata") 134 | section.characteristics = lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ# make the section writable :O 135 | 136 | 137 | section = lief.PE.Section(DECRYPT_SECTION) 138 | section.characteristics = lief.PE.SECTION_CHARACTERISTICS.CNT_CODE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ | lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE 139 | content,_,_ = strip_function("decrypt", lief.parse(STUB)) 140 | 141 | section.content = content 142 | section = original_binary.add_section(section) 143 | 144 | section = lief.PE.Section(TRAMPOLINE_SECTION) 145 | section.characteristics = lief.PE.SECTION_CHARACTERISTICS.CNT_CODE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ | lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE 146 | 147 | section.content = [0x90 for i in range(SZ_BLK_PER_STRING * nb_strings)] # placeholder 148 | section = original_binary.add_section(section) 149 | return original_binary, section 150 | else: 151 | 152 | section = original_binary.get_section(".rodata") 153 | section += lief.ELF.SECTION_FLAGS.WRITE # make the section writable :O 154 | 155 | section = lief.ELF.Section(DECRYPT_SECTION, lief.ELF.SECTION_TYPES.PROGBITS) 156 | section += lief.ELF.SECTION_FLAGS.EXECINSTR 157 | section += lief.ELF.SECTION_FLAGS.WRITE 158 | content,_,_ = strip_function("decrypt", lief.parse(STUB)) 159 | 160 | section.content = content 161 | section = original_binary.add(section, loaded=True) 162 | 163 | section = lief.ELF.Section(TRAMPOLINE_SECTION, lief.ELF.SECTION_TYPES.PROGBITS) 164 | section += lief.ELF.SECTION_FLAGS.EXECINSTR 165 | section += lief.ELF.SECTION_FLAGS.WRITE 166 | 167 | section.content = [0x90 for i in range(SZ_BLK_PER_STRING * nb_strings)] # placeholder # TODO compute in advanced the required size 168 | section = original_binary.add(section, loaded=True) 169 | return original_binary, section 170 | 171 | def get_instructions_size(current_instructions: str, placeholder_value: list) -> int: 172 | 173 | ins, _ = ks.asm(current_instructions.format(*placeholder_value)) 174 | return len(ins) 175 | 176 | def adjust_signedness(offset): 177 | 178 | if type(offset) == int: 179 | offset = hex(offset) 180 | 181 | sign = '-' 182 | if offset[0] == '-': 183 | 184 | sign = '+' 185 | offset = offset[1:] 186 | 187 | return sign + offset 188 | 189 | 190 | """ 191 | lea rdi, str.offset1 ; load the string 192 | mov r12, label1 ; or EIP+len(next_instruction) 193 | jmp decrypt_section ; absolute jmp # end of decrypt section will jmp on r12 194 | label1: 195 | pop rax ; original instruction pointer 196 | jmp rax 197 | 198 | """ 199 | def add_jump_table_section(binary, radare_pipe, string, previous_block_sz, original_instruction): 200 | 201 | proper_assembly = ["push rdi\npush rsi\npush rax\nlea rdi, [rip{}]\n", #offset_to_str, sign to be included 202 | "mov rsi, {}\n", #str_size 203 | "lea rax, [rip{}\n", #offset_to_decrypt_section 204 | "call rax\n", 205 | "pop rax\npop rsi\npop rdi\n", 206 | "lea rdi, [rip{}]\n",# offset_to_str2 # assert unused 207 | "ret"] 208 | 209 | if g_is_pe: 210 | proper_assembly = ["push rcx\npush rdx\npush rax\nlea rcx, [rip{}]\n", #offset_to_str, sign to be included 211 | "mov rdx, {}\n", #str_size 212 | "lea rax, [rip{}\n", #offset_to_decrypt_section 213 | "call rax\n", 214 | "pop rax\npop rdx\npop rcx\n", 215 | "lea rdi, [rip{}]\n",# offset_to_str2 # assert unused TODO cleanup 216 | "ret"] 217 | 218 | string_offset = string["vaddr"] 219 | section = binary.get_section(TRAMPOLINE_SECTION) 220 | binary_base_address = 0 221 | 222 | if g_is_pe: 223 | binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr'] 224 | 225 | new_data_address = binary.get_section(".data").virtual_address 226 | new_decrypt_address = binary.get_section(DECRYPT_SECTION).virtual_address 227 | new_text_address = binary.get_section(".text").virtual_address 228 | 229 | # load string in rdi 230 | offset_to_str = hex(binary_base_address+section.virtual_address-string_offset) 231 | offset_to_str = adjust_signedness(offset_to_str) 232 | crt_ins_size = get_instructions_size(proper_assembly[0], [offset_to_str]) 233 | offset_to_str = hex(binary_base_address+section.virtual_address-string_offset+crt_ins_size+previous_block_sz) 234 | offset_to_str = adjust_signedness(offset_to_str) 235 | assembly = proper_assembly[0].format(offset_to_str) 236 | 237 | # load string size 238 | str_size = string["length"] 239 | assembly += proper_assembly[1].format(str_size) 240 | 241 | # call decrypt_function 242 | sections_offset = section.virtual_address - new_decrypt_address 243 | crt_ins_size = get_instructions_size(assembly + proper_assembly[2], [adjust_signedness(sections_offset)]) 244 | offset_to_decrypt_section = hex(sections_offset + crt_ins_size + previous_block_sz) 245 | offset_to_decrypt_section = adjust_signedness(offset_to_decrypt_section) 246 | assembly += proper_assembly[2].format(offset_to_decrypt_section) 247 | assembly += proper_assembly[3] 248 | 249 | # restore registers 250 | assembly += proper_assembly[4] 251 | 252 | # load original instruction 253 | offset_to_str2 = binary_base_address+section.virtual_address-string_offset 254 | offset_to_str2 += get_instructions_size(assembly+proper_assembly[5], [offset_to_str]) 255 | offset_to_str2 += previous_block_sz 256 | #assembly += proper_assembly[5].format(hex(offset_to_str2)) # original instruction here 257 | assert(original_instruction["mnemonic"] == "lea") # TODO: handle more cases 258 | first_operand = original_instruction["opex"]['operands'][0] 259 | 260 | assert(first_operand["type"] == "reg") 261 | dest_reg = first_operand["value"] 262 | assembly += f"lea {dest_reg}, [rip{adjust_signedness(offset_to_str2)}]\n" 263 | 264 | # return to original instructi"]on 265 | assembly += proper_assembly[-1] 266 | encoding, _ = ks.asm(assembly) 267 | 268 | current_content = section.content[:previous_block_sz] 269 | section.content = current_content + encoding 270 | 271 | # write the new binary to disk 272 | binary.write(BINARY+".patch") 273 | return len(encoding) 274 | 275 | """ 276 | @key: encryption key :str: 277 | @string: the string to encrypt 278 | 279 | As a PoC, Vigenere is used :D 280 | """ 281 | def encrypt_string(key, plaintext): 282 | 283 | universe = [c for c in (chr(i) for i in range(32,127) if not i == 92) ] 284 | uni_len = len(universe) 285 | ret_txt = '' 286 | k_len = len(key) 287 | 288 | for i, l in enumerate(plaintext): 289 | 290 | if l not in universe: 291 | ret_txt += l 292 | else: 293 | txt_idx = universe.index(l) 294 | 295 | k = key[i % k_len] 296 | key_idx = universe.index(k) 297 | code = universe[(txt_idx + key_idx) % uni_len] 298 | ret_txt += code 299 | 300 | return ret_txt 301 | 302 | def xor(key, message): 303 | return ''.join(chr(ord(c)^ord(k)) for c,k in zip(message, cycle(key))) 304 | 305 | """ 306 | relies on radare2 to retrieve all the strings 307 | in a binary. 308 | 309 | \return strings as JSON objects 310 | """ 311 | def get_strings(radare_pipe): 312 | 313 | # list strings in .data section as JSON objects 314 | radare_pipe.cmd("aaa") 315 | all_strings = radare_pipe.cmdj("izj") 316 | return all_strings 317 | 318 | """ 319 | Todo: handle several strings. 320 | """ 321 | def patch_xref(binary, string, radare_pipe, previous_block_sz) -> bool: 322 | 323 | # patch the instruction that originally references the string 324 | # this allows to decrypt beforehand, so as no to alter the 325 | # program's behavior. 326 | xrefs = radare_pipe.cmdj(f"axtj @ {string['vaddr']}") 327 | original_instruction = None 328 | 329 | # For now, several XREFS to the same strings is an unhandled 330 | # case, for simplicity. 331 | if len(xrefs) > 1: 332 | 333 | logging.warning(f"Skipping string \'{string['string']}\' because more than 1 XREF was found") 334 | return False, original_instruction 335 | 336 | # no xref found 337 | elif len(xrefs) < 1: 338 | logging.warning(f"Skipping string \'{string['string']}\' because less than 1 XREF could be found") 339 | return False, original_instruction 340 | 341 | xref = xrefs[0] 342 | 343 | # corner cases that can't be handled right for now 344 | if not xref["opcode"].startswith("lea"): 345 | 346 | logging.warning(f"Skipping string \'{string['string']}\'. Unhandle opcode {xref['opcode']}\'") 347 | return False, original_instruction 348 | """ 349 | if string["section"] in [".rodata", ".rdata"]: 350 | logging.warning(f"Skipping string \'{string['string']}\' because it's located in a read-only section.") 351 | return False, original_instruction 352 | """ 353 | 354 | logging.info(f"Encrypting string \'{base64.b64decode(string['string'])}\'...") 355 | location = xref["from"] 356 | 357 | # store original instruction infomration 358 | original_instruction = radare_pipe.cmdj(f"aoj @ {location}") 359 | switch_address= binary.get_section(TRAMPOLINE_SECTION).virtual_address 360 | binary_base_address = 0 361 | 362 | # LIEF creates new sections for PE with virtual_address relative to image base. 363 | if g_is_pe: 364 | binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr'] 365 | 366 | jmp_destination = binary_base_address+switch_address - location + previous_block_sz # displacement between the original instruction and the switch section 367 | assembly = f"call {hex(jmp_destination)}" 368 | tmp_encoding, _ = ks.asm(assembly) 369 | 370 | # TODO: clean up below 371 | res = "" 372 | for i in tmp_encoding: 373 | if i < 10: 374 | res += "0" + str(hex(i))[2:] 375 | else: 376 | res += str(hex(i))[2:] 377 | 378 | res += "9090" 379 | radare_pipe.cmd(f"wx {res} @ {hex(location)}") 380 | return True, original_instruction 381 | 382 | def encrypt_strings(binary): 383 | 384 | r2 = r2pipe.open(BINARY+".patch", flags=["-w"]) 385 | all_strings = get_strings(r2) 386 | previous_block_sz = 0 387 | nb_encrypted_strings = 0 388 | 389 | for index, string in enumerate(all_strings): 390 | 391 | decoded_string = base64.b64decode(string["string"]) 392 | binary = lief.parse(BINARY+".patch") # is this needed? 393 | 394 | # hook the binary where the string is referenced. Skip if the string 395 | # is used several times. 396 | can_proceed, original_instruction = patch_xref(binary, string, r2, previous_block_sz) 397 | 398 | if not can_proceed: 399 | continue 400 | 401 | # encrypt the string in .data (or whatever else) section. 402 | encrypted = encrypt_string(KEY, base64.b64decode(string["string"]).decode()) # convert_encoding(string["type"]) 403 | encoded = base64.b64encode(encrypted.encode()).decode() 404 | r2.cmd(f"w6d {encoded} @ {string['vaddr']}") 405 | 406 | # prepare the trampoline for the hook. 407 | # takes care of decrypting the string and resuming the original control flow. 408 | binary = lief.parse(BINARY+".patch") # is this needed? 409 | previous_block_sz += add_jump_table_section(binary, r2, string, previous_block_sz, original_instruction[0]) # TODO handle > 1 opcodes 410 | nb_encrypted_strings += 1 411 | 412 | logging.info(f"Successfully encrypted {nb_encrypted_strings}/{len(all_strings)} strings!") 413 | 414 | if __name__ == "__main__": 415 | 416 | if len(sys.argv) > 1: 417 | BINARY = sys.argv[1] 418 | 419 | if len(sys.argv) > 2: 420 | STUB = sys.argv[2] 421 | 422 | logging.info(f"Encrypting strings of {BINARY}") 423 | logging.info(f"Decryption routine will be copied from {STUB}") 424 | 425 | original_binary = lief.parse(BINARY) 426 | 427 | if original_binary.format == lief.EXE_FORMATS.ELF: 428 | logging.info("ELF executable detected") 429 | 430 | elif original_binary.format == lief.EXE_FORMATS.PE: 431 | logging.info("PE Executable detected.") 432 | g_is_pe = True 433 | 434 | else: 435 | logging.error("Unrecognized binary") 436 | exit(1) 437 | 438 | new_binary, section = add_section(original_binary) 439 | 440 | # make a copy of the original binary 441 | new_binary.write(BINARY+".patch") 442 | 443 | # parse strings references and encrypt 444 | encrypt_strings(new_binary) 445 | -------------------------------------------------------------------------------- /test_find_bad_strings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | import unittest.mock 5 | import tempfile 6 | import find_bad_strings as fbs 7 | import hashlib 8 | import os 9 | import shutil 10 | import random 11 | 12 | def test_is_all_blacklisted(): 13 | strings_refs = [] 14 | 15 | for i in range(100): 16 | tmp = fbs.StringRef() 17 | tmp.index = i 18 | strings_refs += [tmp] 19 | 20 | available_idx = [str_ref.index for str_ref in strings_refs] 21 | 22 | rnd_blacklist = random.choices(available_idx, k=random.randint(0,len(available_idx)//2)) 23 | 24 | assert not (fbs.is_all_blacklisted(strings_refs, rnd_blacklist)) 25 | 26 | assert fbs.is_all_blacklisted(strings_refs, available_idx) 27 | 28 | def test_parse_strings(): 29 | data_blob = """[Strings] 30 | Num Paddr Vaddr Len Size Section Type String 31 | 000 0x00099640 0x18009a240 13 14 (.rdata) ascii kiwi_exec_cmd 32 | 001 0x00099650 0x18009a250 4 5 (.rdata) ascii kiwi 33 | 002 0x00099658 0x18009a258 27 56 (.rdata) utf16le \nmimikatz(powershell) # %s\n 34 | 003 0x00099690 0x18009a290 5 12 (.rdata) utf16le hello 35 | 004 0x000996a0 0x18009a2a0 50 102 (.rdata) utf16le ERROR mimikatz_initOrClean ; CoInitializeEx: %08x\n 36 | 005 0x00099708 0x18009a308 4 10 (.rdata) utf16le INIT 37 | 006 0x00099718 0x18009a318 5 12 (.rdata) utf16le CLEAN 38 | 007 0x00099730 0x18009a330 36 74 (.rdata) utf16le >>> %s of '%s' module failed : %08x\n""" 39 | strings_refs = fbs.parse_strings(data_blob) 40 | 41 | assert len(strings_refs) == 8 42 | assert strings_refs[-1].index == 7 43 | assert strings_refs[0].index == 0 44 | assert strings_refs[7].length == 36 45 | 46 | def md5(fname): 47 | hash_md5 = hashlib.md5() 48 | with open(fname, "rb") as f: 49 | for chunk in iter(lambda: f.read(4096), b""): 50 | hash_md5.update(chunk) 51 | return hash_md5.hexdigest() 52 | 53 | def test_patch_binary(): 54 | string = "3122 0x000cef20 0x1800cfb20 23 48 .rdata utf16le \\pipe\\protected_storage" 55 | string_ref = fbs.StringRef(3122, 0x000cef20, 0x1800cfb20, 23, 48, ".rdata", "utf16le", "\pipe\protected_storage") 56 | string_ref.should_mask = True 57 | index = int(string.split()[0]) 58 | assert string_ref.index == index 59 | assert string_ref.content == "\pipe\protected_storage" 60 | md5_before = md5("test_cases/ext_server_kiwi.x64.dll") 61 | tmp_bin = tempfile.NamedTemporaryFile() 62 | shutil.copyfile("test_cases/ext_server_kiwi.x64.dll", tmp_bin.name) 63 | print("tmp file name = " + tmp_bin.name) 64 | filename = tmp_bin.name 65 | all_strings_ref = fbs.parse_strings(filename) 66 | len_strings_before = len(all_strings_ref) 67 | 68 | # replace the string with something else 69 | fbs.patch_string(filename, string_ref, unmask_only=False) 70 | all_strings_ref2 = fbs.parse_strings(filename) 71 | 72 | assert(len(all_strings_ref2) == len_strings_before) 73 | # check that the replacement worked 74 | assert all_strings_ref2[index].content != string_ref.content 75 | print(f"Replaced by : {all_strings_ref2[index].content}") 76 | 77 | # check that no other string contains this value (in case offsets are wrong) 78 | assert not(any(string_ref.content in string.content for string in all_strings_ref2)) 79 | 80 | # re-set the string 81 | string_ref.should_mask = False 82 | all_strings_ref[index].should_mask = False 83 | #fbs.patch_string(filename, string_ref, unmask_only=False) 84 | fbs.patch_string(filename, all_strings_ref[index], unmask_only=False) 85 | all_strings_ref = fbs.parse_strings(filename) 86 | 87 | # check that the original string was put back 88 | string = all_strings_ref[index].content 89 | assert all_strings_ref[index].content == string_ref.content 90 | new_md5 = md5(filename) 91 | 92 | # check files are the same 93 | assert md5_before == new_md5 94 | tmp_bin.close() 95 | pass 96 | 97 | 98 | """ 99 | replace the actual scan with Windows Defender by a 100 | fake one, so as to check if the bissection algorithm works 101 | as expected. 102 | """ 103 | def mock_scan(filepath): 104 | #return True 105 | all_strings_ref = fbs.parse_strings(filepath) 106 | known_strings = ['Ask a privilege by its id', 'RUUU', 'CERT_NCRYPT_KEY_HANDLE_TRANSFER_PROP_ID', 'text', 'LocalAlloc', 'viewstack', '0000//...-', 'CERT_SUBJECT_NAME_MD5_HASH_PROP_ID', 'HeapReAlloc', 'Preshutdown service'] 107 | 108 | 109 | ##return any(s.content in known_strings for s in all_strings_ref) 110 | for i in all_strings_ref: 111 | if i.content in known_strings: 112 | print(f"---> Found bad string {i.content} at index {i.index}, address = {i.paddr}") 113 | return True 114 | return False 115 | 116 | @unittest.mock.patch("find_bad_strings.scan", side_effect=mock_scan) 117 | @unittest.mock.patch("find_bad_strings.os.chdir") 118 | #@unittest.mock.patch("find_bad_strings.validate_results") 119 | def test_bissection(mock_scan, mock_chdir): 120 | """known_strings = ["Pass-the-ccache [NT6]", 121 | "ERROR kuhl_m_crypto_l_certificates ; CryptAcquireCertificatePrivateKey (0x%08x)\\n", 122 | "ERROR kuhl_m_crypto_l_certificates ; CertGetCertificateContextProperty (0x%08x)\\n", 123 | "ERROR kuhl_m_crypto_l_certificates ; CertGetNameString (0x%08x)\\n", 124 | "lsasrv.dll", 125 | "ERROR kuhl_m_lsadump_sam ; CreateFile (SYSTEM hive) (0x%08x)\\n", 126 | "SamIFree_SAMPR_USER_INFO_BUFFER", 127 | "KiwiAndRegistryTools", 128 | "wdigest.dll", 129 | "multirdp", 130 | "logonPasswords", 131 | "credman", 132 | "[%x;%x]-%1u-%u-%08x-%wZ@%wZ-%wZ.%s", 133 | "n.e. (KIWI_MSV1_0_CREDENTIALS KO)"] 134 | """ 135 | 136 | known_strings = ['Ask a privilege by its id', 'RUUU', 'CERT_NCRYPT_KEY_HANDLE_TRANSFER_PROP_ID', 'text', 'LocalAlloc', 'viewstack', '0000//...-', 'CERT_SUBJECT_NAME_MD5_HASH_PROP_ID', 'HeapReAlloc', 'Preshutdown service'] 137 | 138 | tmp = tempfile.NamedTemporaryFile() 139 | shutil.copyfile("test_cases/ext_server_kiwi.x64.dll", tmp.name) 140 | 141 | fbs.ORIGINAL_BINARY = "test_cases/ext_server_kiwi.x64.dll" 142 | blacklist = fbs.bissect(tmp.name) 143 | assert len(blacklist) > 0 144 | all_strings_ref = fbs.parse_strings("test_cases/ext_server_kiwi.x64.dll") 145 | 146 | try: 147 | for i in blacklist: 148 | assert all_strings_ref[i].index == i 149 | assert all_strings_ref[i].content in known_strings 150 | 151 | for str in known_strings: 152 | assert any(str in x.content for x in all_strings_ref) 153 | #assert len(blacklist) == len(known_strings) 154 | 155 | except AssertionError: 156 | print(blacklist) 157 | for i in blacklist: 158 | print(list(filter(lambda x: x.index == i, all_strings_ref))) 159 | raise AssertionError 160 | 161 | 162 | @pytest.mark.parametrize('list1,list2,output', [ 163 | ([], [1], [1]), 164 | ([1], [1], [1]), 165 | ([2,3], [4], [2,3,4]), 166 | ([2,3,34,3], [1,0,3,1,1,1], [0,1,2,3,34])]) 167 | def test_merge_unique(list1, list2, output): 168 | assert fbs.merge_unique(list1, list2) == output 169 | 170 | def test_merge_unique_param_return(): 171 | toto = [1,2,3,4,5,6] 172 | res = [] 173 | for i in toto: 174 | res = fbs.merge_unique(res, [i]) 175 | assert res == toto 176 | 177 | 178 | @pytest.mark.parametrize('list1,list2,output', [ 179 | ([], [1], False), 180 | ([1], [1], True), 181 | ([2, 3], [4], False), 182 | ([2, 3, 34, 3], [1, 0, 3, 1, 1, 1], False), 183 | ([4,3,2], [2,3,4], True)]) 184 | def test_is_equal_unordered(list1, list2, output): 185 | assert fbs.is_equal_unordered(list1, list2) == output 186 | 187 | def test_validate_results(): 188 | all_strings = fbs.get_all_strings("test_cases/ext_server_kiwi.x64.dll") 189 | all_strings_ref = fbs.parse_strings(all_strings) 190 | 191 | blacklist = [x.index for x in all_strings_ref] 192 | 193 | fbs.validate_results(fbs.BINARY, blacklist, all_strings_ref) 194 | 195 | def test_hide_section(): 196 | filepath = os.path.abspath("test_cases/ext_server_kiwi.x64.dll") 197 | 198 | binary = fbs.get_binary(filepath) 199 | fbs.hide_section(".text", filepath, binary) 200 | --------------------------------------------------------------------------------