Manțocării cu Mono Cecil

Să presupunem că, total benign și mânat de duhul blândeții, ai nevoie să ajustezi capabilitățile oferite de o aplicație la al cărei cod sursă nu ai acces neam. Să mai presupunem că aplicația este scrisă-n .NET (ce convenabil!) și că ajustarea capabilităților e controlată direct dintr-o clasă anume dintr-un assembly oarecare, prin niște proprietăți ce returnează un simplu true/false (cel puțin legat de aspectele care importă).

Motanul Patraulea, făcându-mi code review

Motanul Patraulea, făcându-mi code review

Poți să livrezi la rândul tău scuze sau poți… să-ți creezi o variantă modificată a assembly-urilor în cauză astfel încât să-ți poți vedea mai departe de lucru, de viață și de facturile care te vor îmbogăți dincolo de cele mai umede visuri ale avarului.

Și, de 10 Euro, poți să iei un ecler și un cico nevesti-tii că te suportă așa programator cum ești (ah, drama fiecărui om ce face umbră pământului, unii cu folos, alții fără: vrea să fie primit de semenii lui așa cum e, dar nimeni nu-i obligat să o facă!). Iar dacă nu ai sau e prea cățea independentă ca să merite, poți lua de 10 Euro bobițe pentru motanul Patraulea.

Sunt programator, nu filozof, vreau să văd cod

Ca să prinzi cod ai nevoie de undiță și nadă. În cazul de față nada este IL Spy iar undița-i Mono Cecil (nu, nu e Cecilia de la 5). IL Spy pentru cercetarea terenului și Mono Cecil pentru executarea operațiunii (care seamănă întrucâtva cu lobotomia woke).

Cercetarea terenului trebuie să ne spună ce (ce proprietăți sau metode), unde (ce assembly) și sub ce formă (în ce direcție să le alterăm) trebuie modificat, iar Mono Cecil ajută la injectarea codului IL corespunzător în punctele respective.

Este, evident, nevoie de niște minime cunoștințe de IL, adică, în ceea ce ne privește, de cum să returnezi true sau cum să returnezi false. Din fericire, asta nu-i mare dificultate și nici nu cere o experiență prealabilă, afară de-a ști ce-i cu IL-ul în general și cu faptele lui.

Mai întâi, deci, namespace-urile necesare (la drept vorbind, VS2022, pașă darnic fiind, le adaugă din oficiu, iar cele 2-3 versiuni de dinainte aveau ceva similar, dar manual; însă în vremurile-ntunecate trebuia să te ducă bila și să fii suficient de harnic sa le cauți singur, ceea ce nu-s și nici nu mă duce așa mult, pentru că altfel aș fi folosit System.Reflection.Emit și nu Mono Cecil):

using Mono.Cecil;
using Mono.Cecil.Cil;

Apoi, trebuie încărcat assembly-ul și localizată clasa care trebuie modificată (folosind API-ul din Mono Cecil):

AssemblyDefinition targetAssembly = AssemblyDefinition
	.ReadAssembly( "TargetAssembly.dll" );

TypeDefinition targetType = targetAssembly.MainModule.Types
	.FirstOrDefault( t => t.Name == "TargetClass" );

Dacă există clasa căutată, se caută proprietățile dorite și se modifică. Apoi se adaugă sare după gust și se mestecă la foc mic până pare potrivit:

if ( targetType != null )
{
	PropertyDefinition pSmuff = targetType.Properties
		.FirstOrDefault( p => p.Name
			== "Smuff" );

	PropertyDefinition pStuff = targetType.Properties
		.FirstOrDefault( p => p.Name
			== "Stuff" );

	PropertyDefinition pZbuff = targetType.Properties
		.FirstOrDefault( p => p.Name
			== "Zbuff" );

	if ( pSmuff != null
		&& pStuff != null
		&& pZbuff != null )
	{
		RewriteToReturnTrue( pSmuff );
		RewriteToReturnFalse( pStuff );
		RewriteToReturnTrue( pZbuff );

		targetAssembly.Write( "TargetAssembly.Patched.dll" );
	}
}

Înainte de a vedea cum arată RewriteReturnToTrue și RewriteReturnToFalse, trebuie să descriu și ce trebuie obținut. Proprietățile în cauză sunt read-only, adică doar furnizează valori, nu pot fi scrise de nicio culoare. Deci nu putem să modificăm codul cele folosește, ci trebuie să le modificăm valorile returnate:

Smuff returnează false, dar este nevoie să returneze true;
Stuff returnează true, dar este nevoie să returneze false;
Zbuff returnează false, dar trebuie să returneze true.

Acum trebuie aflat cum ar arăta return true, respectiv return false scrise în cod IL:

//return true
IL_0000: ldc.i4.1 
IL_0001: ret

//return false
IL_0000: ldc.i4.0 
IL_0001: ret

Ce se-ntâmplă-n secvențele de mai sus este că se încarcă 1 (pentru true) sau 0 (pentru false) pe stiva de evaluare și apoi se iese din metodă și se-ntoarce valoarea de pe stiva de evaluare către apelant (ret).

Așadar, iată și cele două metode lipsă la apel:

private static void RewriteToReturnTrue(PropertyDefinition pDef)
{
	RewriteToReturnBooleanValue( pDef, true );
}

private static void RewriteToReturnFalse( PropertyDefinition pDef )
{
	RewriteToReturnBooleanValue( pDef, false );
}

După cum vedeți, v-am păcălit. Dar nu fără rost, căci lăsăm deducțiile ca exercițiu pentru cititor.  Între timp, iată și RewriteToReturnBooleanValue:

private static void RewriteToReturnBooleanValue( PropertyDefinition pDef, 
	bool retValue )
{
	pDef.GetMethod
		.Body
		.Instructions
		.Clear();

	ILProcessor ilProp = pDef.GetMethod
		.Body
		.GetILProcessor();

	Instruction inst1 = ilProp.Create( retValue 
		? OpCodes.Ldc_I4_1 
		: OpCodes.Ldc_I4_0 );
	Instruction inst2 = ilProp.Create( OpCodes.Ret );

	ilProp.Append( inst1 );
	ilProp.Append( inst2 );
}

Aici se întâmplă trei lucruri (evident, premisa de la care se pleacă este că proprietatea este read-only):

– se elimină orice instrucțiuni IL ar avea corpul proprietății;
– se creează setul de instrucțiunii pentru return true sau return false, după caz;
– se se atașează corpului proprietății.

În final se poate verifica din nou cu IL Spy că ceea ce s-a generat este rezultatul așteptat.

Referințe

Understanding Common Intermediate Language (CIL)
OpCode Ret
OpCode Ldc_I4_0
OpCode Ldc_I4_1