Skip to content

Commit

Permalink
Improve compatibility with MSVC applications that use (dynamic) TLS
Browse files Browse the repository at this point in the history
The TLS pointer must be in a read-only section, which is not the case for
Themida dumps because they merge .text/.rdata/.data into a RWX section.
  • Loading branch information
Hendi48 committed May 3, 2024
1 parent a5011af commit 56ea451
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 35 deletions.
17 changes: 13 additions & 4 deletions Debugger.pas
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ TEFLRecord = record
TDebugger = class(TThread)
private
FExecutable, FParameters: string;
FCreateDataSections: Boolean;
FProcess: TProcessInformation;
FImageBase, FBaseOfData: NativeUInt;
FPESections: array of TImageSectionHeader;
Expand Down Expand Up @@ -114,7 +115,7 @@ TDebugger = class(TThread)
protected
procedure Execute; override;
public
constructor Create(const AExecutable, AParameters: string; ALog: TLogProc);
constructor Create(const AExecutable, AParameters: string; ACreateData: Boolean; ALog: TLogProc);
destructor Destroy; override;
end;

Expand All @@ -124,10 +125,11 @@ implementation

{ TDebugger }

constructor TDebugger.Create(const AExecutable, AParameters: string; ALog: TLogProc);
constructor TDebugger.Create(const AExecutable, AParameters: string; ACreateData: Boolean; ALog: TLogProc);
begin
FExecutable := AExecutable;
FParameters := AParameters;
FCreateDataSections := ACreateData;
Log := ALog;

FThreads := TDictionary<Cardinal, THandle>.Create(32);
Expand Down Expand Up @@ -1543,9 +1545,16 @@ procedure TDebugger.FinishUnpacking(OEP: NativeUInt);

FHideThreadEnd := True;
TerminateProcess(FProcess.hProcess, 0);

if FCreateDataSections then
with TPatcher.Create(FN) do
try
ProcessMkData;
finally
Free;
end;

Log(ltGood, 'Operation completed successfully.');
if Pos('\maplesto', LowerCase(FExecutable)) <> 0 then
Log(ltGood, 'Don''t forget to MakeDataSect.');
end;

function TDebugger.DetermineIATAddress(OEP: NativeUInt; Dumper: TDumper): NativeUInt;
Expand Down
1 change: 1 addition & 0 deletions Magicmida.dproj
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_RemoteDebug>false</DCC_RemoteDebug>
<AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>
<BT_BuildType>Debug</BT_BuildType>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
Expand Down
252 changes: 245 additions & 7 deletions Patcher.pas
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,27 @@ TPatcher = class
procedure ShrinkPE;
procedure ShrinkExportSect;

procedure MSCreateDataSections;
// Proper section characteristics are important for MSVC applications because they
// check them during C runtime initialization (especially .rdata).
procedure MapleCreateDataSections;
procedure MSVCCreateDataSections;

function FindDataStartMSVC6: NativeUInt;
function FindDynTLSMSVC14(out DynTLSInit: NativeUInt): Boolean;
//function FindDataStartByDisasm: NativeUInt;
public
constructor Create(const AFileName: string);
destructor Destroy; override;

procedure Process;
procedure ProcessShrink;
procedure ProcessMkData;

procedure DumpProcessCode(hProcess: THandle);
end;

implementation

uses Unit2, Debugger;
uses Unit2, Debugger, StrUtils;

{ TPatcher }

Expand All @@ -51,7 +56,7 @@ destructor TPatcher.Destroy;
inherited;
end;

procedure TPatcher.Process;
procedure TPatcher.ProcessShrink;
begin
ShrinkPE;
ShrinkExportSect;
Expand All @@ -61,8 +66,21 @@ procedure TPatcher.Process;
end;

procedure TPatcher.ProcessMkData;
var
Lower: string;
PosMS: Integer;
begin
MSCreateDataSections;
Lower := LowerCase(FFileName);
PosMS := Lower.LastIndexOf('maplestory');
if (PosMS > 0) and (Pos('.exe', Lower, PosMS) < PosMS + 10) then
MapleCreateDataSections
else if PE.NTHeaders.OptionalHeader.MajorLinkerVersion = 14 then // MSVC 2015+
MSVCCreateDataSections
else
begin
Log(ltInfo, 'Data section creation not available for this compiler.');
Exit;
end;

