.NET schutz

Hallo werte Hackerboard-User


Vor geraumer Zeit habe ich hier mal ein Thema eröffnet (Reverse Engineering) und bin eigentlich positiv gestimmt,
was die Qualität der Antworten und allg. hier an Beiträge angeht.

Die Beiden folgenden Beiträge:
Schutz vor dekompilieren von C#-Anwendungen bzw. von .NET
Sicheres programmieren von Anwendungen
... sind schon gut ein Jahr vergangen und klären eingetlich vieles an meinen Fragen und fördern meine Ideen.

Nun möchte ich es doch noch genau wissen, bzw. habe Fragen zu gewissen Techniken.

Schon mal vorweg: Es geht nicht um eine Trialversion, sondern nur um Schutz gegen 'einfach weiter geben' und Einsicht in den Source.
Folgendes habe ich bereits implementiert:

1) Einige Anti-Debugger tricks
2) Anti VMware
3) Obfuscation (eigenbau)
4) PE-Checks
5) Keyfile / Hardware Abhängig
6) Nativwall (eigenabu)

a) Stimmt es, dass Olly, Windasm32 & Co. für .NET (fast) unbrauchbar sind?
(da ja ausschliesslich mit IL gearbeitet wird?)

b) Ich verschlüssle sämtlich Strings. Wieviel mehr Zeitaufwand hat da ein Cracker?
wir nehmen an, es sind ca. 20 verscheidene verschlüsslungsroutinen

c) Ich prüfe die Processliste/Registry nach bekannten Debugger & tools. Lohnt sich das überhaupt?

d) Gibt es Möglichkeiten zu prüfen, ob etwas gepatch wurde? also nicht mittels MD5-Hash, sondern im Programmfluss selbst?

e) Wenn ich einen Nativen Packer verwende, und verhindere das an die eigentliche .NET Antwendung ein Debugger sich anhängen kann, wie sicher ist sowas?

f) Gibt es, abgesehen von der Hardwarebindung, eine andere Art die einfache Weitergabe zu erschweren?

Habt ihr noch weitere Dinge die ich implementieren könnte? Anregungen und tipps allg. zu .NET?

Freundlich grüsst
Netonator
 
Hallo Netonator,

genau mit dieser Problematik habe ich mich auch schon beschäftigt.

Original von Netonator
c) Ich prüfe die Processliste/Registry nach bekannten Debugger & tools. Lohnt sich das überhaupt?
Das kann man mit der IsDebuggerPresent-Funktion prüfen.

Aber den Großteil deiner Fragen wurde schon in dem von mir gestellt Thread(s. dein Beitrag oben) von beantwortet.

the_uxreal
 
Ja. IsDebuggerPresent kann pratkisch von jedem Debugger entschärft werden.
Bei der Registry checks geht es eigentlich nur darum die Andwendung zu 'warnen'. Schliesslich haben normale User keine Debugger Installiert.

Aja, und wenn du Managed-Debugger detecten willst:

Code:
Debugger.IsAttached

Gibt dir zurück ob ein Debugger angehängt ist.

Freundlichst grüsst
Net.
 
Original von Netonator
a) Stimmt es, dass Olly, Windasm32 & Co. für .NET (fast) unbrauchbar sind?
(da ja ausschliesslich mit IL gearbeitet wird?)

Ja. Praktisch "ungenießbar". Was man da einfach mitverfolgen kann ist die Intepretation der Zwischensprache.

Original von Netonator
b) Ich verschlüssle sämtlich Strings. Wieviel mehr Zeitaufwand hat da ein Cracker?
wir nehmen an, es sind ca. 20 verscheidene verschlüsslungsroutinen

Nicht mehr als nur bei einer Verschlüsselungsroutine.
Jeder halbwegs gewiefte Cracker wartet einfach bis das Programm die Strings selbst entschlüsselt. ;)
Anders sieht es da natürlich bei Decrypt-On-Demand aus. Aber auch dort kann man die Entschlüsselungsroutine, die in das Programm natürlich eingebaut werden muss, für seine Zwecke missbrauchen und selbst für jeden String aufrufen, ohne je die Verschlüsselungsart verstehen zu müssen.

