Neste projecto pretende-se testar a fiabilidade e segurança de uma pequena aplicação em C, um gestor de "passwords", empregando análise estática, "fuzzing", testes unitários de software, e "runtime sanitizers". Será usado um "container" Docker para a realização do trabalho.
O trabalho pode ser realizado individualmente ou em grupos de 2 alunos. Entregue o seu código (arquivo ZIP) e relatório (PDF) por email ao docente até 7 de Janeiro de 2019.
Precisa de instalar o Docker Engine Community Edition, disponível para Linux, MacOS ou Windows.
Obtenha o arquivo ZIP com o material base do projecto na página da disciplina.
Consulte o ficheiro README-docker.txt
incluído no arquivo ZIP.
O pwm
é um gestor rudimentar (e inseguro!) de ficheiros de "passwords"
com o formato a seguir ilustrado:
admin:68ce6c40:f699d9208839b227a050ce73c8558662
steve:d28d18a9:3b7ac81dfd7bcb735cd0bc596f821afc
linus:02279492:d57aef52733f7ea2fa9f08cd7c44c0ed
edrdo:64507481:8ba7a666b19c26b302db038d093eaae7
Cada linha tem 3 campos: o id do utilizador, um "salt" de 4 bytes obtido de /dev/urandom
, e um "checksum" MD5 gerado a partir do "salt" e da "password" do utilizador. O utilizador admin
é especial: aberturas posteriores do ficheiro requerem a confirmação da "password" deste utilizador, e este utilizador não pode também ser apagado (supostamente!).
A seguir é ilustrada uma execução de uma (de uma versão "correcta" do) programa pwm
(na pasta pwmsafe
) que deu origem ao ficheiro exemplo.
PWM command (e.g. 'help') [1]: init passfile.txt Qses1819!
>> Command: 'init' [ 'passfile.txt' 'Qses1819!' ]
<< 'init' -- success.
PWM command (e.g. 'help') [2]: add bill Gates!0
>> Command: 'add' [ 'bill' 'Gates!0' ]
<< 'add' -- success.
PWM command (e.g. 'help') [3]: add steve sb0J.Mac
>> Command: 'add' [ 'steve' 'sb0J.Mac' ]
<< 'add' -- success.
PWM command (e.g. 'help') [4]: add linus T0rvalds
>> Command: 'add' [ 'linus' 'T0rvalds' ]
<< 'add' -- success.
PWM command (e.g. 'help') [5]: add edrdo inv_pass
>> Command: 'add' [ 'edrdo' 'inv_pass' ]
PWM error! Invalid password 'inv_pass'!
<< 'add' -- failure (error code 8) !
PWM command (e.g. 'help') [6]: add edrdo Zxy199.
>> Command: 'add' [ 'edrdo' 'Zxy199.' ]
<< 'add' -- success.
PWM command (e.g. 'help') [7]: add edrdo ZZZZ1x9
>> Command: 'add' [ 'edrdo' 'ZZZZ1x9' ]
PWM error! User 'edrdo' already exists!
<< 'add' -- failure (error code 6) !
PWM command (e.g. 'help') [8]: update edrdo ZZZZ1x9
>> Command: 'update' [ 'edrdo' 'ZZZZ1x9' ]
<< 'update' -- success.
PWM command (e.g. 'help') [9]: delete bill
>> Command: 'delete' [ 'bill' ]
<< 'delete' -- success.
PWM command (e.g. 'help') [10]: save
>> Command: 'save' [ ]
<< 'save' -- success.
PWM command (e.g. 'help') [11]: quit
>> Command: 'quit' [ ]
<< 'quit' -- success.
Outra execução a partir do ficheiro gerado anteriormente poderia ser:
PWM command (e.g. 'help') [1]: open passfile.txt Qses1819!
>> Command: 'open' [ 'passfile.txt' 'Qses1819!' ]
<< 'open' -- success.
PWM command (e.g. 'help') [2]: list
>> Command: 'list' [ ]
admin:68ce6c40:f699d9208839b227a050ce73c8558662
steve:d28d18a9:3b7ac81dfd7bcb735cd0bc596f821afc
linus:02279492:d57aef52733f7ea2fa9f08cd7c44c0ed
edrdo:64507481:8ba7a666b19c26b302db038d093eaae7
4 users
<< 'list' -- success.
PWM command (e.g. 'help') [3]: clear
>> Command: 'clear' [ ]
<< 'clear' -- success.
PWM command (e.g. 'help') [4]: quit
>> Command: 'quit' [ ]
<< 'quit' -- success.
Use o comando help
para obter referência dos comandos disponíveis ou help <cmd>
para um comando específico:
PWM command (e.g. 'help') [1]: help
>> Command: 'help' [ ]
Available commands:
init <file> <admin-pass> : initialize PWM data (without saving to file)
open <file> <admin-pass>: open PWM file supplying admin password.
save : save PWM data to file.
clear: clear all PWM data in memory (all changes lost)
add <user> <pass> : add user/password pair
delete <user> : delete user
update <user> <pass> : update password for user
check <user> <pass> : check that password is correct for user
list PWM entries
quit : quits the program
help [<cmd>]: displays this message or help for specific command
<< 'help' -- success.
PWM command (e.g. 'help') [2]: help add
>> Command: 'help' [ 'add' ]
add <user> <pass> : add user/password pair
<< 'help' -- success.
O código encontra-se no directório pwm
. Para compilar execute:
cd pwm
make clean all
O código é gerado pelo compilador clang (versão 7.0) em modo "debug", permitindo caso deseje usar o "debugger" gdb, e instrumentados para deteção de falhas através dos "sanitizers" UndefinedBehaviorSanitizer e o AddressSanitizer.
O código está organizado em vários ficheiros:
pwm.h
: "header file" com as declarações de tipos e funções;main.c
: código do ponto de entrada do programa (função main
);commands.c
: tratamento de comandos introduzidos pelo utilizador;core.c
: funções nucleares para a manipulação de ficheiros de "password";validation.c
: funções de validação para identificar nomes e passwords válidas permitidos pelo sistema;utils.c
: funções utilitárias como tratamento de input, geração de "salt" e cálculo do "hash" MD5 para "passwords".md5.c
: código da RSA para cálculo de "checksums" MD5, ligeiramente adaptado;md5test.c
: programa de teste do MD5 (poderá verificar que produz resultados idênticos a utilitários como md5sum
).As vulnerabilidades deliberadamente introduzidas no pwm
incluem:
A versão sem vulnerabilidades deliberadamente introduzidas está disponível na pasta pwmsafe
(apenas) em formato binário.
O código usa um pequeno conjunto de funções da biblioteca C. As principais usadas são:
strcmp
, strcpy
, strchr
;memcpy
, memcmp
;malloc
, free
, strdup
;gets
, fgets
, fopen
, fclose
, fprintf
, putchar
, fputc
, fread
.No "container" Docker pode executar man function
para consultar
a documentação de function
, ex:
man strcpy
Veja pwm/validation.c
para o código de validação nas funções
pwm_is_valid_user
e pwm_is_valid_password
.
Um nome de utilizador deverá ser considerado válido por pwm_is_valid_user
se:
'a'
a 'z'
.Uma password deverá ser considerada válida por pwm_is_valid_password
se (as restrições não são particularmente fortes!):
a
a z
.A
a Z
.0
a 9
.. , : ! ?
. No directório p2/gtest
encontrará testuser.cpp
e testpass.cpp
,
codificando testes unitários sobre pwm_is_valid_user
e pwm_is_valid_password
, respectivamente.
Os testes são estruturados usando a "framework" Google Test e relatórios de cobertura de código são gerados
pela ferramenta gcov.
Ao executar make
os testes serão compilados e executados, obtendo:
Detalhes sobre a execução dos testes, identificando os testes que passaram e os que falharam.
Um ficheiro validation.c.gcov
com a análise de cobertura dos testes.
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from test_user
[ RUN ] test_user.short_length
[ OK ] test_user.short_length (0 ms)
[ RUN ] test_user.valid_user
testuser.cpp:11: Failure
Expected equality of these values:
PWM_OK
Which is: 0
pwm_is_valid_password("ruiruiruir")
Which is: 8
[ FAILED ] test_user.valid_user (0 ms)
[----------] 2 tests from test_user (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] test_user.valid_user
1 FAILED TEST
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from test_pass
[ RUN ] test_pass.short_length
[ OK ] test_pass.short_length (0 ms)
[ RUN ] test_pass.valid_pass1
testpass.cpp:11: Failure
Expected equality of these values:
PWM_OK
Which is: 0
pwm_is_valid_password("Aa9!zzzzz")
Which is: 8
[ FAILED ] test_pass.valid_pass1 (0 ms)
[----------] 2 tests from test_pass (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] test_pass.valid_pass1
1 FAILED TEST
File 'validation.c'
Lines executed:39.29% of 28
Branches executed:10.00% of 40
Taken at least once:5.00% of 40
No calls
Creating 'validation.c.gcov'
O objectivo é programar testes por forma a identificar "bugs" nas funções de validação e satisfazer determinados critérios de cobertura. Deverá acertar o código por forma a que este funcione correctamente. Os "bugs" em causa são identificáveis por inspeção manual do código com relativa facilidade.
Descreva no relatório os "bugs" que encontrou e as mudanças no código que operou para um funcionamento correcto de pwm_is_valid_user
e pwm_is_valid_password
.
Em mais detalhe, deverá:
Programe testes em testuser.cpp
por forma a atingir uma cobertura
de saltos ("branch coverage") de 100% em pwm_is_valid_user
. Analise o ficheiro validation.c.gcov
após cada execução para avaliar a cobertura.
Inspecione os erros obtidos pelos testes falhados e veja se correspondem a "bugs" ou a testes mal programados.
Programe testes em testpass.cpp
por forma a atingir uma cobertura
"pair-wise coverage" (PWC) tendo uma conta uma estratégia de testes por "input space partitioning". Para o efeito defina blocos apropriados para as seguintes cacterísticas sobre o argumento de input (a "password"):
testpass.cpp
:
Em termos de cobertura estrutural, os testes que programou atingem 100% de cobertura de saltos sobre pwm_is_valid_password
? Se não for o caso, explique porque estes não foram satisfeitos pelos testes PWC e escreva novos testes para atingir esse nível de cobertura.
Em pwm/AnalysisAndCompilerWarnings.txt
são agrupados uma série de avisos (no total de 5) emitidos pelo clang ou pelo clang static analyzer. Pode executar o script runAnalyzer.sh
para obter as mesmas mensagens de aviso.
== 1 ==
main.c:23:9: warning: Call to function 'gets' is extremely insecure as it can always result in a buffer overflow
if (gets(line) == NULL) {
^~~~
== 2 ==
commands.c: In function 'pwm_handle_command':
commands.c:68:6: warning: format not a string literal and no format arguments [-Wformat-security]
printf(command_args[j]);
^
== 3 ==
core.c:14:3: warning: Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119
strcpy(node -> user, user);
^~~~~~
== 4 ==
core.c:60:3: warning: Attempt to free released memory
free(pwm);
^~~~~~~~~
== 5 ==
core.c:208:22: warning: Use of memory after it is freed
node -> next = node -> next -> next;
^~~~~~~~~~~~~~~~~~~~
Para cada caso:
Explique sucintamente por que o código é sensível em termos de segurança e/ou fiabilidade.
Explique como durante a execução do pwm
a vulnerabilidade se pode manifestar, levando a um "crash" detectado pelos "sanitizers" ou a um erro lógico. Identifique a sequência de comandos em causa dados ao PWM. Se não conseguir talvez o uso de "fuzzers" (ver próxima secção) ajude.
Acerte o código por forma a corrigir o problema (e mencione a mudança que operou no relatório).
Será que usando "fuzzing" descobrimos mais vulnerabilidades?
No directório fuzzing
encontra o seguinte material
que pode
grammar.blab
: uma gramática para uso com o blab;gblab.sh
: script para gerar exemplos de input com o blab;gradamsa.sh
: script para gerar exemplos de input com o radamsa;samples
: directório com uma série de exemplos gerados pelo blab ou blab+radamsa;compare.sh
: um script que executa a versão "safe" (em pwmsafe
) e a "unsafe" e verifica se há diferença de comportamento.Por exemplo poderá executar o pwm
usando o ficheiro samples/blab1.txt
:
../pwm/pwm < samples/blab1.txt
ou comparar a versão "safe" com a "unsafe" usando
./compare.sh samples/blab1.txt
Para cada vulnerabilidade que encontre com um ficheiro "fuzzed":
Repare que um adversário (com acesso de escrita ao ficheiro de "passwords") poderia remover ou alterar a ordem de entrada do utilizador "admin" de um ficheiro de "passwords" sem que o pwm
note. Explique porquê e como um adversário poderia tirar partido dessa vulnerabilidade. Modifique a função pwm_open
por forma a corrigir a falha.
Da mesma forma um adversário sem muitos conhecimentos poderia duplicar uma linha do ficheiro e modificar apenas o utilizador para ficar com "password" equivalente ao utilizador original (que talvez tenha uma "password conhecida"). Isto acontece porque o "checksum" MD5 ter em conta apenas o "salt" e o "password", mas não o nome do utilizador. Generalize pwm_hash_password
em utils.c
para
isso acontecer e modifique a lógica do programa em outros pontos
necessários.
Consegue conduzir um ataque de "stack-smashing" ao programa PWM (original) de forma a obter uma "shell"? Explique como em princípio isso seria possível e, caso o tenha levado a cabo, como o conseguiu materializar e inclúa no seu arquivo ZIP os ficheiros / "scripts" que usou.
Apesar de todos os esforços, o PWM não deixa de ser um gestor de "passwords" rudimentar. Que revisão ou adição de funcionalidades acha que deveriam ser contempladas para um gestor de passwords mais robusto?