PE.SaveToStream(FStream);
FStream.SaveToFile(FFileName);
Expand Down Expand Up @@ -172,7 +190,7 @@ procedure TPatcher.ShrinkExportSect;
FStream := NS;
end;

procedure TPatcher.MSCreateDataSections;
procedure TPatcher.MapleCreateDataSections;
var
Mem: PByte;
DataStart, DataSize, A, ZEnd, ZStart, GfidsSize: NativeUInt;
Expand Down Expand Up @@ -246,7 +264,7 @@ procedure TPatcher.MSCreateDataSections;
Inc(A);

if ZSize = 0 then
raise Exception.Create('Data section doesn''t contain null bytes');
raise Exception.Create('Data section doesn''t contain zeroes');

// Sometimes first byte of following section is 0
if ZEnd and $FFF = 1 then
Expand Down Expand Up @@ -349,6 +367,226 @@ function TPatcher.FindDataStartMSVC6: NativeUInt;
Dec(Result, PE.NTHeaders.OptionalHeader.ImageBase);
end;

function TPatcher.FindDynTLSMSVC14(out DynTLSInit: NativeUInt): Boolean;
var
DynTLSCode: NativeUInt;
CodePtr, GetPtrFunc, DynTLSInitPtr: PByte;
begin
{
call __scrt_get_dyn_tls_init_callback
mov esi, eax
xor edi, edi
cmp [esi], edi
jz short ??
push esi
call __scrt_is_nonwritable_in_current_image
}
DynTLSCode := FindDynamic('8BF033FF393E74??56E8', PByte(FStream.Memory) + $1000, FStream.Size - $1000);
if DynTLSCode = 0 then
begin
Log(ltInfo, 'DynTLS code sequence not found.');
Exit(False);
end;

CodePtr := PByte(FStream.Memory) + $1000 + DynTLSCode;
if (CodePtr - 5)^ <> $E8 then
begin
Log(ltInfo, 'DynTLS code sequence mismatch.');
Exit(False);
end;

GetPtrFunc := CodePtr + PInteger(CodePtr - 4)^;
if GetPtrFunc^ = $E9 then // another indirection via jmp
GetPtrFunc := GetPtrFunc + PInteger(GetPtrFunc + 1)^ + 5;
if GetPtrFunc^ <> $B8 then
begin
Log(ltInfo, 'DynTLS call analysis failed.');
Exit(False);
end;

DynTLSInit := PCardinal(GetPtrFunc + 1)^ - PE.NTHeaders.OptionalHeader.ImageBase;
DynTLSInitPtr := PByte(FStream.Memory) + DynTLSInit;

Log(ltInfo, Format('[MSVC] dyn_tls_init at %.8X', [PCardinal(DynTLSInitPtr)^]));

// If the function pointer points to 0, the compiler places this var in a writable section
// and we can't use it as a separator.
if PCardinal(DynTLSInitPtr)^ = 0 then
DynTLSInit := 0;

Result := True;
end;

