CSAPP Bomb Lab
芜湖,完成实验来记录一下啦~
这个lab看的我真是眼皮发麻,框框就是反汇编->c语言形式->优化c语言 最后我才能写出来,整的我睡觉都是 mov... lea...
难道适中,感觉只要耐心一点就没问题,好了现在开始正文...
小技巧
用gdb调试的技巧
shell
gdb --tui bomb #打开类似界面的东东
(gdb) layout asm #汇编界面
(gdb) layout split #分割窗口
(gdb) layout re #寄存器界面
完整后整个界面就是下面的啦
我经常使用的gdb命令
shell
x/【n】(x)/(d)/(s) 内存地址 #以x/d进制查看n个字节对应的内存 括号里面x代表16进制,d是10进制,s是字符串显示
x/x $rax 查看rax对应内存的值16进制显示
b n 在n处打断点
set $pc = 0x11111 将程序跳转到0x11111处执行
info r 查看寄存器信息,不过有上面的也不用这个了
stepi 单步执行汇编指令
s 单步执行代码(遇到能进去的函数会进去)
n 单步执行代码(不进去)
finish 直接跳出当前函数
#上面用到了界面,可以使用下面的指令切换焦点窗口
focus n 切换到下面的窗口
focus p 切换上面的窗口
我这边用的windows终端,感觉也是很不错,接下来就开始我们的逆向工程了
说实话,下面只是记录一下我的逆向过程,以及大致思路,没写出来的小伙伴不建议查看昂,还是自己一次一次stepi比较好,当然逆向的代码可以对比,看看我的有多少错误。
phase1
第一个锻炼一下看汇编代码的能力,只要进去把某个内存的东西输出一下就ok了
phase2
进入到执行函数会看到 read_six_numbers
这就直接猜到要输入6个数字了
进去看一下
哟,居然有一个sscanf,这不太熟(仅限我不熟啊),百度一下
c
int sscanf(const char *str, const char *format, ...)
昂~,这不就是个类似输入的函数吗
然后使用gdb查看一下参数
哦对,这个参数都是标准的,第一个参数都是%rdi ,然后以此类推, %rsi , % rdx , %rcx ... 忘了就记住4个
shell
x/s $rsi
-> "%d %d %d %d %d %d"
哟,果然是6个数字,直接输入6个数字,继续往下
然后你就会发现一个规律,验证一下就能找到咯
phase3
这个继续做他,上手我就是一个stepi
哟呵,老朋友了,看一看这次要输入几个数
c
x/s $rsi
-> "%d %d"
这次就俩个数啊
然后往下就是一堆
诶?怎么这么熟悉呢(翻一番书),我擦这不就是switch吗
我直接就是一手 反c语言, 反c语言中...(其实就是照着对应一条一条套, jmp改goto 寄存器改变量,然后优化)
c
void phase(string input){
usigned char t ; // 0xc
usigned char t1 ; //0x8
string fmt= "%d %d";
char res = ssacnf(input , fmt , t1 ,t ); //sscanf("123456 ", "%s", buf);
if(res <= 1){
explode_bomb();
return ;
}
if(t1 > 7){
goto L1;
}
switch(t1){
case 0: // // 0x400f7c
res = 0xcf; //207
break;
case 2: // // 0x400f83
res = 0x2c3; // 707
break;
case 3: // // 0x400f8a
res = 0x100; // 256
break;
case 4: // // 0x400f91
res = 0x185; // 389
break;
case 5: // // 0x400f98
res = 0xce; // 206
break;
case 6: // // 0x400f9f
res = 0x2aa; // 682
break;
case 7: // // 0x400fa6
res = 0x147; // 327
break;
L1: explode_bomb();
case ?: // // 0x400fb2
res = 0x0;
break;
case 1:
res = 0x137; // 311
}
if(res == t){
// success
return ;
}
explode_bomb();
}
我这一看,好家伙,还不止一个正确答案,一堆啊,随便选一个吧
其中有一个地方没搞明白他的结构,只优化成上面这样了。
phase4
继续就是一个stepi
老朋友又来了这次输入几个数呀
这次是俩个数字(就不贴图了)
看见这次汇编挺短还以为很容易呢,正高兴的看到个func4,好家伙还调函数了
先把上面的汇编弄成能看的c语言出来,挺短的
这里面有个test %eax,%eax 标准是 eax&eax 其实就是判断 eax是不是0
c++
void phase_4(string input){ // di si dx cx
// 3 int in the stack
int t1 , t2; // ? 0
auto res = sscanf(input , "%d %d" , t1 , t2);
if(res != 2){
bomb();
}
if(t1 > 14){
bomb();
}
int res = func4(t1 , 0 , 14);
if(res == 0){
if(t2 == 0){
goto L1;
}
}
bomb();
L1: success
}
然后进去fun4看一看
这小子还调上自己了,继续一步一步转成能看的c
c++
int fun4(int a , int b int c){
//1 byte in the stack
//int res = c - b;
//int d = res >> 31; // -1 or 0
//res = (d + res) / 2;
//d = a + res;
if(b > c) b += 1;
int d = a + (c - b) / 2;
if(d > a){
res = func4(a , b , d - 1);
return res * 2;
}
res = 0;
if(c < a){
res = func4(a , c + 1 , c);
res = res * 2 + 1;
}
return res
}
emmm就算转成代码,这求解...用计算机?快算了带入一个0发现可以通过,其他答案看你们啦
phase5
这次看看能不能碰到老朋友啦
哟换人了,换成 string_length,这不是求字符串长度的吗,看起来这次是要求长度了
看到了一个
cmp $0x6 , %eax
啊这,不是6个字符就给我爆炸啊
确定了数量,看看下面还有啥规则吧
这一写就是好几小时呀...
这个逆向的时候,会发现一系列从其他地放的字符串,譬如
这个0x40245e就有一个字符串
还有一个
这里有一串字符串,需要自己辨别一下
最后逆向出来的代码就是下面
c++
char * A[] = "maduiersnfotvbyl" // 0x4024b0
void phase_5(string input){ // rdi
// 32 byte
int t1 = %fs:0x28; // rbx | 0x18 + rsp
int res = string_length(input);
char * result[6];
if(res != 6){
bomb();
}
goto L1; // phase_5 + 39
for(int i = 0; i < 6; i++){
char rdx = inptu[i];
int index = input[i] & 15;
rdx = A[index];
result[index] = rdx;
}
res = strings_not_equal(result , "flyers"); // 9 15 14 5 6 7
if(res != 0){
bomb();
}
L1:
}
代码挺短,一看哈呀,这个地方还有 & 15 来确定字符呢,嘎嘎绕蒙
昂~,就是根据我输入的字符然后 % 15 来确定下标位置呀,然后根据A[]数组的字符查出来连起来和flyers相同就行,于是拿出我的密码本来吧
根据顺序,框框就是画,然后每一列的值输入进去都是能通过的。
终于解密了这个了,框框累,其实也可以直接写代码暴力搜索的,不过我还是选择了人工破译,至于为什么 ~
不会写代码破解...
phase6
还是往常看stepi
assembly
0X00000000004010f4 <phase_6>:
0X4010f4: push %r14
0X4010f6: push %r13
0X4010f8: push %r12
0X4010fa: push %rbp
0X4010fb: push %rbx
0X4010fc: sub $0x50,%rsp
0X401100: mov %rsp,%r13
0X401103: mov %rsp,%rsi
0X401106: callq 0X40145c <read_six_numbers>
0X40110b: mov %rsp,%r14
0X40110e: mov $0x0,%r12d
#第一部分
.L0
0X401114: mov %r13,%rbp
0X401117: mov 0x0(%r13),%eax
0X40111b: sub $0x1,%eax
0X40111e: cmp $0x5,%eax
0X401121: jbe .L1
0X401123: callq 0X40143a <explode_bomb>
.L1
0X401128: add $0x1,%r12d
0X40112c: cmp $0x6,%r12d
0X401130: je .L4
0X401132: mov %r12d,%ebx
.L2
0X401135: movslq %ebx,%rax
0X401138: mov (%rsp,%rax,4),%eax
0X40113b: cmp %eax,0x0(%rbp)
0X40113e: jne .L3
0X401140: callq 0X40143a <explode_bomb>
.L3
0X401145: add $0x1,%ebx
0X401148: cmp $0x5,%ebx
0X40114b: jle .L2
0X40114d: add $0x4,%r13
0X401151: jmp .L0
#第二部分
.L4
0X401153: lea 0x18(%rsp),%rsi
0X401158: mov %r14,%rax
0X40115b: mov $0x7,%ecx
.L5
0X401160: mov %ecx,%edx
0X401162: sub (%rax),%edx
0X401164: mov %edx,(%rax)
0X401166: add $0x4,%rax
0X40116a: cmp %rsi,%rax
0X40116d: jne .L5
0X40116f: mov $0x0,%esi
0X401174: jmp .L9
#第三部分
.L6
0X401176: mov 0x8(%rdx),%rdx
0X40117a: add $0x1,%eax
0X40117d: cmp %ecx,%eax
0X40117f: jne .L6
0X401181: jmp .L8
.L7
0X401183: mov $0x6032d0,%edx
.L8
0X401188: mov %rdx,0x20(%rsp,%rsi,2)
0X40118d: add $0x4,%rsi
0X401191: cmp $0x18,%rsi
0X401195: je .L10
.L9
0X401197: mov (%rsp,%rsi,1),%ecx
0X40119a: cmp $0x1,%ecx
0X40119d: jle .L7
0X40119f: mov $0x1,%eax
0X4011a4: mov $0x6032d0,%edx
0X4011a9: jmp .L6
#第四部分
.L10
0X4011ab: mov 0x20(%rsp),%rbx
0X4011b0: lea 0x28(%rsp),%rax
0X4011b5: lea 0x50(%rsp),%rsi
0X4011ba: mov %rbx,%rcx
.L11
0X4011bd: mov (%rax),%rdx
0X4011c0: mov %rdx,0x8(%rcx)
0X4011c4: add $0x8,%rax
0X4011c8: cmp %rsi,%rax
0X4011cb: je .L12
0X4011cd: mov %rdx,%rcx
0X4011d0: jmp .L11
#第五部分
.L12
0X4011d2: movq $0x0,0x8(%rdx)
0X4011da: mov $0x5,%ebp
.L13
0X4011df: mov 0x8(%rbx),%rax
0X4011e3: mov (%rax),%eax
0X4011e5: cmp %eax,(%rbx)
0X4011e7: jge .L14
0X4011e9: callq 0X40143a <explode_bomb>
.L14
0X4011ee: mov 0x8(%rbx),%rbx
0X4011f2: sub $0x1,%ebp
0X4011f5: jne .L13
0X4011f7: add $0x50,%rsp
0X4011fb: pop %rbx
0X4011fc: pop %rbp
0X4011fd: pop %r12
0X4011ff: pop %r13
0X401201: pop %r14
0X401203: retq
我滴妈~,让我冷静一下 一下。这么长~,这真的这个代码耗时我好多天
没办法,还是一步一步写呗
这部分我逆向出来的代码丢了,没保存(真的很痛心)但是不想在逆向了,所以这里就简述一下过程吧
前面2部分还是很好写的,还是判断输入的数量,以及输入类型
c
void f1(int a[]){
for(int i = 0; i < 6;i++){
if( a[i] > 6 ) BOOM();
for(int j = i + 1; j < 6; j++){
if( a[i] == a[j]) BOOM();
}
}
}
从前面就可以看出,输入的数字不可以超过6个,然后不能超过6,还不能重复
然后第二部分是把每个a[i] = 7 - a[i];
然后下面这部分代码真是框框goto跳,给我整晕乎的
没办法,我就一步一步stepi,stepi,stepi,stepi,stepi,看寄存器的值
最后发现,这玩意居然有个这个东西
这个是把rbx + 8处的内存放到rax然后这居然又把额,就是*rax 复制给了eax。
这这这,相当于内存中存了个地址,然后猜一下估计是个链表了,然后就是框框用x/x n 查内存,
最后还发现了一个 0x6032d0这直接纯地址,看见就不一般,直接查一下
撕,gdb直接给出这玩意是个node了,然后看一下他内存里的值,在查一下,发现这不就是个
c++
struct node{
int v1;
int v2;
struct node * next;
}
v2记录的是我们输入的值(好像是)
v1是他自己的值
然后呢?继续stepi,跳了几个小时发现,
phase_6 +130 到 pahse_6 + 181这部分的代码,就是上面的第三部分代码,好像是在排序啊,我擦。赶紧带入数字一看,我丢,这不就是给node里面的东西排序吗这下有眉目了,接着整。
直接看第五部分怎么就爆炸的代码。发现满足序列递减就可以了,然后看node里的值,我们可以推出合法的链表项顺序应为3->4->5->6->1->2
,因为第二步操作的存在,每个数x
应该取7-x
,即4 3 2 1 6 5
secret_phase
芜湖,终于搞完了实验上网看看别人写的代码吧~
开心逛中... 我擦,怎么还有secret_phase这个我咋没有,看看代码
c
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
最后一句看起来不像是完成的话呀,翻译一下,嗯?还有秘密的代码,我在找找
最后发现,在最后面的phase_defused()中还有一个秘密入口呢
这不是老朋友sscanf又来了,但是这次好像没让我们输入啊。看到有个注释,num_input_strings.还看到莫名的地址,看到地址我就想看他一下是啥。
这怎么意思是输入2个数字一个字符串?,发现还是从0x603870这块获取的值,这块是哪的值啊
看一下他的上下文
x/128s 0x603870
翻着翻着发现
这不是我之前输入的吗,昂~,懂了,就是之前phase4的输入的东西,这里又获取到了,发现之前输入的后面还应该输入一个字符串,接着stepi看看要输入什么字符串。
然后在之前的4答案处补上这个字符串继续往下走
发现了一个secret_phase函数进去看看
代码比较短(相比上一个)
逆向一下吧
你会发现这玩意也有指针,而且还不一一个,优化优化的代码就成下面这样的
c
void secret_phase(){
chaar * input = read_line();
long int res = strcol(input , NULL , 10);
if(res > 1001){ // 0x3e8 == 1000
bomb();
}
T * rdi = 0x6030f0;
res = fun7(rdi , res);
if(res != 2){
bomb();
}
...
}
fun7(T * node , long int val){
if(node != NULL){
return -1;
}
if(node->val == val){
return 0;
}
if(node->val > val){
return fun7(nqode->left , val) * 2;
}
return fun7(node->right , val) * 2 + 1;
}
这不就是个二叉树吗,然后看看人家的值计算一下答案就会出来的,结果发现,还是改4层的二分树,这还能难道我这个算法高手?框一下一个答案
24,错了,撕~,发现内存里的值看出了应该是22,最后就对了。(答案不止一个)
至此我的第二个lab就完成啦~