Trucos de ELF en Go


En esta nota, aprenderemos c贸mo obtener el c贸digo de m谩quina de una funci贸n Go directamente en tiempo de ejecuci贸n, imprimirlo usando un desensamblador, y en el camino descubriremos varios trucos como obtener la direcci贸n de una funci贸n sin llamarla.


Advertencia : este mini art铆culo no le ense帽ar谩 nada 煤til.


Valor de funci贸n en Go


Primero, determinemos qu茅 es una funci贸n Go y por qu茅 necesitamos el concepto de valor de funci贸n .


Esto se explica mejor con el documento Go 1.1 Function Calls . El documento no es nuevo, pero la mayor parte de la informaci贸n en 茅l sigue siendo relevante.


En el nivel m谩s bajo, siempre es un puntero al c贸digo ejecutable, pero cuando usamos funciones / cierres an贸nimos o pasamos una funci贸n como interface{} , este puntero est谩 oculto dentro de alguna estructura.


El nombre de la funci贸n en s铆 no es una expresi贸n, por lo tanto, dicho c贸digo no funciona:


 // 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

Pero al mismo tiempo, podemos obtener el function value la function value trav茅s del mismo nombre de funci贸n:


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

Este c贸digo se inicia, pero imprimir谩 la direcci贸n de una variable local en la pila, que no es exactamente lo que quer铆amos. Pero, como se mencion贸 anteriormente, la direcci贸n de la funci贸n todav铆a est谩 all铆, solo necesita saber c贸mo acceder a ella.


El paquete reflect.Value.Call() depende de este detalle de implementaci贸n para ejecutar exitosamente reflect.Value.Call() . All铆 (reflect / makefunc.go) puede espiar el siguiente paso para obtener la direcci贸n de la funci贸n:


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

El c贸digo anterior muestra una idea b谩sica que puede refinar a una funci贸n:


 // 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 } 

La add1 funci贸n add1 se puede add1 llamando a funcAddr(add1) .


Obtener un bloque de c贸digo de funci贸n de m谩quina


Ahora que tenemos la direcci贸n del comienzo del c贸digo de funci贸n de la m谩quina, nos gustar铆a obtener el c贸digo completo de la funci贸n. Aqu铆 debe poder determinar d贸nde termina el c贸digo de la funci贸n actual.


Si la arquitectura x86 tuviera instrucciones de longitud fija, no ser铆a tan dif铆cil y varias heur铆sticas podr铆an ayudarnos, entre las cuales:


  • Como regla, al final del c贸digo de funci贸n hay una paliza de INT3 instrucciones INT3 . Este es un buen marcador para el final del c贸digo de funci贸n, pero puede faltar.
  • Las funciones con un marco distinto de cero para la pila tienen un pr贸logo que verifica si esta pila necesita expandirse. En caso afirmativo, se realiza un salto al c贸digo inmediatamente despu茅s del c贸digo de la funci贸n, y luego un salto al inicio de la funci贸n. El c贸digo que nos interesa estar谩 en el medio.

Pero deber谩 decodificar honestamente las instrucciones, porque un byte by-pass puede encontrar el byte INT3 dentro de otra instrucci贸n. Calcular la longitud de una instrucci贸n para omitir tampoco es tan f谩cil, porque es x86, beb茅 .


La direcci贸n de una funci贸n en el contexto del paquete de runtime de runtime veces se denomina PC , para enfatizar la capacidad de usar la direcci贸n en alg煤n lugar dentro de la funci贸n, y no solo el punto de entrada de la funci贸n. El resultado de funcAddr puede usarse como argumento para la funci贸n runtime.FuncForPC() para obtener runtime.Func sin llamar a la funci贸n en s铆. A trav茅s de transformaciones inseguras de A帽o Nuevo, podemos acceder a runtime._func , que es informativo, pero no muy 煤til: no hay informaci贸n sobre el tama帽o del bloque de c贸digo de funci贸n.


Parece que sin la ayuda de los ELF no podemos hacer frente.


Para las plataformas donde los ejecutables tienen un formato diferente, la mayor parte del art铆culo seguir谩 siendo relevante, pero deber谩 usar no debug/elf , sino otro paquete de debug .

El ELF que se esconde en tu programa