{
This was a fun idea but it fails when executables have a construct like this as their first written access:
mov ecx, offset FOO
call DereferenceAndWriteEcx
function TPatcher.FindDataStartByDisasm: NativeUInt;
var
Dis: _Disasm;
Res: Integer;
Addresses: TDictionary<NativeUInt, Boolean>;
AddressesList: TList<NativeUInt>;
CandidateStart, CandidateEnd, Base: NativeUInt;
i, j: Integer;
OK: Boolean;
begin
FillChar(Dis, SizeOf(Dis), 0);
Dis.Archi := 32;
Dis.EIP := NativeUInt(FStream.Memory) + PE.Sections[0].Header.VirtualAddress;
Dis.VirtualAddr := PE.Sections[0].Header.VirtualAddress;
CandidateStart := PE.NTHeaders.OptionalHeader.BaseOfData;
CandidateEnd := PE.Sections[0].Header.VirtualAddress + PE.Sections[0].Header.Misc.VirtualSize;
Base := PE.NTHeaders.OptionalHeader.ImageBase;
Addresses := TDictionary<NativeUInt, Boolean>.Create;
// Disassemble the entire text section and collect written addresses.
while Dis.VirtualAddr < CandidateStart do
begin
Res := Disasm(Dis);
if Res <= 0 then
begin
Inc(Dis.EIP);
Inc(Dis.VirtualAddr);
Continue;
end;
if ((Dis.Argument1.ArgType and $F0000000) = MEMORY_TYPE) and
(Dis.Argument1.Memory.BaseRegister = 0) and
(Dis.Argument1.Memory.Displacement <> 0) and
(Dis.Argument1.AccessMode <> READ) and
(NativeUInt(Dis.Argument1.Memory.Displacement) >= CandidateStart + Base) and
(NativeUInt(Dis.Argument1.Memory.Displacement) < CandidateEnd + Base) then
begin
Addresses.AddOrSetValue(Dis.Argument1.Memory.Displacement, True);
end;
Inc(Dis.EIP, Res);
Inc(Dis.VirtualAddr, Res);
end;
AddressesList := nil;
try
if Addresses.Count = 0 then
Exit(0);
AddressesList := TList<NativeUInt>.Create(Addresses.Keys);
AddressesList.Sort;
if AddressesList.Count < 3 then
begin
Log(ltInfo, Format('Only few mem writes, picking first reference for .data: %.8x.', [AddressesList[0] - Base]));
Exit((AddressesList[0] - Base) and not $FFF);
end;
for i := 0 to AddressesList.Count - 1 do
begin
// Check if we have a set of 3 addresses that are all within $40 of each other.
// This is a pretty dumb heuristic, 3 and $40 are randomly chosen.
OK := True;
for j := i + 1 to i + 2 do
if AddressesList[j] > AddressesList[j - 1] + $40 then
OK := False;
if OK then
begin
Log(ltInfo, Format('Heuristic picked %.8x as first actual .data write.', [AddressesList[i] - Base]));
Exit((AddressesList[i] - Base) and not $FFF);
end;
end;
Result := 0;
finally
AddressesList.Free;
Addresses.Free;
end;
end;
}

procedure TPatcher.MSVCCreateDataSections;
var
i: Integer;
DynTLS: NativeUInt;
BaseOfData, DataStart, DataSize, RDataStart, RDataSize: Cardinal;
Name: AnsiString;
begin
BaseOfData := PE.NTHeaders.OptionalHeader.BaseOfData;
if (BaseOfData > PE.Sections[0].Header.VirtualAddress) and
(BaseOfData < PE.Sections[0].Header.VirtualAddress + PE.Sections[0].Header.Misc.VirtualSize) and
((BaseOfData and $FFF) = 0) then
begin
if not FindDynTLSMSVC14(DynTLS) then
Exit;

// This is by no means exact. We keep the writable data section as large as possible because
// we can't determine its real size in a generic way and we don't want to risk access violations.
if DynTLS <> 0 then
begin
DataStart := (DynTLS + $1000) and not $FFF;
end
else
begin
DataStart := BaseOfData + $1000;
Log(ltInfo, 'Setting .rdata size to just 1000 (no reference point for actual size)');
end;

PE.AddSectionToArray;
PE.AddSectionToArray;
for i := High(PE.Sections) downto 3 do
PE.Sections[i] := PE.Sections[i - 2];

Inc(PE.NTHeaders.FileHeader.NumberOfSections, 2);

// .data at [2]
Name := '.data';
DataSize := PE.Sections[3].Header.PointerToRawData - DataStart;
FillChar(PE.Sections[2], SizeOf(TPESection), 0);
Move(Name[1], PE.Sections[2].Header.Name[0], Length(Name));
with PE.Sections[2].Header do
begin
Misc.VirtualSize := DataSize;
VirtualAddress := DataStart;
PointerToRawData := DataStart;
SizeOfRawData := DataSize;
Characteristics := IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE or IMAGE_SCN_CNT_INITIALIZED_DATA;
end;

