QSES 2019/20 :: Projecto 2

1. Introdução

1.1. Sumário.

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.

1.2. Arquivo ZIP

Obtenha o arquivo ZIP a partir de https://www.dcc.fc.up.pt/~edrdo/aulas/qses/projects/02/qses1920_project2.zip.

1.3. Uso da VM clang

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).

1.4. Software a instalar

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.

1.5. Uso do Docker

Consulte o ficheiro docker/README-docker.txt incluído no arquivo ZIP.

2. PWM

2.1. Descrição

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!).

2.2. Execução

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.

2.3. Código

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:

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.

2.4. Uso de funções da biblioteca C

O código usa um pequeno conjunto de funções da biblioteca C. As principais usadas são:

3. Testes unitários

3.1. Regras de validação

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:

  1. Tiver entre 4 e 10 caracteres.
  2. For formado apenas por letras minúsculas: '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!):

  1. Tiver entre 6 e 12 caracteres.
  2. Conter pelo menos uma letra minúscula - a a z.
  3. Conter pelo menos uma letra maiúscula - A a Z.
  4. Conter pelo menos um dígito - 0 a 9.
  5. Conter no máximo um dos seguintes caracteres de pontuação: . , : ! ?.
  6. Não ter nenhuma sub-sequência que conste de uma (pequena) "black-list" (ver código fonte).

3.2. Execução de testes

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:

  1. Detalhes sobre a execução dos testes, identificando os testes que passaram e os que falharam.

  2. 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'

3.3. Programação de testes / correcção de bugs

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:

  1. 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.

  2. 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").

4. Análise estática

4.1. Execução

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;
                     ^~~~~~~~~~~~~~~~~~~~

4.2. Correção de problemas

Pretende-se que analise os problemas detectados e os resolva com acertos no código. Para cada problema detectado explique no relatório:

  1. Porque o código é sensível em termos de segurança e/ou fiabilidade.

  2. 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.

  3. Como acertou o código por forma a corrigir o problema.

5. "Fuzzing"

Será que usando "fuzzing" descobrimos mais vulnerabilidades?

5.1. Uso de pwmfuzz

5.2. Gerando "seed files" com radamsa e blab

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

5.3. Cobertura de "fuzzing"

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.

5.4. Identificação e correcção de vulnerabilidades

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:

6. "Stack-smashing"

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:

Dicas:

7. Outras vulnerabilidades

  1. 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.

  2. 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.

  3. 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 ...).