thread_45.sh

Python Opcode Gizleme: Güçlü Bir Anti-Analiz Tekniği

0 replies 1 views {"en": "Exploit Development", "tr": "Exploit Geliştirme", "ru": "Разработка эксплойтов"}
0ffset.net
0ffset.net
OP
user
2023-06-18 20:39:05

Python kötü amaçlı yazılımı her zaman kalbimde bir yer tuttu; ilk öğrendiğim dil aynı zamanda temel ters kabuklar ve tuş kaydediciler aracılığıyla enjeksiyon ve uzaktan erişim araçlarını işlemek için kötü amaçlı yazılım geliştirme dünyasını keşfetmek için ilk kullandığım dildi. Python'da geliştirilen kötü amaçlı yazılımların, yazılmasının kolay olması, verimli olmaktan uzak olması, bellek üzerinde hiçbir kontrol sağlamaması, büyük bir ikili dosya halinde derlenmesi ve kolayca kaynağa geri döndürülebilmesi nedeniyle genellikle giriş seviyesi olduğu düşünülür.

Eksikliklerine rağmen Python'da Pupy ve Empire gibi bazı harika araçlar geliştirildi, ancak bunlar daha fazla yetenek sağlamak için C veya PowerShell'in bir karışımını kullanma eğilimindeler.

Yaygın kötü amaçlı yazılımlar açısından, her ikisinin de çıkarılması önemsiz olan PyInstaller veya Py2Exe ile bir ikili dosyaya paketlenmiş kodu görmek alışılmadık bir durum değildir ve kendi çıkarma yöntemlerine sahip garip PyArmor şifreli komut dosyasıyla karşılaşabilirsiniz.

Bununla birlikte, bir süredir en çok ilgilendiğim belirli bir gizleme tekniği Python betiğinin "altındaki" bir katmandı; Python işlem kodlarının kendisi.

Opcode Yeniden Eşleme

Şimdi beni yanlış anlamayın, bu hiçbir şekilde yeni bir teknik değil, PyXie RAT adlı ilginç bir uzaktan erişim aracı hakkında okuduktan sonra bir süredir araştırdığım bir şey. I believe the technique also made its way into a Flare-ON challenge as well, so its likely used by more than just PyXie RAT.

Bu şaşırtma tekniğinin nasıl çalıştığına dair ayrıntılara girmeden önce, öncelikle Python'un gerçekte ne olduğuna bakmamız gerekiyor ve bunu yapmak için tipikprint(“Merhaba Dünya”)kodunu ayıracağız.

Python Opcode'larına Genel Bakış

Zaten bildiğiniz gibi, Python yorumlanmış bir dildir, yani bir formatı diğerine dönüştürmek için bir tercüman gerektirir; bu durumda standart Python sözdizimini makine koduna dönüştürür. Artıkprint(“Merhaba Dünya”) and*push 0x0040010;call printfarasında birkaç adım daha var, bunlar TutorialsPoint tarafından oldukça güzel bir şekilde özetlenmiştirhere.

Basitçe söylemek gerekirse, Python yorumlayıcısıprint(“Merhaba Dünya”)komutunu alacak ve onu daha küçük parçalara ayıracaktır. This is first converted to an abstract syntax tree (AST) before being converted to Python bytecode. Ağacın basit tek astarımız için nasıl görünebileceğini görmek için Pythonastmodülünü kullanabiliriz:

>>> import ast
>>> code = "print(\"Hello World\")"
>>> tree = ast.parse(code)
>>> print(ast.dump(tree, indent=4))
Module(                            // anything that can be run in python is a module
  body=[                           // is a list of expressions/statements
     Expr(                         // body is an Expression rather than Statement
         value=Call(               // indicate a call to a function is going to happen
           func=Name(id='print', ctx=Load()),  // will Load() print function 
             args=[
               Constant(value='Hello World')], // Constant string being passed to function
             keywords=[]))],
  type_ignores=[])
>>> 

Bu AST ağacı daha sonra yorumlayıcı tarafından gerekli bayt kodunu oluşturmak için kullanılır, bu aşağıdakine benzer:

>>> compile("print(\"Hello World\")", "", "eval").co_code
b'\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00'

Bayt kodu artık doğrudan Python tarafından çalıştırılabilir, onu .co_code olmadanexec()öğesine iletmeniz Merhaba Dünya'yı görüntüleyecektir. Bunudismodülüne aktarırsak, aşağıdaki ayrıştırmayı elde ederiz:

dis.dis(b'\x97\x00\x02\x00\x65\x00\x64\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x53\x00')
          0 RESUME                   0
          2 PUSH_NULL
          4 LOAD_NAME                0
          6 LOAD_CONST               0
          8 PRECALL                  1
         12 CALL                     1
         22 RETURN_VALUE

Bize opcode değerlerine yönelik anımsatıcılar sağlamasa da, aşağıdakileri oluşturmak için python kaynak kodunun kendisi de dahil olmak üzere açık kaynak bilgilerini kullanabiliriz:

\x97\x00                                    RESUME                   0
\x02\x00                                    PUSH_NULL
\x65\x00                                    LOAD_NAME                0
\x64\x00                                    LOAD_CONST               0
\xa6\x01\x00\x00                            PRECALL                  1
\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00    CALL                     1
\x53\x00                                    RETURN_VALUE

Artık LOAD_CONST ile yüklenen sabitin 0 tamsayısı olduğunu fark edebilirsiniz - bu kesinlikle beklenen "Merhaba Dünya" dizesi değildir. Peki, bu 0, co_consts yapısı/listesi programı için bir dizin olarak kullanılır, eğer sorgularsak aşağıdakileri elde ederiz:

>>> compile("print(\"Hello World\")", "", "eval").co_consts
('Hello World',)

ABC gibi farklı bir dize için ek bir baskı eklemek üzere basit talimatı genişletirsek, parçalara ayrılmış kod şöyle görünür:

>>> dis.dis(compile(func, "", "exec"))
  0           0 RESUME                   0

  1           2 PUSH_NULL
              4 LOAD_NAME                0 (print)
              6 LOAD_CONST               0 ('Hello World')
              8 PRECALL                  1
             12 CALL                     1
             22 POP_TOP

  2          24 PUSH_NULL
             26 LOAD_NAME                0 (print)
             28 LOAD_CONST               1 ('ABC')
             30 PRECALL                  1
             34 CALL                     1
             44 POP_TOP
             46 LOAD_CONST               2 (None)
             48 RETURN_VALUE

Gördüğünüz gibi, LOAD_CONST'un ikinci örneği 0 yerine 1 değerini alır - co_consts yapısını sorguladığımızda artık listede iki giriş olduğunu görebiliriz:

>>> compile(func, "", "exec").co_consts
('Hello World', 'ABC', None)

Bütün bunlar, Python bayt kodunu parçalara ayırmanın ve orijinal kaynağa çok benzeyen koda geri derlemenin son derece kolay olmasının nedenidir; all of the required constants, instructions, and other information (including variable names!) can be found within the PYC file.

>>> compile("secret_variable=\"password\"", "", "exec").co_names
('secret_variable',)

Dolayısıyla, en son sürüm için bir Python kod çözücü mevcut olmasa bile, işlevselliği analiz etmek ve anlamak hala oldukça mümkündür ve karmaşıklığa bağlı olarak, ChatGPT'nin onu Python koduna derlemeye çalışması bile mümkün olabilir.

Bu projenin amacı şaşırtmadır ve parçalara ayrılabilen veya en kötü durumda tamamen kaynak koduna dönüştürülebilen bir komut dosyasına sahip olmak pek iyi değildir.

Peki ya ortalama Python yorumlayıcınızla ayrıştırılamayan bazı Python kodlarını bir şekilde derleyebilseydik? Peki ya LOAD_NAME ya da POP_TOP yerine, işlem kodu değerini BUILD_LIST ya da PUSH_NULL ile değiştirseydik, böylece standart bir yorumlayıcı onu doğru şekilde ayrıştıramasaydı ne olurdu?

Temel Opcode Yeniden Eşleme Önkoşulları:

Tartıştığımız gibi, her Python anımsatıcısının kendisine eşlenen kendi tamsayı değeri (opcode) vardır; bu, pop, push, ret vb.'nin kendi tamsayı değerlerine sahip olduğu montaja benzer şekilde. Bu Python işlem kodları, yukarıda gördüğümüz gibi dis modülü kullanılarak ayrıştırılabilir.

Bu durumda Assembly ve Python arasındaki fark, değiştirilmiş işlem kodlarını işlemek için derleme için tam bir yorumlayıcı oluşturmadığımız sürece, onu yürütmenin imkansız olmasıdır (bkz. VM karartması). Öte yandan Python açık kaynaklıdır ve bu kaynağı yerel olarak oluşturabiliyoruz. Dolayısıyla, Python kaynağındaki opcode değerlerini değiştirirsek, bunu bir Python yorumlayıcısına oluşturursak ve sonra onu Python betiğimizi derlemek için kullanırsak, (teoride) yalnızca "özel" yorumlayıcımız tarafından çalıştırılabilecek bir betiğimize sahip olmalıyız. Kendi yorumlayıcımızı oluşturmak zorunda kalmaktan, VM bağlamları, getir-kod çöz-gönder döngüleri vb. hakkında endişelenmekten çok daha kolaydır.

Bu aynı zamanda yalnızca bizim tercümanımız tarafından başarılı bir şekilde ayrıştırılabileceği anlamına da gelir ve Python yorumlayıcısında tersine mühendislik yapmak oldukça zahmetli olabilir, dolayısıyla kodu kırmaya çalışan analistleri yavaşlatacaktır. Yorumlayıcı olmadan, komut dosyası çalışmaz ve (kolayca) analiz edilemez; dolayısıyla PYC dosyaları diske dokunursa ancak yorumlayıcı bellekten silinirse, herhangi bir IOC'yi bulmak zor olacaktır.

Peki bunu tam olarak nasıl yapabiliriz?

Aslında oldukça basit; Python yorumlayıcısının kaynak kodunda, değiştirmemiz gereken işlem kodları hakkında temel bilgileri içeren 3 ana dosya vardır. Yalnızca 1 dosyayı değiştirirsek, işlem kodları ve bunların anımsatıcı değerleri arasında uyumsuzluk olabilir ve bu da derleme hatasına neden olabilir. Bu dosyalar:

Python-3.11.4/Include/opcode.h
Python-3.11.4/Python/opcode_targets.h
Python-3.11.4/Lib/opcode.py

Şimdi uymamız gereken bazı temel kurallar var, bunlardan bazıları bu makalede tartışılmıştı.postTheoremOne'dan Matías Aguirre tarafından.

Dikkat edilmesi gereken ana kural, HAVE_ARGUMENT anımsatıcısıdır.işlem kodu.h. Bundan sonraki herhangi bir anımsatıcının bir veya iki argüman içerdiği kabul edilir, bu nedenle bunu buna göre ele aldığımızdan emin olmamız gerekir (böylece argümanları kabul eden ve etmeyen anımsatıcılar arasında işlem kodu değerlerini değiştirmekten kaçınırız). Değerlerine bakılmaksızın değiştirildiğinde sorunlara neden olan bazı ek işlem kodları vardır; dolayısıyla son komut dosyasında bunların yalnız bırakıldığını göreceksiniz - POP_JUMP_FORWARD_IF_NONE, MAKE_CELL ve BINARY_OP_ADAPTIVE dahil. Daha iyi bir obfuscator yapmak istiyorsanız, bunları değiştirebilmek bir sonraki adım için harika bir adım olacaktır.

Değerleri değiştirmeye başlamadan önce her dosyanın düzenini anlamamız gerekir.

İşlem kodu.hbasit bir sabit kodlanmış anımsatıcı listesi (Python talimatları) ve bunların tamsayı değerlerini içerir - bunlar çok kolay değiştirilebilir.

Opcode_targets.hbir içerirstatik geçersiz * yapıadlandırılmışopcode_target'larHer biri bir anımsatıcıyı temsil eden 256 giriş içeren. Anımsatıcıların sırasının tamsayı değeriyle eşleştiğinden emin olmamız gerekir; dolayısıyla POP_TOP'a 4 değeri atanırsa, 4 konumunda olması gerekir (1, 2 ve 3 değerlerine sahip başka anımsatıcıların da olduğu varsayılarak)




⚠️ Bu konu 0ffset.net botu tarafindan otomatik olarak ice aktarilmistir.

🔗 Kaynak Baglantisi: https://www.0ffset.net/development/malware-development/obfuscating-python-opcodes/

Thread Statistics

Views 1
Replies 0
Author 0ffset.net
Created 2023-06-18
Status
Open