L'ARM c'est tout mignon tout plein, c'est censé être simple étant donné que c'est une architecture RISC (reduced instruction set computer) mais quand on se trouve devant des instructions comme STMFD SP!, {R4-R8,R10,LR} ou SUB R0, R0, R3,LSL#4 ou encore devant un listing qui ressemble à ca :

LDR     R3, =0xB896
MOV     R1, R0,LSR#16
MUL     R12, R1, R3
EOR     R12, R12, R0
MOV     R12, R12,LSL#16
MOV     R3, R12,LSR#16
MOV     R2, R3,LSL#13
SUB     R2, R2, R3,LSL#2
RSB     R2, R3, R2
MOV     R2, R2,LSL#3
ADD     R2, R2, R3
EOR     R2, R2, R1
MOV     R2, R2,LSL#16
MOV     R3, R2,LSR#16
MOV     R0, R3,LSL#9
SUB     R0, R0, R3,LSL#7
RSB     R0, R3, R0
MOV     R1, R0,LSL#4
ADD     R0, R0, R1
EOR     R0, R0, R12,LSR#16
LDR     R1, =0x4520
MOV     R0, R0,LSL#16
MOV     R0, R0,LSR#16
MUL     R3, R0, R1
EOR     R3, R3, R2,LSR#16
ORR     R0, R0, R3,LSL#16
BX      LR

eh bien on prend peur.

Pourtant une fois qu'on s'est familiarisé avec le format des instructions ARM on se rend compte qu'elles sont très facilement transcriptable (je doute de l'existence de ce mot mais bon) en C.

