PicoCtf2023

文章发布时间:

最后更新时间:

文章总字数:
2.1k

预计阅读时间:
9 分钟

第一次打国外的比赛 题型也都很新颖 记录一下做出来的题目

two-sum

签到题 考的是整形溢出 直接给了源码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>

static int addIntOvf(int result, int a, int b) {
result = a + b;
if(a > 0 && b > 0 && result < 0)
return -1;
if(a < 0 && b < 0 && result > 0)
return -1;
return 0;
}

int main() {
int num1, num2, sum;
FILE *flag;
char c;

printf("n1 > n1 + n2 OR n2 > n1 + n2 \n");
fflush(stdout);
printf("What two positive numbers can make this possible: \n");
fflush(stdout);

if (scanf("%d", &num1) && scanf("%d", &num2)) {
printf("You entered %d and %d\n", num1, num2);
fflush(stdout);
sum = num1 + num2;
if (addIntOvf(sum, num1, num2) == 0) {
printf("No overflow\n");
fflush(stdout);
exit(0);
} else if (addIntOvf(sum, num1, num2) == -1) {
printf("You have an integer overflow\n");
fflush(stdout);
}

if (num1 > 0 || num2 > 0) {
flag = fopen("flag.txt","r");
if(flag == NULL){
printf("flag not found: please run this on the server\n");
fflush(stdout);
exit(0);
}
char buf[60];
fgets(buf, 59, flag);
printf("YOUR FLAG IS: %s\n", buf);
fflush(stdout);
exit(0);
}
}
return 0;
}

我们直接输入sum1 sum2的值 只要满足两个判断式中的一个就行

1
2
if(a > 0 && b > 0 && result < 0)
if(a < 0 && b < 0 && result > 0)

这里我选择打第一个判断式 只要a+b超过了无符号int型范围就行

二者值都为2147483647即可

babygame01

脑洞比较大的一道题 不过还在逻辑之内 好好分析一番

image-20230317135241366

ida看一下伪代码

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+1h] [ebp-AA5h]
int v5[2]; // [esp+2h] [ebp-AA4h] BYREF
char v6; // [esp+Ah] [ebp-A9Ch]
char v7[2700]; // [esp+Eh] [ebp-A98h] BYREF
unsigned int v8; // [esp+A9Ah] [ebp-Ch]
int *v9; // [esp+A9Eh] [ebp-8h]

v9 = &argc;
v8 = __readgsdword(0x14u);
init_player(v5);
init_map(v7, v5);
print_map(v7, v5);
signal(2, (__sighandler_t)sigint_handler);
do
{
do
{
v4 = getchar();
move_player(v5, v4, v7);
print_map(v7, v5);
}
while ( v5[0] != 29 );
}
while ( v5[1] != 89 );
puts("You win!");
if ( v6 )
{
puts("flage");
win();
fflush(stdout);
}
return 0;
}

写博客重新复现的时候 决定把每个函数都过一遍 顺便加强我的代码审计能力

1
2
3
4
5
6
7
8
9
10
int __cdecl init_player(int a1)
{
int result; // eax

*a1 = 4;
*(a1 + 4) = 4;
result = a1;
*(a1 + 8) = 0;
return result;
}

a1的地址等同于v5 v5这个数组就定义了两个元素 分别用来存放此时我们处于map上的位置

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
Elf32_Dyn **__cdecl init_map(int a1, _DWORD *a2)
{
Elf32_Dyn **result; // eax
int i; // [esp+8h] [ebp-Ch]
int j; // [esp+Ch] [ebp-8h]

result = &GLOBAL_OFFSET_TABLE_;
for ( i = 0; i <= 29; ++i )
{
for ( j = 0; j <= 89; ++j )
{
if ( i == 29 && j == 89 )
{
*(a1 + 2699) = 'X';
}
else if ( i == *a2 && j == a2[1] )
{
*(90 * i + a1 + j) = player_tile;
}
else
{
*(90 * i + a1 + j) = '.';
}
}
}
return result;
}

a1相当于v7 v7定义了非常大一串 用来开辟一块内存给地图的存放

利用for循环对map进行了初始化 除了两个特殊的地方 用户的初始位置和终点 其他都为’.’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl print_map(int a1, int a2)
{
int i; // [esp+8h] [ebp-10h]
int j; // [esp+Ch] [ebp-Ch]

clear_screen();
find_player_pos(a1);
find_end_tile_pos(a1);
print_flag_status(a2);
for ( i = 0; i <= 29; ++i )
{
for ( j = 0; j <= 89; ++j )
putchar(*(90 * i + a1 + j));
putchar(10);
}
return fflush(stdout);
}

for循环遍历打印出map

接下来用getchar赋值了v4 传入下面这个函数充当a2

