QSES 2018/19 :: 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", 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.

1.2. Software a instalar

Precisa de instalar o Docker Engine Community Edition, disponível para Linux, MacOS ou Windows.

1.3. Arquivo ZIP

Obtenha o arquivo ZIP com o material base do projecto na página da disciplina.

1.4. Uso do Docker

Consulte o ficheiro 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 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!).

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.

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.

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 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:

2.4. Vulnerabilidades

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.

2.5. 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:

No "container" Docker pode executar man function para consultar a documentação de function, ex:

man strcpy

3. Testes unitários sobre funções de validação de utilizadores e passwords

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: . , : ! ?.

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

[==========] 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'

3.3. Programação de testes

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á:

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

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

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

4. Correcção de vulnerabilidades

4.1. Vulnerabilidades identificadas estaticamente

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:

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

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

  3. Acerte o código por forma a corrigir o problema (e mencione a mudança que operou no relatório).

4.2. Uso de "fuzzing"

Será que usando "fuzzing" descobrimos mais vulnerabilidades?

No directório fuzzing encontra o seguinte material que pode

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":

  1. Identifique o comando em particular que causou a falha;
  2. Identifique o ponto de falha no código;
  3. Tente resolver o problema no código e repita a experiência com o mesmo ficheiro de input;
  4. Exponha a deteção e resolução do problema no relatório.

5. Outras vulnerabilidades / desafios

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

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

  4. 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?