Original von Netonator
c) Ich prüfe die Processliste/Registry nach bekannten Debugger & tools. Lohnt sich das überhaupt?

Sozusagen eine Blacklist?
Nachteile: Bringt nur gegen Anfänger etwas und gibt auch False Positives, also Ärger bei "ehrlichen" Usern, nur weil irgendein Prozessname identisch ist.
Was schaust du in der Registry nach?
Verbietest du jemandem auch solche Tools nur installiert zu haben?

Original von Netonator
d) Gibt es Möglichkeiten zu prüfen, ob etwas gepatch wurde? also nicht mittels MD5-Hash, sondern im Programmfluss selbst?

Du könntest bei wichtigen Stellen versteckte Sanity-Checks durchführen. CheckPoints müssen durchlaufen werden, Prüfsummen müssen am Ende stimmen und solche Scherze. Allerdings ist das schon etwas Aufwand und man kann damit nur sehr bestimmte Stellen und nur grobe Integrität gewährleisten.

Original von Netonator
e) Wenn ich einen Nativen Packer verwende, und verhindere das an die eigentliche .NET Antwendung ein Debugger sich anhängen kann, wie sicher ist sowas?

Naja, in etwa so sicher wie der Packer selbst. Allerdings schon noch etwas leichter, weil man weiss wonach man "suchen" muss, bzw. wie jede .NET Executable sich initialisieren muss. (_CorExeMain..)
Es dürfte also leichter sein eine silbern glänzende Kugel als eine Heunadel im Heuhaufen zu finden. ;)

Original von Netonator
f) Gibt es, abgesehen von der Hardwarebindung, eine andere Art die einfache Weitergabe zu erschweren?

Man muss sich an etwas einzigartigem binden. Wenn du also einer Person dein Programm gibst, dann muss derjenige etwas haben, was man datentechnisch verarbeiten kann, aber nicht einfach mal so digital kopierbar ist.
Eine schwierige Sache. Andere Ideen wären vlt. eine Gesichtserkennung per Webcam oder bei Notebooks mit Fingerabdruckscannern.
Etwas weniger handzuhaben wäre zum Beispiel das Prüfen der IP-Adresse des Kunden übers Internet. (sei sie statisch).
Alle Methoden lassen sich natürlich auch aushebeln.
Wenn man es geschickt angeht, benutzt man die Daten, die binden sollen, als Key zur Entschlüsselung des Programms im Speicher. (wenn diese genügend abstrakt sind und nicht einfach zu verraten sind, IP: Nein. HardwareID: Ja)
In solchen Fällen muss zumindest der Cracker eine gültige Lizenz haben bzw. derjenige muss von einem Cracker angeleitet werden einen Memory-Dump oder ähnliches zu machen.

Original von Netonator
Habt ihr noch weitere Dinge die ich implementieren könnte? Anregungen und tipps allg. zu .NET?

Ich denke mal die üblichen Methoden um Variablennamen usw. unkenntlich zu machen, hast du schon gemacht?
Einen IL-Obfuscator. Der wirklich den Code sehr verkompliziert. Also fast wie Junk-Code, der am Ende aber noch einen kleinen Teil der Gesamtaufgabe erfüllt, so dass man diesen nicht einfach kürzen kann.
Oder eine kleine IIL (intermediate-intermedita language xD) erschaffen. Also sozusagen den ganzen IL-Code ein deine IIL übersetzen und diese dann in deiner eigenen VM laufen zu lassen, die daraus wieder IL-Code erzeugt und ausführt zur Laufzeit (interpretiert).
 
Original von +++ATH0
Original von Netonator
d) Gibt es Möglichkeiten zu prüfen, ob etwas gepatch wurde? also nicht mittels MD5-Hash, sondern im Programmfluss selbst?
Man kann mit der ReadProcessMemory-Funktion prüfen, ob etwas gepatcht worden ist. Wenn man allerdings die ReadProcessMemory-Funktion wiederum patcht, bringt es wenig. Ein "Hook" auf eine Speicherzelle könnte man auch umsetzen, so dass wenn es gepatcht wird, dass man einen anderen Code ausführt.
 