Le format général des instructions arm est la suivante : Instruction destination , source1, [source2, [LSL/LSR #n]]

LSL et LSR servant à décaler source2 de n bit vers la gauche (L) ou vers la droite (R).

ainsi SUB R0, R0, R3,LSL#4 est équivalent à r0 = r0 - (r3 << 4)

Il suffit alors de se familiariser avec les instruction ARM telles que :

  • RSB : reverse substract, au lieu de faire destination = source1 - source2, elle effectue un destination = source2 - source1
  • EOR : 'équivalent de l'instruction XOR en x86
  • STMFD : push une série de registres, le '!' servant à mettre à jour le registre de pile
  • LDMFD : l'instruction inverse
  • BL : équivalent du call
  • BCC : équivalent des jcc en x86
  • LDR : load register, affecte à destination le DWORD pointé par source
  • STR : store register, affecte au DWORD pointé par la source la valeur de source

enfin il faut savoir que les fonctions ARM n'utilisent que très peu la pile ou uniquement pour stocker des tableaux, le compilateur a en effet 13 registres généraux pour effectuer ses calculs, stocker ses variables ce qui rend l'analyse de code ARM sous IDA assez pénible, impossible de faire comme en x86 et renommer tout plein de variables. De plus l'adresse de retour des fonction n'est théoriquement pas situé sur la pile mais dans un registre : r14 ou LR sous IDA et le retour d'une fonction ne se fait pas avec un retn mais en plaçant directement la valeur de LR dans PC (ou r15, l'équivalent de eip en ARM) soit à l'aide de LDMFD si LR a été enregistré dans la pile (couple STMFD SP!, {R4-R12,LR} [...] LDMFD SP!, {R4-R12,PC}, équivalent de notre push ebx | push edi | push esi [...] pop esi | pop edi | pop ebx | retn) soit à l'aide de l'instruction BX LR.

Passons maintenant à l'analyse du programme :

Les APIs ne sont pas appelées directement mais via l'exécution de la suite d'instruction suivante :

ADR     R12, X
ADD     R12, R12, #0x1000
X:
LDR     PC, [R12,#X2]!

Ne me demandez pas pourquoi, je ne sais pas. Pour retrouver les APIs appelées il suffit donc de regarder l'adresse se situant en X+0x1000+X2, une fois fait cela nous voyons directement quelles APIs sont appelées

Nous trouvons donc rapidement que la routine exécutée à chaque connexion à l'aide d'un pthread_create est située en 0x88C4. Maintenant que nous savon quoi étudier nous allons retranscrire le code en C, petite précision avant :

la suite d'instructions

MOV     R3, R4,LSL#6
SUB     R5, R3, R4,LSL#4

correspond au code C suivant : r5 = r4*0x30, c'est une optimisation faite par le compilateur, 2 décalages et une soustraction étant moins couteux qu'une multiplication, on a donc :

r5 = (r4 << 6) - (r4 << 4) <=> r5 = r4*2^6 - r4*2^4 <=> r5 = r4 * (2^6 - 2^4) <=> r5 = r4*0x30

passons au code retranscrit en C :

  1. DWORD c1(DWORD d)
  2. {
  3. DWORD a = d >> 16;
  4. DWORD b = ((a - 0x399A) ^ d) & 0xFFFF;
  5. DWORD c = b - 0x459A;
  6. DWORD e;
  7. a = (a ^ c) & 0xFFFF;
  8. e = ((a + 0x70FB) ^ b) & 0xFFFF;
  9. c = (e - 0x2520) ^ a;
  10.  
  11. return e | (c << 16);
  12. }
  13.  
  14. DWORD c2(DWORD h)
  15. {
  16. DWORD r3,r1,r12,r2,r0;
  17. r1 = (h >> 16);
  18. r12 = ((0xB896 * r1) ^ h) << 16;
  19. r3 = r12 >> 16;
  20. r2 = (((((r3 << 13) - (r3 << 2) - r3) << 3) + r3) ^ r1) << 16;
  21. r3 = r2 >> 16;
  22. r0 = ((r3 << 9) - (r3 << 7)) - r3;
  23. r1 = r0 << 4;
  24. r0 = ((r1 + r0) ^ (r12 >> 16)) & 0xFFFF;
  25. r3 = (r0*(0x4520)) ^ (r2 >> 16);
  26. return r0 | (r3 << 16);
  27. }
  28.  
  29. void crypt(PDWORD buff, int size, DWORD (*fun)(DWORD))
  30. {
  31. int i;
  32.  
  33. for (i = 0; i < size/4 ; i++)
  34. {
  35. buff[i] = fun(buff[i]);
  36. }
  37. }
  38.  
  39. void serv(int* p_socket)
  40. {
  41.  
  42. int i,n;
  43. int socket = *p_socket;
  44. char buff[0x34];
  45. char buff2[0x20];
  46.  
  47. for (i = 0x34; i > 0 ; i-= n)
  48. {
  49. if (!(n = recv(socket,&buff[0x34-i],i,0x100)))
  50. {
  51. close(socket);
  52. free(p_socket);
  53. return;
  54. }
  55. }
  56.  
  57. crypt((PDWORD)buff,0x34,c1);
  58.  
  59. switch(*((PDWORD)buff))
  60. {
  61. case 0xCAFEBABE :
  62. memcpy((*nbMess)*0x30+magic,buff+4,0x30);
  63. send(socket,MagicString+(*nearIAT)*0x30,0x30,0)
  64. *nearIAT = (*nbMess + 1) & 0x7F;
  65. break;
  66. case 0xBABECAFE :
  67. for (i = 0; i != 0x80; i++)
  68. {
  69. if (! memcmp(magic+i*0x30,buff+4,0x10))
  70. {
  71. memcpy(buff2,magic+i*0x30+0x10,0x20)
  72. break;
  73. }
  74. }
  75. if (i == 0x80)
  76. {
  77. memcpy(buff2,magic+0x10);
  78. break;
  79. }
  80. crypt((PDWORD)buff2,0x20,c2);
  81. send(socket,buff2,0x20,0);
  82. break;
  83. case 0xBABEBABE :
  84. memcpy(buff2,magic,0x10);
  85. crypt((PDWORD)buff2,0x10,c2);
  86. send(socket,buff2,0x10,0);
  87. for (i = 1; i != 80 ; i++)
  88. {
  89. memcpy(buff2,magic+i*0x30,0x10)
  90. crypt((PDWORD)buff2,0x10,c2);
  91. send(socket,buff2,0x10,0);
  92. }
  93. break;
  94.  
  95. default :
  96. crypt((PDWORD)buff,0x34,c2);
  97. send(socket,buff,0x34,0);
  98. break;
  99. }
  100. shutdown(socket,2);
  101. close(socket);
  102. free(p_socket);
  103. return;

Le message reçus sont déchiffrés à l'aide de la routine c1 (que l'on reversera bientôt ;) )

magic est un buffer de 0x30*0x80 bytes dans le quel sont stockés les messages chiffré à l'aide de la routine c2 envoyés avec le code 0xCAFEBABE, ce buffer peut être dumpé à l'aide de la commande BABEBABE. Le but du challenge étant de dumper puis déchiffrer les messages stockés pour retrouver un flag qu'il faudra ensuite envoyé aux organisateurs.

Pour dumper ce buffer il faut donc envoyer un message commencant par le DWORD 0xBABEBABE, il est donc nécessaire d'écrire la fonction inverse de c1 :

  1. DWORD d1(DWORD f)
  2. {
  3. DWORD a,b,c,d,e;
  4. e = f & 0xFFFF;
  5. c = (f >> 16) & 0xFFFF;
  6. a = (c ^ (e - 0x2520)) & 0xFFFF;
  7. b = (e ^ (a + 0x70FB)) & 0xFFFF;
  8. c = b -0x459A;
  9. a = (a ^ c) & 0xFFFF;
  10. d = (b^(a - 0x399A)) & 0xFFFF;
  11. return a << 16 | d;
  12. }

puis il va falloir déchiffrer le résultat de cette requête en écrivant la fonction inverse de c2, cette fonction n'étant pas directement reversable, c'est plus une fonction de hash que de chiffrement, j'ai décidé de tout simplement bruteforcer la valeur des DWORD (ca prend pas mal de temps ...) à l'aide de la fonction suivante :

  1. DWORD d2(DWORD h)
  2. {
  3. DWORD solution;
  4. for (solution = 0; solution < 0xFFFFFFFF ; solution++)
  5. {
  6. if (c2(solution) == h)
  7. return solution;
  8. }
  9. return 0xFFFFFFFF;
  10. }

Ne sachant pas qu'il fallait rechercher des flags (chaine formatée de façon zarb) je cherchais une string type "passkey : ..." et je n'ai donc pas validé cette épreuve :D

Ensuite j'ai cherché à trouver une faille mais la fonction n'était pas faillible ... Je vais quand même brièvement expliqué comment pourrait être exploité un buffer overflow dans un environnement ARM.

Je vous ai dit un peu plus tôt que l'adresse de retour d'une fonction n'était pas stocké dans la pile mais dans un registre, c'est vrai pour les fonction n'appelant aucune sous fonction mais partiellement faux pour les autres. En effet ce registre étant unique, si une fonction appelle une sous fonction sans enregistrer son registre LR, il sera écrasé par l'adresse de retour de la sous fonction et la fonction l'aura donc dans l'os. C'est pourquoi au début d'une fonction appelant des sous fonctions, LR est enregistré dans la pile puis dépilé dans PC à la fin de la fonction (le fameux couple STMFD SP!, {R4-R12,LR} [...] LDMFD SP!, {R4-R12,PC}). C'est bien sur là que l'exploitation d'un buffer overflow devient possible !

Il suffit d'écraser la valeur de LR enregistrée par l'adresse de notre shellcode ou par l'adresse d'une instruction type MOV PC , SP et le tour est joué ;) (bon là c'est théorique, je ne sais pas vraiment si on trouve aussi facilement ce type d'instruction que les call esp en x86)