Lab1: Data Lab
容易被忽略的注意事项:可以使用的常数范围为0-255,编译标准为c89,后者可以通过“./dlc bits.c”来检查。
15道纯位运算,个个都是重量级,部分题目可能不是最优写法(目前凹到207运算符,不是这也能凹是吧),暂无解析,仅供参考。
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
return 1<<31;
}
/*
* absVal - absolute value of x
* Example: absVal(-1) = 1.
* You may assume -TMax <= x <= TMax
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 10
* Rating: 4
*/
int absVal(int x) {
int s = x>>31;
return (x+s)^s;
}
/*
* bitAnd - x&y using only ~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
return ~(~x|~y);
}
/*
* replaceByte(x,n,c) - Replace byte n in x with c
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: replaceByte(0x12345678,1,0xab) = 0x1234ab78
* You can assume 0 <= n <= 3 and 0 <= c <= 255
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 10
* Rating: 3
*/
int replaceByte(int x, int n, int c) {
int s = n<<3;
return (c<<s)|(x&~(0xff<<s));
}
/*
* mult3div2 - multiplies by 3/2 rounding toward 0,
* Should exactly duplicate effect of C expression (x*3/2),
* including overflow behavior.
* Examples: mult3div2(11) = 16
* mult3div2(-9) = -13
* mult3div2(1073741824) = -536870912(overflow)
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
int mult3div2(int x) {
x += (x<<1);
return (x+((x>>31)&1))>>1;
}
/*
* multFiveEighths - multiplies by 5/8 rounding toward 0.
* Should exactly duplicate effect of C expression (x*5/8),
* including overflow behavior.
* Examples: multFiveEighths(77) = 48
* multFiveEighths(-22) = -13
* multFiveEighths(1073741824) = 13421728 (overflow)
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 3
*/
int multFiveEighths(int x) {
x += (x<<2);
return (x+((x>>31)&7))>>3;
}
/*
* addOK - Determine if can compute x+y without overflow
* Example: addOK(0x80000000,0x80000000) = 0,
* addOK(0x80000000,0x70000000) = 1,
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int addOK(int x, int y) {
return (((x^y)>>31)|~(((x+y)^x)>>31))&1;
}
/*
* bitCount - returns count of number of 1's in word
* Examples: bitCount(5) = 2, bitCount(7) = 3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 40
* Rating: 4
*/
int bitCount(int x) {
int m2 = (0x55<<8)|0x55;
int m4 = (0x33<<8)|0x33;
int m8 = (0x0f<<8)|0x0f;
m2 |= (m2<<16);
m4 |= (m4<<16);
m8 |= (m8<<16);
x += ~((x>>1)&m2)+1;
x = ((x>>2)&m4)+(x&m4);
x = (x+(x>>4))&m8;
x += (x>>8);
x += (x>>16);
return x&0x3f;
}
/*
* isLess - if x < y then return 1, else return 0
* Example: isLess(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLess(int x, int y) {
int ny = ~y;
return ((((x+ny+1)&(x^ny))|(x&ny))>>0x1f)&1;
}
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
int ny = ~y;
return ((((((x+ny+1)&(x^ny))|(x&ny))>>0x1f))&1)|!(x^y);
}
/*
* trueFiveEighths - multiplies by 5/8 rounding toward 0,
* avoiding errors due to overflow
* Examples: trueFiveEighths(11) = 6
* trueFiveEighths(-9) = -5
* trueFiveEighths(0x30000000) = 0x1E000000 (no overflow)
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 25
* Rating: 4
*/
int trueFiveEighths(int x) {
int s = x >> 3;
int r = x & 7;
return s + (s << 2) + (r + (r << 2) + (x >> 31 & 7) >> 3);
}
/*
* parityCheck - returns 1 if x contains an odd number of 1's
* Examples: parityCheck(5) = 0, parityCheck(7) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 4
*/
int parityCheck(int x) {
x ^= (x>>16);
x ^= (x>>8);
x ^= (x>>4);
x ^= (x>>2);
x ^= (x>>1);
return x&1;
}
/*
* rempwr2 - Compute x%(2^n), for 0 <= n <= 30
* Negative arguments should yield negative remainders
* Examples: rempwr2(15,2) = 3, rempwr2(-35,3) = -3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int rempwr2(int x, int n) {
int s = x>>31;
x = (x+s)^s;
x &= ((~0)+(1<<n));
return (x^s)+~s+1;
}
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int howManyBits(int x) {
int n = 0;
x ^= (x<<1);
n += ((!!(x&((~0)<<(n+16))))<<4);
n += ((!!(x&((~0)<<(n+8))))<<3);
n += ((!!(x&((~0)<<(n+4))))<<2);
n += ((!!(x&((~0)<<(n+2))))<<1);
n += (!!(x&((~0)<<(n+1))));
return n+1;
}
/*
* ilog2 - return floor(log base 2 of x), where x > 0
* Example: ilog2(16) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int ilog2(int x) {
int n = 0;
n += ((!!(x&((~0)<<(n+16))))<<4);
n += ((!!(x&((~0)<<(n+8))))<<3);
n += ((!!(x&((~0)<<(n+4))))<<2);
n += ((!!(x&((~0)<<(n+2))))<<1);
n += (!!(x&((~0)<<(n+1))));
return n;
}
Lab2: Bomb Lab
非常值得一做的汇编练习题,硬看懂汇编还是很有成就感的,也能体会到编译器到底是如何把一些结构翻译为汇编语言的。
由于题目彼此间均有差别,答案各不相同,故仅提供基本思路;关于 IA-32 指令更多细节问题请参照教材或官方手册,本文并不会涉及。
注:可能有人题目为 64 位编译(我拿到的是32位编译的),主要差别如下:
- 64 位下指针大小为 64bit(8byte);而32位下为 32bit(4byte);
- 64 位下出入栈单位为 8byte,即堆栈指针寄存器加减 0x08;而 32 位下为 4byte,即加减 0x04;(和前者是同义反复,但前者为源码角度)
- 64 位下函数传参通过寄存器实现;而 32 位下通过参数入栈实现。
set disassembly-flavor att
# gdb 反汇编默认为 Intel 风格, 需改为 AT&T 风格
# AT&T 指令中, 前者为被操作对象, 后者为操作对象
# 如 mov %eax, %edx 是指将 eax 寄存器中值写入到 edx 寄存器中
# gdb常用命令
b <line/func/*addr> # 在第 n 行 / func 函数 / addr 地址处打断点
r # 运行, 到断点处停下
c # 运行到下一个断点
stepi/step # 相当于step into, 对于机器码/源码
nexti/nest # 相当于step over, 对于机器码/源码
x/<n/f/u> <addr/register> # 以 f 格式查看从 addr 开始的 n 个 u 单位内存值 / 寄存器值
# f: x(十六进制形式); d(整数); f(浮点数); c(字符); s(字符串)
# u: b(1 字节); h(2 字节), w(4 字节), g(8 字节)
p/<f> <variable> # 查看变量
i r # 查看寄存器值
disassemble <func> # 查看func对应的汇编指令
7个Bomb主要考点如下:
- phase_1:字符串比较;
- phase_2:循环和栈;
- phase_3:条件分支;
- phase_4:递归和栈;
- phase_5:循环和数组;
- phase_6:循环和链表;
- secret_phase:字符串比较,递归和二叉搜索树。
通用思路如下:
- 找到输入格式要求:函数名或 scanf 中格式化符;
- 找到爆炸函数 <explode_bomb> 并查看前面的判断条件(cmp 或 test);
- 逐个部分分析函数结构,如有必要改写为C源码。
phase_1
直接查看 <strings_not_equal> 函数前入栈的内存值:“x/s <addr>”。
phase_2
栈结构中栈顶为低地址,栈底为高地址,即入栈时 -0x04,而出栈时 +0x04(32位);
test %eax, %eax 为判断 exa 寄存器中值是否为 0,cmp <val/addr/register> %eax 为比较 eax 寄存器中值和前者中的值;
-0xc(%ebp) 处通常用来存放循环结构的计数器;
-0x24(%ebp,%eax,4) 意为 %ebp + 4 * %eax - 0x24 处的内存;
通过分析循环结构得到 6 个数字组成的数列。
跳转指令 | 含义 | 跳转指令 | 含义 | 跳转指令 | 含义 |
jmp | 无条件 | je | 相等 | jne | 不相等 |
ja/jnbe | 无符号大于 | jae/jnb | 无符号大于等于 | jg/jnle | 有符号大于 |
jb/jnae | 无符号小于 | jbe/jna | 无符号小于等于 | jl/jnge | 有符号小于 |
jge/jnl | 有符号大于等于 | jz | 为零 | jnz | 不为零 |
jle/jng | 有符号小于等于 | js | 为负 | jns | 不为负 |
jc | 进位 | jnc | 无进位 | jo | 溢出 |
jno | 无溢出 | jp/jpe | 为偶 | jnp/jpo | 不为偶 |
phase_3
查看 scanf 得到输入格式为“%d %c %d”;可将后续一系列条件分支还原为 if 语句,注意输入顺序和判断变量并不相同;
最终需将第二个输入转换为 ascii 字符。
phase_4
查看 scanf 得到输入格式为“%d %d”;后续进入到函数 <func4> 中;
可将 func4 还原为源码,并得到返回值;由此得到答案。
phase_5
发现要求 <string_length> 为一定长度;
不妨设输入字符为“ch[i]”,发现在循环结构中取“ch[i] & 0x0f”(即 mod 16)作为数组下标,猜测数组大小为 16;
查看数组“x/16xw <addr>”;
通过暴力循环得到符合条件的一组数组下标,并查 ascii 表转换为合适的字符。
phase_6
发现函数 <read_six_numbers> 要求输入 6 个数字;
前两部分要求输入的 6 个数字分别为 1-6 中的一个,并令 arr[i] = 7 - arr[i];
找到 <explode_bomb> 前判断条条件,要求前者大于后者;
找到入栈的静态区地址并查看“x/3xw <addr>”,发现为链表节点,结构为:“int val; int num; node* next”;
依次查看下一个节点"x/3xw <next>",直到 next 为 0(即尾结节的 next 指向 NULL);
同时循环结构中要求按 arr[i] 中值重新排列链表,综合得到要求链表为升序/降序;
由此得到 arr[i],arr[i] = 7 - arr[i] 为最终输入。
secret_phase
查看 <phase_defused> 函数并找到 scanf,发现输入要求为“%d %d %s”,同时再次出现 <strings_not_equal> 函数,至此可得入口为 phase_4 后面加上一个字符串;
还可以通过断点调试进一步验证为 phase_4,即运行后查看“%d %d”内存值变化:“x/s <addr>”恰好为 phase_4 答案;
获得字符串方式和 phase_1 相同,此时即进入 secret_phase。
查看 <secret_phase> 发现进入到函数 <fun7>;
同样改写为源码,发现为递归查找函数,并返回和查找过程有关的值;
找到入栈的静态区地址并查看"x/3xw <addr>",发现为二叉树节点,结构为:“int val; node* left; node* right”;
依次找处全部节点“x/3xw <left>”及"x/3xw <right>",并画出二叉树;
发现二叉树为搜索二叉树,深度为 4;
由要求的返回值反推出搜索过程,对应的节点值即为答案(记得转换为10进制)。
Lab3: Attack Lab
理解了帧栈后比前两个要简单得多,但刚开始一直没注意到需要维护段地址的问题,反而被卡了好久。
具体细节这篇文章已经讲得很清楚了:计算机系统基础实验:缓冲区溢出攻击_缓存区溢出攻击实验-CSDN博客。
4题和5题略有不同,需要注意到<test>函数及<testn>函数均采用了“段地址:偏移量”的形式;段地址存放在%ebx寄存器中,而两个函数末尾均有“mov -0x4(%ebp),%ebx”;为维护段地址(被调用者保存寄存器),需在字符串中对应位置改为%ebx旧值;否则%ebx将会被输入的答案改变导致内存访问错误。
1-5题 Payload 具体格式如下:
- Smoke:48 bytes = 44 bytes [占位符, 即0x00] + 4 bytes [<smoke>函数首地址 (小端序, 下同) ]
- Fizz: 56 bytes = 44 bytes [占位符] + 4 bytes [<fizz>函数首地址] + 4 bytes [占位符] + 4 bytes [cookie]
- Bang:48 bytes = 16 bytes [机器码, 恶意代码] + 28 bytes [占位符] + 4 bytes [<buf>首地址]
- Boom:48 bytes = 11 bytes [机器码] + 25 bytes [占位符] + 4 bytes [段地址原值, 即%ebx存放的值] + 4 bytes [栈地址原值, 即%ebp存放的值] + 4 bytes [<buf>首地址]
- Nitro:528 bytes = 501 bytes [NOP, 即0x90] + 15 bytes [机器码] + 4 bytes [段地址原值] + 4 bytes [占位符] + 4 bytes [5个<buf>首地址中最高地址]
Lab4: Link Lab
原理:phase[n].o 中的 do_phase 全局函数指针变量是初始化后的强符号,main 中的 do_phase 函数仅声明是弱符号,链接后强符号取代弱符号。
phase_1
考察全局变量:发现 do_phase 函数中将 .data节 内容移入%eax 并输出,修改该处内容即可。或可试运行观察输出字符并在全局区定位。
phase_2
考察汇编指令:发现 do_phase 函数中内容为空,写汇编代码,先将预期输出内容 "push",而后 "call" LOCAL(static区)的输出函数的地址实现输出。
phase_3
考察符号解析:发现 do_phase 函数中前面的数组(10 bytes)选用了后面数组(256 bytes)中的某些位置的数值,并将前面的数组输出,但后面的数组在 main 中仅声明而未赋值。声明同名数组并在特定位置赋值即可,强符号取代弱符号。
phase_4
考察跳转表:发现 do_phase 函数中有 switch 条件判断,依次输出跳转后的内容;由于本题要求不能修改代码,查看 .rodata 段得到跳转表。根据预期输出得到跳转表地址,根据前面数组内容将跳转表地址再次转换,并据此修改 .rodata 段即可。
phase_5
重定位节部分内容被抹去,需要手动补上,如下:
感谢 圣托里尼abc 对本文 Lab4 提供的帮助~