Neste projecto pretende-se testar a fiabilidade e segurança de uma pequena aplicação em C, um gestor de "passwords". Para tal serão empregues analisadores estáticos, testes unitários, "fuzzers", e "runtime sanitizers". É disponibilizada uma imagem 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é 10 de Janeiro de 2020.
Obtenha o arquivo ZIP a partir de https://www.dcc.fc.up.pt/~edrdo/aulas/qses/projects/02/qses1920_project2.zip.
Pode usar a máquina clang para se ambientar ao projecto, já que tem todo o software instalado para o efeito e sem precisar de usar o Docker.
$ curl https://www.dcc.fc.up.pt/~edrdo/aulas/qses/projects/02/qses1920_project2.zip -o qses1920_project2.zip
$ unzip qses1920_project2.zip
$ cd qses1920_project2
Para iniciar depois o trabalho, será no entanto conveniente usar um PC ou VM não-partilhados (ver abaixo).
Precisa de instalar o Docker Engine Community Edition, disponível para Linux, MacOS ou Windows.
Poderá criar uma VM para o efeito na Google Cloud, caso seja mais conveniente do que trabalhar no seu PC. Sugestão: crie uma VM com SO Ubuntu 19.04, 1 CPU, e 4 GB de RAM.
Consulte o ficheiro docker/README-docker.txt
incluído no arquivo ZIP.
O pwm
é um gestor rudimentar (e inseguro!) de ficheiros de "passwords",
com um formato algo similar ao ficheiro /etc/shadow
em sistemas Linux (embora mais simples), que é 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.
qses@qses:~/p2/pwmsafe$ ./pwm
PWM command (e.g. 'help') [1]: init passfile.txt Qses1920!
>> Command: 'init' [ 'passfile.txt' 'Qses1920!' ]
<< '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:
qses@qses:~/p2/pwmsafe$ ./pwm
PWM command (e.g. 'help') [1]: open passfile.txt Qses1920!
>> Command: 'open' [ 'passfile.txt' 'Qses1920!' ]
<< '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.
Finalmente, note que um ficheiro contendo comandos pode também ser passado como argumento.
qses@qses:~/p2/pwmsafe$ cat ../pwm/fuzzing/seeds/example.txt
init passfile.txt Qses1920!
add bill Gates!0
add steve sb0J.Mac
add linus T0rvalds
add edrdo inv_pass
add edrdo Zxy199.
add edrdo ZZZZ1x9
update edrdo ZZZZ1x9
delete bill
list
save
quit
qses@qses:~/p2/pwmsafe$ ./pwm ../pwm/fuzzing/seeds/example.txt
PWM command (e.g. 'help') [1]: >> Command: 'init' [ 'passfile.txt' 'Qses1920!' ]
<< 'init' -- success.
PWM command (e.g. 'help') [2]: >> Command: 'add' [ 'bill' 'Gates!0' ]
<< 'add' -- success.
PWM command (e.g. 'help') [3]: >> Command: 'add' [ 'steve' 'sb0J.Mac' ]
<< 'add' -- success.
PWM command (e.g. 'help') [4]: >> Command: 'add' [ 'linus' 'T0rvalds' ]
<< 'add' -- success.
PWM command (e.g. 'help') [5]: >> Command: 'add' [ 'edrdo' 'inv_pass' ]
PWM error! Invalid password 'inv_pass'!
<< 'add' -- failure (error code 8) !
PWM command (e.g. 'help') [6]: >> Command: 'add' [ 'edrdo' 'Zxy199.' ]
<< 'add' -- success.
PWM command (e.g. 'help') [7]: >> Command: 'add' [ 'edrdo' 'ZZZZ1x9' ]
PWM error! User 'edrdo' already exists!
<< 'add' -- failure (error code 6) !
PWM command (e.g. 'help') [8]: >> Command: 'update' [ 'edrdo' 'ZZZZ1x9' ]
<< 'update' -- success.
PWM command (e.g. 'help') [9]: >> Command: 'delete' [ 'bill' ]
<< 'delete' -- success.
PWM command (e.g. 'help') [10]: >> Command: 'list' [ ]
In memory-contents for 'passfile.txt'
admin:376c2577:3f8274b7f0b5771b91474c7b0b52ffe1
steve:eb10ba4f:0418ea8a3ea7d3de01bc621f8e0df17f
linus:8e99c480:90ab55f925b043933ac296156d214557
edrdo:f8c7cafa:c3884e5b837b50f4a8d1f8110bfb3197
4 users
<< 'list' -- success.
PWM command (e.g. 'help') [11]: >> Command: 'save' [ ]
<< 'save' -- success.
PWM command (e.g. 'help') [12]: >> Command: 'quit' [ ]
<< 'quit' -- 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 9.0) em modo "debug", permitindo caso deseje usar o "debugger" gdb, e instrumentados para deteção de falhas através do "sanitizer" AddressSanitizer. É ainda fornecido um programa para "white-box fuzzing" usando a libFuzzer.
O código está organizado em vários ficheiros:
pwm.h
: "header file" com declarações de tipos e funções;pwm.c
: código do ponto de entrada do programa (função main
);pwmfuzz.c
: fuzzer para o pwm
usando a libFuzzer`;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
).Há uma série de faltas (defeitos no código fonte) deliberadamente introduzidas no pwm
, algumas das quais constituem vulnerabilidades de segurança, que incluem:
A versão sem faltas 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
, ... .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 (re)compilados (se necessário) 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.
qses@qses:~/p2/pwm/gtest$ make
[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from test_user
[ RUN ] test_user.short_length
[ OK ] test_user.short_length (0 ms)
[ RUN ] test_user.invalid_char
testuser.cpp:11: Failure
Expected equality of these values:
PWM_INVALID_USER_ID
Which is: 7
pwm_is_valid_user("@rui")
Which is: 0
[ FAILED ] test_user.invalid_char (0 ms)
[ RUN ] test_user.valid_user
testuser.cpp:15: Failure
Expected equality of these values:
PWM_OK
Which is: 0
pwm_is_valid_user("ruiruiruir")
Which is: 7
[ FAILED ] test_user.valid_user (0 ms)
[ RUN ] test_user.valid_user2
[ OK ] test_user.valid_user2 (0 ms)
[----------] 4 tests from test_user (0 ms total)
[----------] Global test environment tear-down
[==========] 4 tests from 1 test suite ran. (1 ms total)
[ PASSED ] 2 tests.
[ FAILED ] 2 tests, listed below:
[ FAILED ] test_user.invalid_char
[ FAILED ] test_user.valid_user
2 FAILED TESTS
[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from test_pass
[ RUN ] test_pass.invalid_pass_length5
[ OK ] test_pass.invalid_pass_length5 (0 ms)
[ RUN ] test_pass.valid_pass_length6
testpass.cpp:11: Failure
Expected equality of these values:
PWM_OK
Which is: 0
pwm_is_valid_password("Aa9za1")
Which is: 8
[ FAILED ] test_pass.valid_pass_length6 (2 ms)
[ RUN ] test_pass.invvalid_pass_with_two_punct
testpass.cpp:15: Failure
Expected equality of these values:
PWM_INVALID_PASSWORD
Which is: 8
pwm_is_valid_password("Aa9za1!!")
Which is: 0
[ FAILED ] test_pass.invvalid_pass_with_two_punct (1 ms)
[ RUN ] test_pass.blacklisted_pass1
testpass.cpp:19: Failure
Expected equality of these values:
PWM_INVALID_PASSWORD
Which is: 8
pwm_is_valid_password("A123zad!")
Which is: 0
[ FAILED ] test_pass.blacklisted_pass1 (1 ms)
[----------] 4 tests from test_pass (6 ms total)
[----------] Global test environment tear-down
[==========] 4 tests from 1 test suite ran. (7 ms total)
[ PASSED ] 1 test.
[ FAILED ] 3 tests, listed below:
[ FAILED ] test_pass.valid_pass_length6
[ FAILED ] test_pass.invvalid_pass_with_two_punct
[ FAILED ] test_pass.blacklisted_pass1
3 FAILED TESTS
File '../validation.c'
Lines executed:83.64% of 55
Branches executed:90.91% of 44
Taken at least once:65.91% of 44
No calls
../validation.c:creating 'validation.c.gcov'
O objectivo é que programe testes por forma a identificar defeitos
no código fonte das funções de validação de utilizadores ou passwords,
e também por forma a atingir uma cobertura de saltos ("branch coverage") de 100 %. Descreva no relatório os defeitos 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 resumo:
Inspecione os erros obtidos pelos testes falhados em testuser.cpp
testpass.cpp
e averigue
que problemas poderão existir no código (certifique-se também
que os testes estão bem programados!). Afine o código.
Programe testes por forma a atingir uma cobertura
de saltos de 100% em pwm_is_valid_user
e pwm_is_valid_password
. Vá adicionando mais testes para melhorar o nível de cobertura (e eventualmente descobrir mais "bugs").
Pode executar o script pwm/run-analyzer.sh
para executar o Clang Static Analyzer sobre o código. Serão exibidas alguns possíveis pontos de vulnerabilidade / falta de fiabilidade do programa:
qses@qses:~/p2/pwm$ ./run-analyzer.sh
[...]
commands.c: In function 'pwm_handle_command':
commands.c:74:6: warning: format not a string literal and no format arguments [-Wformat-security]
printf(command_args[j]);
^~~~~~
[...]
commands.c:277:9: warning: Call to function 'gets' is extremely insecure as it can always result in a buffer overflow
if (gets(line) == NULL) {
^~~~
[...]
core.c:13: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);
^~~~~~
^
core.c:44:5: warning: Attempt to free released memory
free(pwm);
^~~~~~~~~
core.c:213:22: warning: Use of memory after it is freed
node -> next = node -> next -> next;
^~~~~~~~~~~~~~~~~~~~
Pretende-se que analise os problemas detectados e os resolva com acertos no código. Para cada problema detectado explique no relatório:
Porque o código é sensível em termos de segurança e/ou fiabilidade.
Como durante a execução do pwm
o defeito do erro pode levar a um estado de erro, levando a um "crash" detectado pelos "sanitizers" e/ou a um outro tipo de erro. 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.
Como acertou o código por forma a corrigir o problema.
Será que usando "fuzzing" descobrimos mais vulnerabilidades?
Para conhecer as opções do programa execute:
qses@qses:~/p2/pwm$ ./pwmfuzz -help=1
Para um uso equivalente a pwm
forneça um ficheiro de input com comandos:
qses@qses:~/p2/pwm$ ./pwmfuzz fuzzing/seeds/example.txt
Para activar o "fuzzing" tomando como "seed file" um ficheiro:
qses@qses:~/p2/pwm$ ./pwmfuzz -seed=123 -max_total_time=180 -seed_inputs=fuzzing/seeds/example.txt fuzzing/corpus
O directório fuzzing/corpus
acima serve de repositório a um subconjunto de inputs que o libFuzzer gerou e que podem ser reaproveitados em próximas execuções.
Finalmente pode especificar um directório contendo "seed files".
qses@qses:~/p2/pwm$ ./pwmfuzz -seed=123 -max_total_time=180 fuzzing/corpus fuzzing/seeds
As ferramentas radamsa e blab podem ser úteis para gerar automaticamente inputs para por sua vez alimentar pwmfuzz
com dados "representativos" e "variados".
Para ilustração são dados dois scripts, gblab.sh
e gradamsa.sh
e a gramática grammar.blab
para o blab. Adapte estes recursos como achar melhor. Em particular, para tornar mais representativos e de tamanho maior os exemplos gerados pelo blab,
deve extender / adaptar a gramática em grammar.blab
.
Exemplo de uso:
qses@qses:~/p2/pwm/fuzzing$ ./gblab.sh
Running blab - see seeds dir for generated files
qses@qses:~/p2/pwm/fuzzing$ ls seeds
blab1.txt blab2.txt blab4.txt blab6.txt blab8.txt
blab10.txt blab3.txt blab5.txt blab7.txt blab9.txt
qses@qses:~/p2/pwm/fuzzing$ cat seeds/blab1.txt
help delete
init password.txt Qses1920
add edward ZZ34567890129
add admin 123456789012
add charlesf abcdef.e
add charlesf y12!3456
add charles Zz3456789012
list
clear
quit
qses@qses:~/p2/pwm/fuzzing$ ./gradamsa.sh seeds/blab*.txt
Transforming given files using radamsa
seeds/blab1.txt.rad generated
seeds/blab10.txt.rad generated
seeds/blab2.txt.rad generated
seeds/blab3.txt.rad generated
seeds/blab4.txt.rad generated
seeds/blab5.txt.rad generated
seeds/blab6.txt.rad generated
seeds/blab7.txt.rad generated
seeds/blab8.txt.rad generated
seeds/blab9.txt.rad generated
qses@qses:~/p2/pwm/fuzzing$ diff seeds/blab1.txt seeds/blab1.txt.rad
7c7
< add charles Zz3456789012
---
> add \r\n$PATH`xcalc`%n\u0000!!+inf'xcalc$PATH'xcalc%d\r\ncharles Zz3456789012
Ao executar pwmfuzz
são gerados ficheiros de "profiling"
que permitem avaliar a cobertura, tal e qual no caso dos testes
unitários da secção 3.
Pode usar o utilitário gcov.sh ficheiro.c
após uma execução de pwmfuzzer
para gerar um relatório gcov. Estes dados poderão ajudar
a identificar que partes de código não estão a ser atingidas
pelo "fuzzer" e motivar a adição manual ou geração automática de "seed files" em maior número e sobretudo mais expressivas.
Usando os recursos descritos anteriormente, e executando pwmfuzzer
repetidamente, vários crashes poderão se manifestar.
Para cada um destes casos, deverá tentar então identificar o problema
no código fonte e acertá-lo. No relatório:
Preserve ou reponha as chamadas a gets
e a "format string vulnerability" detectadas pela análise estática (em 4). Conduza "stack-smashing attacks" por forma a obter indevidamente uma "shell" no sistema, variando os mecanismos de proteções habilitados. Descreva no relatório o seu trabalho.
Para tal deverá aceder ao directório pwm/ss
, onde encontrará uma Makefile
específica para este efeito, além de versões ligeiramente
diferentes de vários ficheiros e
utilitários que foram usados na folha 5 de exercícios de laboratório.
Notas:
Para compilar o pwm
e outros programas auxiliares use:
make clean all [OPTIONS]
onde os argumentos opcionais OPTIONS
poderão habilitar diferentes
tipos de proteção para o executável pwm
:
CANARIES=1
: habilita o uso de "stack canaries" (canários);NOXSTACK=1
: inibe código executável na stack;PIE=1
: habilita código PIE;Além das proteções configuráveis em tempo de compilação, deverá usar
o script nr.sh
para desabilitar ASLR ao nível do kernel, por exemplo:
./nr.sh ./pwm
(num "container" Docker não é possível alterar a configuração de ASLR /proc/sys/kernel/randomize_va_space
)
Para se ambientar pode começar por replicar os ataques ao programa hello
como no exercício 2 da ficha 5 (passos 1 a 4).
Dicas:
pwm
não é muito diferente do ataque feito para hello
, contudo poderá ser conveniente inserir o "shell code" / apontar o RA mais para "o meio" do buffer de input :) hello
a vulnerabilidade de "format string" deverá permitir obter uma "leak" do endereço do buffer de input.Repare que um adversário (supondo que tem 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.
Mesmo corrigindo os problema anteriores, nada impede "tampering" arbitrário do ficheiro por forma a manipular "à vontade" utilizadores e passwords, ou corromper o ficheiro. Tem alguma(s) proposta(s) para contornar o problema? Explique no relatório os princípios gerais do que propõe (não é obrigado a implementar o que propõe, mas caso esteja inspirado ...).