Ja. Praktisch "ungenießbar". Was man da einfach mitverfolgen kann ist die Intepretation der Zwischensprache.

Dachte ich mir.

Nicht mehr als nur bei einer Verschlüsselungsroutine.
Jeder halbwegs gewiefte Cracker wartet einfach bis das Programm die Strings selbst entschlüsselt.
Anders sieht es da natürlich bei Decrypt-On-Demand aus. Aber auch dort kann man die Entschlüsselungsroutine, die in das Programm natürlich eingebaut werden muss, für seine Zwecke missbrauchen und selbst für jeden String aufrufen, ohne je die Verschlüsselungsart verstehen zu müssen.

Es ist natürlich Decrypt-On-Demand. Wie will man da warten? Klar, das Programm entschlüsslt sie, aber such doch mal im Reflector nach verschlüsselten strings...


Sozusagen eine Blacklist?
Nachteile: Bringt nur gegen Anfänger etwas und gibt auch False Positives, also Ärger bei "ehrlichen" Usern, nur weil irgendein Prozessname identisch ist.

Ja, Blacklist passt ganz gut.
Ich neheme nicht an das ein normales Programmm ebenfalls OllyDbg.exe heisst.

Was schaust du in der Registry nach?
Verbietest du jemandem auch solche Tools nur installiert zu haben?

Selbstverständlich nicht. Wenn ich mir es überlege, bringt das mit der Reigstry nichts, höchstens gegen IDA Pro oder so.

Sanity-Checks durchführen

Was ist denn ein Sanity-Check? wie soll das aussehen?

...initialisieren muss. (_CorExeMain..) ...


Stimmt nicht ganz. Kann man auslassen, wenn man weiss wie.


Ich denke mal die üblichen Methoden um Variablennamen usw. unkenntlich zu machen, hast du schon gemacht?

Ja, wird durch mein Obfuscator bereit gestellt. Also eigentlich alle Features die ein Obfuscator bietet, Control-Flow, etc.

Einen IL-Obfuscator. Der wirklich den Code sehr verkompliziert. Also fast wie Junk-Code, der am Ende aber noch einen kleinen Teil der Gesamtaufgabe erfüllt, so dass man diesen nicht einfach kürzen kann.
Oder eine kleine IIL (intermediate-intermedita language xD) erschaffen. Also sozusagen den ganzen IL-Code ein deine IIL übersetzen und diese dann in deiner eigenen VM laufen zu lassen, die daraus wieder IL-Code erzeugt und ausführt zur Laufzeit (interpretiert).

Genau das interessiert mich.
Wie muss das genau aussehen, Beispiel-pseudocode? Muss ich mir da ne riesige Select case Anweisung vorstellen die von einer Binardatei die eigenen Opcdoes liest und diese Auswertet?

Freundlich grüsst
Net.
 
Original von Netonator
Es ist natürlich Decrypt-On-Demand. Wie will man da warten? Klar, das Programm entschlüsslt sie, aber such doch mal im Reflector nach verschlüsselten strings...

Irgendwann müsste mindestens ein String angezeigt werden. Bedeutet ergo: Kommunikation mit einer Schicht des Betriebsystems.
Von da aus hangele ich mich bis zur Entschlüsselungsroutine. Da dürften einige interessante Strings vorbeifliegen.

Original von Netonator
Was ist denn ein Sanity-Check? wie soll das aussehen?

Naja. Jedes Programm führt irgendwo Berechnungen durch und das passiert nach bestimmten Regeln, nach Häufigkeit, Ausführungsfluss usw.
Als Beispiel: Als sehr billige Methode kann man nun nach diese Regeln immer einen Wert irgendwo absetzen, der immer eins höher sein muss als der zuletzt abgegebene Wert.
Eine Routine kann dann einen Regelbruch bemerken (Anzeichen für Manipulation): "Ich muss die 8 absetzen, dort steht aber nur eine 5. Da ist was faul."

