目录
前言
命令行参数
16进制转字符串
extract_message1
process_keys12
extract_message2
main
process_keys34
因为这个学期基本都在搞CTF的web方向,C语言不免荒废。所幸还会一点指针相关的知识,故第一个安全项目做的挺顺利的,也把思维切换切换,接触点新东西。
贴出源码
#include
#include
int prologue[] = {
0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x20206F74, 0x74786565, 0x65617276, 0x32727463,
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A
};
int data[] = {
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x466D203A, 0x65693A72, 0x43646E20, 0x6F54540A,
0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
0x6F786F68, 0x6E696373, 0x6C206765, 0x796C656B,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x20206F74, 0x74786565, 0x65617276, 0x32727463,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x21687467, 0x63002065, 0x6C6C7861, 0x78742078,
0x6578206F, 0x72747878, 0x78636178, 0x00783174
};
int epilogue[] = {
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x20206F74, 0x74786565, 0x65617276, 0x32727463
};
char message[100];
void usage_and_exit(char* program_name) {
fprintf(stderr, "USAGE: %s key1 key2 key3 key4\n", program_name);
exit(1);
}
void process_keys12(int* key1, int* key2) {
*((int*)(key1 + *key1)) = *key2;
}
void process_keys34(int* key3, int* key4) {
*(((int*)&key3) + *key3) += *key4;
}
char* extract_message1(int start, int stride) {
int i, j, k;
int done = 0;
for (i = 0, j = start + 1; !done; j++) {
for (k = 1; k < stride; k++, j++, i++) {
if (*(((char*)data) + j) == '\0') {
done = 1;
break;
}
message[i] = *(((char*)data) + j);
}
}
message[i] = '\0';
return message;
}
char* extract_message2(int start, int stride) {
int i, j;
for (i = 0, j = start;
*(((char*)data) + j) != '\0';
i++, j += stride)
{
message[i] = *(((char*)data) + j);
}
message[i] = '\0';
return message;
}
int main(int argc, char* argv[])
{
int dummy = 1;
int start, stride;
int key1, key2, key3, key4;
char* msg1, * msg2;
key3 = key4 = 0;
if (argc < 3) {
usage_and_exit(argv[0]);
}
key1 = strtol(argv[1], NULL, 0);
key2 = strtol(argv[2], NULL, 0);
if (argc > 3) key3 = strtol(argv[3], NULL, 0);
if (argc > 4) key4 = strtol(argv[4], NULL, 0);
process_keys12(&key1, &key2);
start = (int)(*(((char*)&dummy)));
stride = (int)(*(((char*)&dummy) + 1));
if (key3 != 0 && key4 != 0) {
process_keys34(&key3, &key4);
}
msg1 = extract_message1(start, stride);
if (*msg1 == '\0') {
process_keys34(&key3, &key4);
msg2 = extract_message2(start, stride);
printf("%s\n", msg2);
}
else {
printf("%s\n", msg1);
}
return 0;
}
在C语言程序中,主函数main()可以有两个参数,用于接收命令行参数。带有参数的函数main()习惯上书写为:
(颇有pearcmd的感觉哈哈哈)
int main(int argc,char *argv[])
{
...
}
argc和argv是函数main()的形参(argc和argv分别是argument count和argument vector的缩写)。用命令行的方式运行程序时,函数main()被调用,与命令行有关的信息作为实参传递给两个形参。
第一个参数argc接收命令行参数(包括命令)的个数;第二个参数argv接受以字符串常量形式存放的命令行参数(包括命令本身也作为一个参数)。字符指针数组argv[]表示各个命令行参数(包括命令),其中argv[0]指向命令,argv[1]指向第1个命令行参数,argv[2]指向第2个命令行参数......argv[argc-1]指向最后一个命令行参数。
审一下源码的这部分
其实就是让我们最少传2个参数(第0个参数为程序名),否则就会exit
这里显然只有data数组有用,只需要转换data数组即可
如果直接用16进制转字符串的网站,会发现转换结果并不正确
于是跟着代码逻辑自己写一版16进制逐字节解码程序
#include
int data[] = {
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x466D203A, 0x65693A72, 0x43646E20, 0x6F54540A,
0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
0x6F786F68, 0x6E696373, 0x6C206765, 0x796C656B,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x20206F74, 0x74786565, 0x65617276, 0x32727463,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x21687467, 0x63002065, 0x6C6C7861, 0x78742078,
0x6578206F, 0x72747878, 0x78636178, 0x00783174
};
void decode1(int* data)
{
for (int i = 0; ; i++) {
char c = *(((char*)data) + i);
putchar(c);
if (c == '\0') break;
}
}
int main()
{
decode1(data);
return 0;
}
两者的差异是什么原因呢?
因为Intel处理器是小端,所以数组里每个int的低位存储在内存的低地址处,即先被转化
例如
int 0x5A33723479(Z3r4y)
在内存中:
79 34 72 33 5A(y4r3Z)
所以我们把data的数据先预处理一下再放入16进制转字符串在线网站即可(虽然没有必要)
接下来看一些关键代码
传入一个start和stride,将data数组转换成字符串
这个函数的功能是,从 data 数组的首地址偏移 start + 1 地址开始,每转换 stride -1 个字符后,就跳过一个字符不转换,重复执行这样的操作直到转换到最后一个字符
已知明文最开始为:From: ,可利用其来倒推出start和stride
这里显然是从第10个字符开始(从0开始计数),每次转换两个字符,故start=9,stride=3
再发现start和stride是由dummy得来的
将dummy最低的字节赋值给start(要等于9),将dummy第二低的字节赋值给stride(要等于3)
即dummy为0x????0309
记录一下低2位转10进制是777
访问以 key1 变量的地址为初始地址,偏移 *key1 的值×sizeof(int) 即4字节的地址,赋成*key2的值。
这里就可以操纵key1地址将其偏移到dummy的地址,再操纵*key2来控制dummy的值
调试看key1和dummy的地址
(左为key1,右为dummy)
计算地址差值
24➗4=9 ,所以只要令*key1=9即可偏移到dummy的地址
再令*key2=777,使得dummy为0x00000309,成功使得start为9,stride为3
回显正确
提示我们要选择key3key4来调用extract2并且避免调用extract1
其实和extract_message1大同小异
从 data 数组的首地址偏移 start 地址开始,每读入一个字符,就跳过stride-1个字符,直到转换到最后一个字符
可见start=9,stride=3 (刚好就是msg1对应的数据,故key1,key2传的值不用改)
通过代码审计,发现只要让extract_message1返回一个空字符串就可以打印出msg2
我们接着看,怎么操纵msg1='\0'
extract1 最初会访问第10个字符(从0开始计数),而第10个字符恰好会在 extract2 (从第9个字符开始,每隔2个取一个)中被忽略不会影响答案。
所以我们可以直接尝试改变 data 数组,使其表示的第10个字符为 \0 ,即将data[2]=0x72464663转变成 0x72004663 。
可以借助 process_keys34 来操纵data[2]
这个函数就是把指向key3指针的地址偏移*key3的值×sizeof(int)即4个字节的地址,将其解引用,自增一个*key4的值
那么我们就可以将指向key3指针的地址偏移到data[2]上,然后利用*key4改变data[2]的值,这里72004663-72464663=-4587520,key4传入-4587520即可
调试观察&key3和data[2]的地址
2597600/4=649400
所以key3传入649400
综上,key1,key2,key3,key4分别传入9 777 649400 -4587520
运行回显正确
实验1还是比较友好的,做的时候没有太坐牢,对于一个C语言小白这种难度刚刚好QWQ