Buscar

Desvendando o Universo H4ck3r - Fabrizio Vesica

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 321 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 321 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 321 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

Capítulo	1
Introdução	..............................................................	5
Entendendo	o	termo	hacker
................................................................6
0	que	é	programação?
........................................................................8
0	que	significa	explorar	a	vulnerabilidade	de	um	programa	..........	12
Capítulo	2
As	técnicas	dos	hackers:	Buffer	Overflow	no	Stack
As	técnicas	gerais	utilizadas	nos	exploits	........................................
16
Sobre	as	permissões	de	acesso	a	arquivos	.....................................	16
Entendendo	o	funcionamento	da	memória	RAM	............................
18
Entendendo	o	funcionamento	das	variáveis	....................................
19
0	Buffer	Overflow
..............................................................................23
Criando	exploits	sem	código	de	exploit	...........................................
33
Explorando	as	variáveis	do	ambiente	de	execução	...................	38
Capítulo	3
As	técnicas	dos	hackers:	Buffer	Overflow	em	Heap	e	BSS
Sobre	os	overflows	baseados	em	heap	e	BSS	................................	50
Capítulo	4
As	técnicas	dos	hackers:	format	string	...............	69
Sobre	os	exploits	baseados	em	format	string	.................................	70
Vulnerabilidade	do	format	string	......................................................
77
Realizando	a	leitura	de	dados	a	partir	de	endereços	de	memória
arbitrários	..............................................	79
Realizando	a	escrita	de	dados	em	endereços	de	memória	arbitrários
.............................................	81
Sobre	o	DPA	-	Direct	Parameter	Access	..........................................
91
Capítulo	5
As	técnicas	dos	hackers:.dtors	e	GOT	..................	97
Entendendo	os	dtors	........................................................................98
Sobrescrevendo	a	GOT	(Global	Offset	Table)	.............................
106
Capítulo	6
Redes	e	segurança	...............................................111
Introdução	........................................................................................
112
Sobre	redes	e	networking	...............................................................
112
Conhecendo	o	modelo	Open	System	Interconnection	.................	112
Algumas	informações	importantes	sobre	as	camadas	do	modelo	OSI
............................................................	117
	
	
As	palavras	hacker	e	hacking	são	geralmente	associadas	a	ações
negativas,	 como	 vandalismo	 eletrônico,	 espionagem	 industrial,
interceptação	 de	 informações	 sigilosas	 e	 particulares,	 para	 citar	 as
mais	 comuns.	 De	 fato,	 hoje	 em	 dia	 o	 termo	 hacker	 é	 empregado
para	definir	 criminosos	que	utilizam	os	computadores	para	 fins	de
estelionato,	roubo	e	outros	atos	prejudiciais	às	suas	vítimas.
Mas,	 pelo	 menos	 em	 suas	 origens,	 o	 hacking	 consistia	 em
encontrar,	 em	 determinado	 contexto,	 possibilidades	 alternativas	 e
utilizá-las	de	maneira	 criativa	 para	 resolver	 problemas:	 um	hacker
não	 trabalhava	 para	 infringir	 a	 lei,	 pelo	 contrário,	 sua	 tarefa	 era
assegurar-se	que	 fosse	 respeitada,	monitorando	o	 tráfego	de	dados
nas	redes,	observando	os	potenciais	ataques	a	sistemas	críticos	etc.
Hoje	em	dia	o	termo	hacker	é	usado	para	identificar	tanto	aqueles
que	 escrevem	 linhas	 de	 códigos	 quanto	 os	 que	 aproveitam	 suas
vulnerabilidades	e	falhas	utilizando	exploits.
Saiba	mais...
No	jargão	da	segurança	da	informação,	um	exploit	é	um
programa	de	computador,	uma	porção	de	dados	ou	uma
seqüência	de	comandos	que	se	aproveita	das	vulnerabilidades	de
um	sistema	computacional	-	como	o	próprio	sistema	operacional
ou	serviços	de	interação	de	protocolos,	como	por	exemplo,	os
servidores	Web.	São	geralmente	elaborados	por	hackers	como
programas	de	demonstração	das	vulnerabilidades,	a	fim	de	que
as	falhas	sejam	corrigidas,	ou	por	crackers	com	objetivo	de
ganhar	acesso	não	autorizado	a	sistemas.	Por	isso	muitos
crackers	não	publicam	seus	exploits,	conhecidos	como	Odays.
Até	meados	dos	anos	90,	acreditava-se	que	os	exploits
exploravam	exclusivamente	problemas	em	aplicações	e	serviços
para	plataforma	Unix.	A	partir	do	final	da	década,	especialistas
demonstraram	a	capacidade	de	explorar	vulnerabilidades	em
plataformas	de	uso	massivo,	por	exemplo,	sistemas	operacionais
Win32	(Windows	9x,	NT,	2000	e	XP).	Como	exemplo,	temos	o
CodeRed,	o	MyDoom,	o	Sasser	em	2004	e	o	Zotob	em	2005.
Para	que	um	exploit	possa	atacar,	o	sistema	precisa	ter	uma
vulnerabilidade,	ou	seja,	um	meio	de	comunicação	com	a	rede
que	possa	ser	usado	para	entrar	no	sistema,	uma	porta	ou	uma
console.
Um	exploit	muito	usado	é	o	sistema	RPC	do	Windows:	o	usuário
localiza	 a	 porta	 e	 envia	 para	 a	 porta	 RPC	 uma	 seqüência	 de
bytes,	ao	serem	recebidos	eles	são	interpretados	pelo	servidor,	o
que	 causam	propositadamente	uma	pane	no	 sistema,	 que	passa
então	 uma	 seqüência	 de	 ordens	 para	 controlar	 a	 CPU.	 Assim,
essa	seqüência	de	informações	toma	conta	do	PC	e	abre-o	para	o
hacker	que	aguarda	na	outra	ponta.
No	 sistema	 Linux,	 quando	 existem	 vulnerabilidades	 elas	 são
sempre	publicadas,	como	já	ocorreu	no	sistema	Apache,	Samba
ou	 MySQL,	 que	 também	 apresentam	 vulnerabilidades	 e
possibilitam	o	controle	do	PC	por	um	hacker	remoto.
Embora	os	dois	tipos	de	hackers	(desenvolvedores	e	exploradores
de	falhas)	trabalhem	com	finalidades	bem	diferentes,	ambos
utilizam	técnicas	parecidas	de	problem	solving	(resolução	de
problemas).	Essas	técnicas	consistem	em	descobrir	e	desenvolver
uma	solução	inteligente	para	um	determinado	problema,	evitando	os
esquemas	mais	óbvios	e	tradicionais.	Isso	envolve	o	uso	das	regras
no	computador	de	maneira	absolutamente	imprevista,	obtendo
resultados	que,	aos	olhares	menos	experientes,	parecem	mágicas.
Em	vista	disso,	um	hack	nada	mais	é	que	uma	solução	criativa	e
diferente	 dos	 esquemas	 tradicionais	 para	 resolver	 um	problema	de
maneira	 eficiente.	 Os	 desenvolvedores	 utilizam	 os	 hacks	 para
impedir	 ataques	 aos	 seus	 sistemas;	 já	 os	 hackers	 exploradores	 de
vulnerabilidades	 criarão	hacks	para	 burlar	 os	 sistemas	de	proteção
criados	pelos	desenvolvedores.
Em	 função	 do	 incrível	 aumento	 da	 potência	 de	 cálculos	 dos
computadores	 modernos,	 houve	 uma	 inversão	 de	 tendência	 no
desenvolvimento	dos	códigos	de	programas:	se	nas	décadas	de	1970
e	1980	se	trabalhava	para	criar	códigos	mais	enxutos	possíveis	para
economizar	cálculos	do	processador,	hoje	os	desenvolvedores	visam
criar	 programas	 no	 menor	 tempo	 possível,	 mesmo	 que	 isso
comporte	 a	 criação	 de	 códigos	 inutilmente	 extensos.	 De	 fato,
trabalhar	 horas	 e	 horas	 na	 lapidação	 de	 um	 código	 não	 apresenta
nenhuma	 vantagem	 econômica,	 pois	 a	 diferença	 em	 termos	 de
tempo	de	execução	do	programa	se	resume	em	poucos	milésimos	de
segundo.
Assim,	 a	 elegância	 dos	 programas	 passou	 a	 ser	 um	 aspecto
secundário,	 ou	 melhor,	 insignificante,	 mas	 não	 para	 os	 hackers.
Esses	 apreciadores	 da	 programação	mais	 pura	 não	 visam	 o	 lucro,
mas	 sim	 criam	 códigos	 incrivelmente	 curtos	 e	 eficientes	 para
conseguir	extrair	de	seus	velhos	8086	e	Commodore	64	até	o	último
bit	 de	 funcionalidade.	 Imagine	 o	 que	 significa	 para	 um	 hacker
conseguir	driblar	com	seu	velho	286	o	 sistema	de	proteção	de	um
mega-servidor	que	adota	a	tecnologia	mais	avançada	disponível	no
mercado!
Fica	claro	que	o	conhecimento	da	programação,	desde	sua	lógica
até	a	criação	de	linhas	de	código,	é	imprescindível	para	quem	deseja
saber	mais	sobre	o	mundo	dos	hackers	e,	quem	sabe,	se	tornar	um.
O	que	é	programação?
Programação	é	um	conceito	bastante	simples.	De	maneira	geral,
um	programa	nada	mais	é	que	uma	seqüência	de	instruções	escritas
em	uma	linguagem	específica.	Os	programas	são	usados	em	todos
os	contextos	da	sociedade	atual	e	mesmo	aqueles	que	dizem	detestar
a	tecnologia	os	empregam	(ou	são	vítimas	deles)	todos	os	dias:	as
indicaçõespara	chegar	a	um	endereço,	as	receitas	de	cozinha,	as
regras	de	um	jogo	de	futebol,	são	exemplos	de	programas	que
executamos	no	nosso	dia-a-dia.
Veja	um	exemplo:
•siga	 em	 frente	 na	 Avenida	 São	 Paulo	 em	 direção	 norte	 até	 a
primeira	rotatória;
•vire	à	direita	na	Rua	Dom	Pedro	II	e	mantenha-se	à	esquerda;
•prossiga	até	o	 semáforo:	 se	a	 rua	à	esquerda	estiver	 interditada,
continue	 reto	 e	 vire	 à	 esquerda	 no	 segundo	 semáforo,	 caso
contrário	vire	à	esquerda	no	primeiro	semáforo;
•no	 cruzamento	 com	 a	Avenida	Castelo	Branco,	 vire	 à	 direita	 e
siga	reto	até	o	número	1.504.
Basta	conhecer	e	entender	o	português	para	poder	seguir	essas
simples	instruções	até	chegar	ao	endereço	desejado.
Os	 computadores,	 porém,	 não	 utilizam	 a	 nossa	 linguagem,
portanto,	 não	 entenderiam	 as	 instruções	 descritas	 nos	 tópicos	 do
exemplo:	 para	 que	 um	 computador	 execute	 uma	 seqüência	 de
comandos	 é	 preciso	 escrever	 as	 instruções	 utilizando	 um	 idioma
específico,	 chamado	 linguagem	 de	 máquina.	 Trata-se	 de	 uma
linguagem	extremamente	complexa,	constituída	por	uma	seqüência
de	bits	e	bytes	brutos	incompreensíveis	para	a	quase	totalidade	das
pessoas;	além	disso,	dependendo	da	arquitetura	do	computador,	essa
linguagem	pode	variar:	para	escrever	um	programa	na	linguagem	de
máquina	para	processadores	x86	por	exemplo,	é	preciso	conhecer	o
valor	 associado	 a	 cada	 comando,	 de	 que	 maneira	 as	 instruções
interagem	entre	elas	e	outros	inúmeros	detalhes.
Para	resolver	esse	problema	foi	criado	um	intérprete,	ou	seja,	um
sistema	 que	 traduz	 códigos	 mais	 simples	 em	 instruções	 na
linguagem	 de	 máquina.	 Trata-se	 do	 Assembler,	 uma	 linguagem
menos	 hostil	 que	 utiliza	 nomes	 para	 as	 diversas	 instruções	 e
variáveis	no	lugar	de	valores	binários.
Todavia,	 até	 mesmo	 o	 Assembier	 está	 longe	 de	 ser	 uma
linguagem	intuitiva	e	de	fácil	compreensão.	Os	nomes	de	comandos
são	 complexos	 e	 esquisitos	 e	 variam	 de	 acordo	 com	 a	 plataforma
para	a	qual	se	está	programando,	isto	é,	a	linguagem	Assembler	para
processadores	 Intel	 é	diferente	da	usada	para	processadores	Sparc.
Assim,	 programas	 escritos	 para	 serem	 executados	 em	 plataformas
x86	não	funcionarão	em	computadores	baseados	em	sistemas	Sparc.
Por	esse	motivo,	 surgiu	a	necessidade	de	criar	um	outro	 tipo	de
intérprete	 que	 fosse	 capaz	 de	 resolver	 esses	 problemas	 de
compatibilidade.	Nasceram,	então,	os	Compiladores,	que	convertem
uma	linguagem	de	alto	nível	em	linguagem	de	máquina.
As	 linguagens	 de	 alto	 nível	 são	 muito	 mais	 compreensíveis	 do
que	 o	 Assembler	 e	 podem	 ser	 convertidas	 em	 vários	 tipos	 de
linguagens,	 de	 acordo	 com	 a	 arquitetura	 do	 computador	 que	 irá
executar	o	programa.	Em	outras	palavras,	se	um	programa	é	escrito
empregando	 uma	 linguagem	 de	 alto	 nível,	 ele	 será	 escrito	 apenas
uma	vez,	mas	poderá	ser	compilado	por	um	compilador	específico
para	 cada	 arquitetura.	São	 exemplos	de	 linguagens	de	 alto	 nível	 o
C#,	C++,	Fortran,	Java,	PHP,	ASP,	Delphi,	Visual	Basic	e	outras.
Como	dissemos,	um	programa	escrito	em	uma	linguagem	de	alto
nível	é	muito	mais	intuitivo	do	que	um	escrito	em	Assembler,	mas,
mesmo	assim,	deve	seguir	normas	exatas	de	sintaxe,	caso	contrário
o	compilador	não	conseguirá	convertê-las.
Os	programadores	utilizam	também	outro	tipo	de	linguagem,	que
chamam	de	pseudocódigo.	Trata-se	de	uma	seqüência	de	instruções
escritas	 na	 linguagem	 humana,	 porém,	 organizadas	 em	 uma	 estru
tura	praticamente	 idêntica	à	de	uma	linguagem	de	programação	de
alto	nível.	0	pseudocódigo	não	é	usado	ao	escrever	o	código	de	um
programa,	mas	é	muito	útil	ao	programador	para	ordenar	as	idéias	e
definir	um	esboço	do	que	será	o	programa.
Veja,	no	exemplo	a	seguir,	como	ficaria	a	seqüência	de	indicações
descritas	no	início	do	tópico:
Cada	instrução	é	especificada	utilizando	uma	linha	de	código	e	as
linhas	são	divididas	em	estruturas	de	controle	separadas.
No	 exemplo	 anterior,	 resumimos	 as	 instruções	 de	 maneira
extremamente	 simples,	mas,	 na	 verdade,	 a	 série	 de	 ações	 a	 serem
realizadas	 para	 cada	 etapa	 pode	 ser	 muito	 mais	 complexa.	 Por
exemplo,	para	 a	 instrução	vire	 à	 esquerda	 no	primeiro	 semáforo	 é
necessário	 executar	 muitas	 operações	 como	 localizar	 o	 semáforo,
diminuir	 a	 velocidade,	 reduzir	 a	 marcha,	 acionar	 a	 seta,	 girar	 o
volante	no	sentido	anti-horário,	acelerar,	aumentar	a	marcha	etc.
Há	 casos,	 como	 este,	 em	 que	 uma	 mesma	 seqüência	 de	 ações
pode	 ser	 aplicada	 em	 mais	 de	 uma	 circunstância,	 como	 por
exemplo,	 todas	 as	 vezes	 que	 for	 preciso	 virar	 à	 esquerda	 em
correspondência	 de	 um	 determinado	 ponto	 de	 referência.	 Para
simplificar	 o	 código	 e	 evitar	 repetir	 as	 mesmas	 instruções	 várias
vezes	no	mesmo	programa,	podemos	criar	uma	função,	ou	seja,	um
conjunto	de	comandos	e	seus	respectivos	argumentos	resumidos	em
um	 único	 comando	 que	 será	 chamado	 no	 momento	 oportuno	 do
programa.
Veja,	a	seguir,	um	exemplo	do	pseudocódigo	de	uma	função:
Assim,	 o	 pseudocódigo	 do	 nosso	 programa	 de	 indicações	 para
chegar	 a	 determinado	 local,	 no	 caso	 à	Avenida	Castelo	Branco	n°
1.504,	ficaria	assim:
No	código	anterior,	destacamos	todas	as	vezes	que	a	função	virar
foi	 chamada	 juntamente	 com	 seus	 argumentos,	 ou	 seja,	 os	 dados
relativos	 à	 direção	 e	 o	 ponto	 de	 referência.	 Sempre	 que	 a	 função
virar	 for	 chamada	no	 código,	 todas	 as	 ações	 em	 seu	 interior	 serão
executadas	com	base	nos	argumentos	especificados	pelo	usuário.
O	que	significa	explorar	a	vulnerabilidade	de	um	programa
Saber	explorar	os	pontos	fracos	de	um	programa	é	um	elemento
essencial	ao	hacking.	Os	programas	são	conjuntos	de	regras	e
instruções	a	serem	executados	em	determinada	ordem	para	informar
ao	computador	o	que	ele	deve	fazer.	Explorar	as	vulnerabilidades
signi-	fica	encontrar	uma	maneira	inteligente	de	fornecer	ao
computador	instruções	diferentes	daquelas	que	o	programa	inicial
foi	criado	para	executar.	Visto	que	um	programa	de	computador
pode	fazer	somente	aquilo	que	foi	especificado	em	seu	código,	as
falhas	na	segurança	são	imperfeições	na	arquitetura	do	programa	ou
do	ambiente	em	que	o	código	será	executado,	devido	a	erros
cometidos	pelos	programadores.	Todavia,	é	preciso	muito
conhecimento,	criatividade	e	inteligência	para	encontrar	essas	falhas
e	escrever	códigos	que	as	usem	com	o	objetivo	de	assumir	o
controle	do	computador.
Não	 faltam	 exemplos	 de	 falhas	 de	 programas	 exploradas	 para
utilizar	computadores	para	outras	finalidades,	e	os	alvos	são	os	mais
diversos:	 de	 agências	 bancárias	 ao	 Pentágono,	 dos	 servidores	 da
NASA	a	 sites	 de	 compras	on-line,	 e	 assim	por	diante.	Basta	 fazer
uma	 rápida	 busca	 na	 Internet	 para	 descobrir	 que	 os	 ataques	 de
hackers	 estão	 em	 crescimento	 espantoso,	 o	 que	 é	 sintoma	 de	 dois
fatores	 preocupantes:	 o	 primeiro	 é	 que,	 por	 razões	 meramente
econômicas,	a	segurança	dos	programas	vem	sendo	deixada	sempre
em	segundo	plano,	priorizando	a	rapidez	na	comercialização,	o	que
favorece	a	vulnerabilidade;	o	segundo	é	que,	devido	à	popularização
dos	 computadores	 e	 ao	 surgimento	 de	 pontos	 de	 acesso	 anônimos
(Lan-houses,	Internet-bars	etc.)	ser	um	hacker	está	se	tornando	cada
vez	mais	fácil	e	menos	perigoso.
	
	
Existem	 nos	 programas	 erros	 e	 falhas,	 geralmente	 recorrentes,
encontrados	 com	as	mesmas	 características	 em	diferentes	 códigos,
assim	 foram	 criadas	 técnicas	 gerais	 que	 permitem	 explorar	 essas
falhas	em	diversas	situações.
Os	 tipos	mais	 comuns	 de	 exploits	 são	 buffer	 overflow	 e	 format
string,	 usados	 para	 assumir	 o	 controle	 da	 execução	 do	 programa
para	em	seguida	 inserir	 trechos	de	código	malignos	com	propósito
de	 executar	 qualquer	 tipo	 de	 comando	 no	 computador	 vítima	 do
ataque.
0	 uso	 desses	 exploits	 pressupõe	 que	 o	 hacker	 possua	 um	 bom
conhecimento	 sobre	 os	 sistemas	 de	 permissãode	 acesso	 aos
arquivos,	 variáveis,	 alocação	 de	 espaço	 na	 memória,	 funções	 e
linguagem	Assembler.
Sobre	as	permissões	de	acesso	a	arquivos
0	Linux	é	um	sistema	operacional	multiusuário	em	que	os
privilégios	absolutos	sobre	o	sistema	são	dados	a	um	usuário
chamado	root.	Há	também	outras	contas	de	usuário	que	podem	ser
dividas	em	grupos	com	privilégios	diferentes.	Cada	usuário	pode
pertencer	a	mais	de	um	grupo	e	cada	grupo	pode	conter	muitos
usuários.	As	regras	que	gerenciam	as	permissões	de	acesso	aos
arquivos	podem	ser	definidas	tanto	para	grupos	quanto	para
usuários	específicos,	para	que	cada	usuário	do	sistema	possa	acessar
somente	os	arquivos	e	pastas	aos	quais	possua	permissão.	Quem
define	as	permissões	de	acesso,	isto	é,	quais	usuários	poderão	abrir
os	arquivos,	é	o	proprietário	dos	arquivos	e	pastas,	ou	seja,	quem	os
criou.
Há	três	tipos	de	permissão	de	acesso	a	arquivos	e	pastas:
•leitura:	permite	que	um	ou	mais	usuários	abram	o	arquivo,	mas
não	será	possível	efetuar	alterações;
•gravação:	 permite	 que	 o	 arquivo	 seja	 aberto,	 alterado	 e	 salvo
pelos	usuários	com	este	privilégio;
•execução:	 permite	 iniciar	 a	 execução	 de	 arquivos	 com	 essa
característica.
Essas	 permissões	 podem	 ser	 concedidas	 ou	 revogadas	 em	 três
campos,	denominados	user,	group	e	other.	0	campo	user	determina
o	 que	 o	 usuário	 poderá	 fazer	 com	 o	 arquivo	 (leitura,	 gravação	 ou
execução),	o	campo	group	define	o	que	os	outros	usuários	do	grupo
poderão	 fazer	 e	 o	 campo	 other	 especifica	 as	 permissões	 para	 os
demais	usuários	do	sistema.
As	 permissões	 são	 representadas	 na	 tela	 pelas	 letras	 r(read=
leitura),	 w(write=escrita/gravação)	 e	 x	 (execute	 =	 executar),
dispostas	em	três	colunas	adjacentes	relacionadas,	respectivamente,
ao	 usuário	 (user),	 ao	 grupo	 (group)	 e	 aos	 outros	 (other).	 Veja	 o
exemplo	a	seguir:
A	 linha	 de	 código	 vista	 no	 exemplo	 indica	 que	 o	 usuário	 tem
permissão	 para	 leitura,	 gravação	 e	 execução	 (-rwx),	 os	 outros
usuários	 do	 mesmo	 grupo	 possuem	 privilégios	 para	 leitura	 e
gravação	(-rw)	e	os	demais	usuários	do	sistema	podem	apenas	ler	e
executar	(-rx).
Em	determinadas	 circunstâncias,	 como	por	 exemplo,	 a	 alteração
da	 senha,	 pode	 ser	 necessário	 conceder	 a	 certo	 usuário	 privilégios
para	 realizar	 uma	 operação	 que	 requeira	 as	 permissões	 do	 usuário
root;	 todavia,	 transformar	 um	 usuário	 comum	 em	 root	 é	 algo
altamente	 desaconselhável	 para	 a	 segurança	 do	 sistema.	 Assim,
podemos	 escrever	 o	 código	 do	 programa	 de	 maneira	 que	 opere
como	se	qualquer	usuário	logado	no	sistema	fosse	root;	desta	forma,
dentro	 do	 ambiente	 do	 programa,	 todos	 os	 usuários	 poderão
executar	as	 tarefas	desejadas	 sem	esbarrar	em	obstáculos	causados
pelas	 permissões	 de	 acesso	 ao	 sistema	 operacional.	 Este	 tipo	 de
permissão	é	 denominado	permissão	 suid	 (do	 inglês	 set	 user	 id,	 ou
seja,	 definir	 a	 identidade	 do	 usuário).	 Quando	 um	 programa	 com
permissão	 suid	 é	 executado,	 o	 euid	 (effective	 user	 id	 -	 identidade
real	 do	 usuário)	 é	 substituído	 pela	 identidade	 do	 proprietário	 do
aplicativo,	 isto	é,	do	usuário	que	o	 instalou	no	sistema.	Da	mesma
forma,	 assim	 que	 o	 programa	 é	 fechado	 o	 user	 id	 volta	 a	 ser	 o
"euid".	 Há	 também	 a	 permissão	 sgid	 (do	 inglês	 Set	 Group	 Id-
Definir	 a	 Identidade	do	Grupo),	 que	opera	 de	maneira	 idêntica	 ao
suid,	 porém,	 altera	 temporariamente	 a	 identidade	 do	 grupo	 de
usuários.
Suponhamos,	 por	 exemplo,	 que	 você	 fez	 o	 logon	 num	 sistema
Linux	e	deseja	alterar	a	sua	senha	de	acesso:	será	preciso	executar
um	programa	específico,	chamado	passwd,	que	pertence	ao	usuário
root	e	cuja	permissão	suid	encontra-se	ativada;	ao	executar	passwd,
o	 seu	 user	 id	 é	 substituído	 temporariamente	 pelo	 id	 root	 para	 que
seja	 possível	 alterar	 a	 senha	 e,	 ao	 terminar,	 é	 restaurado
automaticamente	o	seu	user	id	original.
Programas	 como	 o	 passwd,	 pertencentes	 ao	 usuário	 root	 e	 cuja
permissão	suid	é	ativa,	são	chamados	comumente	de	programas	suid
root.	 Situações	 como	 essa	 são	 um	 prato	 cheio	 para	 o	 hacker	 que
deseja	 desviar	 o	 fluxo	 de	 execução	 do	 programa	 para	 assumir	 o
controle	do	sistema.	Um	programador	que	consiga	modificar	o	fluxo
de	um	programa	suid	root	para	que	possa	inserir	e	executar	nele	um
trecho	 de	 código	malicioso,	 poderá	 fazer	 qualquer	 coisa,	 como	 se
fosse	 o	 usuário	 root.	 Será	 capaz,	 por	 exemplo,	 de	 abrir	 uma	 nova
console	do	sistema	para	acessar	todos	os	recursos	do	sistema,	criar
novas	contas	de	usuários,	alterar	senhas,	acessar	dados	confidenciais
armazenados	no	computador,	gerenciar	o	tráfego	dos	dados	na	rede
etc.
Como	 dissemos,	 um	 programa	 é	 simplesmente	 um	 conjunto	 de
regras	 escritas	 em	 uma	 linguagem	 de	 alto	 nível	 (Delphi,	 Visual
Basic,	 Java,	 C++	 etc.)	 que	 determina	 as	 instruções	 a	 serem
executadas	 pelo	 computador.	 Por	 trabalharem	 em	 linguagens	 tão
distantes	 da	 linguagem	 de	máquina,	 geralmente	 os	 programadores
ignoram	detalhes	como	as	variáveis	de	memória,	stack,	ponteiros	de
execução	e	outros	comandos	de	baixo	nível	que	não	são	visíveis	nas
linguagens	 de	 alto	 nível.	 Assim,	 um	 hacker	 com	 domínio	 da
linguagem	de	máquina	 e	 que	 conheça	 os	 comandos	 resultantes	 da
compilação	de	um	código	escrito	em	linguagem	de	alto	nível,	saberá
exatamente	o	que	o	processador	 irá	 fazer	 e	 estará	 em	condição	de
criar	 um	 código	 que,	 em	 algum	momento	 da	 execução	 do	 código
original,	desvie	o	fluxo	de	execução	para	outro	código	que	ele	criou
e	inseriu	no	programa.	Fica	claro	que	o	conhecimento	das	regras	de
programação	em	baixo	nível	é	indispensável	para	o	hacker.
Entendendo	o	funcionamento	da	memória	RAM
A	memória	de	um	computador	é	constituída	por	um	conjunto	de
bytes	destinados	ao	armazenamento	temporário	de	dados,	esse
armazenamento	é	feito	associando	cada	dado	a	um	endereço
numérico	que	especifica	a	alocação	das	informações	na	memória.	0
acesso	à	memória	é	feito	utilizando	os	endereços	de	alocação,	lendo
ou	gravando	dados	no	endereço	especificado	pelo	processador.
Os	processadores	modernos	baseados	na	 tecnologia	x86	da	 Intel
utilizam	 esquemas	 de	 endereçamento	 de	 dados	 de	 32	 e	 64	 bits,	 o
que	significa	que,	dependendo	da	arquitetura	do	processador,	podem
existir	até	264	endereços	de	memória	possíveis.	As	variáveis	de	um
programa	 são	 determinadas	 posições	 da	 memória	 usadas	 para
armazenar	 informações.	 0	 ponteiro	 é	 um	 tipo	 especial	 de	 variável
usado	para	gravar	os	endereços	da	memória	com	objetivo	de	poder
buscar	as	informações	necessárias.
Durante	 a	 execução	 de	 um	 programa,	 as	 informações	 nele
contidas	 precisam	 ser	 copiadas	 para	 que	 possam	 ser	 usadas	 em
pontos	diferentes.	Entretanto,	copiar	grandes	quantidades	de	dados
em	 vários	 endereços	 é	 uma	 operação	 inviável,	 pois	 ocuparia
inutilmente	muito	espaço	na	memória,	além	disso,	o	gerenciamento
dessa	 movimentação	 dos	 dados	 na	 memória	 seria	 crítico,	 pois
haveria	uma	quantidade	considerável	de	dados	a	serem	alocados,	o
que	 geraria	 uma	 lista	 de	 endereços	 de	memória	muito	 extensa.	 A
solução	desse	problema	são	os	ponteiros	que,	ao	invés	de	copiarem
um	bloco	de	dados,	seu	respectivo	endereço	é	armazenado	em	uma
variável	ponteiro	 (com	 tamanho	de	4	bytes),	 de	modo	que	 sempre
que	o	programa	precisar	daquelas	 informações,	o	ponteiro	 indicará
ao	processador	em	que	local	da	memória	pode	encontrá-las.
Os	 processadores	 também	 possuem	 uma	 memória	 interna,	 de
tamanho	 muito	 pequeno,	 conhecida	 como	 registro.	 Existem	 tipos
especiais	 de	 registros	 cuja	 função	 é	 monitorar	 as	 operações
executadas	durante	a	execução	de	um	código	de	programa.	Um	dos
registros	mais	 conhecidos	 é	 o	 EIP	 (Extended	 Instruction	 Pointer),
que	armazena	os	endereços	das	instruções	que	serão	executados	na
seqüência,	ou	seja,	logo	após	o	comando	que	está	sendo	processadono	momento.	Outros	registros	de	32	bits	usados	como	ponteiros	são
o	EPB	(Extended	Base	Pointer)	e	o	ESP	(Extended	Stack	Pointer),
este	último	é	específico	para	o	gerenciamento	da	parte	superior	do
stack,	 ou	 seja,	 a	 última	 área	 livre	 no	 empilhamento	 dos	 dados	 na
memória.
Entendendo	o	funcionamento	das	variáveis
Ao	programar	em	uma	linguagem	de	alto	nível,	as	variáveis
devem	ser	declaradas	especificando	o	tipo	de	dado	que	irão	armaze
nar;	e	esses	dados	podem	ser	do	tipo	numérico,	alfanumérico	ou	de
estruturas	definidas	pelo	usuário.	Essa	distinção	dos	tipos	de	dados
é	muito	importante,	pois	permite	calcular	o	espaço	de	memória
necessário	a	cada	variável.	Um	número	inteiro,	por	exemplo,	requer
4	bytes	de	espaço,	já	para	um	caractere	de	texto	é	suficiente	apenas
1	byte;	assim,	para	um	número	inteiro	são	reservados	32	bits,
enquanto	para	um	caractere	bastam	8	bits.
As	variáveis	também	podem	ser	declaradas	como	vetores,	isto	é,
uma	 lista	 de	 elementos	 de	 um	mesmo	 tipo	 de	 dados.	 Um	 vetor	 é
geralmente	chamado	buffer	e	um	vetor	que	contenha	dados	do	tipo
alfanumérico	 é	 definido	 como	 string.	 Durante	 a	 execução	 de	 um
código,	 o	 sistema	 usa	 ponteiros	 para	 armazenar	 o	 endereço	 do
primeiro	 elemento	 de	 um	 buffer,	 esses	 ponteiros	 devem	 ser
declarados	utilizando	um	asterisco	(*)	antes	do	nome	da	variável.
0	 gerenciamento	 da	 memória	 dos	 processadores	 que	 usam
arquitetura	 x86	 possui	 uma	 característica	 que	 se	 revelará	 muito
importante	no	futuro:	a	ordem	dos	bytes	nas	words	(palavras)	de	4
bytes,	 conhecido	 no	 jargão	 com	 o	 nome	 de	 little	 indian	 (pequeno
índio),	que	faz	com	que	o	byte	considerado	menos	significativo	seja
o	primeiro	na	ordem.
Vetores	e	bytes	nulos
Em	uma	variável	do	tipo	vetor,	podem	ser	armazenados	10	bytes,
dos	quais	apenas	alguns	são	usados	de	fato.	Por	exemplo,	se
armazenarmos	em	um	vetor	a	palavra	casa,	o	vetor	conterá	10	bytes,
mas	apenas	5	deles	contêm	valores	reais,	enquanto	os	outros	5	serão
bytes	supérfluos.	0	0	(zero),	ou	byte	null	(nulo),	é	usado	como
delimitador	para	encerrar	a	linha,	informado	a	qualquer	instrução
que	trabalhe	com	os	dados	do	vetor	que	deverá	desconsiderar	os
bytes	sucessivos.	Veja	o	exemplo	a	seguir	para	entender	melhor:
Tabela	2.1:	Representação	esquemática	de	uma	variável	vetorial.
0	exemplo	da	Tabela	2.1	mostra	a	representação	esquemática	de
uma	variável	vetorial.	Nela	temos	10	posições	(numeradas	de	0	a	9),
note	que	os	caracteres	da	palavra	casa	ocupam	apenas	as	primeiras
quatro	posições	e	que,	na	quinta	posição,	foi	inserido	um	byte	nulo
(zero).	Isso	fará	com	que	quando	um	trecho	de	código	do	programa
ler	as	informações	contidas	no	vetor	processará	apenas	as	primeiras
quatro	posições,	desconsiderando	os	bytes	das	posições	5	a	9.	0	byte
nulo	atuou	como	um	comando	que	instruiu	o	código	do	programa
para	que	parasse	o	processamento	na	posição	4.
Sobre	as	seções	da	memória	de	programa
A	memória	de	programa	é	dividida	em	cinco	seções:	Text,	Data,
BSS,	Heap	e	Stack.	Cada	seção	representa	uma	porção	específica	da
memória	reservada	a	um	determinado	fim.
A	seção	Text
A	 seção	 Text	 (texto)	 é	 utilizada	 para	 armazenar	 o	 código	 do
programa	 compilado	 na	 linguagem	 de	 máquina;	 a	 execução	 das
instruções	 contidas	 nessa	 seção	 não	 é	 seqüencial,	 devido	 às
estruturas	de	controle	e	às	funções	de	alto	nível,	que	em	Assembler
são	compiladas	em	branch,	jump	e	call.	Quando	um	programa	está
sendo	 executado,	 o	 ponteiro	 EIP	 é	 posicionado	 na	 primeira
instrução	da	seção	Text,	a	partir	daí	o	processador	executa	um	loop
(repetição)	que	faz	o	seguinte:
a.primeiramente	lê	as	instruções	apontadas	pelo	EIP;
b.depois	adiciona	o	comprimento	(em	bytes)	da	instrução	ao	EIP;
c.então	executa	a	instrução	lida	no	primeiro	passo;
d.por	fim,	retorna	ao	início	desse	processo.
A	instrução	pode	ser	um	jump	ou	um	call	que	faz	com	que	o
ponteiro	EIP	seja	deslocado	para	outro	endereço	de	memória;	o
processador	não	tomará	parte	dessa	transferência,	pois	sabe	que	a
execução	não	é	seqüencial.	Se,	por	exemplo,	o	EIP	for	deslocado	no
passo	(c),	o	processador	retornará	de	qualquer	forma	ao	passo	(a)	e
lerá	a	nova	instrução	apontada	pelo	EIP.
Na	 seção	Text	 a	 permissão	 para	 a	 gravação	 é	 desabilitada,	 pois
esta	porção	de	memória	é	destinada	a	armazenar	apenas	o	código	do
programa	 na	 linguagem	 de	 máquina,	 e	 não	 a	 eventuais	 variáveis
usadas	no	decorrer	da	execução	do	código.	Essa	é	uma	maneira	de
impedir	que	alguém	possa	alterar	o	código	do	programa.
As	seções	Data	e	BSS
As	seções	Data	e	BSS	são	reservadas	para	o	armazenamento	das
variáveis	 globais	 e	 estáticas;	 a	 seção	 Data	 é	 preenchida	 com	 as
variáveis	 globais	 (inicializadas	 no	 começo	 do	 código),	 strings	 e
outras	 constantes	 que	 serão	 usadas	 durante	 todo	 o	 processo	 de
execução	 do	 programa;	 a	 seção	 BSS	 contém	 as	 partes
correspondentes	 não	 inicializadas.	 Estas	 seções	 permitem	 a
alteração	 de	 seus	 conteúdos,	 porém,	 têm	 seus	 tamanhos	 fixos	 e
predefinidos.
A	seção	Heap
A	seção	Heap	é	usada	para	as	demais	variáveis	do	programa.	Ao
contrário	das	seções	Data	e	BSS,	seu	tamanho	não	é	fixo	e	pode	ser
alterado	 para	 aumentá-lo	 ou	 reduzi-lo	 de	 acordo	 com	 as
necessidades.
Toda	 a	 porção	 de	 memória	 da	 seção	 Heap	 é	 gerenciada	 por
algoritmos	 de	 alocação	 e	 desalocação	 que	 reservam	 endereços	 de
memória	quando	necessário	 e	os	desocupam	quando	não	 são	mais
utilizados,	tornando-os	disponíveis	para	novos	dados.
A	seção	Stack
A	seção	Stack	também	pode	ter	seu	tamanho	alterado	e	é	utilizada
para	 armazenar	 dados	 contextuais	 gerados	 durante	 a	 chamada	 de
funções	 no	 interior	 do	 código.	 Quando	 um	 programa	 chama	 uma
função,	 esta	 possuirá	 seu	 próprio	 conjunto	 de	 variáveis	 que	 serão
transferidas	 e	 o	 código	 da	 função	 será	 alocado	 em	 uma	 posição
diferente	da	seção	Text.	Devido	à	mudança	do	contexto,	o	ponteiro
EIP	 deverá	 mudar	 de	 posição	 quando	 uma	 função	 for	 chamada.
Então	o	Stack	será	usado	para	gravar	todas	as	variáveis	transferidas
e	 o	 endereço	 do	 ponto	 ao	 qual	 o	 ponteiro	 EIP	 deverá	 retornar	 ao
término	da	execução	da	função.
Tecnicamente	 falando,	 o	 Stack	 é	 definido	 como	 uma	 "estrutura
abstrata	de	dados	usada	com	freqüência"	e	funciona	de	acordo	com
um	esquema	conhecido	com	o	nome	de	"first-in,	 last-out"	 (FILO),
que	determina	que	o	primeiro	valor	inserido	no	Stack	seja	o	último	a
ser	extraído.	Em	outras	palavras,	funciona	como	uma	série	de	caixas
empilhadas,	sendo	que	a	primeira	caixa	apoiada	será	a	última	a	ser
retirada	e	não	se	pode	retirá-la	sem	remover	primeiro	as	que	estão
sobre	 ela.	 Em	 informática,	 se	 usa	 o	 termo	 push	 para	 colocar	 um
dado	no	Stack,	e	o	termo	pop	quando	o	dado	é	retirado.
Como	o	próprio	nome	Stack	(pilha)	indica,	essa	seção	de	memória
é,	de	fato,	uma	estrutura	de	dados	empilhados	uns	sobre	os	outros.	0
registro	ESP	é	usado	para	rastrear	o	endereço	do	último	elemento	do
Stack,	que	muda	em	continuação	na	medida	em	que	se	 realizam	o
push	e	o	pop	dos	dados.
A	característica	FILO	do	Stack	é	muito	útil	para	armazenar	dados
contextuais:	 quando	 uma	 função	 é	 chamada,	 vários	 elementos	 são
inseridos	 no	 Stack	 em	 uma	 estrutura	 denominada	 Stack	 frame.	 0
registro	EBP	(também	chamado	Trame	pointer-	ponteiro	do	frame)
é	 usado	 para	 fazer	 referência	 às	 variáveis	 presentes	 no	 frame	 do
Stack	 atual.	 Cada	 Stack	 frame	 contém	 os	 argumentos	 da	 função
chamada,	 suas	 variáveis	 locais	 e	 dois	 ponteiros	 necessários	 para
restaurar	a	situação	 inicial:	o	 saved	 frame	pointer	 e	o	 endereço	de
alocação	 ao	qual	 o	 código	 deve	 retornar.	 0	 saved	 frame	 pointer	 é
usado	para	redefinir	o	EBP	para	o	seu	valor	anterior	à	chamada	da
função,	enquanto	o	endereço	de	 retorno	 redefine	o	EIP	na	posição
da	situação	sucessiva	à	chamada	da	função.
O	Buffer	Overflow
A	linguagem	de	alto	nível	C,	em	suas	versões	e	derivações,	é	a
mais	utilizada	pelos	hackers	devido	à	suaflexibilidade	para	a
criação	de	programas.
Saiba	Mais...
0	C	é	uma	linguagem	de	programação	de	propósito	geral,
estruturada,	imperativa,	procedural,	de	alto	nível	e	padronizada.
Criada	em	1972	por	Dennis	Ritchie,	nos	laboratórios	Bell,	para
ser	usada	no	sistema	operacional	UNIX.	Desde	então,	espalhou-
se	por	muitos	outros	sistemas	operacionais,	e	tornou-se	uma	das
linguagens	de	programação	mais	usadas.
A	linguagem	C	tem	como	ponto	forte	a	sua	eficiência	e	é	a
linguagem	de	programação	preferida	para	o	desenvolvimento	de
sistemas	e	softwares	de	base,	apesar	de	também	ser	usada	para
desenvolver	programas	de	computador.	É	também	muito	usada
no	ensino	de	ciências	da	computação,	mesmo	não	tendo	sido
projetada	para	estudantes	e	apresentando	algumas	dificul	dades
no	seu	uso.	Outra	característica	importante	do	C	é	sua
proximidade	do	código	de	máquina,	que	permite	que	um
projetista	seja	capaz	de	fazer	algumas	previsões	de	como	o
software	irá	se	comportar,	ao	ser	executado.	0	C	tem	como	ponto
fraco	a	falta	de	proteção	que	dá	ao	programador.	Praticamente
tudo	que	se	expressa	em	um	programa	em	C	pode	ser	executado,
como	por	exemplo,	pedir	o	vigésimo	membro	de	um	vetor	com
apenas	dez	membros.	Os	resultados	são	muitas	vezes	totalmente
inesperados	e	os	erros,	difíceis	de	encontrar.	Muitas	linguagens
de	programação	foram	influenciadas	pelo	C,	sendo	que	a	mais
utilizada	atualmente	é	o	C++,	que	por	sua	vez	foi	uma	das
inspirações	para	o	Java.
0	desenvolvedor	que	programa	utilizando	a	linguagem	C	deve	se
responsabilizar	pela	integridade	dos	dados,	pois	se	deixar	essa	tarefa
por	conta	do	compilador,	os	códigos	resultantes	na	linguagem	de
máquina	serão	extremamente	lentos	e	inutilmente	complexos,
devido	às	inúmeras	verificações	que	seriam	feitas	na	integridade	de
cada	variável	do	ambiente.
A	 simplicidade	 da	 linguagem	 C	 permite	 ampliar	 o	 poder	 de
controle	 do	 programador	 sobre	 o	 código	 e,	 como	 conseqüência,	 a
eficiência	dos	programas.	Por	outro	lado,	essa	flexibilidade	pode	se
tornar	 uma	 verdadeira	 faca	 de	 dois	 gumes,	 pois	 quando	 o
programador	 não	 dedica	 a	 atenção	 necessária	 à	 integridade	 do
código	que	está	escrevendo,	poderá	ocasionar	 inúmeros	problemas
de	vulnerabilidade,	perdas	de	memória	e	buffer	overflow.	Em	outras
palavras,	 uma	 vez	 que	 uma	 variável	 é	 alocada	 em	 determinado
endereço	 da	 memória,	 não	 existem	 mecanismos	 de	 proteção
automática	 que	 garantam	 que	 o	 conteúdo	 da	 variável	 caiba	 no
espaço	 de	 memória	 que	 lhe	 foi	 designado;	 se,	 por	 exemplo,	 o
desenvolvedor	 instruir	 o	programa	para	que	 armazene	10	bytes	de
dados	em	um	endereço	de	memória	com	capacidade	para	apenas	8
bytes,	 esta	 operação	 não	 será	 impedida	 pela	 linguagem	 e	 poderá
provocar	 o	 travamento	 (crash)	 do	 sistema.	 Este	 fenômeno	 é
conhecido	no	âmbito	da	informática	com	o	nome	de	buffer	overflow
ou	 buffer	 overrun,	 pois	 os	 2	 bytes	 de	 dados	 excedentes	 irão
transbordar	 (overflow)	 para	 fora	 do	 espaço	 de	 memória	 alocado,
provocando	 a	 sobrescrita	 de	 dados	 já	 existentes	 nos	 espaços	 de
memória	adjacentes.
Veja,	no	exemplo	a	seguir,	um	código	que	provocaria	um	buffer
overflow:
0	 código	 exibido	 contém	 uma	 função	 chamada	 função	 _
BufferOverflow,	associada	a	um	ponteiro	de	linha	chamado	str,	que
copia	qualquer	valor	armazenado	naquele	endereço	de	memória	em
um	buffer	 (variável	de	 função	 local)	para	o	qual	 foram	reservados
20	bytes:
A	função	principal	do	código	aloca	um	buffer	de	128	bytes	que
foi	chamado	big_string	e	utiliza	uma	estrutura	de	repetição	(ou
loop)	para	preencher	todo	o	buffer	com	letras	"A":
Em	 seguida,	 é	 chamada	 novamente	 a	 função	 função-
BufferOverflow	que	usa	como	parâmetro	o	ponteiro	que	aponta	para
o	buffer	de	128	bytes:
Isto	 criará	 sérios	 problemas,	 pois	 a	 função	 função	 _
BufferOverflow	tentará	armazenar	128	bytes	de	dados	em	um	buffer
para	 o	 qual	 foram	 reservados	 apenas	 20	 bytes	 de	 espaço	 na
memória.	Assim,	os	108	bytes	de	dados	excedentes	irão	transbordar
(overflow)	 invadindo	 os	 espaços	 de	 memória	 adjacentes,
sobrescrevendo	qualquer	informação	que	estiver	armazenada	neles.
0	resultado	é	que	a	aplicação	travará.
Um	hacker	pode	explorar	esse	tipo	de	falha	no	código	para	causar
o	travamento	de	uma	aplicação	inserindo	valores	não	previstos	que
induzam	o	código	a	sobrecarregar	o	buffer;	e,	ainda,	poderá	assumir
o	 controle	 do	 código,	 pois,	 ao	 ocorrer	 o	 overflow,	 alguns	 dados
serão	sobrescritos	e,	portanto,	perdidos.
Tentemos	 entender	 melhor	 como	 isso	 acontece.	 Examinando	 o
código	do	exemplo,	vimos	que	quando	a	função	_	BufferOverflow	é
chamada,	 é	 inserido	 um	 novo	 stack	 frame	 no	 Stack.	 Quando	 a
função	 é	 executada	 pela	 primeira	 vez	 no	 código,	 a	 estrutura	 do
Stack	seria	basicamente	a	seguinte:
Tabela	2.2:	Código	estrutural	do	Stack.
Em	seguida,	quando	a	função	tenta	armazenar	128	bytes	de	dados
no	buffer	de	20	bytes,	os	108	bytes	excedentes	transbordam,
sobrescrevendo	o	conteúdo	dos	espaços	de	memória	adjacentes,	que
são	o	ponteiro	do	frame	do	Stack,	o	endereço	de	retorno	e	o
parâmetro	da	função.	Assim,	ao	concluir	a	execução	da	função,	o
código	tenta	voltar	ao	endereço	de	retorno	que,	devido	ao	overflow
do	buffer	de	20	bytes,	foi	preenchido	com	letras	"A"	(0x41	em
código	hexadecimal);	então	o	ponteiro	EIP	tenta	voltar	ao	endereço
de	memória	0x41414141,	que	não	existe	ou	não	contém	instruções
válidas,	por	isso	a	execução	do	código	é	interrompida	e	ocorre	o
travamento	do	aplicativo.
Esse	 tipo	 de	 buffer	 overflow	 é	 chamado	 overflow	 baseado	 no
Stack,	pois	o	derramamento	dos	dados	excedentes	acontece	na	área
de	 Stack	 da	 memória.	 Os	 overflows	 podem	 ocorrer	 em	 outros
segmentos	 da	memória,	 como	Heap	 ou	BSS,	mas	 os	 baseados	 no
Stack	 são	 os	 mais	 versáteis	 e	 interessantes,	 pois	 permitem
sobrescrever	o	conteúdo	do	endereço	de	retorno.
De	 fato,	 o	 travamento	 do	 programa	 em	 si	 não	 é	 relevante,
enquanto	que	a	razão	pela	qual	ocorre	o	travamento	é	fundamental
para	 os	 hackers.	 No	 exemplo	 anterior	 vimos	 que	 o	 endereço	 de
retorno	 foi	 preenchido	 com	 caracteres	 "A"	 que,	 interpretados	 em
código	 hexadecimal,	 fizeram	 o	 ponteiro	 do	 Stack	 frame	 ir	 para	 o
endereço	de	memória	0x41414141.	Se	o	endereço	de	retorno	fosse
preenchido	com	outros	valores,	poderíamos	 fazer	com	que,	 após	a
execução	da	função,	o	SFP	apontasse	para	um	endereço	de	memória
que	contivesse	um	código	executável,	fazendo	com	que	a	execução
do	código	fosse	para	um	endereço	de	retorno	diferente	do	que	havia
sido	 definido	 pelo	 programador.	 Desta	 maneira,	 o	 hacker	 acaba
assumindo	o	controle	do	fluxo	de	execução	do	programa,	pois	pode
endereçar	o	SFP	para	um	endereço	de	memória	no	qual	ele	mesmo
inseriu	outro	código	qualquer,	com	a	finalidade	que	desejar.
É	nesse	momento	que	o	hacker	realiza	a	injeção	de	bytecode	-	um
trecho	 de	 código	 independente	 do	 restante	 do	 programa	 que	 pode
ser	inserido	dentro	do	buffer.	A	criação	de	um	bytecode	é	bastante
complexa	 e	 exige	 muita	 experiência	 e	 malícia	 por	 parte	 do
programador.	0	bytecode	que	 será	 inserido	deverá	 ser	 interpretado
pelo	 programa	 principal	 como	 um	 simples	 buffer	 de	 dados	 e,
portanto,	 não	 poderá	 conter	 certos	 caracteres	 especiais	 e	 também
não	 poderá	 depender	 de	 nenhuma	 variável	 do	 código	 principal,
mantendo-se	totalmente	desvinculado	e	autônomo.
Entre	os	bytecodes	mais	comuns,	o	shellcode	é	o	mais	conhecido:
trata-se	de	um	código	de	programa	que	cria	uma	Shell	(ou	console)
que	permite	ao	hacker	acessar	todo	o	sistema	com	os	privilégios	de
administrador	(root).	Veja	um	exemplo:
Trata-se	de	um	exemplo	de	código	de	programa	vulnerável	muito
parecido	com	a	função	_	BufferOverflow	que	vimos	anteriormente:
ele	 utiliza	 apenas	 um	 parâmetro	 e	 tenta	 armazenar	 o	 valor	 desse
parâmetro	no	buffer	de	300	bytes.	Na	verdade,	esse	código	não	faz
nada	 de	 realmente	 prejudicial	 ao	 sistema,	 apenas	 gerencia	 amemória	de	maneira	incorreta.
Para	 que	 o	 código	 se	 torne	 útil	 ao	 hacker,	 é	 necessário	 que	 o
controle	do	 código	 seja	 assumido	pelo	usuário	 root	 e	que	o	bit	 de
permissão	suid	seja	configurado	como	"on".	Supondo	que	o	código
acima	 se	 refira	 a	 um	 programa	 de	 nome	 "ataque"	 criado	 por	 nós,
para	que	este	programa	se	torne	suid,	deveríamos	fazer	o	seguinte:
Agora	que	"ataque"	é	um	programa	 root	vulnerável	a	um	buffer
overflow,	basta	inserir	o	código	que	gere	um	buffer	e	que	possa	ser
inserido	 dentro	 do	 programa	 principal.	 0	 buffer	 deverá	 conter	 o
shellcode	desejado	e	sobrescrever	o	endereço	de	retorno	do	Stack	de
modo	que	o	shellcode	seja	executado.	Para	isso	precisamos	saber	o
endereço	de	memória	em	que	o	shellcode	foi	armazenado,	o	que	não
é	fácil,	pois	como	já	dissemos,	o	Stack	é	dinâmico,	ou	seja,	os	dados
são	 realocados	 continuamente.	 Além	 disso,	 há	 outra	 complicação,
os	4	bytes	nos	quais	o	endereço	de	retorno	é	armazenado	no	stack
frame	devem	ser	sobrescritos	exatamente	com	o	valor	do	endereço
de	retorno	desejado	para	executar	o	shellcode.	Então,	o	hacker	deve
não	 só	 descobrir	 o	 endereço	 de	memória	 que	 contém	o	 shellcode,
mas	 também	 conseguir	 sobrescrever	 os	 4	 bytes	 que	 compõem	 tal
endereço	 exatamente	 em	 cima	 dos	 4	 bytes	 que	 continham	 o
endereço	de	retorno	original	definido	pelo	programador.
Em	casos	como	esse,	são	empregadas	duas	técnicas	para	resolver
os	problemas:
1.A	primeira,	conhecida	com	o	nome	NOP	sled	(No	Operation),
consiste	em	inserir	uma	instrução	de	1	único	byte	que	não	faz	nada.
Essas	instruções	são	usadas	em	determinadas	circunstâncias	para
fazer	com	que	sejam	executados	ciclos	de	cálculos	vazios	que,	por
razões	de	sincronização,	são	exigidos	na	arquitetura	dos
processadores	Sparc	para	garantir	a	correta	execução	de	seqüências
de	ciclos	de	cálculos.	No	nosso	caso,	porém,	instruções	NOP	sled
são	usadas	como	expedientes:	o	hacker	criará	uma	longa	fila	de
instruções	NOP	sled	e	as	inserirá	antes	do	shellcode;	quando	o
ponteiro	EIP	vai	para	um	endereço	qualquer	presente	nas	NOP	sled,
o	EIP	será	incrementado	de	uma	unidade	para	cada	instrução	NOP
até	alcançar	o	shellcode.	Se	o	endereço	de	retorno	for	sobrescrito
com	um	endereço	qualquer	presente	na	NOS	sled,	o	ponteiro	EIP
pulará	para	o	shellcode,	que	será	executado	corretamente.
2.A	segunda	técnica	consiste	em	despejar	no	final	do	buffer	uma
série	de	instâncias	contíguas	do	endereço	de	retorno	desejado.	Desta
forma	a	execução	do	programa	passará	para	o	novo	endereço,	desde
que	uma	das	instâncias	inseridas	no	buffer	sobrescreva	exatamente
os	4	bytes	do	endereço	armazenado	no	ponteiro	do	stack	frame
(SFP).
0	 resultado	 da	 manipulação	 do	 buffer	 seria	 uma	 estrutura
basicamente	igual	à	mostrada	a	seguir:
Tabela	2.3:	Resultado	da	manipulação	do	buffer.
Mesmo	dominando	essas	duas	técnicas,	é	preciso	conhecer	a
posição	aproximada	do	buffer	na	memória	para	só	então	descobrir	o
endereço	de	retorno	correto.	Para	encontrar	a	localização	do
endereço	de	memória	de	maneira	aproximada,	pode-se	utilizar	o
stack	pointer	(ponteiro	do	stack),	pois	subtraindo	deste	ponteiro	um
valor	apropriado	é	possível	obter	o	endereço	relativo	de	uma
variável	qualquer	e,	visto	que	no	nosso	programa	o	primeiro
elemento	do	Stack	é	o	buffer	no	qual	estamos	inserindo	o	shellcode,
o	endereço	de	retorno	correto	deve	ser	o	próprio	stack	pointer,	o	que
significa	que	o	offset	deve	ser	próximo	do	zero.
Vejamos	 um	 exemplo	 de	 código	 com	 o	 objetivo	 de	 violar	 um
programa	 criando	 um	 buffer	 e	 inserindo-o	 em	 um	 programa
vulnerável:
Observação:	os	códigos	dos	exemplos	foram	encontrados	na
Internet	em	sites	que	os	oferecem	gratuitamente.	0	leitor	poderá
encontrar	outros	inúmeros	exemplos,	se	desejar.
Tentaremos	 compreender	 melhor	 o	 significado	 desse	 código:
trata-se	 de	 um	 exploit	 projetado	 para	 criar	 um	 código	 malicioso,
disfarçado	 de	 buffer	 de	 dados	 e	 inseri-lo	 em	 um	 programa
vulnerável	 para	 que	 possamos	 assumir	 o	 controle	 do	 fluxo	 de
execução	e	então	executar	um	shellcode	que	será	inserido	quando	o
programa	travar	em	função	do	buffer	overflow.
Inicialmente	 o	 código	 adquire	 o	 stack	 pointer	 atual.	 Veja	 no
exemplo	a	seguir:
E	subtrai	dele	um	valor	de	offset	(que	neste	caso	é	zero).
Então	 a	 memória	 do	 buffer	 é	 alocada	 no	 Heap	 -	 buffer	 =
malloc(600);	 -	 e	 todo	 o	 buffer	 é	 preenchido	 com	 o	 endereço	 de
retorno	desejado:
Em	 seguida,	 os	 primeiros	 200	 bytes	 do	 buffer	 são	 preenchidos
com	uma	NOP	sled	(na	 linguagem	de	máquina	para	processadores
com	arquitetura	x86,	a	instrução	NOP	é	dada	com	0x90):
Depois,	o	 shellcode	é	posicionado	após	à	NOP	sled,	deixando	o
restante	do	buffer	preenchido	com	o	endereço	de	retorno.	Visto	que
a	 última	 posição	 de	 um	 buffer	 é	 identificada	 por	 um	 byte	 nulo
(zero),	o	buffer	termina	com	zero:
Por	 fim,	 o	 código	 chama	 uma	 outra	 função	 que	 executa	 o
programa	vulnerável	e	insere	o	buffer	alterado:
Executando	 o	 programa,	 o	 resultado	 na	 tela	 informaria	 que	 a
posição	 do	 SFP	 é,	 por	 exemplo,	 o	 endereço	 Oxbffff978	 e	 que	 o
mesmo	será	também	o	endereço	de	retorno	desejado.
Criando	exploits	sem	código	de	exploit
A	criação	de	códigos	de	exploit	para	violar	e	assumir	o	controle
de	programas	é,	sem	dúvida	alguma,	uma	técnica	amplamente
utilizada	e	muito	eficaz.	Todavia,	a	interação	do	hacker	com	o
sistema	a	ser	violado	passa	pela	intermediação	do	compilador,	que
se	encarregará	de	traduzir	o	código	gerado	em	linguagem	de
máquina.	A	presença	desse	intermediário	entre	o	hacker	e	a
máquina	é	vista	por	muitos	como	uma	limitação	que	reduz	as
possibilidades	de	interação.
Utilizando	 determinadas	 linguagens	 de	 programação,	 é	 possível
criar	exploits	diretamente	em	linha	de	comando,	sem	a	necessidade
de	criar	um	programa	para	isso.	Vejamos	um	exemplo:	o	comando
print	da	 linguagem	Perl,	quando	usado	com	astúcia,	pode	 ser	uma
ferramenta	poderosa	para	violar	programas.
Saiba	mais...
Perl	é	uma	linguagem	de	programação	multiplataforma	usada
em	aplicações	de	missão	crítica	em	todos	os	setores,	sendo
destacado	o	seu	uso	no	desenvolvimento	de	aplicações	Web	de
todos	os	tipos.	Foi	criada	por	Larry	Wall	em	dezembro	de	1987.
A	origem	do	Perl	remonta	ao	shell	scripting,	Awk	e	a	linguagem
C.	Está	disponível	para	praticamente	todos	os	sistemas
operacionais,	embora	seja	usado	mais	comumente	em	sistemas
Unix	e	compatíveis.	Originalmente,	o	nome	foi	definido	por
Larry	Wall	em	referência	à	parábola	Pérola,	de	Mateus	13	(a
grafia	foi	mudada	de	Pearl	para	Perl	por	já	ter	sido	registrada	por
outra	linguagem	de	programação).	Algumas	possíveis	expansões
fo	ram	posteriormente	propostas,	como	Practical	Extraction	and
Report	Language	e	Pathologically	Eclectic	Rubbish	Lister,	este
último	tendo	sido	proposto	pelo	próprio	Larry	Wall,	conhecido
por	sua	personalidade	sarcástica	e	criativa.	0	Perl	é	uma	das
linguagens	preferidas	por	administradores	de	sistema	e	autores
de	aplicações	Web.	É	especialmente	versátil	no	processamento
de	cadeias	(strings),	manipulação	de	texto	e	no	pattern	matching
implementado	através	de	expressões	regulares,	além	de
consumir	menor	tempo	de	desenvolvimento.
A	 linguagem	 Perl	 já	 foi	 portada	 para	 mais	 de	 100	 diferentes
plataformas	 e	 é	 bastante	 usada	 em	 desenvolvimento	 Web,
finanças	e	bioinformática.
No	 geral,	 a	 sintaxe	 de	 um	 programa	 em	 Perl	 se	 parece	 muito
com	 a	 de	 um	 programa	 em	 linguagem	 C.	 Existem	 variáveis,
expressões,	atribuições,	blocos	de	código	delimitados,	estruturas
de	 controle	 e	 sub-rotinas.	 Além	 disso,	 Perl	 foi	 bastante
influenciado	 pelas	 linguagens	 de	 shell	 script,	 pois	 todas	 as
variáveis	 são	 precedidas	 por	 um	 cifrão	 ($).	 Essa	 marcação
permite	 identificar	 perfeitamente	 as	 variáveis	 de	 um	programa,
onde	 quer	 que	 elas	 estejam.	 Um	 dos	 melhores	 exemplos	 da
utilidade	desse	recurso	é	a	interpolação	de	variáveis	diretamente
no	 conteúdo	 de	 strings.0	 Perl	 também	 possui	 muitas	 funções
integradas	 para	 tarefas	 comuns	 como	 ordenação	 e	 acesso	 de
arquivos	em	disco.
Perl	utiliza	as	listas	de	Lisp,	as	arrays	associativas	(tabelas	hash)
de	awk	e	as	expressões	regulares	de	sed.	 Isso	 tudo	simplifica	e
facilita	qualquer	forma	de	interpretação	e	tratamento	de	textos	e
dados	 em	 geral.	 A	 linguagem	 suporta	 estruturas	 de	 dados
arbitrariamente	 complexas.	Ela	 também	possui	 recursos	 vindos
da	programação	funcional	(as	funções	são	vistas	como	um	outro
valor	qualquer	para	uma	sub-rotina,	por	exemplo)	e	um	modelo
de	 programação	 orientada	 a	 objetos.	 Perl	 também	 possui
variáveis	com	escopo	léxico,	que	tornam	mais	fácil	a	escrita	de
código	mais	robusto	e	modularizado.
Todas	 as	 versões	 do	 Perl	 possuem	 gerenciamento	 de	memória
automático	 e	 tipificação	 dinâmica.	 Os	 tipos	 e	 necessidades	 de
cada	 objeto	 de	 dados	 no	 programa	 são	 determinados
automaticamente;	 a	 memória	 é	 alocada	 ou	 liberada	 de	 acordo
com	o	necessário.	A	 conversão	 entre	 tipos	 de	 variáveis	 é	 feita
automaticamente	em	tempo	de	execução	e	conversões	ilegais	são
erros	fatais.
A	 linguagem	 Perl	 permite	 executar	 instruções	 "em-linha"
utilizando	a	opção	-e,	como	no	exemplo	a	seguir:
Com	esse	comando,	 solicitamos	ao	Perl	que	execute	 a	 instrução
que	está	dentro	das	aspas	que,	neste	caso,	consiste	em	imprimir	50
vezes	a	palavra	"Olá".
Qualquer	caractere	pode	ser	impresso	também	utilizando	a	sintaxe
\x??	 (em	 que	 "??"	 é	 o	 código	 hexadecimal	 do	 caractere	 que
desejamos	 imprimir);	 por	 exemplo,	 para	 imprimir	 a	 letra	 "A"	 50
vezes,	poderíamos	usar	esse	comando:
É	possível	também	concatenar	seqüências	de	caracteres	usando	o
ponto	(.),	como	no	exemplo	a	seguir:
Cujo	resultado	seria	o	seguinte:
A	substituição	do	comando	basti	é	obtida	com	um	acento	grave	():
qualquer	 comando	 (desde	 que	 válido)	 que	 se	 encontre	 dentro	 do
conjunto	 de	 acentos	 graves	 é	 executado	 e	 o	 respectivo	 output	 é
exibido	na	tela.
Como	vimos	no	último	exemplo	do	tópico	anterior,	um	código	de
exploit	basicamente	pega	o	stack	pointer,	altera	um	buffer	e	 insere
esse	buffer	 alterado	 em	um	programa	vulnerável.	Com	a	 ajuda	do
Perl,	 da	 substituição	 de	 comando	 e	 de	 um	 endereço	 de	 retorno
aproximado,	podemos	iniciar	um	código	de	exploit	na	própria	linha
de	 comando,	 executando	 o	 programa	 vulnerável	 e	 utilizando	 os
acentos	 graves	 para	 inserir	 um	 buffer	 modificado	 no	 primeiro
argumento.
Referindo-nos	ao	mesmo	exemplo	do	tópico	anterior	(o	Código	de
Exploit	1),	deveremos	primeiramente	criar	a	NOP	Sled:	no	código
do	exemplo,	 foram	 utilizados	 200	 bytes	 de	NOP	Sled	 (lembrando
que	o	có	digo	hexadecimal	para	uma	 instrução	NOP	sled	é	0x90).
Veja	como	essa	operação	seria	realizada	em	Perl:
Então,	 o	 shellcode	 deverá	 ser	 adicionado	no	 final	 da	NOP	 sled.
Em	diversos	 casos	pode	 se	 revelar	 extremamente	útil	 armazenar	o
shellcode	em	um	arquivo,	então	vamos	fazê-lo.	Visto	que	todos	os
bytes	já	estão	escritos	em	hexadecimal	na	parte	inicial	do	Código	de
Exploit	 1,	 bastará	 escrevê-los	 em	 um	 arquivo,	 operação	 que	 pode
ser	 realizada	 com	 a	 ajuda	 de	 um	 editor	 hexadecimal	 ou,	 mais
simplesmente,	 usando	 o	 comando	 print	 do	 Perl	 e	 especificando
como	destino	um	arquivo,	como	mostrado	no	exemplo	a	seguir:
Desta	 forma	 obtivemos	 um	 arquivo	 de	 nome	 Shellcode	 que
contém	 nosso	 shellcode,	 que	 poderá	 ser	 inserido	 facilmente	 em
qualquer	 local	 dentro	 do	 conjunto	 de	 acentos	 graves	 utilizando	 o
comando	cat.	0	exemplo	a	seguir	mostra	como	adicionar	o	shellcode
à	NOP	sled	existente:
Agora	 precisamos	 concatenar	 o	 endereço	 de	 retorno,	 repetindoo
várias	vezes.	Neste	ponto	surge	um	problema:	no	Código	de	Exploit
1	do	 tópico	anterior,	o	buffer	 foi	preenchido	desde	o	 início	com	o
endereço	de	retorno,	o	que	garantiu	que	tal	endereço	fosse	alinhado
corretamente,	pois	é	constituído	de	grupos	de	4	bytes.	Ao	gerar	um
buffer	 de	 exploit	 na	 linha	 de	 comando	 em	 Perl,	 esse	 alinhamento
deve	ser	feito	manualmente.	Isto	significa	que	a	quantidade	de	bytes
da	 NOP	 sled	mais	 o	 shellcode	 deve	 ser	 um	 número	 divisível	 por
quatro.	 Entretanto,	 o	 shellcode	 é	 composto	 por	 46	 bytes	 e	 a	NOP
sled	por	200:	basta	um	rápido	cálculo	para	descobrir	que	o	total	de
bytes	(246	no	nosso	caso)	não	é	divisível	por	quatro,	pois	faltam	2
bytes	 para	 que	 isso	 seja	 possível.	 Isto	 acarreta	 que	 o	 endereço	 de
retorno	 será	 desali	 nhado	 em	 2	 bytes,	 portanto,	 a	 sobrescrita	 dos
bytes	 não	 será	 exata	 e	 a	 execução	 do	 programa	 irá	 parar	 em	 um
endereço	de	memória	inválido	(ou	vazio).
Para	esclarecer,	a	Figura	2.1	representa	de	maneira	esquemática	a
diferença	 entre	 endereços	 de	 retorno	 desalinhados	 e	 corretamente
alinhados.
Figura	 2.1:	 Em	 "a"	 os	 blocos	 de	 4	 bytes	 com	 os	 endereços	 de
retorno	estão	desalinhados;	 em	 "b"	 os	 blocos	 estão	 corretamente
alinhados	e	prontos	para	serem	sobrescritos.
Para	resolver	este	problema,	podemos	acrescentar	2	bytes	à	NOP
sled	para	que	os	endereços	de	retorno	sejam	alinhados	e	sobrescritos
corretamente.	Veja	como	fazer	no	exemplo	a	seguir:
Veja	que	o	comando	para	execução	da	NOP	sled	foi	repetido	202
vezes,	 e	 não	 200,	 como	 anteriormente;	 isto	 fez	 com	 que	 fossem
criados	mais	 2	 bytes,	 resolvendo	o	 empecilho	do	 alinhamento	 dos
blocos	de	4	bytes	que	formam	o	endereço	de	retorno.
Agora	 que	 a	 primeira	 parte	 do	 buffer	 de	 exploit	 está	 alinhada
corretamente,	 é	 preciso	 inserir	 no	 final	 o	 endereço	 de	 retorno
repetido.	Por	meio	do	Código	de	Exploit	1,	conhecemos	a	posição
em	que	o	apontador	do	stack	frame	se	encontrava	antes	da	execução
e	usamos	essa	informação	para	descobrir	de	maneira	muito	próxima
a	posição	do	endereço	de	retorno,	que	é	Oxbffff978.	Podemos	usar
o	comando	print	do	Perl	para	imprimir	esse	endereço	usando:
Note	que	a	ordem	dos	bytes	de	endereço	está	invertida	devido	ao
sistema	 de	 ordenamento	 dos	 bytes	 no	 modo	 little	 indian	 na
arquitetura	x86.
Visto	que	o	comprimento	desejado	para	o	buffer	de	exploit	é	de
aproximadamente	600	bytes	e	a	NOP	sled	e	o	shellcode	ocupam	248
bytes,	 com	 um	 rápido	 cálculo	 descobrimos	 que	 o	 endereço	 de
retorno	deve	ser	repetido	88	vezes,	operação	que	pode	ser	realizada
utilizando	o	seguinte	comando	Perl:
Explorando	as	variáveis	do	ambiente	de	execução
Pode	ocorrer	que	o	espaço	de	memória	alocado	para	um	buffer
seja	pequeno	demais	para	hospedar	um	shellcode.	Em	casos	como
esse	é	possível	esconder	o	shellcode	em	uma	variável	do	ambiente.
As	variáveis	de	ambiente	são	utilizadas	normalmente	pelo	sistema
para	diversas	operações,	sendo	que	o	mais	importante	é	que	essas
variáveis	são	armazenadas	em	uma	região	da	memória	para	a	qual
pode	ser	endereçada	a	execução	do	programa.
Portanto,	se	um	buffer	é	muito	pequeno	para	conter	a	NOP	sled,	o
shellcode	 e	 o	 endereço	 de	 retorno	 repetido	 para	 a	 substituição,
podemos	armazenar	a	NOP	sled	e	o	shellcode	em	uma	variável	do
ambiente	 e	 alterar	 o	 endereço	 de	 retorno	 para	 que	 aponte	 para	 o
endereço	 de	memória	 que	 contém	 a	 variável	 do	 ambiente	 na	 qual
escondemos	o	shellcode.
0	 exemplo	 a	 seguir	 traz	 um	 trecho	 de	 código	 vulnerável,	 que
chamaremos	MyProgram	 -	 1,	 no	qual	 é	 utilizado	um	buffer	muito
pequeno	para	o	shellcode:
Agora,	vejamos	como	compilar	esse	código	e	definir	o	suid	root
que	o	torne	realmente	vulnerável:
Observando	o	código	do	MyProgram	_	1	podemos	reparar	que	o
buffer	é	de	apenas	5	bytes	-	charbuffer[5];)	-	o	que	nos	impede	de
inserir	o	shellcode,	que	deverá	ser	armazenado	em	outro	lugar.
No	Código	de	Exploit	1,	usamos	a	função	execl()	para	executar	o
programa	 vulnerável	 com	 o	 buffer	 alterado	 no	 primeiro	 exploit.
Existe	 outra	 função,	 chamada	 execleO,	 praticamente	 idêntica,
porém,	com	um	argumento	a	mais	que	identifica	o	ambiente	no	qual
deve	acontecer	o	processo	de	execução	do	programa.	Esse	ambienteé	representado	por	um	vetor	de	ponteiros	e	linhas	de	texto	(strings)
que	 terminam	com	um	byte	nulo	 (zero),	um	para	cada	variável	do
ambiente.	0	próprio	array	(conjunto)	termina	com	um	ponteiro	nulo.
Podemos	explorar	 essa	 estrutura	para	 criar	um	shellcode	usando
um	 conjunto	 de	 ponteiros,	 o	 primeiro	 dos	 quais	 aponta	 para	 o
shellcode,	 e	 o	 segundo	 constituído	 por	 um	 ponteiro	 nulo.	 Assim,
podemos	 chamar	 a	 função	 execre()	 utilizando	 esse	 ambiente	 para
executar	 o	 segundo	 programa	 vulnerável,	 sobrescrevendo	 o
endereço	 de	 retorno	 com	 o	 endereço	 da	 variável	 de	 ambiente	 que
contém	o	shellcode.
0	cálculo	para	descobrir	o	endereço	da	variável	não	é	complexo:
em	sistemas	Linux,	o	endereço	será:
Visto	 que,	 neste	 caso,	 obteremos	 um	 endereço	 exato	 e	 não
aproximado,	não	será	necessário	o	uso	da	NOP	sled.	0	que	acontece
no	 buffer	 de	 exploit	 é	 que	 o	 endereço	 é	 repetido	 um	 número	 de
vezes	suficiente	para	fazer	com	que	o	endereço	de	retorno	no	Stack
seja	sobrescrito	corretamente	(são	suficientes	40	bytes	para	realizar
esta	operação).
Veja	o	código:
Quando	 o	 programa	 é	 compilado	 e	 executado,	 o	 resultado	 é	 o
seguinte:
Essa	 mesma	 técnica	 pode	 ser	 utilizada	 mesmo	 sem	 nenhum
programa	 de	 exploit.	 Na	 Shell	 bash	 as	 variáveis	 de	 ambiente	 são
definidas	 e	 exportadas	 usando	 o	 comando	 export	 varname=value.
Usando	o	 export,	 o	Perl	 e	 os	 acentos	 graves,	 é	 possível	 inserir	 no
ambiente	o	shellcode	e	uma	NOP	sled	de	grandes	dimensões.	Veja	a
seguir:
0	próximo	passo	consiste	em	descobrir	o	endereço	desta	variável
de	ambiente,	o	que	pode	ser	feito	utilizando	um	debugger	(como	o
gdb,	 por	 exemplo)	 ou	mais	 simplesmente	 escrevendo	um	pequeno
código	que	faça	isso	para	nós.	Vejamos	ambas	as	maneiras.
Descobrindo	o	endereço	da	variável	de	ambiente	com	a	ajuda	do
debugger
0	debugger	(depurador	de	código)	permite	abrir	o	programa
vulnerável	e	inserir	um	ponto	de	interrupção	logo	no	início	do
código.	Desta	forma,	a	execução	do	programa	começará,	mas	será
interrompida	antes	que	qualquer	operação	seja	realizada.	Então,
pode-se	examinar	atentamente	a	memória	a	partir	do	stack	pointer	e
deslocar-se	para	frente	usando	o	comando	gdb	x/20s	$esp;	isso	fará
com	que	sejam	exibidas	as	vinte	linhas	de	memória	após	a	posição
atual	do	ponteiro	do	Stack.	0	"x"	na	linha	de	comando	significa
examinar,	20s	indica	que	queremos	ver	20	linhas	(strings)	que
terminem	por	NULL	(nulo).	Repete-se	a	operação	até	encontrar	a
linha	com	a	variável	de	ambiente	na	memória.	Em	seguida,
realizamos	a	depuração	(debug)	do	programa	MyProgram	1	com
gdb	para	examinar	o	Stack	e	en	contrar	o	shellcode	armazenado	na
variável	de	ambiente	SHELLCODE:
Após	encontrarmos	o	endereço	no	qual	é	armazenada	a	variável
do	ambiente	SHELLCODE,	usamos	o	comando	x/s	para	examinar	a
linha	 em	 questão	 que,	 no	 nosso	 caso,	 se	 refere	 ao	 endereço	 de
memória	 Oxbffffce5.	 0	 endereço,	 porém,	 inclui	 a	 linha
SHELLCODE=,	 portanto,	 será	 preciso	 adicionar	 16	 bytes	 ao
endereço	para	obter	um	endereço	localizado	em	um	local	indefinido
da	NOP	sled.
0	 debugger	 (depurador)	 detectou	 que	 o	 endereço	 Oxbffffce5
encontra-se	muito	próximo	ao	início	da	NOP	sled,	e	o	shellcode	está
armazenado	 na	 variável	 de	 ambiente	 SHELLCODE.	 De	 posse
dessas	informações	e	recorrendo	mais	uma	vez	à	ajuda	do	Perl	e	dos
acentos	 graves,	 podemos	 criar	 o	 exploit	 do	 programa	 vulnerável
MyProgram	_	1,	como	mostra	o	exemplo	a	seguir:
Descobrindo	o	endereço	da	variável	de	ambiente	com	a	criação	de
um	programa
Para	obter	o	endereço	de	uma	variável	de	ambiente,	podemos
também	escrever	um	simples	programa	de	ajuda,	chamado	helper.
Neste	programa	utiliza-se	a	função	getenv()	para	localizar	o
primeiro	argumento	da	variável	do	ambiente.	Caso	a	função	não
encontre	nada,	será	exibida	uma	mensagem,	mas	se	a	variável	for
localizada	corretamente,	seu	endereço	será	exibido.
Veja	um	exemplo	da	estrutura	do	código	de	um	helper:
Então,	basta	mandar	compilar	e	executar	o	programa	para	que	o
endereço	da	variável	de	ambiente	SHELLCODE	seja	encontrado	e
exibido	 na	 tela.	 Veja	 como	 proceder	 observando	 a	 seqüência	 de
comandos	a	seguir:
Comparando	 o	 resultado	 obtido	 pelo	 depurador	 gdb	 com	 o	 que
acabamos	de	obter	com	o	helper	nota-se	que	os	endereços	 são	um
pouco	diferentes:
Tabela	2.4.
Isso	acontece	porque	o	contexto	em	que	o	helper	atua	é	diferente
daquele	em	que	é	executado	o	programa	vulnerável.	Contudo,	neste
caso	específico,	os	100	bytes	da	NOP	sled	que	utilizamos	são
suficientes	para	compensar	essas	pequenas	divergências	nos
resultados.
Para	 conseguirmos	 descobrir	 com	 precisão	 o	 endereço	 de
memória,	é	preciso	analisar	a	diferença	entre	os	endereços	obtidos.
Sabemos,	por	exemplo,	que	o	comprimento	do	nome	do	programa
em	 execução	 influencia	 em	 parte	 o	 endereço	 das	 variáveis	 do
ambiente.	 Então,	 podemos	 alterar	 o	 nome	 do	 programa	 helper	 e
realizar	experiências	até	obter	o	mesmo	resultado	nos	dois	métodos.
Realizar	experiências	 (e	 ter	muita	paciência)	é	 fundamental	para
que	 um	 hacker	 consiga	 chegar	 até	 o	 resultado	 desejado,	 pois
geralmente	são	necessárias	inúmeras	tentativas	até	conseguir	injetar
um	código	malicioso	em	um	programa	vulnerável.
No	 próximo	 capítulo	 aprenderemos	 outras	 técnicas	 para	 causar
um	overflow	nas	áreas	Heap	e	BSS	da	memória.	Caso	precise	rever
esses	 conceitos,	 leia	 novamente	 o	 tópico	 Sobre	 as	 seções	 da
memória	de	programa,	neste	capítulo.
	
	
Sobre	os	overflows	baseados	
em	heap	e	BSS
No	capítulo	anterior	vimos	como	criar	exploits	que	causam
overflow	na	área	do	buffer	do	stack	da	memória	do	programa.
Existem	também	vulnerabilidades	relacionadas	aos	overflows	dos
buffers	das	seções	heap	e	BSS	-	para	saber	mais	sobre	as	seções	da
memória,	leia	o	tópico	Sobre	as	seções	da	memória	de	programa	do
Capítulo	2.	Esses	tipos	de	overflow	não	são	padronizados,	como	os
do	stack,	mas	quando	usados	corretamente	se	tornam	muito
eficazes.
No	caso	de	overflows	baseados	em	heap	e	BSS,	não	há	endereço
de	retorno	a	ser	sobrescrito;	portanto,	seu	funcionamento	se	baseia
no	 registro	 de	 variáveis	 importantes	 na	 memória	 logo	 após	 um
buffer	 que	 pode	 sofrer	 overflow,	 ou	 seja,	 um	 transbordamento	 de
dados.	Imagine,	por	exemplo,	uma	variável	fundamental	do	sistema
que	armazena	as	permissões	de	acesso	dos	usuários	ao	 sistema	ou
do	status	de	um	login,	se	esta	variável	for	armazenada	logo	após	um
buffer	 de	 memória	 sujeito	 a	 overflow,	 podemos	 sobrescrever	 o
conteúdo	da	variável	alterando	seu	conteúdo	e,	neste	caso,	alterando
as	 permissões	 de	 acesso.	Ou,	 ainda,	 se	 um	ponteiro	 de	 função	 for
armazenado	 após	 um	 buffer	 sujeito	 a	 overflow,	 podemos
sobrescrever	 o	 endereço	 para	 o	 qual	 ele	 aponta,	 enviando	 a
execução	 do	 programa	 para	 a	 execução	 do	 shellcode	 quando	 a
função	for	chamada.
É	de	suma	importância	entender	que	o	funcionamento	dos	exploit
de	overflow	baseados	em	heap	e	BSS	dependem	fundamentalmente
do	layout	da	memória	do	programa	e,	por	esse	motivo,	identificar	os
pontos	fracos	é	um	pouco	mais	complexo.
Nos	 tópicos	 a	 seguir	 veremos	 alguns	 códigos	 de	 exemplo	 para
compreender	 o	 funcionamento	 desses	 exploits.	 Como	 no	 capítulo
anterior,	 lembramos	ao	leitor	que	todos	os	exemplos	citados	foram
obtidos	realizando	uma	busca	cuidadosa	em	sites	livres	específicos
na	 Internet,	 sinta-se	 à	 vontade	 para	 buscar	 outros	 exemplos,
aprimorando	assim	seu	aprendizado.
Overflow	baseado	no	heap
Neste	tópico	veremos	um	simples	programa	sujeito	ao	overflow
da	seção	heap.	0	que	interessa	nesse	caso	não	é	o	funcionamento	do
programa	em	si	(que	chamaremos	MyProgram_2),	pois	se	trata
apenas	de	um	exemplo,	e	sim	identificar	os	pontos	fracos	que	o
tornam	vulnerável	a	ataques	hacker.
Observe	atentamente	o	código	a	seguir:
Analisemos	 a	 estrutura	 do	 código	 para	 compreender	 seu
funcionamento.
Tudocomeça	com	a	declaração	das	variáveis	 -	como	de	praxe	 -
em	seguida	é	alocado	o	espaço	de	memória	no	heap	(neste	caso,	20
bytes):
Mais	 adiante,	 o	 espaço	 de	 memória	 alocado	 é	 preenchido	 com
dados:
Durante	 a	 execução	 do	 programa	 serão	 exibidas	 algumas
mensagens	de	debug	(depuração),	como	mostra	o	trecho	de	código	a
seguir:
Então,	o	arquivo	recebe	dados	e	em	seguida,	é	aberto:
Uma	estrutura	condicional	se	encarrega	de	verificar	se	o	arquivo
criado	 está	 vazio	 (NULL)	 e,	 caso	 esteja,	 exibirá	 uma	 mensagem
avisando	que	houve	erro	ao	abrir	o	arquivo:
No	prompt	de	comando	do	sistema,	bastará	compilar	o	programa,
definir	o	suid	como	root	e	executar	para	ver	o	resultado:
Como	dissemos,	 trata-se	de	um	programa	extremamente	simples
que	aceita	apenas	um	argumento	e	insere	o	seu	conteúdo	no	arquivo
/tmp/notes.	 Contudo,	 apesar	 da	 simplicidade	 de	 sua	 estrutura,	 o
programa	myprogram_2	apresenta	uma	característica	que	merece	a
nossa	 atenção:	 a	 memória	 para	 a	 variável	 userinput	 é	 alocada	 no
heap	antes	da	memória	para	a	variável	outputfile,	e	as	informações
de	 depuração	 (debug)	 exibidas	 durante	 a	 execução	 ajudam	 a
entender	 melhor	 isso,	 pois	 userinput	 encontra-se	 na	 posição
0x8049840,	 enquanto	 inputfile	 está	 na	 posição	 0x80498e8.	 A
distância	entre	os	dois	endereços	(distance	between)	é	de	24	bytes	e,
visto	 que	 o	 primeiro	 buffer	 é	 encerrado	 com	NULL,	 a	 quantidade
máxima	 de	 dados	 que	 podem	 ser	 inseridos	 nesse	 buffer	 sem	 que
haja	overflow	deverá	ser	de	23	bytes.
Se	tentássemos	preencher	o	buffer	da	variável	userinput	com	mais
de	 23	 bytes,	 os	 bytes	 excedentes	 iriam	 transbordar,	 invadindo	 o
buffer	 de	 outputfile	 e	 gerando	 um	 resultado	 diferente	 do	 esperado
pelo	programador,	como	no	exemplo	a	seguir:
Na	 primeira	 linha	 de	 comando,	 iniciamos	 o	 programa
myprogram_2	especificando	como	argumento	um	texto	de	35	bytes
(muito	além	dos	 23	 que	 ele	 suporta).	Conseqüentemente,	 o	 trecho
"texto	 extra",	 que	 não	 coube	 no	 buffer,	 transbordou	 invadindo	 o
buffer	de	outputfile,	o	que	resultou	na	gravação	dos	dados	não	em
/tmp/	notes,	como	se	queria,	mas	em	um	arquivo	que	foi	nomeado
"texto-extra".	Isso	ocorreu	porque	uma	linha	é	lida	até	encontrar	um
byte	null,	portanto,	toda	a	linha	digitada	como	argumento	é	gravada
no	arquivo	como	userinput.
Visto	 que	 myprogram_2	 é	 um	 programa	 suid	 que	 serve	 para
acrescentar	 dados	 a	 um	 arquivo	 específico,	 podemos	 inserir	 no
arquivo	qualquer	dado,	desde	que	indiquemos	corretamente	o	nome
do	arquivo	no	qual	essas	informações	devem	ser	adicionadas.
Uma	 maneira	 interessante	 de	 explorar	 essa	 falha	 é	 adicionar
informações	 no	 arquivo	 /etc/passwd,	 em	 sistemas	 Linux.	 Este
arquivo	contém	todos	os	nomes	de	usuários,	os	ID	e	as	consoles	de
login	para	todos	os	usuários	com	acesso	ao	sistema,	portanto,	basta
inserir	nele	um	novo	ID	para	ganhar	o	acesso	ao	sistema	mesmo	não
possuindo	um	login.
Os	 campos	 do	 arquivo	 de	 sistema	 /etc/passwd	 são	 delimitados
com	um	caractere	dois-pontos	(:)	e	sua	estrutura	é	a	seguinte:
•o	primeiro	campo	é	o	nome	de	login;
•o	segundo	campo	contém	a	senha	(password);
•o	terceiro	armazena	o	User	ID	(uid);
•o	quarto	campo	é	o	Group	ID	(gid);
•o	quinto	campo	é	o	Username	(nome	do	usuário);
•o	sexto	campo	contém	o	diretório	principal	do	usuário	home);
•o	sétimo	campo	é	a	console	(shell)	de	login.
Os	campos	com	as	senhas	contêm	apenas	caracteres	"x",	pois	são
criptografados	e	o	conteúdo	real	é	armazenado	em	outro	local	em
um	arquivo	shadow.	Isso	pode	parecer	um	problema,	mas	sua
solução	é	extremamente	simples:	quando	o	campo	é	deixado	vazio,
nenhuma	senha	é	solicitada.	Além	disso,	para	qualquer	item	em	que
o	usuário	tenha	o	User	ID	"0"	(zero)	são	concedidos	os	privilégios
de	root.	Então,	nosso	objetivo	será	inserir	no	arquivo	/etc/passwd
um	usuário	adicional	com	privilégios	de	root	e	sem	a	necessidade	de
informar	senha	para	o	acesso;	para	isso,	a	linha	a	ser	inserida	no
arquivo	seria	como	mostra	a	Figura	3.1	a	seguir:
Figura	3.1:	A	linha	de	comando	que	usaremos	para	adicionar	um
novo	usuário	no	arquivo	/etc/passwd.
A	tipologia	desse	exploit	baseado	no	overflow	do	Heap	não
permitirá	inserir	a	linha	de	comando	como	mostra	a	Figura	3.1
exatamente	como	ela	é,	pois	a	linha	deve	terminar	com	o	nome	do
arquivo	no	qual	as	informações	devem	ser	inseridas,	neste	caso
/etc/passwd;	por	outro	lado,	se	acrescentássemos	o	nome	do	arquivo
à	linha	de	comando,	obteríamos	um	erro,	pois	a	sintaxe	do	comando
ficaria	errada.	Podemos	contornar	esse	problema	usando	um	link
simbólico	ao	arquivo,	de	maneira	que	a	linha	de	comando	possa
terminar	com	/etc/passwd	e,	ao	mesmo	tempo,	ser	uma	linha	válida
para	o	arquivo	das	passwords.
Veja,	no	exemplo	a	seguir,	como	fazer:
Assim,	 o	 arquivo	 /tmp/etc/passwd	 aponta	 para	 a	 shell	 de	 login
/,bin/	bash	e,	portanto,	passa	a	ser	também	uma	shell	de	login	válida
para	 o	 arquivo	 das	 senhas.	 Utilizando	 esse	 truque,	 a	 linha	 de
comando...
...passa	 a	 ser	 uma	 linha	 válida	 para	 preencher	 um	 registro	 do
arquivo	 de	 senhas.	 Precisamos	 apenas	 realizar	 uma	 pequena
modificação	 de	maneira	 que	 a	 parte	 que	 antecede	 /etc/passwd,	 ou
seja,	 newuser::0:0:eu:/root:/tmp	 tenha	 comprimento	 de	 exatos	 24
bytes	(agora	tem	26);	então,	vamos	alterá-la	para:
Pronto!	A	nossa	 linha	está	pronta	para	 ser	 inserida	no	programa
vulnerável	ao	overflow	de	heap.	Lembre-se	que	estamos	explorando
o	overflow	para	inserir	no	arquivo	com	a	lista	de	usuários	e	senhas
de	acesso	um	novo	usuário	sem	senha	e	com	todos	os	privilégios	de
root	(super-usuário	administrador	do	sistema).
Então,	vejamos	como	isso	é	feito	analisando	o	código	a	seguir:
Desta	 forma,	 criamos	 um	 novo	 usuário	 cujo	 nome	 de	 login	 é
nuser	 que	 pode	 acessar	 o	 sistema	 sem	 necessidade	 de	 informar
senha	e	que	possui	 todas	as	permissões	de	acesso	do	super-usuário
root.	 Com	 essa	 conta	 o	 hacker	 assumirá	 o	 controle	 total	 sobre	 o
sistema.
Overflow	baseado	nos	ponteiros	de	funções
Neste	tópico	trataremos	dos	overflows	realizados	na	seção	de
memória	chamada	BSS.	Para	contextualizar	o	uso	desse	tipo	de
exploit,	exemplificaremos	seu	uso	no	código	de	um	simples	jogo
baseado	nas	probabilidades	(que	chamaremos	MyGame_1):	para
jogar	são	necessários	10	pontos	iniciais	e	o	objetivo	do	jogo	é
adivinhar	um	número	de	1	a	20	escolhido	randomicamente	pelo
programa,	se	acertarmos	ganharemos	100	pontos	(seremos
informados	constantemente	sobre	a	nossa	pontuação	por	meio	de
mensagens	que	aparecerão	na	tela).
Vejamos	o	código	a	seguir:
Observação:	o	 trecho	de	código	que	subtrai	os	pontos	do	saldo
estaria	 aqui,	 no	 local	 desta	 caixa,	 ele	 foi	 omitido,	 pois	 é
irrelevante	para	o	nosso	exemplo.
Observação:	o	trecho	de	código	que	soma	os	100	pontos	ao	saldo
estaria	 aqui,	 no	 local	 desta	 caixa,	 ele	 foi	 omitido,	 pois	 é
irrelevante	para	o	nosso	exemplo.
Vamos	 analisar	 as	 partes	 do	 código	para	 compreender	melhor	 o
funcionamento	desse	programa.
Como	 sempre,	 primeiramente	 são	 inicializadas	 as	 variáveis	 e
definidos	os	tamanhos	dos	buffers.	É	exibido	também	um	pequeno
texto	que	 informa	ao	usuário	o	que	deve	fazer	e	como	obter	ajuda
(devido	ao	uso	do	comando	printf):
Então	o	randomizador	é	zerado	e	o	ponteiro	de	função	é	definido
de	maneira	que	aponte	para	a	função	game:
Logo	 em	 seguida,	 são	 exibidas	 na	 tela	 algumas	 mensagens	 de
debug,	úteis	para	manter-nos	informados	sobre	o	que	está	ocorrendo
durante	a	execução	do	código:
Agora	 o	 programa	 verifica	 se	 o	 usuário	 utilizou	 os	 argumentos
"help"	ou	"-h"	para	que	seja	exibido	o	texto	de	ajuda	do	programa.
Se	o	usuário	digitou	"help"	ou	"-h"	logo	após	o	nome	do	programa,
então	o	texto	será	mostrado.	Caso	contrário	será	chamada	a	função
game	por	meio	do	ponteiro	de	função:
Finalmente	 o	 jogo	 começa.	 0	 programa	 verifica	 se	 o	 usuário
digitou	 um	 número	 válido	 de	 1	 a	 20,	 pois,caso	 contrário,	 será
exibida	 uma	 mensagem	 solicitando	 a	 digitação	 de	 um	 número
válido:
Então	é	sorteado	um	número	randômico,	que	é	comparado	com	o
número	 digitado	 pelo	 usuário	 como	 argumento	 do	 programa
MyGame_1.	 Se	 o	 número	 sorteado	 for	 igual	 ao	 do	 usuário,	 é
iniciada	 a	 função	 jackpot,	 caso	 contrário	 o	 usuário	 receberá	 uma
mensagem	informando	que	perdeu	o	jogo:
A	 função	 jackpot	 avisa	 o	 usuário	 que	 ele	 ganhou	 e	 soma	 100
pontos	ao	saldo:
Vejamos	 o	 que	 aconteceria	 na	 tela	 do	 nosso	 computador	 ao
compilar	 e	 executar	 o	 programa	 (os	 comandos	 dados	 estão
destacados	em	negrito):
Ajuda:
Como	dissemos,	 trata-se	 de	 um	 simples	 programa	 de	 exemplos,
sem	utilidade	 prática.	Entretanto,	 há	 um	aspecto	 desse	 código	que
merece	 a	 nossa	 atenção:	 o	 buffer	 é	 declarado	 de	maneira	 estática
antes	 do	 ponteiro	 de	 função	 que	 também	 foi	 declarado	 como
estático:
Isto	 significa	 que	 ambos	 os	 elementos	 (buffer	 e	 ponteiro)
encontram-se	 na	 seção	 BSS	 da	 memória	 do	 programa.	 As
informações	de	debug	mostradas	durante	a	execução	indicam	que	o
buffer	 está	 na	 posição	 0x8049c74	 e	 o	 ponteiro	 de	 função	 está	 no
endereço	0x80049c88:	a	diferença	entre	os	dois	endereços	é	de	20
bytes.	Então,	se	inserirmos	21	bytes	no	buffer,	o	vigésimo	primeiro
irá	 transbordar	 (overflow)	 no	 ponteiro	 de	 função,	 como	 mostra	 o
exemplo	a	seguir:
Observe	(em	negrito)	o	overflow	de	dados.
0	 "jogo"	 foi	 executado	 duas	 vezes	 e	 em	 ambas	 informamos	 um
argumento	 inválido	 e	maior	do	que	20	bytes.	No	primeiro	 caso,	 o
vigésimo	primero	 byte	 é	 o	 byte	 nulo	 (zero)	 que	 encerra	 a	 linha	 e,
visto	que	o	ponteiro	da	função	é	gravado	ordenando	seus	bytes	com
sistema	"little	indian",	o	byte	menos	significativo	é	sobrescrito	com
0x00,	 por	 isso	 recebe	 o	 novo	 endereço	 0x8048600.	 0	 resultado
("illegal	 instruction")	 é	 dado	 porque	 nesse	 endereço	 não	 há
nenhuma	instrução	válida	para	o	ponteiro	de	função.
Se	 outro	 byte	 transbordar	 do	 buffer	 da	 BSS,	 o	 byte	 nulo	 se
desloca	 para	 a	 esquerda	 e	 o	 vigésimo	 segundo	 byte	 sobrescreve	 o
byte	 menos	 significativo	 do	 ponteiro	 de	 função.	 Na	 segunda
execução,	usamos	 a	 letra	 "A",	 que	 em	hexadecimal	 corresponde	 a
0x41.
0	exemplo	a	seguir	demonstra	que	não	só	é	possível	sobrescrever
partes	do	ponteiro	de	função,	mas	também	permite	controlá-lo.	Se	o
hacker	 fizer	 com	 que	 quatro	 bytes	 de	 dados	 transbordem	 da	 BSS
para	o	ponteiro	de	função,	ele	poderá	sobrescrever	todo	o	endereço
do	ponteiro.	Veja	a	seguir:
Como	podemos	observar,	o	ponteiro	da	função	é	sobrescrito	com
os	caracteres	"ABCD",	representados	em	hexadecimal	e	em	ordem
inversa	 -	 de	 acordo	 com	 o	 esquema	 "little	 indian"	 -	 pelos	 valores
correspondentes,	como	mostrados	na	Tabela	3.1	a	seguir:
Tabela	3.1:	Representação	em	hexadecimal	e	em	ordem	inversa	-	de
acordo	com	o	esquema	"little	indian".
Tanto	nesse	último	exemplo	quanto	na	segunda	tentativa	do
exemplo	anterior,	o	programa	trava,	devido	a	um	erro	de
segmentação,	pois	tenta	deslocar	o	ponteiro	de	função	para	um
endereço	onde	não	há	função	alguma.	Todavia,	a	partir	do	momento
que	é	possível	controlar	o	ponteiro	de	função,	é	possível	controlar
também	o	fluxo	de	execução	do	programa,	bastando	para	isso
inserir	no	lugar	de	"ABCD"	um	endereço	válido,	no	qual	está
armazenada	uma	função	qualquer	a	ser	chamada	pelo	código	na
hora	certa.
0	 comando	 nm	 lista	 os	 símbolos	 contidos	 nos	 arquivos
especificados	 e	pode	 ser	 utilizado	para	descobrir	 os	 endereços	das
funções	 de	 um	 determinado	 programa,	 como	mostra	 o	 exemplo	 a
seguir:
Afunção	jackpot()	é	um	alvo	perfeito	para	um	eventual	exploit:	as
probabilidades	de	ganhar	o	jogo	são	totalmente	desfavoráveis	para	o
jogador,	mas	se	o	ponteiro	da	função	for	sobrescrito	com	o	endereço
da	 função	 jackpot(),	 o	 jogo	 sequer	 começará,	 pois	 logo	 no	 início
será	chamada	a	função	para	somar	100	pontos	ao	saldo	do	jogador.
Para	causar	o	overflow	do	buffer	da	BSS	de	modo	que	o	ponteiro
da	 função	 pule	 para	 o	 endereço	 0804871c	 (onde	 se	 encontra	 a
função	 jackpot(),	 destacada	 em	 negrito	 na	 listagem	 anterior),
podemos	usar	o	seguinte	comando,	que	emprega	o	comando	printf
entre	acentos	graves:
Repare	 que	 logo	 após	 as	 mensagens	 de	 debug,	 o	 código	 pulou
diretamente	para	a	execução	da	função	jackpot,	somando	facilmente
100	pontos	para	o	nosso	saldo,	sem	depender	da	sorte.
A	 vulnerabilidade	 desse	 jogo	 seria	 ainda	 maior	 se	 o	 programa
fosse	definido	como	suid	root,	da	seguinte	maneira:
De	fato,	agora	que	o	programa	é	executado	como	root,	é	possível
controlar	 seu	 fluxo	 de	 execução,	 o	 que	 pode	 permitir	 obter
facilmente	 uma	 shell	 de	 root	 no	 sistema.	 Para	 isso,	 podemos
armazenar	o	shellcode	em	uma	variável	de	ambiente:
Caso	 o	 leitor	 queira	 rever	 como	 utilizar	 variáveis	 de	 ambiente
para	 armazenar	 o	 shellcode,	 consulte	 novamente	 o	 tópico
Explorando	 as	 variáveis	 do	 ambiente	 de	 execução,	 no	 Capítulo	 2
deste	livro.
Vejamos	no	exemplo	a	seguir	como	fazer:
Geralmente,	 o	 conceito	 de	 buffer	 overflow	 é	 bastante	 simples:
podemos	 fazer	 com	que	 os	 dados	 sejam	 colocados	 além	do	 limite
predefinido	e	aproveitar	esta	característica	para	modificar	o	fluxo	do
programa	e,	com	isso,	seu	resultado.
	
	
A	 criação	 de	 exploits	 baseados	 no	 format	 string	 é	 uma	 técnica
razoavelmente	 nova	 e	 adotada	 pelos	 hackers	 recentemente.	Assim
como	ocorre	com	os	exploits	baseados	no	overflow	do	buffer,	essa
técnica	também	visa	sobrescrever	dados	para	assumir	o	controle	do
fluxo	de	execução	de	programas.	Esses	exploits	se	baseiam	também
em	 erros	 de	 programação	 que,	 ao	 menos	 aparentemente,	 não
parecem	afetar	diretamente	a	segurança	do	programa.
Todavia,	 desde	 que	 essa	 técnica	 se	 tornou	 conhecida	 no	mundo
dos	desenvolvedores,	ficou	mais	fácil	identificar	e	corrigir	os	pontos
fracos	 responsáveis	 pelas	 vulnerabilidades	 relacionadas	 a	 format
string.
Os	 format	 string	 são	 empregados	 por	 funções	 que	 prevêem	 a
formatação,	 como	 por	 exemplo,	 a	 função	 printf().	 Tratam-se,
geralmente,	de	funções	que	aceitam	um	format	string	como	primeiro
argumento,	 seguido	 pelos	 demais	 parâmetros	 que	 dependem	 do
próprio	format	string.
Nos	exemplos	dos	capítulos	anteriores	utilizamos	com	freqüência
a	função	printf	0,	como	no	trecho	de	código	mostrado	a	seguir,	que
foi	retirado	do	programa	mygame_1	usado	no	Capítulo	3:
Nessa	linha	de	código	o	format	string	é	"Você	escolheu	o	número
%d\n".	A	função	exibe	na	tela	essa	linha	e,	ao	mesmo	tempo,	realiza
uma	operação	quando	encontra	o	parâmetro	%d,	fazendo	com	que	o
argumento	sucessivo	à	função	seja	exibido	como	um	número	inteiro
decimal.	 Veja	 na	 Tabela	 4.1	 a	 seguir,	 outros	 tipos	 de	 formato
similares:
Tabela	4.1:	Outros	tipos	de	formato	similares.
Todos	os	parâmetros	de	formato	adquirem	os	dados	como	valores,
não	 como	 ponteiros	 de	 valores.	 Há	 também	 outros	 parâmetros	 de
formato	que	exigem	o	uso	de	ponteiros	de	valores,	como	os	listados
a	seguir	(Tabela	4.2):
Tabela	4.2:	Outros	parâmetros	de	formato	que	exigem	o	uso	de
ponteiros	de	valores.
0	parâmetro	de	formato	%s	espera	receber	um	endereço	de
memória	e	exibe	os	dados	relativos	a	esse	endereço	até	encontrar
um	byte	nulo.	0	parâmetro	9.n	também	precisa	receber	o	endereço
de	memória	a	ser	analisado	para,	em	seguida,	retornar	o	número	de
bytes	escritos	até	o	momento	no	endereço	de	memória	especificado.
Funções	 como	 printf()	 analisam	 o	 valor	 de	 format	 string	 e
realizam	determinada	operação	todas	as	vezes	que	é	encontrado	um
parâmetro	 de	 formato:	 cada	 parâmetro	 de	 formato,	 por	 sua	 vez,
necessita	da	transferência	de	uma	variável:	se,	por	exemplo,	houver
três	parâmetros	de	formato	em	um	format	string,	serão	necessários
outros	três	argumentos	para	a	função.
Vejamos,	 no	 código	 exibido	 a	 seguir,	 um

Continue navegando