Original von Netonator
Stimmt nicht ganz. Kann man auslassen, wenn man weiss wie.

War eh nurn Beispiel. Ich glaub du wirst mir zustimmen, dass eine .NET Executable auffällt wie ein bunter Hund. ;)

Original von Netonator
Genau das interessiert mich.
Wie muss das genau aussehen, Beispiel-pseudocode? Muss ich mir da ne riesige Select case Anweisung vorstellen die von einer Binardatei die eigenen Opcdoes liest und diese Auswertet?

Als ersten Schritt müsstest du natürlich einen IL-Disassembler schreiben.
Dann überlegt man sich ein Instruction Set mit einem Turing-vollständigen Repertoire an Anweisungen und einen Register-Satz und übersetzt Schritt für Schritt jede IL-Instruktion in möglicherweise auch mehrere der eigenen Instruktionen. Aber das wird das kein Zuckerschlecken.
 
War eh nurn Beispiel. Ich glaub du wirst mir zustimmen, dass eine .NET Executable auffällt wie ein bunter Hund.

Ja du hast nicht unrecht. .NET Executable sind seehr einfach zu erkennen.

Als ersten Schritt müsstest du natürlich einen IL-Disassembler schreiben.

Reflector ;)

Dann überlegt man sich ein Instruction Set mit einem Turing-vollständigen Repertoire an Anweisungen und einen Register-Satz und übersetzt Schritt für Schritt jede IL-Instruktion in möglicherweise auch mehrere der eigenen Instruktionen. Aber das wird das kein Zuckerschlecken.

Hmm klingt interessant. Aber dann wird eigentlich 2 mal interpretiert, was die App langsam macht.
ich werde mich vermutlich an eine dynamische IL generation halten, somit ist es mit Reflector nicht möglich direkt VB/C# Code auszulesen, wobei man auch das umgehen kann, wenn man weiss wie!

Hat noch jemand andere ideen.

Gruss
.NET
 
Für bestimmte Zwischenberechnungen könntest Du auch eine eigene VM erstellen. Hört sich komplizierter an, als es ist:
Ich würde da einfach eine Stackmachine nehmen:
http://www.ece.cmu.edu/~koopman/stack_computers/sec3_2.html
d.h der arbeitet nicht mit Registern, sondern holt sich alle Operanden vom Stack und schreibt da auch das Ergebnis hin. Da braucht man also nur IP=InstructionPointer und Equal/Signed Flags sowie ein Code-Array, Stack-Array/Stackimplementierung und ein Datenarray.
Dann definierst Du einen grundsätzlichen Befehlssatz:
Code:
Opcode Operand
0001:    ADD  (holt sich 2 Werte vom Stack und addiert diese)
0002:    Sub  (genauso, nur mit Subtraktion)
.... weitere Rechenarten
0010     0000: Load Var (als Var kann man eine Ziffer angeben und intern einfach mit dataarray[var] arbeiten)
0011     0000: store Var 
0012:    cmp (holt sich die 2 variablen vom Stack und vergleicht diese, stetzt dann bei Übereinstimmung den Equal Flag, bei Var1<Var2 den Signed Flag)
0013    0000: JE  Addr   jmp if equal zu Adresse (sprich, wenn Equal Flag gesetzt ist)
0014    0000: JS  Addr   jmp if smaller zu Adresse (wenn Signed Flag gesetzt ist)
0015    0000: jg   Addr   jmp if greater zu Adresse (wenn weder Equal noch Signed gesetzt sind)
0016    0000: call Addr
Ein Interpreter dafür ist relativ schnell geschrieben. Wenn man diesen auch noch obfuscated, wird die "Zurückentwicklung" kein Zuckerschlecken ;)
Mit Macros bzw ein bisschen mehr Aufwand kann man auch die Opcodes für jede Anwendung zufällig generieren. Und natürlich muss man sich nicht an die strenge Nummerierung oder "1 Byte für Operator, 1 Byte für Operand" halten ;)

