CreateRemoteThread para Linux

Mitsuha traz novos fluxos O WinAPI possui uma função CreateRemoteThread que permite iniciar um novo thread no espaço de endereço de outro processo. Ele pode ser usado para uma variedade de injeções de DLL, tanto para fins ruins (truques em jogos, roubo de senhas etc.), como para corrigir um erro em um programa em execução em tempo real ou adicionar plug-ins a locais onde eles não estavam. fornecido.


Em geral, essa função possui um utilitário de aplicativo duvidoso, portanto, não é de surpreender que o Linux não tenha um análogo pronto de CreateRemoteThread. No entanto, eu queria saber como isso pode ser implementado. Estudar o tópico se transformou em uma boa aventura.


Vou falar detalhadamente sobre como, com a ajuda da especificação ELF, algum conhecimento da arquitetura x86_64 e das chamadas do sistema Linux, escreva seu próprio pequeno depurador que pode carregar e executar código arbitrário em um processo já em execução e em funcionamento.


A compreensão do texto exigirá conhecimentos básicos sobre programação de sistemas para Linux: a linguagem C, programas de gravação e depuração, compreensão do papel do código de máquina e memória no computador, o conceito de chamadas do sistema, familiaridade com as principais bibliotecas e leitura da documentação.



Como resultado, consegui "adicionar" a capacidade de visualizar senhas no Gnome Control Center:


demonstração de injeção no Gnome Control Center


Idéias principais


Se não houvesse cláusula nos requisitos sobre o carregamento do código em um processo já em execução, a solução seria extremamente simples: LD_PRELOAD. Essa variável de ambiente permite carregar uma biblioteca arbitrária com o aplicativo. Nas bibliotecas compartilhadas, é possível definir funções do construtor que são executadas quando a biblioteca é carregada.


Juntos, LD_PRELOAD e os construtores permitem que código arbitrário seja executado em qualquer processo usando um carregador dinâmico. Este é um recurso relativamente conhecido, frequentemente usado para depuração. Por exemplo, você pode baixar sua própria biblioteca com o aplicativo, que define as funções malloc () e free (), o que pode ajudar a detectar vazamentos de memória.


Infelizmente, LD_PRELOAD funciona apenas quando o processo é iniciado. Ele não pode ser usado para carregar uma biblioteca em um processo já em execução. Há uma função dlopen () para carregar bibliotecas enquanto o processo está em execução, mas, obviamente, o próprio processo deve chamá-lo para carregar os plugins.


Sobre executáveis ​​estáticos

LD_PRELOAD funciona apenas com programas que usam o carregador dinâmico. Se o programa foi criado com a opção -static , ele inclui todas as bibliotecas necessárias. Nesse caso, a resolução de dependências nas bibliotecas é executada no tempo de construção e o programa geralmente não está pronto e não pode carregar dinamicamente as bibliotecas após a montagem, no tempo de execução.

Em programas montados estaticamente, você pode incorporar código em tempo de execução, mas isso deve ser feito de uma maneira ligeiramente diferente. E isso não é totalmente seguro, pois o programa pode não estar pronto para essa mudança.

Em geral, não existe uma solução conveniente pronta, você precisa escrever sua bicicleta. Caso contrário, você não leria este texto :)


Conceitualmente, para forçar o processo de outra pessoa a executar algum tipo de código, é necessário executar as seguintes ações:


  1. Obtenha controle no processo de destino.
  2. Carregue o código na memória do processo de destino.
  3. Prepare o código baixado para execução no processo de destino.
  4. Organize a execução do código baixado pelo processo de destino.

Vamos lá ...


Obtendo controle no processo


Antes de tudo, precisamos subordinar o processo de destino à nossa vontade. Afinal, geralmente os processos executam apenas seu próprio código, ou o código das bibliotecas carregadas, ou os resultados da compilação JIT. Mas certamente não é o nosso código.


Uma opção é usar algum tipo de vulnerabilidade no processo que permite que você assuma o controle. Um exemplo clássico dos tutoriais: estouro de buffer, permitindo reescrever o endereço de retorno na pilha. É divertido, às vezes até funciona, mas não é adequado para o caso geral.


Usaremos outra maneira honesta de obter controle: depurar chamadas do sistema . Depuradores interativos podem perfeitamente parar processos de terceiros, avaliar expressões e muitas outras coisas. Eles podem - nós podemos.


No Linux, a chamada principal do sistema de depuração é ptrace () . Ele permite que você se conecte a processos, examine seu status e controle o progresso de sua execução. O ptrace () é bastante bem documentado por si só, mas os detalhes de seu uso são claros na prática.


Carregando código na memória do processo


No caso de estouros de buffer, a carga útil ( código do shell ) geralmente é incluída no conteúdo que estourou o mesmo buffer. Ao usar o depurador, o código necessário pode ser gravado diretamente na memória do processo. No WinAPI, há uma função especial WriteProcessMemory para isso. O Linux para esse fim está em conformidade com a maneira UNIX: para cada processo no sistema, existe um arquivo / proc / $ pid / mem , que exibe a memória desse processo. É possível gravar algo na memória do processo usando a entrada / saída usual.


Preparando código para execução


Apenas escrever código na memória não é suficiente. Ele ainda precisa ser gravado na memória executável . No caso de gravar através de uma vulnerabilidade, existem dificuldades não triviais com isso, mas como podemos controlar completamente o processo de destino, não será um problema encontrar ou alocar a memória "correta".


Outro ponto importante de preparação é o próprio código do shell. Nele, provavelmente desejaremos usar algumas funções das bibliotecas, como entrada e saída, primitivas gráficas e assim por diante. No entanto, temos que escrever o código da máquina, que por si só não tem idéia dos endereços de todas essas funções legais nas bibliotecas. De onde você os tira?