a1用来存储用户位置 a3则是存放map

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
_BYTE *__cdecl move_player(_DWORD *a1, char a2, int a3)
{
_BYTE *result; // eax

if ( a2 == 'l' )
player_tile = getchar();
if ( a2 == 'p' )
solve_round(a3, a1);
*(a1[1] + a3 + 90 * *a1) = '.';
switch ( a2 )
{
case 'w':
--*a1;
break;
case 's':
++*a1;
break;
case 'a':
--a1[1];
break;
case 'd':
++a1[1];
break;
}
result = (a1[1] + a3 + 90 * *a1);
*result = player_tile;
return result;
}

大体是利用’wsad’控制用户位置 不过存放两个特殊的指令

‘l’可以把player_tile替换成其后面的字符

‘p’则可以直接把用户位置移动到终点

最后利用result存储用户位置移动后的地址 在对应地址存入player_tile

1
2
3
4
5
6
if ( v6 )
{
puts("flage");
win();
fflush(stdout);
}

最后回到main函数 如果到达了终点 则对v6进行判断 如果为真 就输出flag

分析完了程序 来捋一捋思路 目的无非就是要修改到v6的值 不过程序不存在任意写也无法栈溢出覆盖到v6

不过还是存在着一个细微的漏洞 可以做到数组溢出 如果我们使得v5中的元素值为负呢?

也就是达到了map边界后仍然向边界外面移动 这个时候就会往非法内存处写入值

这对于v6的值会有什么影响呢 我们知道 在栈结构不被破坏的情况下 固定索引到的栈地址存放的一定是v6

那么我们只需要破坏栈结构 使其索引到的是其他值就行了

image-20230318113925813

这个时候我们来尝试一下 进行一下数组溢出 将位置移动到左上角后 再输入a

image-20230318114212851

image-20230318114250243

可以看到此时栈的结构就成功被破坏了 不过此时我们仍然无法通过if

byte ptr的作用在于指明访问的内存单元是一个字节单元 也就是只读入一个字节的数据

image-20230318114546367

此时还是0 那么只需要利用同样的手法多试几次 最后成功获得flag

VNE

从来没做过这样的题目 说实话还是挺好玩的 感觉和awd有点像

一开始没有给我们附件 启动靶机后 提示让我们进行ssh连接

image-20230318115150252

这里我使用的软件是finalshell

image-20230318115327880

连上了以后 照着提示下载了bin文件 是题目的附件

保护全开 ida看一看

image-20230318115442076

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
int v4; // ebx
__int64 v5; // rax
__int64 v6; // rax
__int64 v7; // rax
const char *v8; // rax
__int64 v9; // rax
__int64 v10; // rax
char v12; // [rsp+3h] [rbp-6Dh] BYREF
unsigned int v13; // [rsp+4h] [rbp-6Ch]
char *v14; // [rsp+8h] [rbp-68h]
char v15[32]; // [rsp+10h] [rbp-60h] BYREF
char v16[40]; // [rsp+30h] [rbp-40h] BYREF
unsigned __int64 v17; // [rsp+58h] [rbp-18h]

v17 = __readfsqword(0x28u);
v14 = getenv("SECRET_DIR");
if ( v14 )
{
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "Listing the content of ");
v6 = std::operator<<<std::char_traits<char>>(v5, v14);
v7 = std::operator<<<std::char_traits<char>>(v6, " as root: ");
std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
std::allocator<char>::allocator(&v12);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v16, v14, &v12);
std::operator+<char>(v15, "ls ", v16);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v16);
std::allocator<char>::~allocator(&v12);
setgid(0);
setuid(0);
v8 = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(v15);
v13 = system(v8);
if ( v13 )
{
v9 = std::operator<<<std::char_traits<char>>(&std::cerr, "Error: system() call returned non-zero value: ");
v10 = std::ostream::operator<<(v9, v13);
std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>);
v4 = 1;
}
else
{
v4 = 0;
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v15);
}
else
{
v3 = std::operator<<<std::char_traits<char>>(&std::cerr, "Error: SECRET_DIR environment variable is not set");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
v4 = 1;
}
return v4;
}

用c++编写的程序 结合直接运行大概能看懂主要流程 就是根据环境变量SECRET_DIR的值

执行ls SECRET_DIR

加上题目给的提示 所以这里把SECRET_DIR值设置为/root/ 看看root目录下有什么东西

image-20230318124910924

我们的目标就是flag.txt了 当然由于权限问题 我们肯定不能直接cat

image-20230318125047155

这里学习了幽林师傅的思路 我们把/bin目录里面的cat文件拷贝下来 上传到/home/ctf-player 目录中 顺便更改一下PATH 顺便更改cat文件名为ls

这样在调用ls的时候 相当于调用的就是cat了

然后再把SECRET_DIR更改为/root/flag.txt 就可以获取flag了

image-20230318125604016

不过不知道什么问题 使用finalshell执行最后一步的时候 会报如下错 所以我更换了windterm才解决问题

image-20230318125703158