Der Code kann z.B irgendwelche Werte mit der Serial/HardwareID berechnen, die im "richtigen" Code weiterbenutzt werden können. Bei falschen Werten klappt dann z.B das Speichern oder Laden nicht mehr.
Du kannst sogar mit etwas Überlegung Opcodes in den Key einbauen und z.B bei der Initialisierung einen "Verbund" aus Key und HardwareID mit ausführen. Denn Deine VM kann auch ruhig auf "richtige" Speicherwerte in der Anwendung zugreifen und diese verändern.
Schlägt die Initialisierung fehl, kann man mittels
try X
catch print "Registrierungscode stimmt net!"
die Fehlermeldung ausgeben.
Dadurch wird der Key wirklich gut im Programm eingebunden und man wird ohne Key nicht wirklich weiter kommen können. Aber auch mit Key und richtiger HardwareID dürfte es ein ziemlicher Aufwand sein ;)

Der Nachteil ist natürlich, dass Du den Code dafür in der definierten Assembly schreiben musst und damit nicht wirklich komplexe Berechnungen anstellen kannst.
 
Danke CWD für die Antwort.

Die Grosse Preisfrage wäre da noch: in welcher Sprache programmier ich die VM? Reicht dazu .NET? Oder muss ich da auf eine Nativ sprache zurückgreifen?

Weil in .NET gibt es eine Stack Klasse, die imprinzip die grundlegende Funktionen eines Stacks bereit stellt. Vielleicht kann man damit was anfangen.

Wie sollen die Instruktionen geladen werden? bau ich mir da eine DLL die mit Parametern gefüttert wird und die Operationen ausführt?

Oder wie muss ich mir das vorstellen?

Freunldich grüsst

Net.
 
Du kannst dafür NET nutzen ;).

Ich glaube, Du denkst zu kompliziert :)
Der Interpreter liest ja nur Daten aus einem Array und macht (im einfachsten Fall) einen Switch-Case bzw IF-Else durchlauf. Der "Code" ist nichts anderes, als die Abfolge Deiner definierten Opcodes=Zahlen. Im einfachsten Fall löst man das mit Macros/Definitionen:
CONST ADD=1
CONST SUB=2
CONST LOAD=3
usw.
Damit kann man schon "ASM" Programme schreiben:
definierst für die eigentliche Bequemlichkeit zuerst Variablen:
Code:
CONST var1=1
CONST var2=2
das ist nur dafür, damit Du "var1" schreiben kannst und nicht Zahlen verwenden musst. Die Zahl benutzt Du im Datenarray als Index.
Bsp:
X=Y*Y
Code:
LOAD Var1
LOAD Var1
MUL
STORE Var2

Beispiel für die Umsetzung des Interpreters (pseudo NET Code):
Code:
int IP=0;
int code[]="Code aus irgendeiner Quelle/Datei/sonstiger Einbindung";
int data[]="array, welches als variablenarray dient"
bool equalflag=false;
bool signedflag=false;

Stack stack=new Stack();

while(ip<code.length)
{
  opcode1=code[ip];
  opcode2=code[ip+1];
  IF (opcode1==ADD)
  {
     int var1=Stack.pop;
     int var2=Stack.pop;
    int result=var1+var2;
    Stack.push(result);   
    IP=IP+1; //da es nur eine 1-Opcode Anweisung war, InstrucitonPointer nur um 1 weiterbewegen 
  }
  ELSE IF (opcode1==....)
  ....
  ELSE IF (opcode==STORE)
  {
    int index=opcode2;
    data[index]=Stack.pop;
   IP=IP+2;  //ist eine 2-Opcodes Anweidung
  } 
  Else IF (opcode1==Load)
  {
     int index=opcode2;
     int wert=data[index];
     Stack.push(wert);
    IP=IP+2;  //ist eine 2-Opcodes Anweidung
  }
  ELSE IF (opcode1==CMP)
  {
    int var1=Stack.pop;
    int var2=Stack.pop;
    if (var1==var2) equalflag=true ELSE equalflag=false;
    if (var1>var2) signedflag=true ELSE signedflag=false;
    IP=IP+1;
  }
  ELSE IF (opcode1==JE)
  {
    if (equalflag==true) ip=opcode2;
  }
 ... usw ...
  ELSE IF (opcode1==HALT)
     Break;    //sofern man einen HALT Opcode definiert, kann man die Ausführung auch frühzeitig beenden
}