La informaci贸n que necesitamos ya est谩 contenida en los metadatos del archivo ELF .


A trav茅s de os.Args[0] podemos acceder al archivo ejecutable en s铆, y ya obtener la tabla de s铆mbolos.


 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) } 

Busca un personaje dentro de elf.File


Todos los caracteres se pueden File.Symbols() utilizando el m茅todo File.Symbols() . Este m茅todo devuelve []elf.Symbol , que contiene el campo Symbol.Size : este es el "tama帽o de la funci贸n" que estamos Symbol.Size . El campo Symbol.Value debe coincidir con el valor devuelto por funcAddr .


Puede buscar el s铆mbolo deseado por direcci贸n ( Symbol.Value ) o por nombre ( Symbol.Name ). Si los caracteres se ordenaran por nombre, ser铆a posible usar sort.Search() , pero esto no es as铆:


Los s铆mbolos se enumerar谩n en el orden en que aparecen en el archivo.

Si a menudo necesita encontrar caracteres en la tabla, debe crear un 铆ndice adicional, por ejemplo, a trav茅s de map[string]*elf.Symbol o map[uintptr]*elf.Symbol .


Como ya sabemos c贸mo obtener la direcci贸n de una funci贸n por su valor, la buscaremos:


 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 } 

Nota : para que este enfoque funcione, necesitamos una tabla de caracteres. Si el binario est谩 construido con ` -ldflags "-s" ', entonces elfLookup() siempre devolver谩 nil . Si ejecuta el programa a trav茅s de go run , puede encontrar el mismo problema. Para ver ejemplos del art铆culo, se recomienda hacer ' go build ' o ' go install ' para obtener archivos ejecutables.

Obtener el c贸digo de funci贸n de la m谩quina


Conociendo el rango de direcciones en las que se encuentra el c贸digo ejecutable, solo queda extraerlo en forma de []byte para un procesamiento conveniente.


 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 } 

Este c贸digo se simplifica intencionalmente para la demostraci贸n. No debe leer ELF cada vez y hacer una b煤squeda lineal en su tabla.


El resultado de la funci贸n funcCode() es un segmento con bytes del c贸digo de funci贸n de la m谩quina. Deber铆a funcAddr() resultado de llamar a 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 

Desmontaje del c贸digo de m谩quina


Para facilitar la lectura del c贸digo de la m谩quina, utilizaremos un desensamblador.


Estoy m谩s familiarizado con los proyectos zydis e Intel XED , por lo que, en primer lugar, mi elecci贸n recae en ellos.


Para Go, puede utilizar el enlace go-zydis , que es lo suficientemente bueno y f谩cil de instalar para nuestra tarea.


Describamos la abstracci贸n de "omitir las instrucciones de la m谩quina", con la ayuda de la cual puede implementar otras operaciones:


 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 } 

Esta funci贸n toma un segmento de c贸digo de m谩quina como entrada y llama a una funci贸n de devoluci贸n de llamada para cada instrucci贸n decodificada.


En base a esto, podemos escribir el printDisasm que 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 ejecutamos printDisasm en el add1 funci贸n add1 , obtenemos el resultado esperado:


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

Validaci贸n de resultados


Ahora intentaremos asegurarnos de que el c贸digo de ensamblador obtenido en la secci贸n anterior sea correcto.


Como ya tenemos un binario compilado, puede usar el objdump suministrado con 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 

Todo converge, solo la sintaxis es ligeramente diferente, lo que se espera.


Expresiones del m茅todo


Si necesitamos hacer lo mismo con los m茅todos, entonces, en lugar del nombre de la funci贸n, usaremos la expresi贸n del m茅todo .


Digamos que nuestro add1 no es realmente una funci贸n, sino un m茅todo de tipo adder :


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

Luego, la llamada para obtener la direcci贸n de la funci贸n se ver谩 como funcAddr(adder.add1) .


Conclusi贸n


Llegu茅 a estas cosas no por casualidad y, tal vez, en uno de los siguientes art铆culos le dir茅 c贸mo se plane贸 utilizar todos estos mecanismos. Mientras tanto, propongo tratar este art铆culo como una descripci贸n superficial de c贸mo el runtime de runtime y el reflect miran nuestras funciones Go a trav茅s del valor de la funci贸n.


Lista de recursos utilizados:


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


All Articles