Para simplificar a vida do sistema operacional e complicar a vida do código malicioso, as bibliotecas geralmente não usam endereços fixos (e contêm o chamado código independente de posição ). Portanto, os endereços não podem ser adivinhados.


Quando o processo é iniciado normalmente, o carregador que executa as realocações é responsável por determinar os endereços exatos das bibliotecas. No entanto, ele cumpre apenas uma vez no início. Se o processo permitir o carregamento dinâmico de bibliotecas, haverá um carregador dinâmico que poderá fazer o mesmo enquanto o processo estiver em execução. No entanto, o endereço do carregador dinâmico também não é fixo.


Em geral, com bibliotecas, existem quatro opções:


  • não use bibliotecas, faça tudo em chamadas de sistema limpas
  • colocar cópias de todas as bibliotecas necessárias no código do shell
  • faça você mesmo o carregador dinâmico
  • encontre um gerenciador de inicialização dinâmico e faça com que ele carregue nossas bibliotecas

Nós escolheremos o último, porque queremos as bibliotecas e gravamos nosso gerenciador de inicialização completo por um longo tempo. Este não é o método mais secreto, nem o mais interessante, mas o mais simples, poderoso e confiável.


Transferência de controle para código


O ptrace () permite que você altere os registros do processador, para que não haja problemas em transferir o controle para o código carregado e preparado: basta escrever o endereço do nosso código no% rip register - e pronto! No entanto, na realidade, nem tudo é tão simples. As dificuldades estão relacionadas ao fato de que o processo depurado realmente não desapareceu e também possui algum tipo de código que foi executado e continuará sendo executado.


Esboço da solução


No total, implementaremos nosso fluxo em um processo de terceiros da seguinte maneira:


  1. Estamos conectados ao processo de destino para depuração.
  2. Encontramos as bibliotecas necessárias na memória:
    • libdl - para carregar uma nova biblioteca
    • libpthread - para iniciar um novo thread
  3. Nós encontramos as funções necessárias nas bibliotecas:
    • libdl: dlopen (), dlsym ()
    • libpthread: pthread_create (), pthread_detach ()
  4. Introduzimos o código do shell na memória do processo de destino:


     void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void *entry = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); } 

  5. Damos o código do shell a ser cumprido.

Como resultado, as bibliotecas farão a coisa certa por nós: elas carregarão nossa biblioteca com o código que precisamos na memória e iniciarão um novo thread executando esse código.


Limitações


A abordagem descrita acima impõe certas limitações:


  • O carregador de inicialização deve ter privilégios suficientes para depurar o processo de destino.
  • O processo deve usar libdl (pronto para carregamento dinâmico de módulos).
  • O processo deve usar libpthread (pronto para multithreading).
  • Aplicativos estáticos não são suportados.

Além disso, eu pessoalmente tenho preguiça de me preocupar com o suporte de todas as arquiteturas, então nos limitaremos a x86_64. (Mesmo um x86 de 32 bits seria mais complicado.)


Como você pode ver, tudo isso põe fim ao uso secreto de alvos maliciosos. No entanto, a tarefa ainda mantém o interesse da pesquisa e até deixa uma fraca oportunidade para uso industrial.


Digressão: sobre o uso de libdl e libpthread


Um leitor profissional experiente pode se perguntar: por que exigir o libdl se as funções internas __libc_dlopen_mode () e __libc_dlsym () já estão embutidas no glibc, e o libdl é apenas um invólucro sobre elas? Da mesma forma, por que exigir libpthread se um novo encadeamento pode ser facilmente criado usando a chamada de sistema clone ()?


De fato, na Internet há longe de um exemplo de como eles são usados:



Eles são mencionados na literatura popular sobre hackers:


  • Aprendendo a Análise Binária do Linux
  • A arte da memória forense

Então porque não? Bem, pelo menos porque não estamos escrevendo código malicioso em que uma solução é adequada, que omite 90% das verificações, ocupa 20 vezes menos espaço, mas também funciona em 80% dos casos. Além disso, eu queria tentar tudo com minhas próprias mãos.


De fato, o libdl não é necessário para carregar a biblioteca no caso da glibc. Seu uso pelo processo indica que está claramente pronto para o carregamento dinâmico de código. Apesar disso, em princípio, você pode se recusar a usar o libdl (já que ainda precisaremos procurar glibc posteriormente).


Por que dlopen () dentro da glibc?

Esta é uma pergunta interessante à sua maneira. Resposta curta: detalhes de implementação.

O ponto é o NSS ( Name Service Switch ) - uma das partes da glibc que fornece tradução de vários nomes: nomes de máquinas, protocolos, usuários, servidores de correio etc. É ela quem é responsável por funções como getaddrinfo () para obter endereços IP através de nome de domínio e getpwuid () para obter informações sobre o usuário por seu identificador numérico.