Wenn Du also in Deinem Programm oben erwähnte Konstanten definiert hast, kannst Du auch schon im Programm kleinere "Netonator-Assembly" Codes einbinden:
Bsp berechnet mit "echten" Usereingaben (einlesen im GUI z.B) in der VM die 10te Potenz und gibt es aus. Wieder Pseudo-NET.
Code:
CONST ADD=1
... wie oben im Bsp...
CONST VAR1=0
CONST VAR2=1
CONST VAR3=2
CONST VAR4=3


int mycode[] =   //hier eine Arraydefinition+initialisierungsregeln in Deiner verwendeten sprache
{   
   ILOAD, 0,  //Loadbefehl für Konstanten würde ich noch mit in die VM aufnehmen, ist nützlich
  STORE, VAR1,
  //4 ter Eintrag: also an der 4-ten Steller im mycode Array 
  LOAD, VAR2,
  LOAD, VAR3,
  MUL,
  STORE, VAR4,
  LOAD, VAR1,
  ILOAD, 1,
  ADD,
  STORE, VAR1,
  LOAD, VAR1,
  ILOAD, 10,
  CMP
  JNE,4 //setze IP auf 4 - gehe also zum 4 Eintrag im mycode Array
}
int data[]=new int[4];
data[0]=0;
data[1]=readuserinput_als_integer;   //Variablen für VM vorinitialisieren
data[2]=readuserinput_als_integer;
data[3]=0;
myVMInterpreter(mycode,data);
output(data[3]));    //VAR4, also Ergebnis, ausgeben
  }

mit mycode[]={daten,daten}
meine ich die Möglichkeite in den meisten Sprachen einen Array vorzuinitialisieren. Du schreibst also quasi nur Zahlen rein. So siehts dann intern im Code aus:
Code:
mycode={1,0,2,0,....}

D.h Du brauchst keine externen DLLs oder externe Loadmöglichkeiten. Alledings solltest Du Dir einen anständigen Befehlssatz überlegen, um Dich nicht totzuprogrammieren ;).
Außerdem kann man sich auch schnell einen "Assembler" schreiben, der die konstanten Strings durch Zahlen ersetzt.

Nach dem gleichen Muster kann man diesen "Netonator-Opcode" auch aus externen Quellen laden :)
Code:
int keyfile[]=read("\\licence.key");
... junkcode ...
int hardwareid[]=....
... junkcode ...
int verknüpft[]=verknüpfe_hardwareid&keyfile_irgendwie();
... junkcode ...
int mycode[]=copy(verknüpft,10,100);   //nutze Keystellen ab 10 bis 100 als Code
int mydata[]=hardwareid;
myVMInterpreter(mycode,mydata);
... junkcode...
interne_variablen_für_sonstwas=data[X];    //initialisiere die vom Programm benutzte Variablen mit berechnetem Code
interne_variablen_für_sonstwas=data[Y];
 
Danke nochmals für dein pseudo Code und Erklärungen.

Was mir noch nicht ganz klar ist:

1. Bei deinem Beispiel des Interprets, woran erkennt man denn 2 Opcodes-Anweisungen?

2. Wenn ich das richtig erkenne nutz du das Data-Array als Wert übergabe, also die Werte mit denen gerechnet werden soll?

oder dienen die

Code:
CONST VAR1=0
CONST VAR2=1
CONST VAR3=2
CONST VAR4=3

...als Variabeln mit dennen gerechnet wird?

Wozu soll den der "ILOAD" gut sein?
Ich werde mir jetzt mal so ein Teil bauen, das Grund gerüst steht schon.
Wie gross sollte den ein Befehlsatz ca. sein?

