【逆向工程-从GDB中观察x86-64数组、指针、结构体在汇编中的表示】此文章归类为:逆向工程。
测试代码
#include <stdio.h>
#include <string.h>
struct Student {
int id;
int score;
char name[16];
};
void process_array() {
int arr[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
}
printf("sum = %d\n", sum);
}
void process_pointer() {
int x = 42;
int *p = &x;
*p = 100;
printf("x = %d\n", x);
}
void process_struct() {
struct Student s;
s.id = 1;
s.score = 95;
strcpy(s.name, "Alice");
printf("id=%d score=%d name=%s\n", s.id, s.score, s.name);
}
int main() {
process_array();
process_pointer();
process_struct();
printf("%d\n",sizeof (struct Student));
return 0;
}运行环境:
Ubuntu 20.04.6 LTS
编译指令:
gcc -g -O0 -o main main.c
gcc版本
gcc version 9.4.0
调试工具:
GDB
开始执行
进入main 函数:
=> 0x5555555552d0 <main>: endbr64
0x5555555552d4 <main+4>: push %rbp
0x5555555552d5 <main+5>: mov %rsp,%rbp
0x5555555552d8 <main+8>: mov $0x0,%eax
0x5555555552dd <main+13>: callq 0x555555555169 <process_array>
传入了一个0到process_array函数内部,但是他没用容rdi去传参,那process_array函数参数可能是一个不定数量的参数
进入process_array函数内部:
这段代码中构建了栈帧,存入了金丝雀的值
0x55555555516d <process_array+4>: push %rbp
0x55555555516e <process_array+5>: mov %rsp,%rbp
0x555555555171 <process_array+8>: sub $0x30,%rsp
0x555555555175 <process_array+12>: mov %fs:0x28,%rax
0x55555555517e <process_array+21>: mov %rax,-0x8(%rbp)
然后清零了eax: 可是eax不是传入的不定长参数浮点数的数量么?不确定 再看看
0x555555555182 <process_array+25>: xor %eax,%eax
继续执行:这段代码在一个连续的区域初始化了一些值:可能是一个数组,
=> 0x555555555184 <process_array+27>: movl $0xa,-0x20(%rbp)
0x55555555518b <process_array+34>: movl $0x14,-0x1c(%rbp)
0x555555555192 <process_array+41>: movl $0x1e,-0x18(%rbp)
0x555555555199 <process_array+48>: movl $0x28,-0x14(%rbp)
0x5555555551a0 <process_array+55>: movl $0x32,-0x10(%rbp)
查看内存,验证内容
(gdb) x/10x $rbp-0x20
0x7fffffffdcf0: 0x0000000a 0x00000014 0x0000001e 0x00000028
0x7fffffffdd00: 0x00000032
继续执行:看起来是设置了两个变量值为0
=> 0x5555555551a7 <process_array+62>: movl $0x0,-0x28(%rbp)
0x5555555551ae <process_array+69>: movl $0x0,-0x24(%rbp)
继续执行:
=> 0x5555555551b5 <process_array+76>: jmp 0x5555555551c7 <process_array+94>
0x5555555551b7 <process_array+78>: mov -0x24(%rbp),%eax
0x5555555551ba <process_array+81>: cltq
0x5555555551bc <process_array+83>: mov -0x20(%rbp,%rax,4),%eax
0x5555555551c0 <process_array+87>: add %eax,-0x28(%rbp)
0x5555555551c3 <process_array+90>: addl $0x1,-0x24(%rbp)
0x5555555551c7 <process_array+94>: cmpl $0x4,-0x24(%rbp)
0x5555555551cb <process_array+98>: jle 0x5555555551b7 <process_array+78>
0x5555555551cd <process_array+100>: mov -0x28(%rbp),%eax
发现一个循环:跳转去0x5555555551c7位置执行代码:
使用了 rbp-0x24中的值和0x4进行比较,判断 rbp-0x24是否小于等于4如果小于进入循环体
先将rbp-4放在了eax中,
ovl $0x0,-0x24(%rbp)
然后可能进行了强制类型转换从4字节升到了8字节,观察后面代码发现,是为了和rbp进行计算,那也就是eax是数组的下标
rbp-4就是循环中的累加参数。
mov -0x20(%rbp,%rax,4),%eax
add %eax,-0x28(%rbp)
从 数组的末尾取出一个值? 然后累加到 rbp-28这个参数内 ,此处发现,编译器并没有按照数组的顺序来进行累加
而是直接从数据末尾去累加的,再次印证了在-O0下可能不会按照顺序去执行代码
尝试还原代码:
int arr[5] = {0x32, 0x28, 0x1e, 0x14, 0xa};
int x=0;
for (int i = 0; i <= 4; i++){
x += arr[i];
}
继续执行:
=> 0x5555555551cd <process_array+100>: mov -0x28(%rbp),%eax
0x5555555551d0 <process_array+103>: mov %eax,%esi
0x5555555551d2 <process_array+105>:
lea 0xe2b(%rip),%rdi # 0x555555556004
0x5555555551d9 <process_array+112>: mov $0x0,%eax
0x5555555551de <process_array+117>: callq 0x555555555070 <printf@plt>
调用printf函数 输出0x96 = 150
继续执行:判断金丝雀的值,后返回main函数
=> 0x5555555551e3 <process_array+122>: nop
0x5555555551e4 <process_array+123>: mov -0x8(%rbp),%rax
0x5555555551e8 <process_array+127>: xor %fs:0x28,%rax
0x5555555551f1 <process_array+136>: je 0x5555555551f8 <process_array+143>
0x5555555551f3 <process_array+138>: callq 0x555555555060 <__stack_chk_fail@plt>
0x5555555551f8 <process_array+143>: leaveq
0x5555555551f9 <process_array+144>: retq
继续执行:
=> 0x5555555552e2 <main+18>: mov $0x0,%eax
0x5555555552e7 <main+23>: callq 0x5555555551fa <process_pointer>
发现又一次清空了eax的值,并不清楚为什么要清零先进入process_pointer函数
=> 0x5555555551fa <process_pointer>: endbr64
0x5555555551fe <process_pointer+4>: push %rbp
0x5555555551ff <process_pointer+5>: mov %rsp,%rbp
0x555555555202 <process_pointer+8>: sub $0x20,%rsp
0x555555555206 <process_pointer+12>: mov %fs:0x28,%rax
0x55555555520f <process_pointer+21>: mov %rax,-0x8(%rbp)
0x555555555213 <process_pointer+25>: xor %eax,%eax
构建栈帧, 保存金丝雀
继续执行:
=> 0x555555555215 <process_pointer+27>: movl $0x2a,-0x14(%rbp)
0x55555555521c <process_pointer+34>: lea -0x14(%rbp),%rax
0x555555555220 <process_pointer+38>: mov %rax,-0x10(%rbp)
将0x2a的值放在了 rbp-0x14位置
(gdb) x/10x $rbp-0x14
0x7fffffffdcfc: 0x0000002a 0xffffdcfc 0x00007fff 0xa69bd200
那可能是 int a = 0x2a;
然后取了rbp-0x14的地址放在了rax中,然后就放在了rbp-0x10中
那可能就是
int *b = &a;
继续执行:
=> 0x555555555224 <process_pointer+42>: mov -0x10(%rbp),%rax
0x555555555228 <process_pointer+46>: movl $0x64,(%rax)
0x55555555522e <process_pointer+52>: mov -0x14(%rbp),%eax
0x555555555231 <process_pointer+55>: mov %eax,%esi
0x555555555233 <process_pointer+57>: lea 0xdd4(%rip),%rdi # 0x55555555600e
0x55555555523a <process_pointer+64>: mov $0x0,%eax
0x55555555523f <process_pointer+69>: callq 0x555555555070 <printf@plt>
movl $0x64,(%rax),修改了指针b指向的值
那就是 *b = 0x64; 此时 a = 0x64即 rbp-0x14中的值为0x64
(gdb) x/10x $rbp-0x14
0x7fffffffdcfc: 0x00000064 0xffffdcfc 0x00007fff 0xa69bd200
然后调用printf函数,输出了a的值
继续执行:返回main函数
0x555555555244 <process_pointer+74>: nop
0x555555555245 <process_pointer+75>: mov -0x8(%rbp),%rax
0x555555555249 <process_pointer+79>: xor %fs:0x28,%rax
0x555555555252 <process_pointer+88>:
je 0x555555555259 <process_pointer+95>
0x555555555254 <process_pointer+90>:
callq 0x555555555060 <__stack_chk_fail@plt>
0x555555555259 <process_pointer+95>: leaveq
0x55555555525a <process_pointer+96>: retq
继续执行:
=> 0x5555555552ec <main+28>: mov $0x0,%eax
0x5555555552f1 <main+33>: callq 0x55555555525b <process_struct>
发现调用前仍然让eax的值为0,猜测可能是因为上面的函数是void类型,所以提前写好了返回值
进入process_struct函数:
=> 0x55555555525b <process_struct>: endbr64
0x55555555525f <process_struct+4>: push %rbp
0x555555555260 <process_struct+5>: mov %rsp,%rbp
0x555555555263 <process_struct+8>: sub $0x20,%rsp
0x555555555267 <process_struct+12>: mov %fs:0x28,%rax
0x555555555270 <process_struct+21>: mov %rax,-0x8(%rbp)
0x555555555274 <process_struct+25>: xor %eax,%eax
老样子,构建栈帧,拿金丝雀的值存起来
继续执行:
=> 0x555555555276 <process_struct+27>: movl $0x1,-0x20(%rbp)
0x55555555527d <process_struct+34>: movl $0x5f,-0x1c(%rbp)
0x555555555284 <process_struct+41>: lea -0x20(%rbp),%rax
0x555555555288 <process_struct+45>: add $0x8,%rax
0x55555555528c <process_struct+49>: movl $0x63696c41,(%rax)
0x555555555292 <process_struct+55>: movw $0x65,0x4(%rax)
0x555555555298 <process_struct+61>: mov -0x1c(%rbp),%edx
0x55555555529b <process_struct+64>: mov -0x20(%rbp),%eax
0x55555555529e <process_struct+67>: lea -0x20(%rbp),%rcx
0x5555555552a2 <process_struct+71>: add $0x8,%rcx
0x5555555552a6 <process_struct+75>: mov %eax,%esi
0x5555555552a8 <process_struct+77>: lea 0xd67(%rip),%rdi # 0x555555556016
0x5555555552af <process_struct+84>: mov $0x0,%eax
先是在一块连续的区域存储了两个4字节的数据, 0x1和0x5f 即arr[2] = {0x5f, 0x1};
然后取rbp-0x20这个地址放在了rax中, 让这个地址+8 此时rax就等于 rbp-0x20+0x8这个地址为rbp-0x18
然后将0x63696c41放入了rax的地址rbp-0x18中,gdb中观察到这个是一个字符串Alic
后将rax+0x4 = rbp-0x18+0x4 = rbp-0x14 位置写入了2字节0x65 -> movw $0x0065,0x4(%rax) 最后一个字符的值,以及/0
有意思的是,他并没有一次写入字符串的值,而是分两次去写入了字符串的值
查阅后发现,在strcpy函数或者类似的字符串操作在被编译器优化后的表现形式,通过指针来写入内存
那可能就是 char *a = "ALice";
此时的栈帧结构可能是
rbp-> 栈帧
rbp-0x8 = 金丝雀
rbp-0x14 = 0x0065 = e\0
rbp-0x18=0x63696c41 = Alic
rbp-0x1c=0x5f 4字节
rbp-0x20=1 4字节
然后调用printf函数,传入了
rdi 参数1 格式化字符串, esi 参数2 ebp-20的值1, edx 参数3 ebp-1c的值 0x5f; ecx参数4 rbp-0x18的地址
因为参数是连续的,并且数据大小不一致,判断此前的赋值操作都是对结构体进行赋值
结构体的结构就可能是
struct {
char b[]; 具体字符上限未知,如果是指针的话,代码应该会有开辟堆空间的动作,没看到就认为是一个字符数组
int c;
int d;
}
s.a = 0x65;
strcpy(s.b, "Alice");
d = 1;
printf("字符串", 1, 0x5f, "Alice");
关于调用函数前将eax赋值为0的试验
尝试试验:
修改函数
float process_array(float x) {
int arr[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
}
printf("sum = %d\n", sum);
return x;
}编译后反汇编
12d9: f3 0f 1e fa endbr64
12dd: 55 push %rbp
12de: 48 89 e5 mov %rsp,%rbp
12e1: f3 0f 10 05 4b 0d 00 movss 0xd4b(%rip),%xmm0 # 2034 <_IO_stdin_used+0x34>
12e8: 00
12e9: e8 7b fe ff ff callq 1169 <process_array>
12ee: b8 00 00 00 00 mov $0x0,%eax
没有传递al
再次修改
int process_array(int x) {
int arr[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
}
printf("sum = %d\n", sum);
return x;
}编译后反汇编
00000000000012d5 <main>:
12d5: f3 0f 1e fa endbr64
12d9: 55 push %rbp
12da: 48 89 e5 mov %rsp,%rbp
12dd: bf 01 00 00 00 mov $0x1,%edi
12e2: e8 82 fe ff ff callq 1169 <process_array>
因为rax是函数的返回值,有没有可能是因为函数的返回值为空所以提前设置了0呢,再次试验
int process_array() {
int arr[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
}
printf("sum = %d\n", sum);
return 1;
}编译后反汇编:
00000000000012d4 <main>:
12d4: f3 0f 1e fa endbr64
12d8: 55 push %rbp
12d9: 48 89 e5 mov %rsp,%rbp
12dc: b8 00 00 00 00 mov $0x0,%eax
12e1: e8 83 fe ff ff callq 1169 <process_array>
发现是没有关系的。
现象为 函数参数为空时,会在调用函数之前加上一句 mov eax,0 但是不为空时,则不会。
更多【逆向工程-从GDB中观察x86-64数组、指针、结构体在汇编中的表示】相关视频教程:www.yxfzedu.com