Como determinar se um ponteiro está em rodata [duplicado]

Dec 08 2020

Posso saber se um ponteiro está na seção rodata de um executável? Como em, editar os dados desse ponteiro causaria uma interceptação do sistema em tempo de execução.

Exemplo (usando um ponteiro de caractere C):

void foo(char const * const string) {
    if ( in_rodata( string ) ) {
        puts("It's in rodata!");
    } else {
        puts("That ain't in rodata");
    }
}

Agora eu estava pensando que, talvez, pudesse simplesmente comparar o ponteiro com a seção rodata.

Algo na linha de:

if ( string > start_of_rodata && string < end_of_rodata ) {
    // it's in rodata!
}

Este é um plano / ideia viável? Alguém tem ideia de como eu poderia fazer isso?

(Existe alguma informação do sistema de que alguém possa precisar para responder a isso?) Estou executando o programa em uma plataforma Linux.

Respostas

Kubahasn'tforgottenMonica Dec 08 2020 at 12:04

Duvido que possa ser portátil

Se você não quiser mexer com scripts de linker ou usando APIs de consulta de mapa de memória específicas da plataforma, uma abordagem de proxy é bastante portátil em plataformas com proteção de memória, se você quiser saber apenas se o local é gravável, somente leitura , ou nenhum. A ideia geral é fazer um teste de leitura e um teste de gravação. Se o primeiro for bem-sucedido, mas o segundo falhar, é provável que .rodataseja um segmento de código. Isso não diz "é rodata com certeza" - pode ser um segmento de código ou alguma outra página somente leitura, como mapeamento de memória de arquivo somente leitura que tem cópia na gravação desabilitada. Mas isso depende do que você tinha em mente para este teste - qual era o propósito final.

Outra advertência é: para que isso seja, mesmo que remotamente seguro, você deve suspender todos os outros threads no processo ao fazer este teste, pois há uma chance de você corromper algum estado ao qual o código em execução em outro thread pode se referir. Fazer isso de dentro de um processo em execução pode gerar casos difíceis de depurar que irão parar de espreitar e aparecer durante uma demonstração do cliente. Portanto, em plataformas que suportam isso, é sempre preferível gerar outro processo que irá suspender o primeiro processo em sua totalidade (todos os threads), investigá-lo, gravar o resultado no espaço de endereço do processo (em alguma variável de resultado), retomar o processo e se encerrar. Em algumas plataformas, não é possível modificar o espaço de endereço de um processo de fora e, em vez disso, você precisa suspender o processo na maior parte ou completamente, injetar um thread de teste, suspender os outros threads restantes, deixar o probe fazer seu trabalho, escrever uma resposta para alguma variável combinada termina e depois retoma todo o resto da segurança de um processo externo.

Para simplificar, o exemplo a seguir presumirá que tudo é feito de dentro do processo. Mesmo que exemplos autocontidos "totalmente capazes" que funcionam em processo cruzado não sejam muito longos, escrever essas coisas é um pouco entediante, especialmente se você quiser curto, elegante e pelo menos correto - imagino um dia realmente inteiro de trabalhos. Então, em vez disso, farei alguns esboços e deixarei que você preencha os espaços em branco (ha).

janelas

Exceções estruturadas são lançadas, por exemplo, devido a falhas de proteção ou divididas por zero. Para realizar o teste, tente uma leitura do endereço em questão. Se for bem-sucedido, você sabe que é pelo menos uma página mapeada (caso contrário, ele lançará uma exceção que você pode detectar). Em seguida, tente escrever lá - se isso falhar, é somente leitura. O código é quase enfadonho:

static const int foo;
static int bar;

#if _WIN32
typedef struct ThreadState ThreadState;
ThreadState *suspend_other_threads(void) { ... }
void resume_other_threads(ThreadState *) { ... }

int check_if_maybe_rodata(void *p) {
  __try {
    (void) *(volatile char *)p;
  } __finally {
    return false;
  }
  volatile LONG result = 0;
  ThreadState *state = suspend_other_threads();
  __try {
    InterlockedExchange(&result, 1);
    LONG saved = *(volatile LONG*)p;
    InterlockedExchange((volatile LONG *)p, saved);
    InterlockedExchange(&result, 0); // we succeeded writing there
  } __finally {}
  resume_other_threads(state);
  return result;
}

int main() {
  assert(check_if_maybe_rodata(&foo));
  assert(!check_if_maybe_rodata(&bar));
}
#endif

Suspender os encadeamentos requer percorrer a lista de encadeamentos e suspender cada encadeamento que não seja o atual. A lista de todos os threads suspensos deve ser criada e salva, para que mais tarde a mesma lista possa ser percorrida para retomar todos os threads.

Certamente existem ressalvas, e os threads do WoW64 têm sua própria API para suspensão e retomada, mas é provavelmente algo que, em circunstâncias controladas, funcionaria bem.

Unix

A ideia é aproveitar o kernel para verificar o ponteiro para nós "no comprimento do braço" para que nenhum sinal seja lançado. O tratamento de sinais POSIX que resultam de falhas de proteção de memória requer a correção do código que causou a falha, inevitavelmente forçando você a modificar o status de proteção da memória do código . Não é tão bom. Em vez disso, passe um ponteiro para um syscall que você sabe que deve ter sucesso em todas as circunstâncias normais para ler do endereço apontado - por exemplo /dev/zero, abrir e gravar naquele arquivo a partir de um buffer apontado pelo ponteiro. Se isso falhar EFAULT, é porque buf [estar] fora do seu espaço de endereçamento acessível. Se você não consegue nem ler daquele endereço, não .rodatatenho certeza.

Em seguida, faça o inverso: de um aberto /dev/zero, tente um readpara o endereço que você está testando. Se a leitura for bem-sucedida, não eram dados somente leitura. Se a leitura falhar com EFAULTisso, provavelmente significa que a área em questão foi somente leitura, já que a leitura foi bem-sucedida, mas a gravação não.

Em todos os casos, seria mais preferível usar APIs de plataforma nativa para testar o status de mapeamento da página na qual o endereço que você tenta acessar reside, ou ainda melhor - para percorrer a lista de seções do executável mapeado (ELF no Linux , PE no Windows) e ver exatamente o que foi para onde. Não é de alguma forma garantido que em todos os sistemas com proteção de memória a .rodataseção ou seu equivalente será mapeado somente para leitura, portanto, a imagem do executável mapeada no processo em execução é a autoridade final. Isso ainda não garante que a seção atualmente seja mapeada como somente leitura. Uma mprotectchamada ou semelhante poderia tê-lo alterado, ou partes dele, para serem graváveis, até mesmo modificado e, em seguida, talvez alterado de volta para somente leitura. Você então teria que fazer a soma de verificação da seção se o formato do executável fornecer tais dados, ou mmapo mesmo binário em algum outro lugar na memória e comparar as seções.

Mas sinto o cheiro de um problema XY: o que você está realmente tentando fazer? Quero dizer, com certeza você não quer apenas verificar se um endereço é .rodataválido por curiosidade. Você deve ter alguma utilidade para essas informações, e é esse aplicativo que decidirá, em última instância, se essa .rodataverificação deve estar no radar. Pode ser, pode não ser. Com base apenas na sua pergunta, é um sólido "quem sabe?"