Gruss
 
1. Bei deinem Beispiel des Interprets, woran erkennt man denn 2 Opcodes-Anweisungen?
Naja, "hardcodierung" ;)
Man entwirft ja zuerst den Befehlsatz - und da überlegt man sich, ob die Anweidung einen oder zwei Opcodes haben soll.
Das erste ist immer eine Anweisungs(ID) und das zweite dann je nach dem - Adresse, Variable usw.
Wenn man den Interpreter umsetzt, schaut man sich den eigenen Entwurf an ;).
Code:
 IF (Opcode1=JMP)
bei JMP muss man ja logischerweise noch ein Ziel haben und sollte daher 2 Opcodes nutzen.

zu
2. Wenn ich das richtig erkenne nutz du das Data-Array als Wert übergabe, also die Werte mit denen gerechnet werden soll?
korrekt. Ist problematisch mit Beispielen in Pseudocode, da manche Sachen in einigen Sprachen etwas total anders bedeuten.
Gemeint ist hier sowas wie in C:
Code:
#define VAR1 0
#define VAR2 1
also dass der Compiler statt der Konstanten bitte selbstständig die Nummerwerte im Code nutzen soll.
Die Variablennummer wird wie erwähnt (bei der einfachsten implementierung) als Indexwert für das Array genutzt.
Du kannst natürlich auch alle Werte im Codearray haben - musst nur dann beim Schreiben des "ASMCodes" aufpassen und Platz freilassen. Oder auch direkt vom Interpreter aus in "normale" Variablen schreiben.

Wozu soll den der "ILOAD" gut sein?
Ich werde mir jetzt mal so ein Teil bauen, das Grund gerüst steht schon.
Wie gross sollte den ein Befehlsatz ca. sein?
ILOAD - Integer Load. D.h der Interpreter soll das nächste Opcode nicht als Variablenindex sondern als eigenständigen Wert betrachten. Damit kann man dann in seinem "ASM Code" auf einfache Weise Konstanten laden. Ist mir halt erst beim schreiben des letzen Bsp. eingefallen.
wäre sowas:
Code:
Else IF (opcode1==Load)
  {
     int index=opcode2;
     int wert=data[index];
     Stack.push(wert);
    IP=IP+2;  //ist eine 2-Opcodes Anweidung
  }
Else IF (opcode1==ILoad)
  {
     int wert=opcode2;
     Stack.push(wert);
    IP=IP+2;  //ist eine 2-Opcodes Anweidung
  }


Wie groß der Befehlssatz sein soll - ehrlich gesagt, keine Ahnung :)
Aber ich meine, größtenteils alle nötigen Befehle schon abgedeckt zu haben. Du kannst gerne noch nach MIC1 Befehlssatz suchen (damit habe ich Stackmachines gelernt und die Befehlsnamen stammen auch aus dem Raum).
http://www.ontko.com/mic1/ijvm.conf
damit sollte man so ziemlich alles was umsetzen können. Wobei Du eher auf die eigenen Bedürfnisse achten solltest (die VM ist ja notfalls leicht erweiterbar).
Naja, beachten sollte man dann noch, dass NET eventuell die Konstantennamen "schlauerweise" beibehält - also unbedingt mal prüfen. Und dass Du nach der Testfase den Interpreter ein bisschen "manuell" Obfuscatest - sprich statt
Code:
IF (opcode1==ADD)
  {
     int var1=Stack.pop;
     int var2=Stack.pop;
    int result=var1+var2;
    Stack.push(result);   
    IP=IP+1; //da es nur eine 1-Opcode Anweisung war, InstrucitonPointer nur um 1 weiterbewegen 
  }
eher sowas
Code:
IF (opcode1==ADD)
  {
     int var1=Stack.pop;
     int var2=Stack.pop;
     String junk=var1.toString;
     int junk2=junk[i]&var2;
    Stack.push(junk2);
    ...junk...
    junk[y]=var2*junk2;
    Stack.pop;
    ... usw...
    int result=var1+var2;
    Stack.push(result);   
    IP=IP+1; //da es nur eine 1-Opcode Anweisung war, InstrucitonPointer nur um 1 weiterbewegen 
  }
