Link do meu repositório no GitHub
Introdução
Resolvi escrever esse artigo porque tava sem ideia do que escrever, daí resolvi documentar um COFF Loader que eu tinha escrito um tempo atrás.
Primeiro de tudo
Primeiro de tudo você precisa saber como funciona o processo de compilação de um programa.
A criação de um executável a partir da compilação de um programa tem 3 etapas:
Pré-processamento: interpretação das diretivas do pré-compilador;
Compilação: o compilador gera um assembly a partir da source e cria um object file;
Linking: o processo de linkar os object files e bibliotecas no executável.
O que é COFF?
A definição de COFF é a seguinte:
COFF significa Common Object File Format, é o formato de arquivo gerado pelos compiladores após a fase de geração de código (que normalmente inclui apenas o machine code ou o assembly).
O Common Object File Format (COFF) é um formato pra executáveis, objetos-código (object codes) e shared libraries.
Originário do Unix, a Microsoft criou a sua própria variante do formato COFF.
Os object files do COFF (extensão *.obj
e *.o
) contêm a seguinte estrutura:
Header
;Sections
(seções);Symbols Table
.
As seções podem conter informações de relocalização que especificam como os dados da seção devem ser modificados pelo loader e, em seguida, como os dados da secção devem ser carregados na memória.
Por exemplo, a seção
.text
contém informações que especificam que parte do código deve ser substituída e que parte deve ser referenciada na memória.
A versão modificada do COFF utilizado no Cobalt Strike (vulgo Beacon Object File) integra funções que podem interagir com o beacon do CS.
Analisando o COFF
Em termos gerais, um ficheiro objeto produzido por um compilador, montador ou tradutor representa o ficheiro de entrada do ligador. Após o processo de ligação, é gerado um executável ou uma biblioteca que contém uma combinação de diferentes partes do ficheiro-objeto. O conteúdo do ficheiro-objeto não é diretamente executável, mas é um código relocalizável.
O header do COFF está estruturado em vários campos, como se pode ver abaixo:
Dentro de um código: (clang
)
Analisando os campos
Machine
: Define a arquitetura da maquina.NumberOfSections
: Número de seções que o arquivo possui.TimeDateStamp
: Unix timestamp.SizeOfOptionalHeader
: Tamanho do Optional Header.Characteristics
: Atributos do arquivo.
A ideia de um COFF Loader seria basicamente navegar pelo conteudo do COFF e extrair o assembly juntamente com os dados de relocação e depois efetuar as relocações.
Ao analisar o COFF, as seguintes etapas devem ser seguidas
Obter o mapeamento do objeto de arquivo COFF
Obter um ponteiro para o cabeçalho COFF
Alocar memória extra para estruturas internas de análise
Analisar todas as seções COFF, incluindo dados e relocações
Alocar memória extra para a criação da tabela de símbolos interna
Analisar e salvar toda a tabela de símbolos
Resolver endereços de símbolos na memória
Corrigir realocações
Criando o COFF Loader
Agora chegou a hora de criar o loader, primeiro vou começar pelas estruturas
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
Importando as libs necessarias obvio, agora vou implementar as estruturas do COFF
typedef struct
{
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} coffee; // <-- COFF header
typedef struct
{
uint32_t VirtualAddress;
uint32_t SymbolTableIndex;
uint16_t Type;
} realoc; // <-- realocacao de memoria
typedef struct
{
char Name[0x08];
uint32_t VirtualSize;
uint32_t VirtualAddress;
uint32_t SizeOfRawData;
uint32_t PointerToRawData;
uint32_t PointerToRelocations;
uint32_t PointerToLinenumbers;
uint16_t NumberOfRelocations;
uint16_t NumberOfLinenumbers;
uint32_t Characteristics;
} sectionc; // <-- secoes do COFF
Agora vou criar umas funções para a realocação
void *alocavirt(size_t tam) {
void *mem = VirtualAlloc(NULL, tam, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
return(mem);
}
void parsec(FILE *fpp, void *rd, uint32_t offset, uint32_t tam) {
fseek(fpp, offset, SEEK_SET);
fread(rd, tam, 0x01, fpp);
}
void realoc(void *addrb,
realoc *vaddr,
int cntfor,
uint32_t offset) {
for(int i=0x00; i<cntfor; i++) {
realoc *reloc = &vaddr[i];
uint32_t *vddrlc = (uint32_t*)((uint8_t*)addrb + offset +
reloc->VirtualAddress);
*vddrlc += (uint32_t)(addrb);
}
}
a função alocavirt
serve pra carregar uma seção do COFF na memória com o PAGE_EXECUTE_READWRITE,
a função parsec
serve pra handling, e a realoc
é pra mapear cada seção do COFF pra memória allocada com base no seu Virtual Address.
Agora completando o código:
int main(int argc, char *argv[]) {
if(argc != 0x02) {
printf("%s [coff]\x0d\x0a", argv[0x00]);
return(0x01);
}
DWORD memoria;
void *baddr = alocavirt(0x10000);
FILE* file = fopen(argv[0x01], "rb");
coffee header;
fread(&header, sizeof(coffee), 0x01, file);
sectionc *ssemllc = malloc(header.NumberOfSections*sizeof(sectionc));
fread(ssemllc, sizeof(sectionc), header.NumberOfSections, file);
for(int i=0x00; i<header.NumberOfSections; i++) {
sectionc *section = &ssemllc[i];
void *sectionaddr =
(void*)((uintptr_t)baddr + section->VirtualAddress);
parsec(file,
sectionaddr,
section->PointerToRawData,
section->SizeOfRawData);
if(section->NumberOfRelocations>0x00) {
fseek(file, section->PointerToRelocations, SEEK_SET);
realoc *iraa =
malloc(section->NumberOfRelocations*sizeof(realoc));
fread(iraa,
sizeof(realoc),
section->NumberOfRelocations,
file);
realoc(baddr,
iraa,
section->NumberOfRelocations,
section->VirtualAddress);
free(iraa);
}
}
VirtualProtect(baddr, 0x10000, PAGE_EXECUTE_READ, &memoria); //0x40
entry ent =
(entry)(baddr+ssemllc[0x00].VirtualAddress);
ent();
free(ssemllc);
fclose(file);
VirtualFree(baddr, 0x00, MEM_RELEASE);
return(0x00);
}
TL;DR: Essa parte é pra abrir um COFF, ler ele, formatar os headers, aloca a memória e carrega cada seção, depois aplica realocações com base no NumberOfRelocations
em cada header de seção e salta pro entrypoint pra execução, realizando uma execução in-memory.
Código Final: kaIIsyms/caffeine
Bom, eu sei que o código ta meio confuso e talvez mal explicado, mas como eu sempre digo: meus artigos não são voltados para o público. Se eu entendo, está bom.