// .rdata at [1]
Name := '.rdata';
RDataStart := BaseOfData;
RDataSize := DataStart - RDataStart;
FillChar(PE.Sections[1], SizeOf(TPESection), 0);
Move(Name[1], PE.Sections[1].Header.Name[0], Length(Name));
with PE.Sections[1].Header do
begin
Misc.VirtualSize := RDataSize;
VirtualAddress := RDataStart;
PointerToRawData := RDataStart;
SizeOfRawData := RDataSize;
Characteristics := IMAGE_SCN_MEM_READ or IMAGE_SCN_CNT_INITIALIZED_DATA;
end;

Dec(PE.Sections[0].Header.Misc.VirtualSize, RDataSize + DataSize);
Dec(PE.Sections[0].Header.SizeOfRawData, RDataSize + DataSize);

with PE.Sections[0].Header do
Log(ltInfo, Format('.text : %.8x ~ %.8x', [VirtualAddress, VirtualAddress + Misc.VirtualSize]));
with PE.Sections[1].Header do
Log(ltInfo, Format('.rdata: %.8x ~ %.8x', [VirtualAddress, VirtualAddress + Misc.VirtualSize]));
with PE.Sections[2].Header do
Log(ltInfo, Format('.data : %.8x ~ %.8x', [VirtualAddress, VirtualAddress + Misc.VirtualSize]));
end
else
Log(ltInfo, 'Assuming sections are not merged.');

// Rename first section and remove WRITE characteristic.
Name := '.text'#0#0#0;
Move(Name[1], PE.Sections[0].Header.Name[0], Length(Name));
PE.Sections[0].Header.Characteristics := PE.Sections[0].Header.Characteristics and not IMAGE_SCN_MEM_WRITE;
end;

procedure TPatcher.DumpProcessCode(hProcess: THandle);
var
StartAddr, EndAddr, NumRead: NativeUInt;
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ Magicmida is a Themida auto-unpacker that works on some 32-bit applications. It

Functions:
* Unpack: Unpacks the binary you select. The unpacked binary will be saved with an `U` suffix.
* MakeDataSects: Restores .rdata/.data sections. Only works on very specific targets.
* Dump process: Allows you to enter the PID of a running process whose .text section will be dumped (overwritten) into an already unpacked file. This is useful after using Oreans Unvirtualizer in OllyDbg. Only works properly if MakeDataSects was done before.
* Auto create data sections: Restores .rdata/.data sections. Only works on specific targets. This is a must for MSVC applications using Thread Local Storage because they don't work properly otherwise.
* Dump process: Allows you to enter the PID of a running process whose .text section will be dumped (overwritten) into an already unpacked file. This is useful after using Oreans Unvirtualizer in OllyDbg. Only works properly if data sections were created.
* Shrink: Deletes all sections that are no longer needed (if you unvirtualized or if your binary does not use virtualization). Warning: This will break your binary for non-MSVC compilers.

Note: The tool focuses on cleanness of the resulting binaries. Things such as VM anti-dump are explicitly *not* fixed. If your target has a virtualized entrypoint, the resulting dump will be broken and won't run (except for MSVC6, which has special fixup code to restore the OEP).

Important: Never activate any compatibility mode options for Magicmida or for the target you're unpacking. It would very likely screw up the unpacking process due to shimming.

Windows sometimes decides to auto-apply compatibility patches to an executable if it crashed before. This AppCompat information is stored in the registry and is linked to the exact path of your executable. This can be a problem if you're upgrading to a newer Magicmida version that has fixes for your target. You can try moving your target to a different path or look around in the subkeys of `HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags`.

## Anti-anti-debugging

Newer versions of Themida detect hardware breakpoints. In order to deal with this, injecting ScyllaHide is supported. A suitable profile is shipped with Magicmida. You just need to download SycllaHide and put `HookLibraryx86.dll` and `InjectorCLIx86.exe` next to `Magicmida.exe`. Do not overwrite scylla_hide.ini unless you know what you're doing.
Loading

0 comments on commit 56ea451

Please sign in to comment.