O NSS possui uma arquitetura modular e os módulos são carregados dinamicamente. Na verdade, para isso, a glibc também exigia mecanismos para carregar dinamicamente bibliotecas. É por isso que quando você tenta usar getaddrinfo () em um aplicativo estaticamente montado, o vinculador imprime um aviso "incompreensível":
 /tmp/build/socket.o: Na função `Socket :: bind ':
 socket.o :(. text + 0x374): aviso: Usando 'getaddrinfo' em links estaticamente
 aplicativos requer em tempo de execução as bibliotecas compartilhadas da versão glibc
 usado para vincular

Quanto aos encadeamentos, um encadeamento geralmente não é apenas uma pilha e código executável, mas também dados globais armazenados no armazenamento local do encadeamento (TLS). A inicialização correta de um novo encadeamento requer a operação coordenada do kernel do SO, um carregador de código binário e um tempo de execução da linguagem de programação. Portanto, uma simples chamada para clone () é suficiente para criar um fluxo que possa gravar no arquivo "Hello world!", Mas isso pode não funcionar para códigos mais complexos que precisam acessar o TLS e outras coisas interessantes ocultas aos olhos do programador de aplicativos.


Outro ponto relacionado ao multithreading são os processos de thread único. O que acontece se criarmos um novo thread em um processo que não foi concebido como multithread? Certo, comportamento vago. De fato, no processo não há sincronização do trabalho entre os threads, que mais cedo ou mais tarde levará à corrupção de dados. Se exigirmos que o aplicativo use libpthread, podemos ter certeza de que ele está pronto para funcionar em um ambiente multithread (pelo menos ele deve estar pronto).


Etapa 1. Conexão ao processo


Primeiro, precisamos nos conectar ao processo de destino para depuração e, posteriormente, desconectá-lo novamente. É aqui que a chamada do sistema ptrace () entra.


Primeiro contato com ptrace ()


Na documentação do ptrace (), você pode encontrar quase todas as informações necessárias:


   Anexar e desanexar
        Uma linha pode ser anexada ao rastreador usando a chamada

            ptrace (PTRACE_ATTACH, pid, 0, 0);

        ou

            ptrace (PTRACE_SEIZE, pid, 0, PTRACE_O_flags);

        PTRACE_ATTACH envia SIGSTOP para este segmento.  Se o rastreador quiser isso
        SIGSTOP para não ter efeito, ele precisa suprimi-lo.  Observe que se
        outros sinais são enviados simultaneamente para este segmento durante a conexão, o
        O traçador pode ver o traçado entrar na parada de entrega de sinal com outros sinais.
        primeiro (s)!  A prática usual é reinjetar esses sinais até
        SIGSTOP é visto e suprime a injeção de SIGSTOP.  O bug do design
        aqui é que um anexo ptrace e um SIGSTOP entregue simultaneamente
        corrida e o concorrente SIGSTOP podem ser perdidos.

Portanto, o primeiro passo é usar PTRACE_ATTACH:


 int ptrace_attach(pid_t pid) { /*     */ if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; /*    */ if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; } 

Após ptrace (), o processo de destino ainda não está pronto para depuração. Nós nos conectamos a ele, mas para um estudo interativo do estado do processo, ele deve ser interrompido. O ptrace () envia um sinal SIGSTOP para o processo, mas ainda precisamos esperar até que o processo realmente pare.


Para aguardar, use a chamada de sistema waitpid (). Ao mesmo tempo, vários casos interessantes de fronteira são dignos de nota. Primeiro, o processo pode simplesmente terminar ou morrer sem ter recebido o SIGSTOP. Nesse caso, não podemos fazer nada. Em segundo lugar, algum outro sinal pode ser enviado anteriormente ao processo. Nesse caso, devemos deixar o processo processá-lo (usando PTRACE_CONT), e a nós mesmos, continuar aguardando mais pelo nosso SIGSTOP:


 static int wait_for_process_stop(pid_t pid, int expected_signal) { for (;;) { int status = 0; /* ,    -  */ if (waitpid(pid, &status, 0) < 0) return -1; /*      —   */ if (WIFSIGNALED(status) || WIFEXITED(status)) return -1; /*   ,     */ if (WIFSTOPPED(status)) { /* *  WSTOPSIG()   , *   ptrace()   *     . */ int stop_signal = status >> 8; /*    ,    */ if (stop_signal == expected_signal) break; /*        */ if (ptrace(PTRACE_CONT, pid, 0, stop_signal) < 0) return -1; continue; } /*   —   */ return -1; } return 0; } 

Desconexão do processo


Parar o processo de depuração é muito mais simples: basta usar PTRACE_DETACH:


 int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; } 

A rigor, desabilitar explicitamente o depurador nem sempre é necessário. Quando o processo do depurador termina, ele é desconectado automaticamente de todos os processos depurados, e os próprios processos retomam o trabalho se forem interrompidos por ptrace (). No entanto, se o processo depurado foi explicitamente interrompido pelo depurador usando o sinal SIGSTOP sem usar ptrace (), ele não será ativado sem o sinal SIGCONT ou PTRACE_DETACH correspondente. Portanto, é melhor desconectar-se dos processos culturalmente.


Configuração Ptrace_scope


O depurador tem controle total sobre o processo que está sendo depurado. Se alguém pudesse depurar alguma coisa, qual seria a extensão para código malicioso! É óbvio que a depuração interativa é uma atividade bastante específica, geralmente necessária apenas para desenvolvedores. Durante a operação normal do sistema, na maioria das vezes não há necessidade de depurar processos.


Por esses motivos, por motivos de segurança, os sistemas geralmente desativam a capacidade de depurar qualquer processo por padrão. O módulo de segurança Yama é responsável por isso, gerenciado através do arquivo / proc / sys / kernel / yama / ptrace_scope. Ele fornece quatro comportamentos:


  • 0 - o usuário pode depurar qualquer processo que ele iniciou
  • 1 - modo padrão, apenas os processos iniciados pelo depurador podem ser depurados
  • 2 - apenas um administrador do sistema raiz pode depurar processos
  • 3 - a depuração é proibida para todos, o modo não é desativado até o sistema reiniciar

Obviamente, para nossos propósitos, será necessário depurar os processos iniciados antes do nosso depurador; portanto, para experimentos, você precisará alternar o sistema para o modo de desenvolvimento escrevendo 0 em um arquivo ptrace_scope especial (que requer direitos de administrador):


 $ sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope' 

ou execute o depurador como administrador:


 $ sudo ./inject-thread ... 

Resultados do primeiro passo


Como resultado, na primeira etapa, somos capazes de conectar-se ao processo de destino como um depurador e depois desconectá-lo.


O processo de destino será interrompido e podemos garantir que o sistema operacional realmente nos veja como um depurador:


 $ sudo ./inject-thread --target $(pgrep docker) $ cat /proc/$(pgrep docker)/status | head Name: docker State: t (tracing stop) <---    Tgid: 31330 Ngid: 0 Pid: 31330 PPid: 1 TracerPid: 2789 <--- PID   Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 64 $ ps a | grep [2]789 2789 pts/5 S+ 0:00 ./inject-thread --target 31330 

Etapa 2. Pesquise bibliotecas na memória


O próximo passo é mais simples: você precisa encontrar na memória do processo de destino a biblioteca com as funções que precisamos. Mas há muita memória, por onde começar a procurar e o que exatamente?


Arquivo / proc / $ pid / maps


Um arquivo especial nos ajudará com isso, através do qual o kernel informa sobre o que e onde o processo está localizado na memória. Como você sabe , no diretório / proc para cada processo, há um subdiretório. E há um arquivo que descreve o cartão de memória do processo:


 $ cat / proc / self / maps
 00400000-0040c000 r-xp 00000000 fe: 01 1044592 / bin / cat
 0060b000-0060c000 r - p 0000b000 fe: 01 1044592 / bin / cat
 0060c000-0060d000 rw-p 0000c000 fe: 01 1044592 / bin / cat
 013d5000-013f6000 rw-p 00000000 00:00 0 [heap]
 7f9920bd1000-7f9920d72000 r-xp 00000000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920d72000-7f9920f72000 --- p 001a1000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920f72000-7f9920f76000 r - p 001a1000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920f76000-7f9920f78000 rw-p 001a5000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7fc3f8381000-7fc3f8385000 rw-p 00000000 00:00 0
 7fc3f8385000-7fc3f83a6000 r-xp 00000000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f83ec000-7fc3f840e000 rw-p 00000000 00:00 0
 7fc3f840e000-7fc3f8597000 r - p 00000000 fe: 01 657286 / usr / lib / locale / locale-archive
 7fc3f8597000-7fc3f859a000 rw-p 00000000 00:00 0
 7fc3f85a3000-7fc3f85a5000 rw-p 00000000 00:00 0
 7fc3f85a5000-7fc3f85a6000 r - p 00020000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f85a6000-7fc3f85a7000 rw-p 00021000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f85a7000-7fc3f85a8000 rw-p 00000000 00:00 0
 7ffdb6f0e000-7ffdb6f2f000 rw-p 00000000 00:00 0 [pilha]
 7ffdb6f7f000-7ffdb6f81000 r-xp 00000000 00:00 0 [vdso]
 7ffdb6f81000-7ffdb6f83000 r - p 00000000 00:00 0 [vvar]
 ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

O conteúdo desse arquivo é gerado imediatamente pelo kernel do sistema operacional a partir de estruturas internas que descrevem as regiões de memória do processo de seu interesse e contém as seguintes informações:


  • intervalo de endereços alocado para a região
  • direitos de acesso à região
    • r/- : leitura
    • w/- : gravação
    • x/- : execução
    • p/s : compartilhando memória com outros processos
  • deslocamento de arquivo (se houver)
  • código do dispositivo em que o arquivo exibido está localizado
  • número do inode do arquivo (se houver)
  • caminho para o arquivo exibido (se houver)

Algumas regiões da memória são mapeadas para arquivos: quando um processo lê essa memória, na verdade ele lê dados dos arquivos correspondentes em um deslocamento específico. Se for possível gravar em uma região, as alterações na memória podem ser visíveis apenas para o próprio processo (mecanismo de cópia na gravação , o modo p é privado) ou sincronizadas com o disco ( s modo s é compartilhado).


Outras regiões são anônimas - essa memória não corresponde a nenhum arquivo. O sistema operacional simplesmente fornece ao processo um pedaço de memória física que ele usa. Essas regiões são usadas, por exemplo, para memória de processo "normal": empilhar e heap. As regiões anônimas podem ser pessoais de um processo ou compartilhadas entre vários processos (mecanismo de memória compartilhada ).


Além disso, existem várias regiões especiais na memória marcadas com os pseudo-nomes [vdso] e [vsyscall]. Eles são usados ​​para otimizar algumas chamadas do sistema.


Estamos interessados ​​em regiões onde o conteúdo dos arquivos da biblioteca é exibido. Se lermos o cartão de memória e filtrarmos as entradas nele pelo nome do arquivo exibido, encontraremos todos os endereços ocupados pelas bibliotecas de que precisamos. O formato do cartão de memória é especialmente conveniente para o processamento de software e é facilmente entendido usando a família de funções scanf ():


 static bool read_proc_line(const char *line, const char *library, struct memory_region *region) { unsigned long vaddr_low = 0; unsigned long vaddr_high = 0; char read = 0; char write = 0; char execute = 0; int path_offset = 0; /*    /proc/$pid/maps */ sscanf(line, "%lx-%lx %c%c%c%*c %*lx %*x:%*x %*d %n", &vaddr_low, &vaddr_high, &read, &write, &execute, &path_offset); /* ,       */ if (!strstr(line + path_offset, library)) return false; /*           */ if (region) { region->vaddr_low = vaddr_low; region->vaddr_high = vaddr_high; region->readable = (read == 'r'); region->writeable = (write == 'w'); region->executable = (execute == 'x'); region->content = NULL; } return true; } 


, libc-2.19.so, :


buraco no libc-2.19.so


2 - ? 51? ? ?


, , .


, , . , , , (, , ).


, ( 4 ). , .


, . — — . 2 — , ( x86_64 4 , 2 , 1 ). .



, :


  • libdl: dlopen() dlsym()
  • libpthread: pthread_create() pthread_detach()

, , . Linux ( address space layout randomization , ASLR). (- , ), — - .


, , , /proc/$pid/maps. , .


3. ELF-


, , , .


:


 $ nm -D /lib/x86_64-linux-gnu/libdl-2.19.so | grep dlopen 0000000000001090 T dlopen 

nm . .


- , nm , . , dlsym().



— ELF-, . procfs. UNIX way, /proc/$pid/mem , — ( /proc/$pid/maps).


Linux mmap(), ( , ). :


 static int map_region(pid_t pid, struct memory_region *region) { size_t length = region->vaddr_high - region->vaddr_low; off_t offset = region->vaddr_low; char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); /*     */ int fd = open(path, O_RDONLY); if (fd < 0) goto error; /*      */ void *buffer = malloc(length); if (!buffer) goto error_close_file; /*   */ if (read_region(fd, offset, buffer, length) < 0) goto error_free_buffer; region->content = buffer; close(fd); return 0; error_free_buffer: free(buffer); error_close_file: close(fd); error: return -1; } static int read_region(int fd, off_t offset, void *buffer, size_t length) { /*      */ if (lseek(fd, offset, SEEK_SET) < 0) return -1; size_t remaining = length; char *ptr = buffer; /* *     .   , *      ,  . */ while (remaining > 0) { ssize_t count = read(fd, ptr, remaining); if (count < 0) return -1; remaining -= count; ptr += count; } return 0; } 

ELF- . , -, , -, .


ELF


ELF — Linux. , , .


ELF . ELF . — , . , — . ELF-.


, libdl-2.19.so :


seções e segmentos libdl-2.19.so


( readelf --headers .)


, , (29 9). — , , . ELF — , . Linux, , LOAD, ( ).


ELF- , . , .


, . «» . .bss, , ( ).


, ELF — , . ...


?


() . , dlsym(), . - .


ELF (. 2-10). , .dynamic , DYNAMIC . .dynamic , :


  • .dynsym — ;
  • .dynstr — ;
  • .hash — -, .

, , ELF:


Procura por segmento DINÂMICO


ELF, (1), (2), (3), (4) , .


ELF →


() ELF <elf.h>, , , . , ELF — . 32- 64- , , , . x86_64, ELF .


ELF- ( Elf64_Ehdr ). ( program headers ), e_phoff e_phnum :


Cabeçalho ELF


— , , ELF- — , , , , .


e_phoff, , . e_phnum e_phentsize .


( ), ELF — 64 .


→ DYNAMIC


. — Elf64_Phdr ( 64- ELF-), . PT_DYNAMIC p_type :


Tabela de segmentos ELF


:


  • p_vaddr — , ;
  • p_memsz — .

.dynamic 0x2D88 ( ). DYNAMIC — 0x202D88. 0x210 (8448) . .


DYNAMIC → .dynsym, .dynstr, .hash


.dynamic, DYNAMIC, . Elf64_Dyn , :


Tags de seção DINÂMICA


8 d_val d_ptr , 8- d_tag , , . :


  • DT_HASH (4) — .hash ( d_ptr)
  • DT_STRTAB (5) — .dynstr ( d_ptr)
  • DT_SYMTAB (6) — .dynsym ( d_ptr)
  • DT_STRSZ (10) — .dynstr ( d_val)
  • DT_NULL (0) —

. .dynamic : , , , .


, DYNAMIC , . , , - , .


.dynamic , . -, .dynstr , ? .



. , .dynsym , . ( «» .symtab, , , . .)



Elf64_Sym , ELF — , , , . dlopen :


Tabela de caracteres ELF


:


  • st_name — ,
  • st_info — ( )
  • st_value

( , nm , dlopen() .text, 0x1090 .)


, .



— - , . ( ). .dynstr , libdl-2.19.so :


Tabela de linhas ELF


, ( «dlopen», 0xA5) , . .


-


.hash - , . - — — ELF-, . , .dynsym, , . ( ) - .


- <elf.h>, (. 2-19). - , :


tabela de hash ELF


onde


  • nbuckets — buckets
  • nchains — chains ( )
  • buckets —
  • chains —

- :


  1. h .
  2. i buckets[h % nbuckets] , .
  3. ( ) , .
  4. chains[i % nchains] .
  5. 3—4 , .

-, ELF:


 static uint32_t elf_hash(const char *name) { uint32_t h = 0; uint32_t g; while (*name) { h = (h << 4) + *name++; g = h & 0xF0000000; if (g) h ^= g >> 24; h &= ~g; } return h; } 

, "dlopen" - 112420542 :


procurar um personagem em uma biblioteca


libdl — , 39 , . - .



, :


  • dlopen() dlsym() libdl
  • pthread_create() pthread_detach() libpthread

, .


. . , .


ELF- . , ( ). , . , , . .


4. -


, , - , : , . - .


-


, -:


 void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void (*entry)(void) = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); } 

?


, — . , , , - — - ! .


— - . , , : .


 /* *      .rodata:   * .         , *        . */ .section .rodata /* *   .       . *      -:    ,  *  ,       . */ .global shellcode_start .global shellcode_address_dlopen .global shellcode_address_dlsym .global shellcode_address_pthread_create .global shellcode_address_pthread_detach .global shellcode_address_payload .global shellcode_address_entry .global shellcode_end /* *   dlopen().     #include <dlfcn.h>, *       . */ .set RTLD_LAZY, 1 .align 8 shellcode_start: /* * void *payload = dlopen(shellcode_address_payload, RTLD_LAZY); * *        x86_64: * * -     %rdi, %rsi, %rdx, %rcx * -     %rax * -      * *         . * *       %rax,    *     . */ lea shellcode_address_payload(%rip),%rdi mov $RTLD_LAZY,%rsi mov shellcode_address_dlopen(%rip),%rax callq *%rax /* * void (*entry)(void) = dlsym(payload, shellcode_address_entry); */ mov %rax,%rdi lea shellcode_address_entry(%rip),%rsi mov shellcode_address_dlsym(%rip),%rax callq *%rax /* * pthread_t thread; * pthread_create(&thread, NULL, entry, NULL); * *            * ,     pthread_create(). */ sub $8,%rsp mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax /* * pthread_detach(thread); * *    ,   ,  *     . */ mov (%rsp),%rdi add $8,%rsp mov shellcode_address_pthread_detach(%rip),%rax callq *%rax /* *   - —    ,     *      ret.    *     ,  *      . */ int $3 /* *       ,   *   ,    - *     .   “  *  ” (global offset table, GOT),   *           . */ .align 8 shellcode_address_dlopen: .space 8 shellcode_address_dlsym: .space 8 shellcode_address_pthread_create: .space 8 shellcode_address_pthread_detach: .space 8 shellcode_address_payload: .space 256 shellcode_address_entry: .space 256 /* *  - . */ shellcode_end: .end 

, . :


 $ as -o shellcode.o shellcode.S 

, , , . : (procedure linkage table, PLT), .


- , (, ) . - .


-


- . , , , . ?


-


, . , . , . , .


(- ), : , , . , , JIT- , . ?



:


  • - ,
  • - ,

, . -, - , . -, . -, , - -, .


, . . x86_64 int $3 — 0xCC — . ptrace() PTRACE_POKETEXT — , 8 , . , , .


, , , : . - , .


?


, ! malloc()!


. , -, . . , mmap():


 void inject_shellcode(const void *shellcode_src, size_t shellcode_size) { void *shellcode_dst = mmap(NULL, shellcode_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); copy_shellcode(shellcode_dst, shellcode_src, shellcode_size); } 

, ptrace() , .



, ? , . Linux x86_64 :


  • %rax
  • — — %rsi, %rdi, %rdx, %r10, %r8, %r9
  • SYSCALL,
  • %rax

ptrace() PTRACE_GETREGS PTRACE_SETREGS. , . - SYSCALL.


: , %rip. , , SYSCALL.


SYSCALL


SYSCALL? , . - , - . — libc. , , , :


 unsigned long find_syscall_instruction(struct library *library) { for (size_t i = 0; i < library->region_count; i++) { struct memory_region *region = &library->regions[i]; if (!(region->readable && region->executable)) continue; const uint8_t *region_data = region->content; size_t region_size = region->vaddr_high - region->vaddr_low; if (region_size < 2) continue; /* * 0F 05 syscall */ for (size_t offset = 0; offset < region_size - 1; offset++) { if (region_data[offset + 0] == 0x0F && region_data[offset + 1] == 0x05) { return region->vaddr_low + offset; } } } return 0; } 

, /proc/$pid/maps . x86_64 , - . , 0x0F 0x05. , , ARM, 0xDF 0x00 ( SVC #0), .


PTRACE_{GET,SET}REGS


:


 int get_registers(pid_t pid, struct user_regs_struct *registers) { int err = 0; if (ptrace(PTRACE_GETREGS, pid, registers, registers) < 0) err = -errno; return err; } 

struct user_regs_struct , <sys/user.h>. . . , varargs :


 static int set_regs_for_syscall(struct user_regs_struct *registers, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, va_list args) { registers->rip = syscall_insn_vaddr; registers->rax = syscall_number; for (int i = 0; i < args_count; i++) { switch (i) { case 0: registers->rdi = va_arg(args, long); break; case 1: registers->rsi = va_arg(args, long); break; case 2: registers->rdx = va_arg(args, long); break; case 3: registers->r10 = va_arg(args, long); break; case 4: registers->r8 = va_arg(args, long); break; case 5: registers->r9 = va_arg(args, long); break; default: return -E2BIG; } } return 0; } static long perform_syscall(pid_t pid, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, ...) { struct user_regs_struct old_registers; struct user_regs_struct new_registers; /* *    ,   *      . */ get_registers(pid, &old_registers); /* *      ,   * ,     . */ new_registers = old_registers; va_list args; va_start(args, args_count); set_regs_for_syscall(&new_registers, syscall_insn_vaddr, syscall_number, args_count, args); va_end(args); set_registers(pid, &new_registers); /* *    ,    *   ,    * (  ),    . *     . */ wait_for_syscall_completion(pid); /* *       *    . *        . */ get_registers(pid, &new_registers); long result = new_registers.rax; set_registers(pid, &old_registers); return result; } 

PTRACE_SYSCALL


: , ?


PTRACE_SYSCALL. PTRACE_CONT, . , - : , .


PTRACE_SYSCALL SIGTRAP : ( ) ( ). , ptrace() , , .


, SIGTRAP:


 static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGTRAP) < 0) return -1; return 0; } void wait_for_syscall_completion(pid_t pid) { wait_for_syscall_enter_exit_stop(pid); wait_for_syscall_enter_exit_stop(pid); } 

— , — (wait_for_process_stop() ). . , .


PTRACE_O_TRACESYSGOOD


, PTRACE_SYSCALL : , , - . , SIGTRAP ( ).


SIGTRAP . PTRACE_O_TRACESYSGOOD, :


  • SIGTRAP — -
  • SIGTRAP | 0x80 —

  int ptrace_attach(pid_t pid) { if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; + /*     */ + unsigned long options = PTRACE_O_TRACESYSGOOD; + if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) + return -1; return 0; } static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; - if (wait_for_process_stop(pid, SIGTRAP) < 0) + if (wait_for_process_stop(pid, SIGTRAP | 0x80) < 0) return -1; return 0; } 

-


- :


 void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; /*   ,  ,  . . */ prepare_shellcode(shellcode_text, shellcode_size); /*   -   */ write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); } 

- : dlopen(), .


 static inline void copy_shellcode(char *shellcode_text, const char *shellcode_addr, const void *data, size_t length) { ptrdiff_t offset = shellcode_addr - shellcode_start; memcpy(shellcode_text + offset, data, length); } static void prepare_shellcode(char *shellcode_text, size_t shellcode_size) { copy_shellcode(shellcode_text, shellcode_start, shellcode_start, shellcode_size); copy_shellcode(shellcode_text, shellcode_address_dlopen, &dlopen_vaddr, sizeof(dlopen_vaddr)); copy_shellcode(shellcode_text, shellcode_address_dlsym, &dlsym_vaddr, sizeof(dlsym_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_create, &pthread_create_vaddr, sizeof(pthread_create_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_detach, &pthread_detach_vaddr, sizeof(pthread_detach_vaddr)); copy_shellcode(shellcode_text, shellcode_address_payload, payload, sizeof(payload)); copy_shellcode(shellcode_text, shellcode_address_entry, entry, sizeof(entry)); } 

, , -:


 extern const char shellcode_start[]; extern const char shellcode_address_dlopen[]; extern const char shellcode_address_dlsym[]; extern const char shellcode_address_pthread_create[]; extern const char shellcode_address_pthread_detach[]; extern const char shellcode_address_payload[]; extern const char shellcode_address_entry[]; extern const char shellcode_end[]; 

, .


- . /proc/$pid/mem, :


 int write_remote_memory(pid_t pid, unsigned long vaddr, const void *data, size_t size) { char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); /*       */ int fd = open(path, O_WRONLY); if (fd < 0) return -1; /*     */ if (lseek(fd, vaddr, SEEK_SET) < 0) { close(fd); return -1; } /*    */ int err = do_write_remote_memory(fd, data, size); close(fd); return err; } static int do_write_remote_memory(int fd, const void *data, size_t size) { size_t left = size; /* *    ,  ,     *   ,       *      . */ while (left > 0) { ssize_t wrote = write(fd, data, left); if (wrote < 0) return -1; data += wrote; left -= wrote; } return 0; } 


, - — « » . . - , .


5.


- . , : %rip -, PTRACE_SETREGS, PTRACE_CONT . .


, , . -? ?



, . , « » . , . :


  • (async-signal-safe)

— . dlopen() pthread_create() . - dlopen(), dlopen() ?


-, , , . , pthread_create() . , ( ). clone().


pthread_create()?

, - , ?

: clone().

, (libc) (pthread). clone() (thread control block, TCB) (thread-local storage, TLS), , . . pthread_create() , .

«», clone() libc pthread. , .


clone() :


  • ?
  • ?
  • -?


: -?


, - : , , , .


. , , . ? : exit(). , .


. exit() -:


 +.set __NR_exit, 60 .set RTLD_LAZY, 1 @@ - /* - *  . - */ - int $3 + /* + * exit(0); + */ + xor %rdi,%rdi + mov $__NR_exit,%rax + syscall 

: exit() — exit() . exit() , exit() — . Linux exit_group().



. . , , PROT_EXEC:


 shellcode_stack_vaddr = remote_mmap(target, syscall_vaddr, 0, SHELLCODE_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN, -1, 0); 

, Linux x86_64 — «» , . mmap() , clone() . , mmap() MAP_GROWSDOWN, , .


PTRACE_O_TRACECLONE


. , - . waitpid(), : , .


— PTRACE_O_TRACECLONE. . , . , , , . , PTRACE_ATTACH , .


-, :


 - unsigned long options = PTRACE_O_TRACESYSGOOD; + unsigned long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACECLONE; if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) return -1; 

-, clone(), PTRACE_EVENT_CLONE, , PTRACE_SYSCALL. :


 -void wait_for_syscall_completion(pid_t pid) +void wait_for_syscall_completion(pid_t pid, long syscall) { wait_for_syscall_enter_exit_stop(pid); + + /*  clone()   PTRACE_EVENT_CLONE */ + if (syscall == __NR_clone) + wait_for_clone_event(pid); wait_for_syscall_enter_exit_stop(pid); } 

:


 static int wait_for_clone_event(pid_t pid) { if (ptrace(PTRACE_CONT, pid, 0, 0) < 0) return -1; int event = SIGTRAP | (PTRACE_EVENT_CLONE << 8); if (wait_for_process_stop(pid, event) < 0) return -1; return 0; } 

clone() PID , . :


 void clear_ptrace_options(pid_t pid) { ptrace(PTRACE_SETOPTIONS, pid, 0, 0); } 

, clone() ptrace(), PTRACE_O_TRACECLONE. , , - .



, - . clone() :


 static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, /*   **  */ shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); if (!shell_tid) return -1; return 0; } 

clone() : , , , . , .


CLONE_FILES, CLONE_FS, CLONE_IO, CLONE_SIGHAND, CLONE_SYSVSEM, CLONE_VM — . , CLONE_FILES , ( fork()). — — , . . , CLONE_VM , , .


CLONE_THREAD : Linux — « », . , , getpid() , kill() — - , execve() — , .


, clone() fork(): , . clone() : , — . . ( , , .)


, pthread_create() , , . ?



fork() :


 pid_t child = fork(); if (child < 0) { /* fork() ,    */ } if (child == 0) { /*     execve() */ } /*     */ 

, . clone() . .


. , clone() , . syscall ret, , . .


SYSCALL + RET


, . , syscall ret:


 -if (region_size < 2) +if (region_size < 3) continue; /* * 0F 05 syscall + * C3 retq */ -for (size_t offset = 0; offset < region_size - 1; offset++) { +for (size_t offset = 0; offset < region_size - 2; offset++) { if (region_data[offset + 0] == 0x0F && - region_data[offset + 1] == 0x05) + region_data[offset + 1] == 0x05 && + region_data[offset + 2] == 0xC3) { return region->vaddr_low + offset; } } 

, .



. prepare_shellcode() , , :


  void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; /*   ,  ,  . . */ prepare_shellcode(shellcode_text, shellcode_size); /*   -   */ write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); + /*    «»   */ + unsigned long retaddr_vaddr = + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8; + write_remote_memory(target, retaddr_vaddr, + &shellcode_text_vaddr, sizeof(shellcode_text_vaddr)); } 

, , .


, , . System V ABI , ( %rsp) 16 . shellcode_stack_vaddr + SHELLCODE_STACK_SIZE : ( 4096 ), 1 . 8 , , retq, - . - :


 - sub $8,%rsp + sub $16,%rsp /*   */ mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax 

, %rsp 16 pthread_create(). SIGSEGV, — pthread_create() , .



, - , clone():


  static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, /*   **  */ - shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8); if (!shell_tid) return -1; return 0; } 

ptrace() SIGSTOP, :


 int ignore_thread_stop(pid_t pid) { return wait_for_process_stop(pid, SIGSTOP); } 

. ptrace():


 void resume_thread(pid_t pid) { ptrace(PTRACE_CONT, pid, 0, 0); } 


, , , exit(). waitpid(). — CLONE_THREAD wait() ,— PTRACE_O_TRACECLONE, :


 int wait_for_process_exit(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (!WIFEXITED(status)) return -1; return WEXITSTATUS(status); } 

pthread , , pthread_join() pthread , . , — . , , .


Memória livre


, - . , - , munmap():


 void remote_munmap(pid_t pid, unsigned long syscall_insn_vaddr, unsigned long addr, size_t len) { perform_syscall(pid, syscall_insn_vaddr, __NR_munmap, 2, (long) addr, (long) len); } static void unmap_shellcode() { remote_munmap(target, syscall_ret_vaddr, shellcode_text_vaddr, SHELLCODE_TEXT_SIZE); remote_munmap(target, syscall_ret_vaddr, shellcode_stack_vaddr, SHELLCODE_STACK_SIZE); } 

, , , — ptrace() . (, SIGSTOP), , ( ):


 int stop_thread(pid_t pid) { if (kill(pid, SIGSTOP) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; } 


, , . PTRACE_DETACH:


 int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; } 


, . , . , .


Conclusão


? . , , .


demonstração de injeção no Gnome Control Center


Linux . GTK+ . , make:


 libpayload.so: payload.c $(CC) $(CFLAGS) $(shell pkg-config --cflags --libs gtk+-3.0) -shared -o $@ $< 

entry() GTK- — GTK UI , :


 #include <glib.h> #include <gtk/gtk.h> static gboolean actual_entry(gpointer _arg) { /*       : */ hook_gtk_entry_constructor(); /*   FALSE,       */ return FALSE; } void entry(void) { /*    -,   */ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, actual_entry, NULL, NULL); } 

, GTK , GtkEntry . "input-purpose" . «», , .


GTK glib — — GtkEntry . constructed(), . :


 static void (*old_gtk_entry_constructed)(GObject *object); static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); /*    */ old_gtk_entry_constructed(object); /*    ,  ,   entry */ } static void hook_gtk_entry_constructor(void) { /*     GtkEntry */ GTypeClass *entry_type_class = g_type_class_peek(GTK_TYPE_ENTRY); GObjectClass *entry_object_class = G_OBJECT_CLASS(entry_type_class); /* *     "constructed"     . */ old_gtk_entry_constructed = entry_object_class->constructed; entry_object_class->constructed = new_gtk_entry_constructed; } 

GtkEntry :


  • , ,

, GtkEntry , , . , :


 static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); old_gtk_entry_constructed(object); /*       */ g_signal_connect(entry, "notify::input-purpose", G_CALLBACK(input_purpose_changed), NULL); /*      */ g_signal_connect(entry, "icon-press", G_CALLBACK(icon_pressed), NULL); /*      */ g_signal_connect(entry, "icon-release", G_CALLBACK(icon_released), NULL); } 

. , . .


 static void input_purpose_changed(GtkEntry *entry) { GtkInputPurpose purpose = gtk_entry_get_input_purpose(entry); if (purpose == GTK_INPUT_PURPOSE_PASSWORD) { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); } else { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, NULL); } } 

: , , , - , :


 static void icon_pressed(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-add"); } static void icon_released(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); } 

Isso é tudo.


GitHub (GPLv2).


, . gdb :


 $ gdb --pid $(pgrep target) \ --batch \ -ex 'compile file -raw shell-code.c' 

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


All Articles