hast. Also ein bisschen verschleiern.

Eventuell lohnt es sich auch einen "ASM Code2Array" Übersetzer zu schreiben. Vor allem weil man dann Labels nutzen kann und nicht manuell die Elemente abzählen muss.
 
Hy CWD

Danke nochmals für deinen Post.

Nun, ich habe das ganze mal in richtigen Code übersetzt. Mich würde interessieren ob ich mit meinem Prototype aufm Holz Weg bin, oder richtig liege:




Code:
Shared RET As Integer 'Rückgabe

Sub
Dim Codebase() As String = _
{"ADD", "5", "ADD", "10", "MUL", "RET"}  'CODE

Call XIL(Codebase) 'VM starten

End Sub



Friend Sub XIL(ByVal Codefile() As String)
Dim IP As Integer = 0
Dim Stack(UBound(Codefile)) As Integer
Dim intStack As Integer = 1

While (IP < UBound(Codefile) + 1)

Select Case Codefile(IP)

Case "ADD"
    Stack(intStack) = CInt(Codefile(IP + 1))
    intStack += 1
    IP += 2
Case "MUL"
    Stack(intStack) = Stack(intStack - 2) * Stack(intStack - 1)
    intStack += 1
    IP += 1
Case "RET"
    RET = Stack(intStack - 1)
    IP += 1
End Select
End While
End Sub

Es ist anzunehmen, dass dieser Code für dich kein Problem sein dürfte und du durchblick hast. Oder?

Lg
 
Naja, ich glaube, das Konzept der Stackmaschine hast Du missverstanden ;)
Irgendwie wird nämlich der Stack bei Dir immer nur erhöht.

Das Prinzip der Stackmaschine ist immer, Operatoren auf dem Stack zu lagern, aber diese auch nach gebrauch wieder vom Stack zu popen.
Bps Addition:
Code:
[stack]
2
2
[Stackend]

ADD holt sich seine Parameter von Stack:

Par1=Stack.pop
[stack]
2
[stackend]

Par2=stack.pop
[stack]
[stackend]

pusht Ergebnis wieder auf den Stack:
stack.push (2+2)
[stack]
4
[stackend]

Bei Dir dagegen:
Code:
Case "ADD"
    Stack(intStack) = CInt(Codefile(IP + 1))   //Weise den zweiten Opcode auf den Stack?
    intStack += 1
    IP += 2
Case "MUL"
    Stack(intStack) = Stack(intStack - 2) * Stack(intStack - 1)  //alte Operatoren bleiben auf dem Stack
    intStack += 1
    IP += 1
Bei Deinem ADD weist Du den Wert einfach dem Stack zu (ist also eher ein PUSH im klassischen Sinne - aber es kann ja auch so gedacht sein).

Dein MUL ist allerdings so nicht korrekt (bzw:bildet einen Memoryleak, da hier nie der Stackspeicher freigegeben wird ;) ).
Er sollte den Speicher für die beiden Operanden freigeben (beide "popen") und nur das Ergebnis drauf pushen:

Code:
Case "MUL"
    int result= Stack(intStack - 2) * Stack(intStack - 1)
    intStack =intStack-2   'beide Operanden freigeben
   Stack(intStack)=result  'Ergebis speichern
    intStack=intStack+1    'Stackpointer auf den nächsten freiverfügbaren Platz
    IP += 1
 
Aha, dachte ich mir. Dann muss ich wohl direkt mit der Stack Klasse arbeiten.
Es war ja wie gesagt nurn Prototype, es ging mir nicht primär um den Stack selbst.

Mhm von einem Memoryleak kann man nicht unbedingt reden, vielleicht ist es einfach nicht effizient. Weil ich erstelle den Stack so gross wie der Code selbst, somit wird nur soviel "Speicher" allokiert wie notwendig.
Aber ich werde den Code ummodeln, mit einem richtigen Stack ;)

Danke & Gruss
 
Zurück
Oben