Astuces ELF dans Go


Dans cette note, nous apprendrons comment obtenir le code machine d'une fonction Go directement lors de l'exécution, l'imprimer à l'aide d'un désassembleur, et en cours de route, nous découvrirons plusieurs astuces comme obtenir l'adresse d'une fonction sans l'appeler.


Attention : ce mini-article ne vous apprendra rien d'utile.


Valeur de fonction dans Go


Déterminons d'abord ce qu'est une fonction Go et pourquoi nous avons besoin du concept de valeur de fonction .


Ceci est mieux expliqué par le document Appels de fonction Go 1.1 . Le document n'est pas nouveau, mais la plupart des informations qu'il contient sont toujours pertinentes.


Au niveau le plus bas, il s'agit toujours d'un pointeur vers du code exécutable, mais lorsque nous utilisons des fonctions / fermetures anonymes ou passons une fonction en tant interface{} , ce pointeur est caché dans une structure.


Le nom de la fonction lui-même n'est pas une expression, par conséquent, un tel code ne fonctionne pas:


 // https://play.golang.org/p/wXeVLU7nLPs package main func add1(x int) int { return 1 } func main() { addr := &add1 println(addr) } 

compile: cannot take the address of add1

Mais en même temps, nous pouvons obtenir la function value la function value via le même nom de fonction:


 // https://play.golang.org/p/oWqv_FQq4hy package main func add1(x int) int { return 1 } func main() { f := add1 // <-------- addr := &f println(addr) } 

Ce code est lancé, mais il affichera l'adresse d'une variable locale sur la pile, ce qui n'est pas exactement ce que nous voulions. Mais, comme mentionné ci-dessus, l'adresse de la fonction est toujours là, il vous suffit de savoir comment y accéder.


Le package reflect dépend de ce détail d'implémentation pour exécuter avec succès reflect.Value.Call() . Là (reflect / makefunc.go), vous pouvez espionner la prochaine étape pour obtenir l'adresse de la fonction:


 dummy := makeFuncStub code := **(**uintptr)(unsafe.Pointer(&dummy)) 

Le code ci-dessus illustre une idée de base que vous pouvez affiner à une fonction:


 // funcAddr returns function value fn executable code address. func funcAddr(fn interface{}) uintptr { // emptyInterface is the header for an interface{} value. type emptyInterface struct { typ uintptr value *uintptr } e := (*emptyInterface)(unsafe.Pointer(&fn)) return *e.value } 

L' add1 fonction add1 peut être add1 en appelant funcAddr(add1) .


Obtention d'un bloc de code de fonction machine


Maintenant que nous avons l'adresse du début du code de fonction machine, nous aimerions obtenir le code machine entier de la fonction. Ici, vous devez pouvoir déterminer où se termine le code de la fonction actuelle.


Si l'architecture x86 avait des instructions de longueur fixe, ce ne serait pas si difficile et plusieurs heuristiques pourraient nous aider, parmi lesquelles:


  • En règle générale, à la fin du code de fonction, il y a un battement des instructions INT3 . C'est un bon marqueur pour la fin du code de fonction, mais il peut être manquant.
  • Les fonctions avec une trame non nulle pour la pile ont un prologue qui vérifie si cette pile doit être développée. Si oui, un saut au code est effectué immédiatement après le code de fonction, puis un saut au début de la fonction. Le code qui nous intéresse sera au milieu.

Mais vous devrez décoder honnêtement les instructions, car un by-by-pass peut trouver l'octet INT3 dans une autre instruction. Calculer la longueur d'une instruction à sauter n'est pas non plus si facile, car c'est x86, bébé .


L'adresse d'une fonction dans le contexte du package d' runtime est parfois appelée PC , pour souligner la possibilité d'utiliser l'adresse quelque part à l'intérieur de la fonction, et pas seulement le point d'entrée de la fonction. Le résultat de funcAddr peut être utilisé comme argument de la fonction runtime.FuncForPC() pour obtenir runtime.Func sans appeler la fonction elle-même. Grâce à des transformations du Nouvel An dangereuses, nous pouvons accéder à runtime._func , qui est informatif, mais pas très utile: il n'y a aucune information sur la taille du bloc de code de fonction.


Il semble que sans l'aide des ELF, nous ne pouvons pas faire face.


Pour les plates-formes où les exécutables ont un format différent, la plupart de l'article restera pertinent, mais vous devrez utiliser non pas debug/elf , mais un autre package de debug .

L'ELF qui se cache dans votre programme


Les informations dont nous avons besoin sont déjà contenues dans les métadonnées du fichier ELF .


Grâce à os.Args[0] nous pouvons accéder au fichier exécutable lui-même et en obtenir déjà la table des symboles.


 func readELF() (*elf.File, error) { f, err := os.Open(os.Args[0]) if err != nil { return nil, fmt.Errorf("open argv[0]: %w", err) } return elf.NewFile(f) } 

Recherchez un personnage dans elf.File


Tous les caractères peuvent être File.Symbols() à l'aide de la méthode File.Symbols() . Cette méthode renvoie []elf.Symbol , qui contient le champ Symbol.Size - c'est la "taille de fonction" que nous Symbol.Size . Le champ Symbol.Value doit correspondre à la valeur renvoyée par funcAddr .


Vous pouvez rechercher le symbole souhaité soit par adresse ( Symbol.Value ), soit par nom ( Symbol.Name ). Si les caractères étaient triés par nom, il serait possible d'utiliser sort.Search() , mais ce n'est pas le cas:


