Self modifying code
 
Appendix A

In deze Appendix is een korte test beschreven die gebruikt kan worden om vast te stellen of een besturingssysteem in combinatie met een gegeven compiler self modifying code toestaat.

De volgende tests worden in deze volgorde uitgevoerd.

  • lezen van code (copiëren van een functie)
  • schrijven van code (overschrijven van een functie)
  • executeren van data (de gecopieerde functie aanroepen)

Deze tests zijn met het onderstaande C code gedaan. Deze code is verre van perfect en heeft op geen enkele wijze een gegarandeerde werking. Als het goed is (als het systeem geen self modifying code toestaat) moet het programma crashen.

#include 
#include 

typedef void (*proc)(char *p);

void proc1(char *p)
{
  int a,b,c,d,e,f,i;i=0;for(c=7;c>2;c--)for(a=5;a<9;a++
  ){b=a-1+c;d=c-6+b;if(c<d){b=c;}else{f=b;b=d;}if(b<c){
  d=a;b=d;}else{f=b;d=a;}if(a<c){b=d;d=a;}else{d=b;f=c;
  }for(e=4;e<6;e++){p[i++]=(a+b+c+d+e+f)&255;i&=255;}}
}

void proc2(char *p)
{
  int i;for(i=0;i<256;i++)p[i]=0;
}

int main()
{
  proc p1,p2;
  char *c1,*c2;
  char *d1,*d2;
  int procsize;
  int i;

  if(proc1>proc2)
  {
    printf("STUPID COMPILER\n");
    return -1;
  }
  p1=proc1;
  p2=proc2;
  c1=(char *)p1;
  c2=(char *)p2;

  procsize=(int)(c2-c1);
  p2=(proc)malloc(sizeof(procsize));
  c2=(char *)p2;

  d1=(char *)malloc(256);
  d2=(char *)malloc(256);

  printf("p1=%p\np2=%p\nsize=%ld\n",
         p1,p2,(long)procsize);
  p1(d1);

  printf("\nREAD CODE...\n");
  for(i=0;i<procsize;i++)
    c3[i]='-';
  for(i=0;i<procsize;i++)
    c3[i]=c1[i];
  for(i=0;(i<procsize)&&(c3[i]=='-');i++);
  if(i==procsize)
    printf("NO CHANGE!!\n");
  else
    printf("OK\n");

  printf("\nWRITE CODE...\n");
  for(i=0;i<procsize;i++)
    c1[i]='-';
  for(i=0;(i<procsize)&&(c1[i]=='-');i++);
  if(i==procsize)
    printf("OK\n");
  else
    printf("NO CHANGE!!\n");

  printf("\nEXECUTE DATA...\n");
  p3(d2);
  for(i=0;(i<256)&&(d1[i]==d1[i]);i++)
    printf("%3d %3d\n",d1[i],d2[i]);
    printf("%d\n",i);
  if(i==256)
    printf("OK\n");
  else
    printf("ERROR IN COMPARE\n");

  return 0;
}
figuur A.1

Het programma copieert de code van een functie naar een stuk geheugen. Deze functie vult een array met waarden. Eerst wordt de lengte van de functie bepaalt door een vergelijking met de functie die er na komt. Dit is één van de zwakke plekken in dit programma.

Daarna wordt het origineel overschreven met karakters. Dan wordt de copie geëxecuteerd. Om het executeren van data te testen kan het 'WRITE CODE' blokje uitgecommentarieerd worden als het schrijven van code niet wordt toegestaan.

De resultaten van het uitvoeren van het gegeven programma zijn getest op de volgende systemen met gegeven compilers. Bij het compileren van de code moeten optimizers e.d. uitgezet worden om een zo eenvoudig mogelijk formaat te krijgen.

systeemcompilercode lezencode schrijvendata executeren
Windows 95 / MS-DOS 6.20 gcc 2.6.3 (djgpp) + go32 1.12 okokok
Windows 95 / MS-DOS 6.20 Borland C++ 3.1 voor Dos okokok
Windows NT 4.00 dosbox gcc 2.6.3 (djgpp) + go32 1.12 okokok
Windows NT 4.00 dosbox Borland C++ 3.1 voor Dos okokfout
Windows 95 Borland C++ 3.1 voor Windows okfoutfout
Windows NT 4.00 Borland C++ 3.1 voor Windows okfoutniet getest
Windows NT 4.00 Borland C++ 5.02 (win32 app.) okfoutfout antwoord
Linux 2.0.0 gcc 2.7.0 okfoutok
SUN OS 5.6 gcc 2.8.1 okfoutok
HP-UX A.09.05 gcc 2.7.2 okokfoute code
figuur A.2

Zoals te zien is wordt het lezen van code in alle geteste gevallen toegestaan. Het overschrijven van code is op veel besturingssystemen niet toegestaan. Er treed dan vaak een general protection fault of iets dergelijks op. Het programma wordt dan beëindigd. Omdat er op dos/windows systemen erg veel verschillende executable formaten zijn, is de test met verschillende compilers geprobeerd.

Het executeren van data gaat niet altijd goed. Vaak komt dit doordat de compiler de functies niet in één stuk in het geheugen zet. Ook worden vaak relatieve spronginstructies gebruikt die bij het verplaatsen van code niet meer correct zijn.

Als het (over)schrijven van code niet is toegestaan, maar het executeren van data wel, is het toch mogelijk om self modifying code te gebruiken. Het programma wordt dan gecopieerd naar het data-gebied en daar geëxecuteerd.

Het simpelweg copiëren van C-functies werkt over het algemeen niet zoals te zien is. Functie-aanroepen van andere functies binnen gecopieerde functies zijn vaak ongeldig, doordat relatieve adressering gebruikt is. Om dit probleem op te lossen kunnen C functiepointers gebruikt worden. Een functiepointer wordt bijvoorbeeld ook gebruikt om de copie van de code te executeren. Functiepointers zijn pas tijdens runtime bekend en worden dus absoluut geadresseerd.