diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..8066c2d --- /dev/null +++ b/README.MD @@ -0,0 +1,29 @@ +# SymbiontPE + +## Description + +Simple tool that allows add custom function (with custom dll) to PE-file import section. After that the dll will be force to loaded by executed app. This allows to easily write proxy dll for the app. + +For examle some app has version.dll in import (this is almost every app). We want to hijack version.dll through placing custom version.dll with payload near app. One solution is write code of payload dll. For correct loading, payload dll must be same bitness and has same import as original version.dll. If we want app to contine workin after dll hijack, than we need add correct code for all functions in our payload dll (e.g. redirect calls to original library). There are some ways of writing those code (link_github). + +But there is another way - we could copy version.dll and add custom function from custom dll. When app started, it loads modified version.dll and OS force app to load custom dll. So, with this approach dll still needed, but this payload dll coulde easily coded, since it has inly two simple requirements - corresponded bittness and export only one function with any prototype (void dummy() {}, will work). + +## Sreenshot + +![Example of using](Screenshots/example.png) + +## Using + +SymbiontPE PathToDll ImportDllName ImportFunctionName OutputPath + +* PathToDll - path to original dll + +* ImportDllName - dll name to be added to import section + +* ImportFunctionName - function name to be added to import section + +* OutputPath - output path for saveing result + +Example of using: + +SymbiontPE C:\Windows\version.dll dummy.dll dummy C:\1\version.dll diff --git a/Screenshots/example.png b/Screenshots/example.png new file mode 100644 index 0000000..be3b063 Binary files /dev/null and b/Screenshots/example.png differ diff --git a/SymbiontPE.sln b/SymbiontPE.sln new file mode 100644 index 0000000..2c11005 --- /dev/null +++ b/SymbiontPE.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26228.9 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SymbiontPE", "SymbiontPE\SymbiontPE.csproj", "{335C42E0-AB15-4EB4-B450-977D94B0741C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {335C42E0-AB15-4EB4-B450-977D94B0741C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {335C42E0-AB15-4EB4-B450-977D94B0741C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {335C42E0-AB15-4EB4-B450-977D94B0741C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {335C42E0-AB15-4EB4-B450-977D94B0741C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SymbiontPE/BWExtensions.cs b/SymbiontPE/BWExtensions.cs new file mode 100644 index 0000000..2e35760 --- /dev/null +++ b/SymbiontPE/BWExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; + +namespace SymbiontPE +{ + public static class BWExtensions + { + public static void WriteZero(this BinaryWriter bw, int count = 1) + { + var zeroes = new byte[count]; + bw.Write(zeroes); + } + + public static void Write4(this BinaryWriter bw, long qword) + { + // Cast once, instead of everytime + bw.Write((UInt32)qword); + } + } +} \ No newline at end of file diff --git a/SymbiontPE/PEFile.cs b/SymbiontPE/PEFile.cs new file mode 100644 index 0000000..0b522a2 --- /dev/null +++ b/SymbiontPE/PEFile.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SymbiontPE +{ + public class PEFile + { + // Data + private byte[] _content; + private bool _is64; + + // Offsets + private int _PEHeaderOffset; + private int _sectionsOffset; + private int _importDataDirRVAOffset; + private int _importDataDirSizeOffset; + + // Sections + private List _sections; + private List _sectionParameters; + + // Import Data + private byte[] _importData; + + // Consts + //private const int SECTION_DESCRIPTOR_SIZE = 40; + + public PEFile(string path) + { + _content = File.ReadAllBytes(path); + // DOS header + if (((_content[0] == 0x4d) && (_content[1] == 0x5a)) || (((_content[0] == 0x5a) && (_content[1] == 0x4d)))) + // MZ + { + _PEHeaderOffset = (Int32) BitConverter.ToUInt32(_content, 0x3c); + // PE header + if ((_content[_PEHeaderOffset] == 0x50) && (_content[_PEHeaderOffset + 1] == 0x45)) // PE + { + const int MACHINE_OFFSET = 4; + var machine = BitConverter.ToUInt16(_content, _PEHeaderOffset + MACHINE_OFFSET); + if (machine == 0x014c) + { + Console.WriteLine(" [.] Machine: pe32"); + _is64 = false; + } + else if (machine == 0x8664) + { + Console.WriteLine(" [.] Machine: pe64"); + _is64 = true; + } + else + throw new Exception("Unsupported machine value"); + + const int SECTION_COUNT_OFFSET = MACHINE_OFFSET + 2; + var countOfSections = BitConverter.ToUInt16(_content, _PEHeaderOffset + SECTION_COUNT_OFFSET); + Console.WriteLine($" [.] Sections count: {countOfSections}"); + + const int OPTIONAL_HEADER_SIZE_OFFSET = SECTION_COUNT_OFFSET + 14; + var optionalHeaderSize = BitConverter.ToUInt16(_content, + _PEHeaderOffset + OPTIONAL_HEADER_SIZE_OFFSET); + Console.WriteLine($" [.] Optional header size: {optionalHeaderSize}"); + + // Optional Header + const int OPTIONAL_HEADER_OFFSET = OPTIONAL_HEADER_SIZE_OFFSET + 4; + var optionalHeaderMagic = BitConverter.ToUInt16(_content, _PEHeaderOffset + OPTIONAL_HEADER_OFFSET); + bool isMagic64; + if (optionalHeaderMagic == 0x010b) + { + Console.WriteLine(" [.] OptHeader Magic: 32bit"); + isMagic64 = false; + } + else if (optionalHeaderMagic == 0x020b) + { + Console.WriteLine(" [.] OptHeader Magic: 64bit"); + isMagic64 = true; + } + else + throw new Exception("Unsupported OptHeader magic value"); + if (_is64 != isMagic64) + throw new Exception("OptHeader magic does not match machine"); + + // Data Directories + const int DATADIR_COUNT = 16; + const int DATADIR_SIZE = 8; + int dataDirsOffset = OPTIONAL_HEADER_OFFSET + optionalHeaderSize - DATADIR_COUNT * DATADIR_SIZE; + _importDataDirRVAOffset = dataDirsOffset + 8; + var importDataDirRVA = BitConverter.ToUInt32(_content, _PEHeaderOffset + _importDataDirRVAOffset); + + _importDataDirSizeOffset = _importDataDirRVAOffset + 4; + var importDataDirSize = BitConverter.ToUInt32(_content, _PEHeaderOffset + _importDataDirSizeOffset); + + // Sections Table + _sectionsOffset = OPTIONAL_HEADER_OFFSET + optionalHeaderSize; + var index = _sectionsOffset; + _sections = new List(); + _sectionParameters = new List(); + for (var i = 0; i < countOfSections; i++) + { + var sectionBytes = new byte[PESectionParameters.SECTION_DESCRIPTOR_SIZE]; + Array.Copy(_content, _PEHeaderOffset + index, sectionBytes, 0, + PESectionParameters.SECTION_DESCRIPTOR_SIZE); + index += PESectionParameters.SECTION_DESCRIPTOR_SIZE; + var p = new PESectionParameters(sectionBytes); + _sectionParameters.Add(p); + _sections.Add(p.Name); + Console.WriteLine($" [.] Find section: '{p.Name}'"); + } + + // Find section with import + var sectionWithImport = -1; + for (var i = 0; i < _sectionParameters.Count; i++) + { + var p = _sectionParameters[i]; + if ((p.VirtualAddress <= importDataDirRVA) && + (p.VirtualAddress + p.SizeOfRawData >= importDataDirRVA + importDataDirSize)) + sectionWithImport = i; + } + if (sectionWithImport == -1) + throw new Exception("Unable to find section with Import Directory"); + Console.WriteLine($" [.] Import found at section '{_sectionParameters[sectionWithImport].Name}'"); + + var importDataDirOffset = importDataDirRVA - _sectionParameters[sectionWithImport].VirtualAddress + _sectionParameters[sectionWithImport].PointerToRawData; + _importData = new byte[importDataDirSize]; + Array.Copy(_content, importDataDirOffset, _importData, 0, importDataDirSize); + return; + } + } + throw new Exception("Bad PE"); + } + + public void FixSizeOfImage() + { + var size = _sectionParameters[_sectionParameters.Count - 1].VirtualSize + _sectionParameters[_sectionParameters.Count - 1].VirtualAddress; + if (size % 0x1000 != 0) + size = ((size / 0x1000) + 1) * 0x1000; + Array.Copy(BitConverter.GetBytes(size), 0, _content, _PEHeaderOffset + 80, 4); + Console.WriteLine(" [.] SizeOfImage fixed"); + } + + public List GetSections() + { + var rv = new List(); + rv.AddRange(_sections); + return rv; + } + + public PESectionParameters GetSectionParams(string sectionName) + { + foreach (var p in _sectionParameters) + { + if (p.Name == sectionName) + return p; + } + throw new Exception($"Section '{sectionName}' not found (GetSectionParams)"); + } + + public void SetSectionParams(string sectionName, PESectionParameters sectParams) + { + for (var i = 0; i < _sectionParameters.Count; i++) + { + if (_sectionParameters[i].Name == sectionName) + { + _sectionParameters[i] = sectParams; + Array.Copy(sectParams.ToBytes(), 0, _content, + _PEHeaderOffset + _sectionsOffset + PESectionParameters.SECTION_DESCRIPTOR_SIZE * i, + PESectionParameters.SECTION_DESCRIPTOR_SIZE); + return; + } + } + throw new Exception($"Section '{sectionName}' not found (SetSectionParams)"); + } + + public void WriteDataAndTrim(string sectionName, UInt32 offset, byte[] bytes) + { + for (var i = 0; i < _sectionParameters.Count; i++) + { + if (_sectionParameters[i].Name == sectionName) + { + var beginSize = _sectionParameters[i].PointerToRawData + offset; + var beginAlign = _sectionParameters[i].PointerToRawData + Align(offset, 0x1000); + var endAlign = Align((UInt32) bytes.Length, 0x200); + var newContent = new byte[beginAlign + endAlign]; + Array.Copy(_content, 0, newContent, 0, beginSize); + Array.Copy(bytes, 0, newContent, beginAlign, bytes.Length); + _content = newContent; + Console.WriteLine($" [.] File new size: {_content.Length}"); + return; + } + } + throw new Exception($"Section '{sectionName}' not found (WriteDataAndTrim)"); + } + + public void Save(string outputPath) + { + File.WriteAllBytes(outputPath, _content); + } + + public UInt32 Align(UInt32 length, UInt32 align) + { + UInt32 rv = 0; + while (rv < length) + rv += align; + return rv; + } + + public UInt32 CalcNewImportSize(string dllName, string dllFunc) + { + return (UInt32)(_importData.Length + 20 + dllName.Length + 1 + 2 + dllFunc.Length + 1 + 0x10 + 0x10); + } + + public byte[] CreateNewImport(string sectionName, UInt32 offset, string dllName, string dllFunc) + { + for (var i = 0; i < _sectionParameters.Count; i++) + { + if (_sectionParameters[i].Name == sectionName) + { + var rv = new byte[CalcNewImportSize(dllName, dllFunc)]; + UInt32 BaseAddr = _sectionParameters[i].VirtualAddress + offset; + using (var mem = new MemoryStream(rv)) + using (var bw = new BinaryWriter(mem)) + { + // Write original without last line (originalImport.Length - 20) + bw.Write(_importData, 0, _importData.Length - 20); + // Write IMPORT ENTRY (20) + bw.Write4(BaseAddr + (_importData.Length + 20 + dllName.Length + 1 + 2 + dllFunc.Length + 1 + 0x10)); + bw.WriteZero(4); + bw.WriteZero(4); + bw.Write4(BaseAddr + (_importData.Length + 20)); + bw.Write4(BaseAddr + (_importData.Length + 20 + dllName.Length + 1 + 2 + dllFunc.Length + 1)); + // Write last line (20) + bw.WriteZero(20); + // Write dll (dllName.Length + 1) + bw.Write(Encoding.ASCII.GetBytes(dllName)); + bw.WriteZero(); + // Write zero-struct (2) + bw.WriteZero(2); + // Write func (dllFunc.Length + 1) + bw.Write(Encoding.ASCII.GetBytes(dllFunc)); + bw.WriteZero(); + // Write Thunks (0x20) + bw.Write4(BaseAddr + (_importData.Length + 20 + dllName.Length + 1)); + bw.WriteZero(12); + bw.Write4(BaseAddr + (_importData.Length + 20 + dllName.Length + 1)); + bw.WriteZero(12); + } + // Copy + return rv; + } + } + throw new Exception($"Section '{sectionName}' not found (CreateNewImport)"); + } + + public void RedirectImportDataDir(string sectionName, UInt32 offset) + { + for (var i = 0; i < _sectionParameters.Count; i++) + { + if (_sectionParameters[i].Name == sectionName) + { + var newImportOff = _sectionParameters[i].VirtualAddress + offset; + Array.Copy(BitConverter.GetBytes(newImportOff), 0, _content, + _PEHeaderOffset + _importDataDirRVAOffset, 4); + var newImportSize = BitConverter.ToUInt32(_content, _PEHeaderOffset + _importDataDirSizeOffset); + newImportSize += 0x14; // FIXME + Array.Copy(BitConverter.GetBytes(newImportSize), 0, _content, + _PEHeaderOffset + _importDataDirSizeOffset, 4); + return; + } + } + throw new Exception($"Section '{sectionName}' not found (RedirectImportDataDir)"); + } + } +} \ No newline at end of file diff --git a/SymbiontPE/PESectionParameters.cs b/SymbiontPE/PESectionParameters.cs new file mode 100644 index 0000000..0f295a9 --- /dev/null +++ b/SymbiontPE/PESectionParameters.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Text; + +namespace SymbiontPE +{ + public class PESectionParameters + { + public const int SECTION_DESCRIPTOR_SIZE = 40; + + public readonly string Name; + public UInt32 VirtualSize; + public readonly UInt32 VirtualAddress; + public UInt32 SizeOfRawData; + public readonly UInt32 PointerToRawData; + /* + public readonly UInt32 PointerToRelocations; + public readonly UInt32 PointerToLinenumbers; + public readonly UInt16 NumberOfRelocations; + public readonly UInt16 NumberOfLinenumbers; + */ + public UInt32 Characteristics; + + public PESectionParameters(byte[] sectionBytes) + { + if (sectionBytes.Length != SECTION_DESCRIPTOR_SIZE) + throw new Exception("Bad section"); + using (var mem = new MemoryStream(sectionBytes)) + using (var br = new BinaryReader(mem)) + { + var nameBts = br.ReadBytes(8); + Name = Encoding.ASCII.GetString(nameBts).TrimEnd('\0'); + VirtualSize = br.ReadUInt32(); + VirtualAddress = br.ReadUInt32(); + SizeOfRawData = br.ReadUInt32(); + PointerToRawData = br.ReadUInt32(); + br.ReadUInt32(); // PointerToRelocations + br.ReadUInt32(); // PointerToLinenumbers + br.ReadUInt16(); // NumberOfRelocations + br.ReadUInt16(); // NumberOfLinenumbers + Characteristics = br.ReadUInt32(); + } + } + + public byte[] ToBytes() + { + var rv = new byte[SECTION_DESCRIPTOR_SIZE]; + using (var mem = new MemoryStream(rv)) + using (var bw = new BinaryWriter(mem)) + { + var nameBts = new byte[8]; + Array.Copy(Encoding.ASCII.GetBytes(Name), nameBts, Name.Length); + bw.Write(nameBts); + bw.Write(VirtualSize); + bw.Write(VirtualAddress); + bw.Write(SizeOfRawData); + bw.Write(PointerToRawData); + bw.WriteZero(4); // PointerToRelocations + bw.WriteZero(4); // PointerToLinenumbers + bw.WriteZero(2); // NumberOfRelocations + bw.WriteZero(2); // NumberOfLinenumbers + bw.Write(Characteristics); + } + return rv; + } + } +} \ No newline at end of file diff --git a/SymbiontPE/Program.cs b/SymbiontPE/Program.cs new file mode 100644 index 0000000..532e448 --- /dev/null +++ b/SymbiontPE/Program.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace SymbiontPE +{ + class Program + { + static void Banner() + { + Console.WriteLine(" ____ _ _ _ ____ _____\n / ___| _ _ _ __ ___ | |__ (_) ___ _ __ | |_|####\\|#####|\n \\___ \\| | | | '_ ` _ \\| '_ \\| |/ _ \\| '_ \\| __|#|_)#|###|\n ___) | |_| | | | | | | |_) | | (_) | | | | |_|####/|#|___\n |____/ \\__, |_| |_| |_|_.__/|_|\\___/|_| |_|\\__|#| |#####|\n |___/\n\n (c) 2019, Xi-Tauw, https://amonitoring.ru\n"); + } + + static void Usage() + { + Console.WriteLine(" Usage:\n SymbiontPE pathToInputDll importDllName importFunction pathToOutputDll\n Parameters:\n - path to existing dll (target of proxy)\n - name of dll to be added to import\n - name of function to be added to import\n - path to save output proxy dll\n Notes:\n Any overlay (e.g. embedded signature) from input dll will be removed.\n importFunction must be present in importDllName for correct loading of library, but it will not be invoked. Payload must be executed from DllEntry.\n Example:\n SymbiontPE C:\\windows\\system32\\version.dll my.dll func C:\\data\\version.dll\n The command create proxy library, that acts like version.dll, but load my.dll at start."); + } + + static void Main(string[] args) + { + Banner(); + if (args.Length != 4) + { + Usage(); + return; + } + AddImportTableFunction(args[0], args[1], args[2], args[3]); + } + + private static void AddImportTableFunction(string path, string dllName, string dllFunc, string outputPath) + { + try + { + Console.WriteLine(" [!] Parse PE file"); + var pe = new PEFile(path); + + var newImportSize = pe.CalcNewImportSize(dllName, dllFunc); + + Console.WriteLine(" [!] Parse sections"); + var sections = pe.GetSections(); + var lastSection = sections[sections.Count - 1]; + var sectParams = pe.GetSectionParams(lastSection); + + Console.WriteLine(" [!] Change last section parameters"); + var originalSize = sectParams.SizeOfRawData; + sectParams.Characteristics |= 0xC0000000; + var addSize = pe.Align(originalSize, 0x1000) + pe.Align(newImportSize, 0x200) - originalSize; + sectParams.VirtualSize += addSize; + sectParams.SizeOfRawData += addSize; + pe.SetSectionParams(lastSection, sectParams); + + Console.WriteLine(" [!] Create new import"); + var newImport = pe.CreateNewImport(lastSection, pe.Align(originalSize, 0x1000), dllName, dllFunc); + + Console.WriteLine(" [!] Write new import"); + pe.WriteDataAndTrim(lastSection, originalSize, newImport); + pe.RedirectImportDataDir(lastSection, pe.Align(originalSize, 0x1000)); + pe.FixSizeOfImage(); + + Console.WriteLine(" [!] Save result"); + pe.Save(outputPath); + Console.WriteLine(" [!] Done"); + } + catch (Exception e) + { + Console.WriteLine($"[-] Crash. Message: {e.Message}"); + } + } + } +} diff --git a/SymbiontPE/Properties/AssemblyInfo.cs b/SymbiontPE/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7111fa0 --- /dev/null +++ b/SymbiontPE/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SymbiontPE")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SymbiontPE")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("335c42e0-ab15-4eb4-b450-977d94b0741c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SymbiontPE/SymbiontPE.csproj b/SymbiontPE/SymbiontPE.csproj new file mode 100644 index 0000000..adf3f12 --- /dev/null +++ b/SymbiontPE/SymbiontPE.csproj @@ -0,0 +1,50 @@ + + + + + Debug + AnyCPU + {335C42E0-AB15-4EB4-B450-977D94B0741C} + Exe + SymbiontPE + SymbiontPE + v4.0 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + \ No newline at end of file