Les symboles seront répertoriés dans l'ordre dans lequel ils apparaissent dans le fichier.

Si vous devez souvent trouver des caractères dans le tableau, vous devez créer un index supplémentaire, par exemple, via map[string]*elf.Symbol ou map[uintptr]*elf.Symbol .


Puisque nous savons déjà comment obtenir l'adresse d'une fonction par sa valeur, nous la rechercherons:


 func elfLookup(f *elf.File, value uint64) *elf.Symbol { symbols, err := f.Symbols() if err != nil { return nil } for _, sym := range symbols { if sym.Value == value { return &sym } } return nil } 

Remarque : pour que cette approche fonctionne, nous avons besoin d'une table de caractères. Si le binaire est construit avec ` -ldflags "-s" ', elfLookup() retournera toujours nil . Si vous exécutez le programme via go run vous pouvez rencontrer le même problème. Pour des exemples de l'article, il est recommandé de faire « go build » ou « go install » pour obtenir les fichiers exécutables.

Obtention du code de fonction de la machine


Connaissant la plage d'adresses dans laquelle se trouve le code exécutable, il ne reste plus qu'à le retirer sous la forme de []byte pour un traitement pratique.


 func funcCode(addr uintptr) ([]byte, error) { elffile, err := readELF() if err != nil { return nil, fmt.Errorf("read elf: %w", err) } sym := elfLookup(elffile, uint64(addr)) if sym == nil { return nil, fmt.Errorf("can't lookup symbol for %x", addr) } code := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ Data: addr, Len: int(sym.Size), Cap: int(sym.Size), })) return code, nil } 

Ce code est intentionnellement simplifié pour la démonstration. Vous ne devez pas lire ELF chaque fois et faire une recherche linéaire sur sa table.


Le résultat de la fonction funcCode() est une tranche avec des octets du code de fonction machine. Elle doit funcAddr() résultat de l'appel de funcAddr() .


 code, err := funcCode(funcAddr(add1)) if err != nil { log.Panicf("can't get function code: %v", err) } fmt.Printf("% x\n", code) // => 48 8b 44 24 08 48 ff c0 48 89 44 24 10 c3 

Démontage du code machine


Pour faciliter la lecture du code machine, nous utiliserons un désassembleur.


Je connais très bien les projets zydis et Intel XED , donc tout d'abord mon choix leur revient.


Pour Go, vous pouvez prendre la liaison go-zydis , qui est assez bonne et facile à installer pour notre tâche.


Décrivons l'abstraction des «instructions de contournement de la machine», à l'aide de laquelle vous pouvez alors implémenter d'autres opérations:


 func walkDisasm(code []byte, visit func(*zydis.DecodedInstruction) error) error { dec := zydis.NewDecoder(zydis.MachineMode64, zydis.AddressWidth64) buf := code for len(buf) > 0 { instr, err := dec.Decode(buf) if err != nil { return err } if err := visit(instr); err != nil { return err } buf = buf[int(instr.Length):] } return nil } 

Cette fonction prend une tranche de code machine en entrée et appelle une fonction de rappel pour chaque instruction décodée.


Sur cette base, nous pouvons écrire le printDisasm nous avons printDisasm :


 func printDisasm(code []byte) error { const ZYDIS_RUNTIME_ADDRESS_NONE = math.MaxUint64 formatter, err := zydis.NewFormatter(zydis.FormatterStyleIntel) if err != nil { return err } return walkDisasm(code, func(instr *zydis.DecodedInstruction) error { s, err := formatter.FormatInstruction(instr, ZYDIS_RUNTIME_ADDRESS_NONE) if err != nil { return err } fmt.Println(s) return nil }) } 

Si nous printDisasm sur le add1 fonction add1 , nous obtenons le résultat tant attendu:


 mov rax, [rsp+0x08] inc rax mov [rsp+0x10], rax ret 

Validation des résultats


Nous allons maintenant essayer de nous assurer que le code assembleur obtenu dans la section précédente est correct.


Comme nous avons déjà un binaire compilé, vous pouvez utiliser l' objdump fourni avec Go:


 $ go tool objdump -s 'add1' exe TEXT main.add1(SB) example.go example.go:15 0x4bb760 488b442408 MOVQ 0x8(SP), AX example.go:15 0x4bb765 48ffc0 INCQ AX example.go:15 0x4bb768 4889442410 MOVQ AX, 0x10(SP) example.go:15 0x4bb76d c3 RET 

Tout converge, seule la syntaxe est légèrement différente, ce qui est attendu.


Expressions de méthode


Si nous devons faire de même avec les méthodes, au lieu du nom de la fonction, nous utiliserons l' expression de méthode .


Disons que notre add1 n'est pas vraiment une fonction, mais une méthode de type adder :


 type adder struct{} func (adder) add1(x int) int { return x + 2 } 

Ensuite, l'appel pour obtenir l'adresse de la fonction ressemblera à funcAddr(adder.add1) .


Conclusion


Je suis arrivé à ces choses non par accident et, peut-être, dans l'un des articles suivants, je vais vous dire comment il était prévu d'utiliser tous ces mécanismes. En attendant, je propose de traiter cet article comme une description superficielle de la façon dont l' runtime et la reflect regardent nos fonctions Go via la valeur de la fonction.


Liste des ressources utilisées:


Source: https://habr.com/ru/post/fr482392/


All Articles