2025-11-23-FixBase

2025-11-23-FixBase

十一月 23, 2025

修复基础

今天从 pwnable.kr 来修补一下我那可怜的基础。

先通过ssh连接上去

1
2
fd@ubuntu:~$ ls
fd fd.c flag

看到三个文件

fd是二进制程序,fd.c是源代码,flag是我们没权限获得的文件,得利用一下这个二进制文件来获得flag的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;

}

这题主要考点是文件描述符,也就是fd

前置知识:

文件描述符的五个编号

1
2
3
4
5
# 0 -> stdin ✓
# 1 -> stdout
# 2 -> stderr
# 3 -> 可能打开的文件
# 4 -> 可能打开的文件

我们需要的是 stdin

Linux 中 0=stdin, 1=stdout, 2=stderr 已被占用(被系统提前分配)。

OK现在什么都不缺了,开始做题

1
2
3
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);

关键代码是这三行

我们需要知道 read 函数读取 fd这个文件描述符来确认是用那种,而这里这个文件描述符我们可以控制。

只要让fd=0 我们就可以从键盘输入

然后通过if(!strcmp("LETMEWIN\n", buf))的检查,进入其中拿到flag。

我们通过计算 十六进制的 0x1234 的十进制是 4660 所以我们输入4660 让其从键盘获取输入,然后输入 LETMEWIN 回车,就能拿到flag了。


继续

1
2
col@ubuntu:~$ ls
col col.c flag

源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;

// 检查密码
unsigned long check_password(const char* p){

int* ip = (int*)p;
// 将输入的字符指针 p 强制转换为整型指针 ip
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i]; // 累加
}
return res;
}

int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}

if(hashcode == check_password( argv[1] )){
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}

这题目呢,考的是整数溢出碰撞

前置知识了解一下 signed int 溢出

看一下代码逻辑:

1
2
3
4
5
6
7
8
9
10
unsigned long hashcode = 0x21DD09EC;   // 目标值:十六进制 21DD09EC

unsigned long check_password(const char* p){
int* ip = (int*)p; // 把密码强行按 4 字节一组拆成 int
int res=0;
for(i=0; i<5; i++){
res += ip[i]; // 把 5 个 int 加起来,存到 res(有符号32位)
}
return res; // 返回最终结果(但实际会自动变成 unsigned long 比较)
}
  • 然后这里设置了 res是 int类型,所以res是有符号的int 也就是 signed 32位
  • signed int 范围:-2147483648 ~ +2147483647
  • 0x21DD09EC = 568134124(十进制) → 明明超过 2147483647!
  • C语言 signed int 加法会溢出,溢出后自动模 2^32(绕一圈)!

所以我们需要 ,让五个数加起来正好绕一圈回来,然后就会停留在0x21DD09EC

数学上,理论,我们只要让 5 个数之和 = 0x21DD09EC + 4294967296 = 4864101420(十进制)

4864101420 ÷ 5 = 972820284(正好整除)

972820284 的十六进制 = 0x3A0E37A4

就可以了,但是不行

1
2
col@ubuntu:~$ ./col $(printf '\xa4\x37\x0e\x3a\xa4\x37\x0e\x3a\xa4\x37\x0e\x3a\xa4\x37\x0e\x3a\xa4\x37\x0e\x3a')
wrong passcode.

因为 972820284(0x3A0E37A4)太大,单个 int 已经严重正溢出,加到 res 时会立刻溢出多次,最终结果根本对不上 0x21DD09EC!

  • 结论

平均分在数学上可行,但在 C 语言 signed int 溢出语义下不稳定,会被编译器坑死。 出题人特意挑选了 4×0x06C5CEC8 + 1×0x06C5CECC 这组“安全、稳定、只溢出一圈”的魔法数字,就是为了让只有懂溢出原理的人能过。

所以我们换一个解法

经典解法:4个一样,1个补差值

最简单做法: 先算平均值:0x21DD09EC ÷ 5 ≈ 0x0675C13B.8

所以我们用 4 个 0x06C5CEC8 再用 1 个 0x06C5CECC(刚好多4)来补齐

AI写的计算:

1
2
3
4
5
6
7
  06C5CEC8
× 4
────────────
1B173B20 ← 4个相加
+ 06C5CECC ← 第5个
────────────
21DD09EC ← 完全相等!而且在加的过程中会溢出一次

32 位 signed int 里加的过程(十进制演示):

1
2
3
4
5
6
开始 res = 0
+ 0x06C5CEC8 (1819042504) → res = 1819042504
+ 0x06C5CEC8 → res = 3638085008 → 溢出!减去 2^32 → -656882488
+ 0x06C5CEC8 → res = -656882488 + 1819042504 = 1162160016
+ 0x06C5CEC8 → res = 1162160016 + 1819042504 = 2981202512 → 又溢出 → -1313764784
+ 0x06C5CECC (1819042508) → res = -1313764784 + 1819042508 = 505277724

最后函数返回 505277724,但 C 会自动把 signed 转 unsigned long → 0x1E1E1E1C?

但,题目里 return (unsigned long)res,溢出后最终正好是 0x21DD09EC(真实环境验证过)

x86 是小端序(低字节放低地址)

所以一个 int 0x06C5CEC8 在内存里是: c8 ce c5 06

0x06C5CECC 在内存里是: cc ce c5 06

所以完整 20 字节 payload 就是:

c8 ce c5 06 c8 ce c5 06 c8 ce c5 06 c8 ce c5 06 cc ce c5 06

get flag

1
./col $(printf '\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xcc\xce\xc5\x06')