J'essaie d'appeler le code en langage machine natif. Voici ce que j'ai jusqu'à présent (il y a une erreur de bus):
char prog[] = {'\xc3'}; // x86 ret instruction
int main()
{
typedef double (*dfunc)();
dfunc d = (dfunc)(&prog[0]);
(*d)();
return 0;
}
Il appelle correctement la fonction et arrive à l'instruction ret. Mais lorsqu'il tente d'exécuter l'instruction ret, il génère une erreur SIGBUS. Est-ce parce que j'exécute du code sur une page dont l'exécution n'est pas autorisée ou quelque chose du genre?
Alors qu'est-ce que je fais mal ici?
Un premier problème pourrait être que l'emplacement où les données de prog sont stockées n'est pas exécutable.
Sous Linux au moins, le binaire résultant placera le contenu des variables globales dans le segment "data" ou ici , qui n'est pas exécutable dans la plupart des cas normaux). .
Le deuxième problème peut être que le code que vous appelez n'est pas valide. Il existe une certaine procédure pour appeler une méthode en C, appelée convention d'appel (vous utilisez peut-être la méthode "cdecl", par exemple). Il se peut que la fonction appelée ne se contente pas de simplement "ret". Vous devrez peut-être également nettoyer la pile, etc., sinon le programme se comportera de manière inattendue. Cela peut s'avérer problématique une fois que vous avez dépassé le premier problème.
Vous devez appeler memprotect afin de rendre la page où prog vit exécutable. Le code suivant effectue cet appel et peut exécuter le texte dans prog.
#include <unistd.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
char prog[] = {
0x55, // Push %rbp
0x48, 0x89, 0xe5, // mov %rsp,%rbp
0xf2, 0x0f, 0x10, 0x05, 0x00, 0x00, 0x00,
//movsd 0x0(%rip),%xmm0 # c <x+0xc>
0x00,
0x5d, // pop %rbp
0xc3, // retq
};
int main()
{
long pagesize = sysconf(_SC_PAGE_SIZE);
long page_no = (long)prog/pagesize;
int res = mprotect((void*)(page_no*pagesize), (long)page_no+sizeof(prog), PROT_EXEC|PROT_READ|PROT_WRITE);
if(res)
{
fprintf(stderr, "mprotect error:%d\n", res);
return 1;
}
typedef double (*dfunc)(void);
dfunc d = (dfunc)(&prog[0]);
double x = (*d)();
printf("x=%f\n", x);
fflush(stdout);
return 0;
}
Comme tout le monde l’a déjà dit, vous devez vous assurer que prog[]
est exécutable. Cependant, si vous écrivez un compilateur JIT, la meilleure façon de le faire consiste à placer le symbole dans une zone exécutable, soit en utilisant un script de l'éditeur de liens, soit en spécifiant la section dans le code C si le compilateur permet, par exemple:
const char prog[] __attribute__((section(".text"))) = {...}
Pratiquement tous les compilateurs C vous permettront de le faire en intégrant le langage Assembly standard dans votre code. Bien sûr, il s’agit d’une extension non standard du C, mais les auteurs de compilateur reconnaissent que c’est souvent nécessaire. En tant qu'extension non standard, vous devrez lire le manuel de votre compilateur et vérifier comment le faire, mais l'extension extension "asm" de GCC est une approche relativement standard.
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
assert(dwRes > 3);
}
Puisqu'il est facile de jeter la pile dans assembleur, les compilateurs vous permettent souvent d'identifier les registres que vous utiliserez dans le cadre de votre assembleur. Le compilateur peut alors s’assurer que le reste de cette fonction évite ces registres.
Si vous écrivez vous-même le code de l'assembleur, il n'y a aucune bonne raison de configurer cet assembleur en tant que tableau d'octets. Ce n'est pas juste une odeur de code - je dirais que c'est une véritable erreur qui ne pourrait arriver qu'en ignorant l'extension "asm" qui est la bonne façon d'intégrer un assembleur dans votre C.
Essentiellement, cela a été réprimé car il s’agissait d’une invitation ouverte aux auteurs de virus. Mais vous pouvez allouer et mettre en mémoire tampon et le configurer avec un code machinec natif en C simple - ce n'est pas un problème. Le problème l'appelle. Bien que vous puissiez essayer de configurer un pointeur de fonction avec l’adresse de la mémoire tampon et de l’appeler, il est très peu probable que cela fonctionne, et très probablement une interruption sur la prochaine version du compilateur si vous parvenez à le persuader de faire ce que vous voulez. . Le mieux est donc simplement de recourir à un assemblage en ligne, de configurer le retour et de passer au code généré automatiquement. Mais si le système protège contre cela, vous devrez trouver des méthodes pour contourner la protection, comme Rudi l’a décrit dans sa réponse (mais très spécifique à un système particulier).
Une erreur évidente est que \xc3
ne renvoie pas le double
que vous prétendez renvoyer.
Vous pouvez éliminer le crash en permettant au compilateur de stocker le tableau dans la section en lecture seule de la mémoire de votre processus (si elle est connue au moment de la compilation). Par exemple, en déclarant le tableau const
.
Exemple:
const char prog[] = {'\xc3'}; // x86 ret instruction
int main()
{
typedef double (*dfunc)();
dfunc d = (dfunc)(&prog[0]);
(*d)();
return 0;
}
Vous pouvez également compiler le code avec la protection de pile désactivée gcc -z execstack
.
Question connexe: