简介 这个Lab属于第三章程序的机器级表示,本章主要介绍了汇编的各种指令以及程序运行时栈和寄存器的变化。通过C的各种语法引入了对应的汇编指令,使得更易理解各种基本汇编指令以及C语法的底层实现。 大部分汇编指令很直白,与常用的高级语言的语法都能对应,所以很快就可以熟悉上手;switch语句比较复杂,多了一个跳转表需要处理;浮点数有自己的寄存器和指令,在函数调用的时候需要多加注意。 本章整体能够比较流畅地看下来,每一部分讲解完成之后都会有对应的练习题帮助加深理解和记忆,感觉比大学时候轻松很多。(可能是本章依旧比较简单,没有深入太多;也有可能大学学过了,还残留着部分知识;更有可能是心态变化了,课程没有那么繁重,每天只需要用2小时学习即可)疑惑点回顾 本章遇到几个印象深刻的疑惑点,先记录一下,部分还没有找到答案。栈帧对齐 第一个疑惑点在函数调用这块(P173),主要是在被调用之前需要将栈帧按16字节对齐。longQ(longx);longP(longx,longy){longuQ(y);longvQ(x);} 以上C代码生成的汇编代码如下(隐去了与本处无关的部分):P:pushqrbppushqrbxsubq8,rspmovqrdi,rbpmovqrsi,rdicallQPLTmovqrax,rbxmovqrbp,rdicallQPLTaddqrbx,raxaddq8,rsppopqrbxpopqrbpret 可以发现汇编代码第4行(subq8,rsp)在栈中分配了8个字节(这一行书上的注释为:对齐栈帧),没有对应的C代码,同时后面也没有使用。所以当时就十分好奇为什么会生成这样的代码,即使有注释还是不明白是什么意思。 经过本地各种尝试和搜索后了解到:这样是为了将栈帧按16字节对齐。函数调用时,rsp要整除16才能成功,即在调用函数P(callP)之前,rsp必定是16的整数倍。运行callP时会将返回地址入栈(P165)(但这一部分仍然算在调用者的栈帧内)。这时进入了被调用的函数P开始位置,但此时rsp模16余8,而在P保存寄存器rbp和rsx后(入栈16字节数据),rsp模16仍然余8,而后续在调用函数Q前再无出入栈的操作,所以为了按16字节对齐,需要再在栈上分配8字节的空间(subq8,rsp)。 后面在数据对齐部分(P191)也讲到这一知识点:任何内存分配函数(alloca,malloc,calloc,realloc)生成的块的起始地址都必须是16的倍数大多数函数的栈帧边界都必须是16字节的倍数(有一些例外)数组分配 第二个疑惑点在缓冲区溢出这块(P195),表现形式和第一个疑惑点一样,都是在栈上多分配了一些空间,但这次表现不同,没找到原因(在这里花费好几小时,仍然没有找到答案,先暂时搁置)。chargets(chars);voidecho(){1。数组的大小合法的任何数也不影响这种模式2。数组的类型换成其他基本类型也不影响这种模式charbuf〔8〕;gets(buf);} 以上C代码生成的汇编代码如下(隐去了与本处无关的部分):echo:subq24,rspleaq8(rsp),rdicallgetsPLTaddq24,rspret 可以发现虽然仍旧是按照16字节对齐,但是多分配了16字节的空间,依照前面的对齐要求,此处我们分配了8字节数组后已经对齐,应该是不需要再多分配空间了。 经过本地各种组合尝试后发现:只有定义了数组会这样(与数组类型无关),基本都会在数组末尾的前面留下一部分不使用的空间。猜测可能与GCC的栈破坏检测特性有关(P199),但不关闭这个特性,仍旧会在金丝雀值的前面有不使用的空间。 不关闭栈破坏检测的汇编代码如下(隐去了与本处无关的部分):echo:subq24,rspmovqfs:40,raxmovqrax,8(rsp)xorleax,eaxmovqrsp,rdicallgetsPLTmovq8(rsp),raxxorqfs:40,raxjne。L4addq24,rspret浮点数类型转换 P208处讲解了GCC生成的浮点数类型转换的汇编代码有两行,而没有使用一条汇编指令直接进行转换。作者也在书中说明不清楚为什么这样处理: 我们不太清楚GCC为什么会生成这样的代码,这样做既没有好处,也没有必要在XMM寄存器中把这个值复制一遍。准备 可以在官网〔1〕下载bomblab相关的程序。 本次需要使用的程序依旧需要在Docker中运行,将本地Lab的目录挂载进容器中即可:dockerruntiv{PWD}:csappubuntu:18。04 进入容器后需要安装gdb:aptgetupdateaptgetyinstallgdb 然后就可以愉快的开始闯关了。闯关 本次Lab给了待使用的bomb程序及其对应的main部分,查看main。c文件,可以发现总共有6个字符串需要输入,必须全部正确才能正确通过这关。BombLabwriteup〔2〕中给了提示,我们可以使用gdb,objdumpt,objdumpd,strings辅助我们通过这关。(这些也在main。c文件开头介绍过) 首先,我们运行gdbbomb开始调试,进入后可以输入help来获取不同指令的帮助文档,也可以查看书本P194进行相关操作。 然后调试获取所需的6个字符串。获取第一个字符串 main。c中给出了第一个字符串将在phase1中进行校验,所以我们需要在phase1入口处打上断点,然后运行程序使其进入phase1函数:在函数phase1入口处打上断点(gdb)breakphase1Breakpoint1at0x400ee0运行bomb(gdb)runStartingprogram:csappbomblabbombwarning:Errordisablingaddressspacerandomization:OperationnotpermittedWelcometomyfiendishlittlebomb。Youhave6phaseswithwhichtoblowyourselfup。Haveaniceday!程序提示输入第一个字符串,我们暂时还不知道,所以随便输入一个idealismxxm程序停在了第一个断点处:函数phase1入口Breakpoint1,0x0000000000400ee0inphase1() 此时我们需要使用disas查看函数phase1的汇编代码:获取phase1函数的汇编代码(gdb)disasDumpofassemblercodeforfunctionphase1:0x400ee00:sub0x8,rsp0x400ee44:mov0x402400,esi0x400ee99:callq0x401338stringsnotequal0x400eee14:testeax,eax0x400ef016:je0x400ef7phase1230x400ef218:callq0x40143aexplodebomb0x400ef723:add0x8,rsp0x400efb27:retqEndofassemblerdump。 查看汇编代码,我们发现内部调用了stringsnotequal来比较两个字符串是否相等,该函数接收两个参数,分别放在edi(我们输入的字符串的起始地址)和edi(待比较字符串的起始地址)中,如果这两个字符串不等,则会调用explodebomb引爆炸弹。 mov0x402400,esi表明比较字符串的起始地址为0x402400,所以我们只需要查看这个地址开始的一段字符串即可获取第一个字符串。然后我们打印这个地址开始的100个字节对应的字符串:(gdb)print(char)0x4024001001BorderrelationswithCanadahaveneverbeenbetter。00000000Wow!Youvedefusedthesecretstage!00flyers 可以获取第一个字符串为:BorderrelationswithCanadahaveneverbeenbetter。获取第二个字符串 我们在上一步获取了第一个字符串,此时我们可以开始获取第二个字符串。让我们重新运行程序(gdbbomb),在phase2入口处打上断点,然后运行程序使其进入phase2函数:在函数phase2入口处打上断点(gdb)breakphase2Breakpoint1at0x400efc运行程序(gdb)runStartingprogram:csappbomblabbombwarning:Errordisablingaddressspacerandomization:OperationnotpermittedWelcometomyfiendishlittlebomb。Youhave6phaseswithwhichtoblowyourselfup。Haveaniceday!输入上一步获得的字符串BorderrelationswithCanadahaveneverbeenbetter。Phase1defused。Howaboutthenextone?由于还不知道第二个字符串,所以随便输入以便进入函数phase2idealismxxmBreakpoint1,0x0000000000400efcinphase2() 此时我们需要使用disas查看函数phase2的汇编代码:获取phase2函数的汇编代码(gdb)disasDumpofassemblercodeforfunctionphase2:0x400efc0:pushrbp0x400efd1:pushrbx0x400efe2:sub0x28,rsp0x400f026:movrsp,rsi0x400f059:callq0x40145creadsixnumbers省略暂时不关注的代码。。。Endofassemblerdump。 可以发现最开始分配了28个字节空间,然后调用了函数readsixnumbers,这个函数的参数是rdi(输入的字符串)和rsi(刚刚分配的空间)。根据现有条件判断应该是要从输入的字符串中读取6个数字,并存储在我们刚刚分配的空间中。 接下来我们就需要给函数readsixnumbers打断点,继续运行至函数readsixnumbers入口处,查看其如何将6个数读取出来:在函数readsixnumbers入口处打上断点(gdb)breakreadsixnumbersBreakpoint2at0x40145c继续运行(gdb)continueContinuing。Breakpoint2,0x000000000040145cinreadsixnumbers()获取readsixnumbers函数的汇编代码(gdb)disasDumpofassemblercodeforfunctionreadsixnumbers:0x40145c0:sub0x18,rsp0x4014604:movrsi,rdx0x4014637:lea0x4(rsi),rcx0x40146711:lea0x14(rsi),rax0x40146b15:movrax,0x8(rsp)0x40147020:lea0x10(rsi),rax0x40147424:movrax,(rsp)0x40147828:lea0xc(rsi),r90x40147c32:lea0x8(rsi),r80x40148036:mov0x4025c3,esi0x40148541:mov0x0,eax0x40148a46:callq0x400bf0isoc99sscanfplt0x40148f51:cmp0x5,eax0x40149254:jg0x401499readsixnumbers610x40149456:callq0x40143aexplodebomb0x40149961:add0x18,rsp0x40149d65:retqEndofassemblerdump。 初步观察汇编代码,发现是通过sscanf函数从我们输入的字符串中获取值,接下来就是判断这6个数字分别存在了什么位置。 书中P120讲解了函数的前六个入参数分别存储在rdi,rsi,rdx,rcx,r8,r9中,P164讲解了从第7个入参开始,参数存储在栈顶。 我们先判断函数sscanf的入参分别是什么:第一个入参(rdi):后续没有改动该寄存器的地方,那么第一个入参就是我们输入的字符串第二个入参(rsi):可以发现esi被指令mov0x4025c3,esi修改了,说明第二个参数仍然是字符串,结合函数定义可知该字符串是模式串,用于读取数字。我们使用(gdb)print(char)0x4025c320查看其开始的20个字符,可得模式串为:dddddd第三个入参(rdx):可以发现rdx被指令movrsi,rdx修改了,说明第三个参数是相对传入指针偏移量为0的位置第四个入参(rcx):可以发现rcx被指令lea0x4(rsi),rcx修改了,说明第四个参数是相对传入指针偏移量为4的位置第五个入参(r8):可以发现r8被指令lea0x8(rsi),r8修改了,说明第五个参数是相对传入指针偏移量为8的位置第六个入参(r9):可以发现r9被指令lea0xc(rsi),r9修改了,说明第六个参数是相对传入指针偏移量为12的位置第七个入参((rsp)):可以发现(rsp)被指令(lea0x10(rsi),rax,movrax,(rsp))修改了,说明第七个参数是相对传入指针偏移量为16的位置第八个入参(0x8(rsp)):可以发现0x8(rsp)被指令(lea0x14(rsi),raxmovrax,0x8(rsp))修改了,说明第八个参数是相对传入指针偏移量为20的位置 由此可知,传入指针指向int〔6〕的数组的起始地址(假设以arr代表该数组),输入串是六个以空格分割的32位有符号数字(int),并且这个六个数字将会按顺序存在arr数组中。 此时我们再看函数phase2完整的汇编代码:(gdb)disasDumpofassemblercodeforfunctionphase2:0x400efc0:pushrbp0x400efd1:pushrbx0x400efe2:sub0x28,rsp0x400f026:movrsp,rsi0x400f059:callq0x40145creadsixnumbers0x400f0a14:cmpl0x1,(rsp)0x400f0e18:je0x400f30phase2520x400f1020:callq0x40143aexplodebomb0x400f1525:jmp0x400f30phase2520x400f1727:mov0x4(rbx),eax0x400f1a30:addeax,eax0x400f1c32:cmpeax,(rbx)0x400f1e34:je0x400f25phase2410x400f2036:callq0x40143aexplodebomb0x400f2541:add0x4,rbx0x400f2945:cmprbp,rbx0x400f2c48:jne0x400f17phase2270x400f2e50:jmp0x400f3cphase2640x400f3052:lea0x4(rsp),rbx0x400f3557:lea0x18(rsp),rbp0x400f3a62:jmp0x400f17phase2270x400f3c64:add0x28,rsp0x400f4068:poprbx0x400f4169:poprbp0x400f4270:retqEndofassemblerdump。 调用完readsixnumbers函数后,我们已经将读取的六个数字按顺序存储在数组arr中,分别对应栈中的值:(rsp),0x4(rsp),0x8(rsp),0xc(rsp),0x10(rsp),0x14(rsp)。接下来我们依次判断每个值是多少:第一个数(arr〔0〕:(rsp)):这个数通过cmpl0x1,(rsp)进行判断,紧接着运行跳转指令je0x400f30phase252判断是否跳过引爆炸弹的函数,表明当(rsp)的值等于1时不会触发爆炸,所以第一个数为:1第二个数(arr〔1〕:0x4(rsp)):通过第一个数的判断后,成功跳转到0x400f3052处,这里的代码类似:intaarr1;intbarr7;,然后跳转到0x400f1727处,连续三条指令表明判断a〔1〕a〔1〕是否等于a〔0〕(即判断arr〔1〕arr〔0〕arr〔0〕是否成立),成立时跳过引爆炸弹的函数,表明当0x4(rsp)的值等于2时不会触发爆炸,所以第二个数为:2第三个数(arr〔1〕:0x4(rsp)):通过第二个数的判断后,成功跳转到0x400f2541处,连续三条指令表明先执行了类似aa1的操作,然后判断ab是否成立(即判断a是否指向了arr6),成立时会接着运行并跳转至0x400f3c64处(表明通过了所有判断,字符串合法);不成立时会紧接着跳转至0x400f1727处,继续前一步中的判断。所以这里其实是一个循环,只有当前数是前一个数的2倍时才不会引爆炸弹,所以第三个数为:4 综上可知六个数分别为:1,2,4,8,16,32,对应的字符串为:12481632。 重新运行程序,依次输入前两个字符串,然后发现输出了Thatsnumber2。Keepgoing!,表明我们第二个字符串也成功获取到了。获取第三个字符串 我们在通过前面的步骤成功获取了前两个字符串,此时我们可以开始获取第三个字符串。让我们重新运行程序(gdbbomb),在phase3入口处打上断点,然后运行程序使其进入函数phase3,并获取phase3汇编代码:。。。Breakpoint1,0x0000000000400f71inphase3()获取函数phase3的汇编代码(gdb)disasDumpofassemblercodeforfunctionphase3:0x400f430:sub0x18,rsp0x400f474:lea0xc(rsp),rcx0x400f4c9:lea0x8(rsp),rdx0x400f5114:mov0x4025cf,esi0x400f5619:mov0x0,eax0x400f5b24:callq0x400bf0isoc99sscanfplt0x400f6029:cmp0x1,eax0x400f6332:jg0x400f6aphase3390x400f6534:callq0x40143aexplodebomb0x400f6a39:cmpl0x7,0x8(rsp)0x400f6f44:ja0x400fadphase31060x400f7146:mov0x8(rsp),eax0x400f7550:jmpq0x402470(,rax,8)0x400f7c57:mov0xcf,eax0x400f8162:jmp0x400fbephase31230x400f8364:mov0x2c3,eax0x400f8869:jmp0x400fbephase31230x400f8a71:mov0x100,eax0x400f8f76:jmp0x400fbephase31230x400f9178:mov0x185,eax0x400f9683:jmp0x400fbephase31230x400f9885:mov0xce,eax0x400f9d90:jmp0x400fbephase31230x400f9f92:mov0x2aa,eax0x400fa497:jmp0x400fbephase31230x400fa699:mov0x147,eax0x400fab104:jmp0x400fbephase31230x400fad106:callq0x40143aexplodebomb0x400fb2111:mov0x0,eax0x400fb7116:jmp0x400fbephase31230x400fb9118:mov0x137,eax0x400fbe123:cmp0xc(rsp),eax0x400fc2127:je0x400fc9phase31340x400fc4129:callq0x40143aexplodebomb0x400fc9134:add0x18,rsp0x400fcd138:retqEndofassemblerdump。 0x400f4300x400f6534:和上一步类似,就不赘述了,主要是通过sscanf获取输入字符串的两个32位有符号整数,假设分别存储在a(0x8(rsp))和b(0xc(rsp))中。如果没有成功获取两个整数,那么就会引爆炸弹,无法继续运行。 0x400f6a390x400f6f44:主要是判断a7是否成立,成立则可继续运行;不成立则跳转至0x400fad106引爆炸弹,无法继续运行。 0x400f71460x400f7550:主要是通过a的值计算如何跳转,即通过07计算跳转的位置,很明显是switch语句的汇编代码。那么0x402470就是跳转表的起始位置,我们运行printx0x40247016查看跳转表的相关数据:查看0x402470开始的16个字节(gdb)printx0x402470161{0x400f7c,0x0,0x400fb9,0x0,0x400f83,0x0,0x400f8a,0x0,0x400f91,0x0,0x400f98,0x0,0x400f9f,0x0,0x400fa6,0x0} 按照顺序对应一下可以发现a对应的跳转位置及对应的代码效果关系如下: a 跳转位置 相关代码将c赋值为 0hr0x400f7c57 0xcf(207) 1hr0x400fb9118 0x137(311) 2hr0x400f8364 0x2c3(707) 3hr0x400f8a71 0x100(256) 4hr0x400f9178 0x185(389) 5hr0x400f9885 0xce(206) 6hr0x400f9f92 0x2aa(682) 7hr0x400fa699 0x147(327) switch语句执行完毕后,将运行0x400fbe1230x400fc4129,主要是判断bc是否成立,不成立则引爆炸弹,不再继续运行;成立则通过跳过引爆炸弹的操作,成功通过校验。 综上,第三个字符串有8个合法值:0207,1311,2707,3256,4389,5206,6682,7327。 我们重新运行程序,在输入第三个字符串时,随意输入上面8个字符串中的一个,控制台都会输出Halfwaythere!告诉我们第三个字符串成功获取。